原文:https://github.com/thpatch/thcrap/tree/master/docs

介绍

此项目一直缺乏文档,本文档的3个主要来源为:

  • 代码注释
  • 询问了解的人士
  • 阅读代码以理解功能。

本文档将尝试解决这些问题。请注意:其中的部分内容会过时,我们可能会忘记进行更新。但其中大部分的内容会保持不变。例如,我们认为不会停止使用不同的补丁。

我们是谁

我们主要是两个“东西”:thcrap 和 thpatch。thcrap,或者叫做东方圈补丁依赖程序(Touhou Community Reliant Automantic Patcher)(无需记忆全名,没人用),是一个东方游戏的补丁工具。你用 thcrap 运行一个东方游戏,thcrap 会修改它以应用各种补丁。它会将文本翻译成英语、法语或克林贡语,它会让你在 ZUN 忘记放入的游戏中玩到咲夜或魅魔,或者它会把你想玩的游戏替换成另一个使用相同引擎的游戏。这些补丁是由我们或社区制作的。thpatch,或者叫东方补丁中心(Touhou Patch Center)(请记住这个全名,大家都用它),是我们托管的网站,网址为 https://thpatch.net/ 。这是一个带有翻译扩展和大量补丁的自定义 MediaWiki。它托管了我们大多数的翻译补丁,并为每个人提供了一个贡献翻译的界面。“Touhou Patch Center”也是我们团队的名称。

补丁(Patch)与仓库(Repositories)

我们的团队专注于补丁,所以补丁是我们软件中最重要的部分是合情合理的。补丁是可以应用于一个或多个游戏的“魔改包”。最流行的例子是thpatch/lang_en补丁:它将游戏文本更改为英语。gamer251/mima补丁会更改游戏中的文件以将自机替换为魅魔。如果你使用 thcrap,那通常是因为你对一个或多个补丁感兴趣。

补丁叠加

你可以在游戏上同时应用多个补丁,这被称为“补丁叠加”。例如,你可以同时应用thpatch/lang_enthpatch/lang_fr。当法语补丁可以提供翻译时,将使用法语补丁,而英语补丁将用于那些法语补丁中尚未翻译的内容。你可以使用thpatch/lang_enDTM/alphes补丁将文本翻译成英语,并用Alphes风格的艺术替换角色立绘。当然,你不限于使用两个补丁。实际上,如果你只选择thpatch/lang_fr,你已经得到了6个补丁:

  • nmlgc/base_tsanmlgc/base_tasofro,这两个补丁大多数情况下是需要的。
  • nmlgc/script_latin,这个补丁增强了使用拉丁字母的语言(例如,它将字体更改为包含更多字符的字体)。
  • nmlgc/western_name_order,用于将名字放在姓氏前的语言。
  • thpatch/lang_en,用于在没有法语翻译时使用,因为我们假设大多数法国人阅读英语比日语擅长。
  • thpatch/lang_fr,这是你选择的补丁。它们按优先级的逆序列出。在尝试打补丁时,thcrap将从底部开始,向上直到找到可以提供替换的补丁。

仓库

仓库,通常缩写为“repos”,因为这个词太长了,是“由一个人/组织制作的一组补丁”。你可能注意到上面提到的所有补丁都遵循something/something_else命名规范。这实际上是repo_name/patch_name。例如,补丁thpatch/lang_enthpatch仓库中的补丁lang_en。我们有两个仓库:nmlgc,包含我们在Github上托管的所有补丁;thpatch,包含从Touhou Patch Center网站生成的所有补丁。所有其他仓库(如之前提到的gamer251DTM)由其他人拥有,并托管这些人的补丁。如果你想制作自己的补丁,你可能需要创建自己的仓库。顺带一提,当我们谈论补丁时,往往会省略仓库名称。例如,我们会讨论base_tsalang_en,而非nmlgc/base_tsathpatch/lang_en。所有lang_*补丁+ skipgame补丁都在thpatch储存库中,其他所有来自我们的补丁都在nmlgc仓库中(nmlgc是thcrap的创始人,所以要区分曾经是“来自nmlgc的补丁”/“使用thpatch网站的人们的补丁”,但当更多人加入团队时,将我们所有人的补丁放在一个Github仓库中更有意义,而这个名字就保留下来了)。

文件

这部分将介绍 thcrap 使用的文件,并在此过程中解释这些文件的概念。这部分内容看似简短,但所有 thcrap 功能都是由一些文件实现的,因此介绍这些文件能提供 thcrap 核心功能的全面概览。在我写下这些文字时,我甚至不知道是否需要“代码”部分。让我们从查看 thcrap 目录开始。

thcrap_configure.exethcrap_loader.exe

与您可能认为的不同,这两个文件并不是 thcrap_configure 和 thcrap_loader。它们只是小型启动器。它们有两项工作:

  • 安装C运行库(若必需)
  • bin目录运行相关的exe。我们需要这两个启动器以便将所有DLL塞进子目录中,用户看不到它们。有关thcrap_configurethcrap_loader的更多详细信息,请参阅“bin”部分。

bin

程序

thcrap_configure

让我们从简单的开始。thcrap_configurethcrap的配置工具。您在第一次运行thcrap时使用它,以选择您想要的补丁和游戏所在的位置。如果您想要新的补丁叠放,或者想添加新游戏,可以再次使用它。您还可以在讨论中抱怨我们缺乏UI设计技能。由于补丁叠放存储在文件中,您可以拥有多个。创建新叠放不会删除旧叠放,除非您创建了同名的叠放。它有以下步骤:

  1. 启动画面
  2. 下载仓库列表
  3. 让用户创建补丁叠放
  4. 下载基础文件
  5. 让用户选择游戏
  6. 询问是否创建快捷方式
  7. 下载特定游戏文件
  8. 结束画面

我将在描述repo.js的“neighbors”字段时描述步骤2。有两个不同的下载步骤,因为:

步骤3需要步骤4和5。
步骤4需要步骤 5。
步骤5需要步骤 7。

在步骤5中,我们在用户的计算机上查找游戏。为此,我们需要游戏exe的哈希值。所有这些哈希值都存储在nmlgc/base_tsa/versions.jsnmlgc/base_tasofro/versions.js中。其他补丁(甚至第三方补丁)如果愿意,也可以提供带有新游戏的version.js。因此,对于步骤5,我们至少需要下载base_tsabase_tasofro的常用文件(即使是不特定于游戏的文件)。并且因为我们将这两个补丁与其它补丁一视同仁,因而需要用户在此之前选择它们。

至于步骤7,如果用户只有红魔乡,他不需要下载3TB的Tasofro格斗作翻译,他只需要下载红魔乡的翻译。因此,我们希望在知道用户拥有的游戏后再执行此步骤。

从用户交互的角度来看,我认为将游戏选择移到补丁叠放创建之前会更好。这是传统补丁工具首先需要询问的问题(也是唯一一个),因此这是用户唯一预期的。此外,我们是为游戏打补丁。如果我们都不知道它们在哪里,怎么能为它们打补丁呢?对我们来说,这很有意义,因为我们只在我们的目录中下载补丁文件并在运行时应用它们,但这与用户的期望不符。我甚至有一个稍微了解thcrap内部结构的朋友,他仍然对游戏选择步骤不在一开始而感到困惑。通过将游戏选择放在第一位,我们可以合并两个下载步骤。我们还可以在补丁选择列表中隐藏与用户游戏不兼容的补丁。在实现方面,我认为最灵活的方法是将游戏哈希值从versions.js移到repo.js。无论如何,所有这些描述了一个潜在的设计改进,而不是当前的实现。

thcrap_loader

第二个可执行文件。你从未直接运行过这个文件,但实际上你一直在间接运行它:由thcrap生成的快捷方式会通过一些参数指向thcrap_loader

thcrap_loader过程分为三部分:

  • 命令行解析
# 用法:
$ thcrap_loader.exe [config.js ...] game

config.js是由thcrap_configure生成的运行配置文件。你可以指定多个运行配置,但最终只有最后一个会被使用。为了向后兼容,如果运行配置是相对路径,则它相对于config目录。

game可以是游戏名称或游戏路径。thcrap_loader将运行这个游戏并对其进行补丁。如果是游戏名称,它将使用 games.js将其解析为游戏路径。

  • 更新(可选)

如果启用了更新,更新用户界面将在此处显示。它首先检查thcrap本身的更新。然后,它会检查“核心文件”的更新——与游戏无关的文件。它们往往相互依赖,而新版thcrap往往依赖于它们保持最新,所以我们先更新它们。接着我们更新用户正在运行的游戏文件,然后运行游戏。

  • 运行并对游戏补丁

thcrap_loader的目标是运行并补丁一个游戏,所以这一步最为重要。补丁由thcrap.dll在进程中完成,因此我们需要将thcrap.dll注入到游戏中。

我们首先以挂起模式运行游戏。Windows会加载程序并在执行任何指令之前将其置于挂起模式。我们找到程序的入口点,并用无限循环指令替换那里的两个字节。然后,我们让程序运行一会儿,直到它到达入口点。这使得它可以执行Windows进程内加载代码,这将初始化我们稍后需要的许多东西。然后,我们在游戏进程中分配一些可执行内存,将一些核心代码写入其中,该代码基本上表示“加载thcrap.dll并运行其初始化函数”,并在游戏进程中创建一个线程,以此可执行内存为其入口点。有了这个,游戏将加载thcrap.dll并运行其初始化函数。然后我们恢复游戏进程,并离开。工作完成。

插件

thcrap.dll

thcrap的核心。所有游戏内的补丁工作都是从这里进行的。所有插件使用的通用函数都在这里。基本上,它包含了一切不需要其他依赖的东西。

thcrap_tsathcrap_tasofro

游戏支持插件。thcrap_tsa提供对上海爱丽丝幻乐团游戏(由ZUN制作的)的支持,而thcrap_tasofro提供对黄昏边境游戏(官方的格斗游戏,刚欲异闻和一些黄昏同人游戏)的支持。在补丁游戏时,只加载相关的游戏支持插件。

thcrap_update

更新插件。它更新thcrap本身和所有补丁。

可以通过重命名或移除来禁用它。

为了让它易于移除,没有二进制文件直接链接到它。thcrap.dllthcrap_update.dll中的每个函数提供包装函数。这些包装函数将尝试加载thcrap_update.dll,并在该DLL不存在时提供一个备用实现。当然,这些备用实现永远不会访问互联网,并将尝试在无网场景中做合理的事情。RepoDiscover_wrapper不会尝试下载新的仓库,只会加载本地的仓库。stack_update_wrapper什么都不做。

win32_utf8

一个UTF-8库。对于许多具有A/W变体的Windows函数(例如,CreateFileACreateFileW),它添加了一个新的U变体(例如CreateFileU)。这个变体可以接受UTF-8字符串,并且还支持用备用编码的字符串。

在thcrap中,我们需要支持多种语言,所以需要支持某种Unicode变体。由于原始游戏不支持任何形式的Unicode,我们需要以某种方式将Unicode注入。Windows上的本机Unicode格式是UTF-16,但UTF-16每2个字节往往有一个NUL字节(至少在英文中),而游戏使用NUL字节作为字符串分隔符(因为它使用的是SHIFT-JIS编码的普通C字符串)。UTF-8设计为在这些场景中更易于使用。它也使用NUL字节作为其字符串分隔符,并且不会允许在字符串中的其他位置出现任何NUL字节。因此,当我们用UTF-8字符串替换游戏的SHIFT-JIS字符串时,字符串结束标记不会改变,基本的字符串操作函数如strlenstrcpy继续工作,查找ASCII子串也能正常工作。有一些边缘情况可能无法工作,但总体来说,它工作得相当好。这就是为什么 thcrap在内部到处使用UTF-8,强制游戏使用UTF-8,并在Windows API边界转换为UTF-16。

thcrap还将游戏中的每个A函数调用替换为对U函数的调用。例如,如果游戏尝试调用TextOutExA,thcrap会将其替换为对 TextOutExU的调用。这是必要的,因为在我们将UTF-8字符串传递给游戏后,需要能够使用它。如果它使用我们的UTF-8字符串调用TextOutExA,Windows会尝试将其作为SHIFT-JIS字符串使用,这将不起作用。因此我们将其替换为TextOutExU,它将尝试首先将字符串参数作为UTF-8字符串使用,然后如果不起作用,再作为SHIFT-JIS字符串使用。这样,它可以与已修补和未修补的字符串一起工作。

依赖库

jansson:一个JSON库,广泛使用。我们将其用于所有的JSON输入/输出(我们有很多这样的操作)。我们在模块之间传递对象时也大量地使用了它,并计划对此进行更改。
libpng:一个PNG库,主要用于图像修补。
zlib:一个压缩库。是libpng的依赖项,并在更新时用于解压缩zip文件。

更新文件

update.json是更新描述文件。它提供了非简单更新的指令,这些更新在下载和解压更新后执行。到目前为止,我们只用过它一次,当时我们将所有内容从根目录移动到子目录。我们希望在运行更新时自动更新所有内容。

该文件是一个对象数组。每个对象描述一个步骤,并具有一个“detect”键,用于确定是否需要执行此步骤。其他键描述更新步骤将执行的操作。

示例:

[
    {
        detect: {
            exist: "a.txt",
            dont_exist: "b.txt"
        },
        delete: [
            "c.txt"
        ],
        move: {
            "*.js": "config/"
        },
        update_repo_paths: {
            cfg_files: "config/*.js",
            old_path: "/",
            new_path: "repos/"
        },
        fix_repo_paths_post_restructuring: {
            cfg_files: "config/*.js",
            broken_prefix: "repos/"
        }
    }
]

该示例描述了一个更新步骤,其中包含所有支持的字段。仅当a.txt存在且b.txt不存在时,该步骤才会被执行。两个字段也可以是数组。当此步骤执行时,c.txt将被移除,根目录中的所有.js文件将被移动到config/目录(如果该目录不存在,将创建它)。请注意,移动操作是唯一支持通配符的操作。在config/.js中,所有运行配置将被编辑,以将repo_paths/替换为repos/。对于config/.js中的所有运行配置,我们将查找repo路径中重复的repos/前缀,并仅保留其中一个,以修复在失败的更新期间多次执行update_repo_paths后出现的错误(这种情况曾经发生过)。

快捷方式

它们由thcrap_configure生成,并提供了一种便捷的方式来运行补丁。它们不指向游戏,而是通过几个参数运行 thcrap_loader

例如,运行地灵殿英文补丁的快捷方式如下:C:\Path\To\thcrap\thcrap_loader.exe en.js th11

日志

这些是thcrap的日志。我们会轮换它们,这意味着thcrap_log.txt始终是最新的日志文件,接下来是 thcrap_log.1.txtthcrap_log.2.txt等。我们会清理较旧的日志文件。它们可以是thcrap_configurethcrap_loader或正在处理中的thcrap的日志。没有什么可探讨的。

配置

此文件夹中有3种配置文件。config.jsgames.js和运行配置。

config.js

此文件仅包含一些配置设置,主要由thcrap_loader UI使用。我们可能会最后记录它们,但它们并不是那么重要。

games.js

games.js是一个可选的游戏列表。它仅包含一些键值对,形如:th10: path/to/th10.exe该文件用于thcrap_loader的第一个参数。第一个参数可以是exe文件的路径,也可以是游戏名称。如果是游戏名称,games.js会提供exe文件的路径。

运行配置

config目录中的其他所有文件都是运行配置。运行配置是thcrap_loader的第二个参数。游戏路径描述了您想运行哪个游戏,而运行配置描述了您想如何运行它。最重要的部分是补丁叠放,它描述了要应用哪些补丁。在介绍中已经描述了补丁叠放的概念。运行配置中的每个补丁都有一个"archive"字段,即补丁根目录的路径。它可以相对于thcrap根目录(最常见)或是绝对路径。

仓库(repos)

仓库包含了许多需要记录的内容。要了解更多信息,请[继续阅读下一部分]。

仓库

在介绍中已经描述过仓库。

repos目录包含每个仓库的一个子目录。旧版本的thcrap_configure会在搜索过程中为每个找到的仓库创建一个目录(稍后会详细说明),因为他们希望将repo.js存储在磁盘上,而新版本则会将repo.js保存在内存中,并仅为选定的补丁写入磁盘。

每个仓库都包含一个描述该仓库的repo.js文件,以及每个选定补丁的一个目录。例如,您可以在磁盘上拥有这些文件:

repos
\ nmlgc
  - repo.js
  \ base_tsa
    - patch.js
    - Other files...
  \ base_tasofro
    - patch.js
    - Other files...
\ thpatch
  - repo.js
  \ lang_en
    - patch.js
    - Other files...

repo.js

repo.js描述了一个仓库及其所有补丁。以下是repo.js文件示例:

{
    "id": "nmlgc",
    "title": "Nmlgc's patch repository",
    "contact": "[email protected]",
    "patches": {
        "aero": "Enable Aero compositing"
    },
    "patchdata": {
        "aero": {
            "flags": "gameplay",
            "games": [ "th125", "th128", "th13", "th14", "th143", "th15", "th16", "th165", "th17" ],
            "dependencies": [
                "base_tsa",
                "thpatch/lang_en"
            ]
        }
    },
    "servers": [
        "https://mirrors.thpatch.net/nmlgc/"
    ],
    "neighbors": [
        "https://srv.thpatch.net"
    ]
}

id

仓库的ID。它必须是唯一的。此外,thcrap的某些部分可能假定仓库目录的名称就是仓库ID。

title

仓库的简短描述,在thcrap_configure中与ID一起显示。

contact

联系仓库所有者的方式。可以是电子邮件地址、Discord、标签、URL等。它被视为纯文本。

patches

此仓库提供的补丁列表,并为每个补丁提供简短描述。此列表决定了哪些补丁会在thcrap_configure中显示。

patchdata(尚未实现,请参见此分支)

关于补丁的更多信息。为了向后兼容,这些信息不在patches部分中。每个条目都会为补丁添加更多信息。目前,其中一些信息在patch.js文件中指定,但在仓库级别提供这些信息将允许我们在不下载很多新文件的情况下,在thcrap_configure 中使用它们进行补丁选择。

flags

用于将补丁归类的标志或一组标志。通过这些标志,用户可以在thcrap_configure中选择只查看语言或游戏玩法的补丁,只有当补丁具有相关标志时,它才会被列出。支持的标志有:

  • core:添加其他补丁使用的核心功能。通常是隐藏的,因为用户不想选择它,他们希望选择一个有用的补丁并将核心补丁作为依赖项引入。
  • language:翻译补丁。
  • gameplay:改变游戏玩法的补丁,比如作弊、速度修改或新模式。
  • graphics:改变图像的补丁,例如精灵图。
  • fanfiction:改变对话的补丁,在游戏中创建一个新的故事。
  • hidden:通常不是唯一的标志。表示补丁应该从默认列表中隐藏。用于被其他补丁替代的补丁,或不活跃且大部分不完整的翻译。

games

补丁支持的游戏列表。

依赖(dependencies)

补丁的依赖列表。当你选择一个补丁时,它的所有依赖会自动被选择。例如,base_tsa是大多数针对正作的补丁所需的。这些补丁应该将nmlgc/base_tsa列为依赖项,以便在选择这些补丁时自动选择它。依赖是递归的。例如,lang_fr不需要依赖base_tsa,因为它已经依赖于lang_en,而lang_en本身依赖于base_tsa

依赖可以列为repo_name/patch_name或仅为patch_name。如果仅提供patch_name,则依赖从与补丁相同的仓库中提取。

服务器(servers)

用于下载仓库的服务器。如果有多个服务器,它们必须拥有完全相同的文件。thcrap_configure可能会尝试在不同的服务器上下载文件以优化带宽。服务器上的文件层次结构应与磁盘上的相同。

邻居(neighbors)

仓库列表并没有硬编码在thcrap中(每次添加新仓库时都更新会很麻烦),因此thcrap需要一种方式来查询它们。它从已知仓库列表开始,然后每个仓库可以推荐新的仓库以供查询。thcrap_configure随后会检查这些新仓库,而这些仓库也可以继续推荐新仓库。所有这些存储库构成了“thcrap网络”。

实际上,thpatch仓库是thcrap网络的核心。thcrap_configure仅知道thpatch存储库的URL,如果thpatch服务器不可用,thcrap会附带一个本地的thpatchrepo.js副本。而thpatchrepo.js在其neighbors字段中包含了thcrap网络中的每个仓库。其他仓库可以在其neighbors字段中添加新仓库,但我认为没有仓库这样做。

请注意,thcrap_configure将根URL作为命令行参数。如果你不想使用thcrap网络,可以这样做。移除thpatchrepo.js,并以你的仓库的URL作为参数运行thcrap_configure。Thcrap将使用你的存储库网络而不是thcrap网络(或者,如果你没有任何neighbors,则只使用你的仓库)。当然,如果你不想拉取整个thcrap网络,你不应该在你的仓中拉取thcrap网络的仓库。如果你需要base_tsa,你需要对其进行分叉并删除(或编辑)neighbors字段。