# 准备工作
通过Harmony,可以将我们编写的代码挂载到游戏本体的函数上运行,为了实现目的,在开始写代码之前,需要找到合适的函数,以此来挂载我们的代码。
# 使用dnSpy寻找目标函数
寻找目标函数,其实就是猜,通过类名和函数名,大致判断该类及该函数的作用。
先打开游戏根目录,如图找到Assembly-CSharp.dll文件,并将这个文件拖到dnSpy的界面中加载。



将资源展开,可以得到很多类名,每个类名展开,又有很多函数名和变量名。

我们要修改游戏原版的卡牌数据,自然是要在加载游戏的时候进行修改,点击上方的放大镜图标(搜索),尝试搜索LoadGame,发现了三个比较像的函数名,都属于GameLoad这个类,因此可以大致推测GameLoad这个类负责加载游戏。

观察GameLoad类,可以发现四个加载函数:
- LoadGame
- LoadGameFile
- LoadGameFilesData
- LoadMainGameData
直接读代码也好,直接patch测试看log也罢,最终选择了Modloader同款LoadMainGameData作为挂载函数(LoadGame在通过新建角色进入游戏的情况下不执行,而LoadMainGameData无论如何都会执行)。
# 编写代码
打开Rider,通常来说,Plugins.cs文件除了mod信息以外不用修改其他代码。
# 模板代码分析
模板中自带了一个GameLoadPatch.cs,即GameLoad这个类,如果后期需要对其他类(GameManager)进行patch,在Patcher文件夹下,模仿GameLoadPatch.cs新建一个GameManagerPatch.cs即可。

以下是模板中自带的代码,我们主要来分析需要修改的地方都起到什么作用。
using HarmonyLib;
namespace CSTI_Example1.Patcher;
[HarmonyPatch(typeof(GameLoad))]
//想要patch的类,这里为GameLoad,如果需要patch其他类,将GameLoad改为其他类名即可,例如[HarmonyPatch(typeof(GameManager))]。
public static class GameLoadPatch
//同上,如果需要patch其他类,将GameLoadPatch改为其他类名即可,例如GameManagerPatch
{
[HarmonyPostfix, HarmonyPatch("LoadMainGameData")]
//HarmonyPostfix:patch分为postfix和prefix,执行顺序为:prefix->游戏原代码->postfix,也就是先执行prefix的代码,再执行LoadMainGameData的代码,最后执行postfix的代码。一般使用postfix。
//HarmonyPatch("LoadMainGameData"):挂载的目标函数名,一定是和类名匹配的。
public static void LoadMainGameData_Postfix()
//函数命名规则:一般为目标函数+postfix/prefix
{
//想添加的代码就写在这里。
}
}
# 例子
用ME做过mod的都知道,如果要修改原版卡牌数据,需要在GameSourceModify中新建目标卡牌文件。
但是ME为了兼容性,很多唯一的词条是不允许修改的,如果多个mod同时对该词条进行修改就会产生冲突。
以芒果永鲜为例,想要芒果实现永鲜,需要将SpoilageTime下的RatePerDaytimePoint修改为0。

已知不论是卡牌还是状态,都有一个独一无二的uid,因此我们可以通过uid来找到目标。
同时卡牌的数据结构是CardData,状态的数据结构是GameStat,这也是为什么新建卡牌是在CardData文件夹中新建,新建状态是在GameStat文件夹中新建。
在游戏运行时,他们统一以UniqueIDScriptable的形式保存在游戏中。
下面两条代码可以实现芒果永鲜,我们来分析每句的作用。
var mango = UniqueIDScriptable.GetFromID<CardData>("190a9472029cca9438d492b2c798c478");
mango.SpoilageTime.RatePerDaytimePoint = 0;
# 代码分析
UniqueIDScriptable中包含了所有卡牌和状态,通过搜索功能GetFromID搜索芒果的uid,并将得到的结果视为CardData类型(如果是状态则是GetFromID<GameStat>)赋值给变量mango。
var mango = UniqueIDScriptable.GetFromID<CardData>("190a9472029cca9438d492b2c798c478");
CardData类型的结构,和ME上的界面是相似的,并且属性名和ME上的key名是一样的。对照ME上的关系,就能找到想要修改的词条在哪里。
如下图所示,ME界面红框属于CardData类型,其中的蓝框就是CardData中的一个属性SpoilageTime,点开SpoilageTime就可以看到绿框RatePerDaytimePoint。
因此, 他们的关系就是CardData->SpoilageTime->RatePerDaytimePoint。
mango.SpoilageTime.RatePerDaytimePoint = 0;

# 举一反三
如果我们现在要修改芒果的描述呢?
还是先看ME界面:红框依旧是CardData,蓝框是它其中的一个属性CardDescription,点开CardDescription,可以看到DefaultText,它存着芒果的描述文本。
他们的关系就是CardData->CardDescription->DefaultText。
但是仅仅修改DefaultText是不够的,游戏本身支持多语言,除了默认文本以外,还会通过LocalizationKey去对应的翻译文件里获取翻译文本。好在当LocalizationKey不存在时,游戏会直接使用默认文本。因此,可以直接将LocalizationKey设置为空。
所以,为了修改芒果的描述,我们需要修改两个属性。
mango.CardDescription.DefaultText = "大芒果";
mango.CardDescription.LocalizationKey = "";

# 总结
想要修改某个属性,可以对照着ME的界面,一级一级展开下去,根据ME的关系来写代码。
例如ME中是这样的关系:CardData->CardDescription->DefaultText,在代码中,CardData就是mango这个变量。用mango替换掉CardData,然后用.替换掉->,就是代码的写法:mango.CardDescription.DefaultText。
# 测试
第一次构建请点击构建解决方案,因为我这个项目之前已经构建过了,所以这里点击重新构建解决方案。

打开游戏,可以看到mod已经加载。

通过控制台调一个芒果出来,可以看到芒果腐烂速率为0,并且休息或睡觉消耗tp,芒果也不会腐烂。

# 代码
最终代码如下:
using HarmonyLib;
namespace CSTI_Example1.Patcher;
[HarmonyPatch(typeof(GameLoad))]
public static class GameLoadPatch
{
[HarmonyPostfix, HarmonyPatch("LoadMainGameData")]
public static void LoadMainGameData_Postfix()
{
var mango = UniqueIDScriptable.GetFromID<CardData>("190a9472029cca9438d492b2c798c478");
mango.SpoilageTime.RatePerDaytimePoint = 0;
}
}