贡献

本页面介绍了如何帮助或为火卫一做出贡献,以及所有的实现方法。

对贡献者的指导

项目结构

假设你已经成功克隆并构建了项目,理应拥有下列项目结构:

  • src/ - 所有项目源文件均位于此。`

    • Commands/ - 快捷键源文件。每个新的快捷键类均继承自PhobosCommandClass(定义在Commands.h中)并且定义在一个分离的文件中,其中有一些方法,然后在Commands.cpp中注册。

    • New/ - 新的游戏内类源文件

      • Type/ - 本项目中实装的新的枚举类型类(即需要注册在某个INI文件中的新的表的类型,例如辐射类)每个枚举类型类均继承自Enumerable.h中的Enumerable<T>(其中T为枚举类型名)。

      • Entity/ - 游戏内实体类在此。

    • Ext/ - 原版引擎类的扩展类源码。每个扩展类均以原版类命名,保存在各自的文件夹下,并包含以下文件:

      • Body.hBody.cpp包含了类和方法的声明和定义以及标准扩展钩子(Hook)。每个扩展类必须包含下列东西以正常运作:

        • ExtData - 继承自Container.hExtension<T>的扩展数据类(其中T是被扩展的类),这也是包含了原版类中新数据的实际类;

        • ExtContainer - 用以存储和查找ExtData实例的映射类;

        • ExtMap - ExtContainer的静态实例;

        • 构造函数,析构函数,序列化,反序列化以及(某些类的)读取INI的钩子。

      • Hooks.cppHooks.*.cpp存储用以新逻辑的非标准钩子。

    • ExtraHeaders/ - 额外的头文件,用以交互或描述游戏中的未包含在YRpp中的类。

    • Misc/ - 未分类的源码,包含不属于任何扩展类的钩子。

    • Utilities/ - 项目中常用的辅助代码。

    • Phobos.cpp/Phobos.h - 扩展引导代码。

    • Phobos.Ext.cpp - 包含为新类或扩展类的一般加工代码。如果你定义了一个新的类,需要将此类加入全局变量MassActions中以进行声明。

  • YRpp/ - 包含了用来交互和描述游戏类的头文件,以及一些用于Syringe的宏。以子模块形式包含。

代码样式指导

我们建立了一系列代码样式规则保证代码的连续性。一些规则已经写入.editorconfig,你可以通过按下Ctrl + K + D来进行自动代码格式调整。此外还有一些需要手工检查的代码规范。

  • 我们使用制表符而非空格作为缩进符号。

  • 大括号永远保持在新行的行首。其中之一的原因是保证多行代码块的收尾分割足够清晰:

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语句,应该保持要么都有大括号要么都没有。

  • 代码中应该有一些空行来分割代码,使得整体更加便于阅读。插入空行可以遵守以下规则:

    • 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)和描述性名称命名,例如pTechnoType用于局部TechnoTypeClass*变量。

  • 类,名空间,类字段和成员以帕斯卡命名法命名。

  • 可以通过INI标签设置的类字段应该和INI标签同名,但用下划线代替点。

  • 指针类型声明时,*挨着类型名。

  • 通过声明一个以pThis作为第一个参数的静态方法来伪造的非静态类扩展方法只能放置在pThis所在的类实例的扩展类中。

    • If it’s crucial to fake __thiscall you may use __fastcall and use void* or void* _ as a second argument to discard value passed through EDX register. Such methods are to be used for call replacement.

  • 钩子以以下方案命名:钩上的函数_钩子的功能类名_钩上的方法_钩子的功能。由于无法为同一钩子定义不同的名称,再次定义(DEFINE_HOOK_AGAIN)的钩子不受此方案的约束。

  • Return addresses should use anonymous enums to make it clear what address means what, if applicable. The enum has to be placed right at the function start and include all addresses that are used in this hook:

DEFINE_HOOK(0x48381D, CellClass_SpreadTiberium_CellSpread, 0x6)
{
    enum { SpreadReturn = 0x4838CA, NoSpreadReturn = 0x4838B0 };

    ...
}
  • Even if the hook doesn’t use return 0x0 to execute the overriden instructions, you still have to write correct hook size (last parameter of DEFINE_HOOK macro) to reduce potential issues if the person editing this hook decides to use return 0x0.

  • 新的游戏“实体”类将使用Class后缀命名(如RadTypeClass)。扩展类将使用Ext后缀命名(如RadTypeExt)。

注解

样式指南并不详尽,将来可能会进行调整。

Git分支模型

Couple of notes regarding the Git practices. We use git-flow-like workflow:

  • master is for stable releases, can have hotfixes pushed to it or branched off like a feature branch with the requirement of version increment and master being merged into develop after that;

  • develop使主要开发分支;

  • feature/-prefixed branches (sometimes the prefix may be different if appropriate, like for big fixes or changes) are so called “feature branches” - those are branched off develop for every new feature to be introduced into it and then merged back. We use squash merge to merge them back in case of smaller branches and sometimes merge commit in case the branch is so big it would be viable to keep it as is.

  • hotfix/-prefixed branches may be used in a same manner as feature/, but with master branch, with a requirement of master being merged into develop after hotfix/ branch was squash merged into master.

  • release/-prefixed branches are branched off develop when a new stable release is slated to allow working on features for a next release and stability improvements for this release. Those are merged with a merge commit into master and develop with a stable version number increase, after which the stable version is released.

  • 当你使用本地和远程分支时,使用快进拉取从远程分支到本地的更改,不要将远程分支合并到本地,反之亦然,这会产生垃圾提交并使分支不可压缩。

These commands will do the following for all repositories on your PC:

  1. remove the automatic merge upon pull and replace it with a rebase;

  2. highlight changes consisting of moving existing lines to another location with a different color.

git config --global pull.rebase true
git config --global branch.autoSetupRebase always
git config --global diff.colorMoved zebra

帮助的方法

引擎改造是一个复杂的过程,很难实现,但也有一些简单的部分并不需要掌握逆向工程的技巧或成为C++魔法师。

研究与逆向工程

你可以通过引擎观察这些逻辑的工作方式,并注意哪些其他东西会影响工作方式,但是你迟早会希望看到它的内部原理。通常使用反汇编器/反编译器(IDAGhidra)之类的工具来解密二进制文件(gamemd.exe)和调试器(Cheat Engine的调试器非常适合)来调试二进制工程。

提示

逆向工程是个很复杂的任务,但是不要灰心,如果你想试试帮忙,可以在我们的Discord频道询问我们,我们乐意帮助😄

注解

汇编语言和C++知识,对计算机体系结构,内存结构,OOP和编译器理论的理解会有所帮助。

开发

当你发现引擎如何工作以及在哪里需要扩展逻辑后,就需要开发代码来实现所需的功能。这是通过声明一个**钩子(hook)**来完成的——在程序执行到达二进制中的特定地址之后将执行的一些代码。 所有开发都是通过C++使用YRpp(它提供了一种与YR代码进行交互并使用Syringe注入代码的方法)完整的,以及通常使用Visual Studio 2017/2019或更高版本。

为项目做贡献

要贡献功能或进行某种更改,你会需要一个Git客户端(我个人建议GitKraken)。Fork,克隆存储库,然后最好创建一个新分支,随后编辑/添加代码或任何你想贡献的内容。提交,推送,启动拉取请求,等待其被审核或合并。

If you contribute something, please make sure:

  • you write documentation for the change;

  • you mention the change in the changelog and migration sections in the what’s new page;

  • you mention your contribution in the credits page.

If your change does not fit in standard criteria or too small that it doesn’t need the above - add [Minor] to your pull request’s title, so the CI won’t yell at you for no reason.

提示

每个拉取请求的推送都会触发一个每日更新,以进行最新的推送提交,因此你可以在PR页面的底部检查生成状态,按Show all checks,转到生成运行的详细信息,并获取包含生成的DLL和PDB的zip(给你的测试人员)。不过请注意GitHub不允许游客下载生成文件。

注解

你会从C++的经验,编程模式的知识,常用技术等中受益。基本的汇编知识将有助于你正确地编写与依赖内存的交互。 此外,还需要对Git和GitHub有基本的了解。

测试

这是任何modder(甚至玩家)都可以完成的工作。查看一个新功能或一项更改,尝试思考所有可能的不同方式的情况,尝试思考任何可能的逻辑缺陷,边缘情况,无法预料的交互或条件等,然后根据你的想法进行测试。如有可能,应将任何错误报告给此库的问题(Issues)部分。

警告

只有通过大量离线和在线的测试,才能实现总体稳定性。大多数modder都有Beta测试团队,所以,如果你希望引擎稳定,请通过让测试人员使用新功能来为引擎做出贡献!另外,下面的清单也可以帮助你更快地确定问题。

测试清单

  • 涵盖了所有可能的有效用例。尝试检查你可以想到的所有有效功能用法,并验证它们是否可以与功能一起正常使用。

  • 正确存档和读档。大多数附加功能(比如新的功能标签)都需要将其存储在已保存的对象信息中。有时这不能正确完成,尤其是在复杂的物体上(比如辐射类型)。请确保所有改进在存档和读档之前和之后均能正常工作(当然,是指在相同版本的火卫一上)。

  • 与其他功能的交互。尝试使用功能与原版或其他库中的其他功能进行链接或交互(例如,一开始尝试从永久心控单位移除心灵控制时,心灵控制移除弹头崩溃了)。

  • 重叠功能无法正常工作(包括来自第三方库(如Ares,HAres,CnCNet生成器DLL)的功能)。考虑一下哪些功能的代码可能与你当前正在测试的代码重叠(从技术上讲,意味着它们修改了相同的代码)。由于该项目的性质,如果其他库的某些功能重叠,则可能碰巧无法正常工作(例如,在实现单位选择权重时,一开始Ares的GroupAs被破坏,使用它的单位不能被正确地同类选择)。

  • 边缘情况。这些是某些特定情况下的情况,通常是由一些极端参数值引起的(例如,原版游戏在Size=0,0,0,0的 PreviewPack 上崩溃而不是不画)。

  • 角落案例。那些类似于边缘情况,但是很难重现,并且通常是由极端参数值的组合引起的。

注解

拥有制作YRmod的知识,好奇心与对细节的敏感性将会有所帮助。

书写说明书

无需任何解释。 如果你完全了解了火卫一中的某些东西是如何工作的,则可以通过在这些文档中写一个详细的描述来提供帮助,或者你可以改进你认为不够详细的文档。

这些文档是用Markdown编写的(非常简单,只需60秒即可学会MD;如果你需要扩展语法方面的帮助,请参阅MyST解析器参考)。火卫一官方说明书使用Sphinx构建文档,Read the Docs进行托管。

提示

你无需安装Python,Sphinx和模块即可查看更改——你创建的每个拉取请求均由Read the Docs自动生成并提供。 就像每日生成一样,在页面底部,按Show all checks,然后在生成运行的详细信息中查看生成的文档。

有两种方式可以编辑文档

  • 通过你的电脑编辑。与贡献更改中说的差不多;文档位于docs文件夹中。

  • 通过在线编辑器编辑。到要编辑的文档中,按右上角的按钮——它将带你到需要编辑的GitHub文件(在右上角寻找铅笔图标)。按下它,便会创建fork,然后你将在fork库中编辑你的文档。你可以提交这些更改(最好是到新分支),并将其变成对主存储库的拉取请求。

注解

好的英语语法和对文档结构的理解就足够了。此外,你还需要一个GitHub帐户。

提供展示功能用的媒体文件

这些将在文档中被使用,并可额外为mod作者带有一个可以指向相应mod的链接。要录制gif,你可以使用GifCam等应用程序。

注解

请提供适当尺寸的屏幕截图,GIF和视频,并且不应有多余或过长的内容。

促进工作

无论您是不是有影响力的Youtuber、CNC相关社区领头或正常玩家,您都可以通过扩散有关该项目的信息来帮助我们。