项目准则与政策
Phobos 维护团队结构
由于项目规则和代码库的复杂性不同,我们建立了下述维护团队结构以协助项目开发和维护:
T3维护者:负责人,主要决策者,决定项目方向,在争议中拥有最终决定权。他们负责项目的愿景和方向,是社区的主要连接点。T2维护者:负责处理较复杂的拉取请求,可以发布版本。T1维护者:负责处理较简单的拉取请求。- 分类员:协助分类/标记/分配拉取请求、问题和讨论,协助与社区沟通。
这些角色根据拉取请求的复杂性和贡献者的经验进行分配,并非一成不变。此外这里只列出了常用的角色,在必要情况下负责人可为贡献者单独分配权限。
关于审查工作量的建议
维护者审查的代码量应大致与其自身提交的 PR 代码量相当。然而,考虑到目前已有大量 PR 积压,我们建议维护者审查的代码量应超过其提交量(例如,达到其提交量的 125% 至 150%)。唯有如此,我们才能持续减少积压的 PR 数量。
目前这并非硬性规定,但我们希望每位维护者都能尽力遵循这一建议。
贡献类型
为分担工作量并使项目更易于管理,我们定义了以下常见的贡献类型:
- Phobos 修复,包括不同步(Reconnection Error)、崩溃(Fatal Error)和文档修复
- 默认复杂度:
T1
- 默认复杂度:
- 原版 Bug 修复
- 默认复杂度:
T1
- 默认复杂度:
- 去硬编码/自定义功能——仅通过 Modder 可用的 INI 或其他方式使得某些功能得以自定义而不增加过多代码。
- 默认复杂度:
T1
- 默认复杂度:
- 复原功能(来自 TS、RA2 等),要求 除 在 YR 中与扩展共同工作所 必须 的更改外 未 进行其他添改(需审查人员验证该情况)
- 默认复杂度:
T1
- 默认复杂度:
- 新功能
- 现有系统扩展——为现有系统添加逻辑,通常无需独立实体或类型类,但可能引入新钩子
- 示例:反馈武器、弹头发射超级武器、使用现有的自定义弹道框架的新弹道类型等
- 默认复杂度:
T1或T2(由拉取请求分配者判断)
- 新系统——通常拥有自己的类,这些类不扩展游戏类/逻辑(或者代码量过大,应分离到单独的类中)
- 示例:自定义弹道框架、抛射体拦截、护盾逻辑等
- 默认复杂度:
T2
- 现有系统扩展——为现有系统添加逻辑,通常无需独立实体或类型类,但可能引入新钩子
- 项目基础设施建设——对项目构建系统的更改、CI、文档更改等
- 默认复杂度:
T2
- 默认复杂度:
- 项目政策更改——对项目指南、贡献指南等的更改
- 默认复杂度:
T3(必须由负责人审查)
- 默认复杂度:
Hint
强烈建议 Modder 们提交新增功能可复用性的反馈(重要内容应通过仓库页面 Pull Requests、Discussions 或 Issue 来跟踪),以避免项目充斥不可复用的一次性功能。
列表并不详尽,欢迎提出修改(或调整任何改进维护方式的项目政策)建议。
如果没有对应的类别——它应当由负责人审查。
可能导致拉取请求更受争议并需要更高级别维护者处理的情况:
- 修改/破坏了原有(或原版)行为
- 需要迁移操作
- 混合的贡献类型
- 当前维护者不确定能否准确评估该拉取请求
贡献流程
为了确保您的贡献顺利加入,请在为项目做出贡献时遵循以下流程:
- 检查是否存在 与您打算贡献的内容相关的 已打开的拉取请求。如果存在——请对其发表评论,看看您是否能够提供帮助,而不是直接打开你自己的拉取请求。我们并不希望 您的有效工作 仅仅因为重复而 被放弃。
- 如果确认不会重复,您应当 与 和您 PR 复杂程度(见 贡献类型)相匹配的 维护者取得联系,以便他们审查/合并您即将提交的 PR 以及 在您提交贡献前进行关键设计方面的讨论。
- 对于较大、较根本性的更改,当您还在学习阶段以及当您正在探索“未知领域”时这 尤其 重要。与维护者保持沟通有助于您避免不必要的精力浪费和返工以及确保您的贡献符合我们的工作方式。不仅如此,这还可以使您在重要事项上尽早得到审查以及以更高的信心尽快得到合并(考虑到我们已有大量 PR,这点尤为重要)
- 目前 Discord 上的 Phobos 频道是进行此类讨论的最佳场所,因为这是联系维护者和讨论您想法最便捷的场所(如果无人回应,可以尝试私信联系经验丰富的维护者)。
- GitHub 的议题、讨论模块以及(尚未完成大量工作的)PR 草稿也同样可以用来讨论,但它们不如 Discord 那样迅捷并且更适合用于长期存储信息,而且通常在即时聊天中直接与某人沟通更容易引起他们的注意。
- 向多位维护者寻求意见也是一个很好的做法——不要总是咨询特定某一位或者某一部分维护者。即便起初由于语言或习惯有所隔阂,我们也应当努力保持彼此之间的相互联系。
- 当我们都对您所想的计划有了清晰的想法后,剩下的就是完成拉取请求中的设计和实现,我们将审查剩余的小问题,然后合并它。
项目结构
假设你已经成功克隆并构建了项目,你应当看到以下结构:
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函数替代就不要引入不必要的宏。
Note
样式指南并不详尽,未来可能会进行调整。
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通过子模块使用 YRpp
当你开发 Phobos 和研究 YR 引擎时常常需要对 YRpp 进行修正。这些修正通常需要提交到 YRpp 仓库,可与 Phobos 中的实际功能分开处理。但许多情况下这些改动需要作为 Phobos 贡献流程的一部分进行提交。要向 YRpp 提交改动你需要在 YRpp 中创建分支,推送后向 YRpp 仓库提交拉取请求。
当你递归克隆 Phobos 时,YRpp 会作为子模块一同克隆。子模块本质上就是嵌套的仓库。你可以像操作普通仓库一样打开它,因此变更可以同步到 Phobos,无需手动重命名文件。
推荐的工作流程如下:
- 在你选择的 IDE 中使用符号重命名功能修改字段和函数名称(Visual Studio 常规版本或 Code 中默认快捷键为
[F2]),此时你将在 Git 客户端中看到以下两个「层级」的变更:- Phobos 仓库:Phobos 代码的常规变更 + YRpp 子模块的变更(显示为一个子模块变更)。
- YRpp 仓库:YRpp 中字段和函数名称的常规变更。
- 在 YRpp 仓库中创建分支(若未创建则先 fork),提交并推送更改,然后发起拉取请求。推送后你在 Phobos 仓库有两个选择:
- 等待合并通过后,将 YRpp 子模块切换至最新提交,然后提交推送——这可以减少提交次数,但无法获取供测试的自动构建;
- 不等待 YRpp 合并,在推送 YRpp 分支变更后立即推送 Phobos——这种方式可以确保 Phobos 拉取请求使用最新构建。注意你必须在 YRpp 分支完成提交推送,否则构建系统无法识别仅在本地存在的变更
- 当 YRpp 拉取请求接受后,你需要再子模块中切换到已合并的最新提交,验证正常编译后,将其提交并推送到 Phobos 拉取请求创建的分支。