贡献
此页面介绍了协助或为 Phobos 项目做出贡献的方法,并列出了项目中所用的贡献指南。
对贡献者的指导
项目结构
假设你已经成功克隆并构建了项目,你应当看到以下结构:
src/- 项目所有的源代码。Commands/- 新快捷键命令源码。每个新的快捷键类均继承自PhobosCommandClass(定义在Commands.h中)并在一个单独的文件中用几种方法定义然后注册到Commands.cpp中。New/- 新增游戏内类的源码。Type/- 新增枚举类型(即需要在 INI 文件中通过注册表部分声明的类型,例如辐射类型)。每个枚举类型类都继承自Enumerable.h中定义的Enumerable<T>类(这里的T是枚举类型名)。Entity/- 表示游戏内实体的类均在此。
Ext/- 原版引擎类的扩展类源码。每个扩展类都保存在一个以原版类命名的单独文件夹中,并包含以下内容:Body.h与Body.cpp包含了类和方法的定义/声明以及标准扩展钩子。每个扩展类必须包含下列内容才能正确工作:ExtData- 继承自Container.h中Extension<T>的扩展数据类(其中T是被扩展的类),这也是包含了原版类中新数据的实际类型;ExtContainer- 一个特殊的映射类的定义,用于储存和查找继承自Container.h的Container<T>的基类实例的ExtData类;ExtMap-ExtContainer的静态实例;构造函数,析构函数,序列化,反序列化以及(对于适当的类)读取 INI 的钩子。
Hooks.cpp和Hooks.*.cpp包含非标准的钩子,用于正确修补新定义逻辑。
ExtraHeaders/- 额外的头文件,用于交互/描述未包含在 YRpp 中的游戏内的类。Misc/- 未分类的源码,包括不属于任意扩展类的钩子。Utilities/- 整个项目通用的辅助代码。Phobos.cpp/Phobos.h- 扩展引导代码。Phobos.Ext.cpp- 包含通用处理代码、新的或扩展的类。如果你定义了一个新的或扩展的类那么你必须将你的新类添加进全局变量MassActions中进行声明。
YRpp/- 包含与游戏二进制文件中所包含的类型进行交互/描述的头文件,以及用于 Syringe 编写钩子的宏。作为子模块包含在内。
代码样式指导
我们制定了代码样式规范以保持一致性。部分规则已在可用情况下通过 .editorconfig 文件强制执行,因此你可以在 Visual Studio 中通过按下 Ctrl + K + D 来自动格式化代码。尽管如此,我们仍建议在提交代码前手动检查代码样式。
我们使用制表符而非空格来缩进代码。
大括号始终应当放在新的一行上(Allman 缩进 风格)。这样做的一个原因是需要对于多行代码的情况清楚地区分代码块的头部和尾部:
if (SomeReallyLongCondition()
|| ThatSplitsIntoMultipleLines())
{
DoSomethingHere();
DoSomethingMore();
}
只有代码块头部和代码块主体均为单行时才应当使用无大括号的代码块,且无大括号的块中不允许存在跨多行的语句拆分以及嵌套的无大括号块:
// OK
if (Something())
DoSomething();
// OK
if (SomeReallyLongCondition()
|| ThatSplitsIntoMultipleLines())
{
DoSomething();
}
// OK
if (SomeCondition())
{
if (SomeOtherCondition())
DoSomething();
}
// OK
if (SomeCondition())
{
return VeryLongExpression()
|| ThatSplitsIntoMultipleLines();
}
只有当大括号块为空时才允许左右括号放置在同一行。
如果使用了 if-else 语句,那么所有代码块应当统一使用大括号或统一不使用大括号以保持一致性。
难以阅读的跨多行的复杂条件语句应被拆分为更小的逻辑单元以提高可读性:
// Not OK
if (This() && That() && AlsoThat()
|| (OrOtherwiseThis && OtherwiseThat && WhateverElse))
{
DoSomething();
}
// OK
bool firstCondition = This() && That() && AlsoThat();
bool secondCondition = OrOtherwiseThis && OtherwiseThat && WhateverElse;
if (firstCondition || secondCondition)
DoSomething();
代码应使用一些空行分隔逻辑部分来提升可读性。以下情况必须使用空行分隔:
return语句(除非该句外仅有一行代码);后续代码中使用的局部变量声明(但若仅用于后续代码块的单行局部变量声明则其后无需空行);
代码块(无论是否有花括号)以及用代码块规则的其他东西(函数,钩子定义,类,名空间等);代码块(无论是否有大括号)或任何使用代码块的结构(函数/钩子定义、类、命名空间等);
钩子注册的输入/输出。
// OK
auto localVar = Something();
if (SomeConditionUsing(localVar))
...
// OK
auto localVar = Something();
auto anotherLocalVar = OtherSomething();
if (SomeConditionUsing(localVar, anotherLocalVar))
...
// OK
auto localVar = Something();
if (SomeConditionUsing(localVar))
...
if (SomeOtherConditionUsing(localVar))
...
localVar = OtherSomething();
// OK
if (SomeCondition())
{
Code();
OtherCode();
return;
}
// OK
if (SomeCondition())
{
SmallCode();
return;
}
在不影响代码可读性的前提下可以使用
auto来隐式声明不必要的显式类型。基础类型禁止使用auto。空的大括号块的括号之间必须保留空格。
为了减少 Git 合并冲突,在频繁修改区域的成员初始化列表及其他类似列表的语法结构中应当采用按项分行且项目分隔符(例如逗号)置于 换行符 之后的书写方式:
ExtData(TerrainTypeClass* OwnerObject) : Extension<TerrainTypeClass>(OwnerObject)
, SpawnsTiberium_Type(0)
, SpawnsTiberium_Range(1)
, SpawnsTiberium_GrowthStage({ 3, 0 })
, SpawnsTiberium_CellsPerAnim({ 1, 0 })
{ }
局部变量和函数/方法参数采用驼峰命名法(使用前缀
p表示指针类型,每个指针嵌套级别都需添加前缀),并配以描述性名称,例如局部变量TechnoTypeClass*应命名为pTechnoType。类、命名空间、类字段和成员始终采用帕斯卡命名法。
可以通过 INI 标签设置的类字段必须与 INI 标签保持完全一致的命名,仅将点号替换为下划线。
指针类型声明时,
*必须紧贴类型声明。通过声明一个以
pThis作为首个参数的静态方法来伪造的非静态类扩展方法只能放置在pThis所在的类实例的扩展类中。如果需要伪造
__thiscall,可以使用__fastcall并将void*或void* _作为第二个参数来丢弃通过EDX寄存器传递的值。此方法仅用于调用替换。
工作函数必须按以下规则命名:
钩上的函数_钩子的功能或类名_钩上的方法_钩子的功能。由于无法为同一钩子定义不同的名称,再次定义(DEFINE_HOOK_AGAIN)的钩子不受此方案的约束。返回地址应在可行的情况下使用匿名枚举进行语义化标注。该枚举必须置于函数起始位置,并包含本钩子中使用的所有地址:
DEFINE_HOOK(0x48381D, CellClass_SpreadTiberium_CellSpread, 0x6)
{
enum { SpreadReturn = 0x4838CA, NoSpreadReturn = 0x4838B0 };
...
}
即使钩子不使用
return 0x0来执行被覆盖的指令,你仍必须编写正确的钩子大小(DEFINE_HOOK宏的最后一个参数),以减少当其他人编辑此钩子时若决定使用return 0x0可能引发的潜在问题。新的游戏内「实体」类应当以
Class后缀命名(例如RadTypeClass)。扩展类应当改用Ext后缀命名(例如RadTypeExt)。不要污染命名空间。
若能使用等效的
constexpr或__forceinline函数替代就不要引入不必要的宏。
备注
样式指南并不详尽,未来可能会进行调整。
Git 分支模型
我们使用 git-flow 之类的工作流:
master用于稳定版本发布,允许直接推送热修复补丁或像功能分支一样分离分支,但需遵循版本号递增要求,并在之后将master分支合并回develop分支;develop是主要开发分支;带有
feature/前缀的分支(有时根据实际情况可能使用不同的前缀,例如大的修复或改动)即所谓的「功能分支」——这些分支从develop分离出来用于引入新的功能,完成后合并回develop。对于小型分支,我们使用压缩合并;若分支规模较大,则可能使用合并提交来维持提交记录的完整性;带有
hotfix/前缀的分支使用方式类似于feature/,但基于master分支创建,要求将hotfix/分支压缩合并到master后必须将master合并回develop;带有
release/前缀的分支在计划发布新的稳定版本时从develop分离,允许同时开发下个版本的功能和改进当前版本的稳定性。这些分支通过合并提交合并到master和develop,并递增稳定版本的版本号,随后发布稳定版本。当你在处理本地与远程分支时应当使用 拉取(快进) 将远程分支的变更同步到本地,不要将远程分支合并到本地分支,反之亦然,否则会产生垃圾提交并使代码无法压缩整理。
这些命令对你电脑上的所有代码仓库执行以下操作:
移除拉取时的自动合并行为,改用变基操作;
用特定颜色高亮显示「移动现有代码行到新位置」这类变更。
git config --global pull.rebase true
git config --global branch.autoSetupRebase always
git config --global diff.colorMoved zebra
协助方式
引擎修改是一个复杂的过程,但其中也包括无需精通逆向工程或成为 C++ 大魔导师即可完成的部分。
研究与逆向工程
你可以通过使用引擎观察其运作机制,并记录哪些因素会影响其行为。但你可能终究还是需要深入其内部原理。通常需要借助反汇编器/反编译器(例如 IDA 和 Ghidra)来解析二进制文件(例如 gamemd.exe)中的代码,以及调试器(Cheat Engine 的调试器对此非常适用)来追踪二进制文件的执行流程。
提示
逆向工程虽复杂但无需畏惧。如果你对此跃跃欲试,那么欢迎你在 Discord 频道向我们提问,我们将乐于为你提供帮助😄
备注
汇编语言 和 C++ 知识、对计算机体系结构、内存结构、OOP 及编译器原理的理解对此大有裨益。
开发
当你理解引擎运作逻辑并确定需要扩展的部分后需要编写代码来实现目标。具体方式是通过声明一个 钩子——即在程序执行到二进制文件特定地址时触发的代码。所有开发工作均使用 C++ 结合YRpp(提供与 YR 代码交互及通过 Syringe 注入代码的功能),通常需要使用 Visual Studio 2017/2019 或更新的版本。
向项目提交更改
贡献功能或进行更改需要使用 Git 客户端(我推荐 GitKraken)Fork 并克隆仓库,建议创建一个新分支,随后编辑/添加代码或进行其他修改。然后提交 -> 推送 -> 发起拉取请求 -> 等待审核或合并。
如果你贡献了什么,请确保:
若变更不符合标准流程或规模过小无需上述步骤,请在拉取请求标题添加 [Minor],以避免 CI 无故报错。
提示
每次推送至拉取请求会触发自动构建,你可以在拉取请求页面底部查看构建状态。点击 Show all checks 进入构建详情页,可下载包含 DLL 和 PDB 的压缩包(供你的测试人员使用),或从自动发布的评论中获取构建文件(需要拥有 GitHub 账号)。
备注
举杯 C++ 经验、编程模式及常用技术知识将有助于提升效率。基础汇编知识 有助于正确编写内存交互钩子。同时需掌握 Git 与 GitHub 的基本使用。
测试
这是任何 modder(甚至纯玩家)均可参与的工作。查看新的功能或改动,尽可能设想所有可能的应用场景、逻辑漏洞、边界情况及意外交互等,并加以验证。发现任何 Bug 请提交至本仓库的 Issue 版块。
警告
总体稳定性仅可通过大量离线/在线游戏测试进行验证。大多 modder 没有测试团队,若期望功能稳定,请组织测试人员参与新功能测试!下方检查清单有助于问题定位。
测试检查清单
涵盖所有有效用例:尽可能验证你所能想到的正常使用场景是否按预期工作。
正确的存档与读档:多数新增内容例如 INI 标签需存入存档中的对象信息。有时这会出现问题,尤其在复杂一些的东西上(例如辐射类型)。请确保所有改进在保存和加载前后(当然是在同一版本的 Phobos 环境下)都能正常工作。
与其他功能的交互:测试该功能与原版或其他库功能的联动(例如:最初的解除心控弹头作用于永久心控目标时会导致崩溃)。
重叠功能的兼容性:(包括来自 Ares、HAres、CNCNet Spanwer DLL 等第三方库的功能)考虑哪些功能的代码可能与你当前所测试功能的代码重叠(从技术上讲;这意味着它们修改了相同代码)。由于项目的性质部分功能可能会出现冲突(例如:最初实现的低优先级框选曾导致 Ares 的
GroupAs被破坏,导致使用它的单位无法被正确按类群选)。边界情况:某些极端情况下的工作状况,通常是一些极端参数值引发的(例如:原版游戏在
[PreviewPack]尺寸为 0 的情况不会跳过渲染而是导致崩溃)。角落案例:类似边界情况,并且难以复现,通常需要多重极端参数加以组合才能引发。
备注
熟悉 YR Mod 制作、具备探索精神以及对细节的敏感将有助于测试效率。
编写文档
无需多言。如果你对 Phobos 中某部分机制理解的相当透彻,那么你可以通过撰写详细说明来做出贡献,或者你也可以改进现有文档中不够详尽的段落。
这些文档使用 Markdown 编写(语法非常简单,60秒即可掌握 MD;如果你扩展语法相关的帮助请参考 MyST 解析器)。我们使用 Sphinx 构建文档,Read the Docs 进行托管。
提示
你无需安装 Python 和 Sphinx 模块即可查看更改——你创建的每个拉取请求均会触发 Read The Docs 的自动生成,类似 Phobos 自动构建版本,滚动至页面底部点击 Show all checks 即可在构建详情中查看渲染后的文档。
有两种方式可以编辑文档:
本地编辑:如 向项目提交更改 中所述,文档位于
docs文件夹。在线编辑:找到要编辑的文档,点击右上角按钮——跳转至 GitHub 文件编辑页(在右上角寻找铅笔图标)。编辑后将自动创建 fork 仓库,你可以提交更改(建议新建分支)并向主仓库发起拉取请求。
备注
拥有足够好的英语语法与对文档结构的理解即可参与。当然你还需要拥有一个 GitHub 账号。
提供展示功能用的素材
这些将在文档中被使用,并附有对应 mod 的链接作为对 mod 作者的鼓励。录制 GIF 推荐使用 GifCam 等工具
备注
请提供适当尺寸的截图/GIF/视频,避免冗余内容与过长的时长。
促进工作
无论你是知名 Youtuber、C&C 社区领袖还是普通玩家都可以通过向其他人宣传本项目来帮助我们。