基础知识 并非必备,但了解概念会极大降低理解成本。本作不牵扯3D,Mod难度是很低的,稍微有点耐心就能学会,不想看可跳过此节。 1. 计算机知识,比如Excel,json格式,CSV,Lua,程序,钩子(Hook),编译和反编译等等概念。 2. 一些Unity引擎的基本概念,比如Mono环境和C#语言、Resources资源路径、ScriptableObject,编辑器和运行时的区别,请自己查阅Unity官方文档。 3. 游戏开发的一点点基本认识,比如什么是策划配表,什么是本地化表,等等。 4. 钩子注入Mono的框架:Harmony,以及各种使用该框架的成品插件管理器。目前活侠传几个插件主要使用的管理器是BepinEx,属于近期使用率最高的。任何比较新的Unity游戏理论上都可以使用该框架。 游戏制作组使用了Fungus插件,这是个专门用来做AVG游戏的Unity工具库,提供了本地化、图片、音乐的支持,带有剧本编辑界面和自动生成运行时lua(基于Moonsharp )的功能,图标是个小蘑菇。它有专门的网站和文档,但是玩家拿到的游戏是已经打包好的运行时版本,编辑器资源被舍弃,所以无法使用其提供的剧本工具。好在小蘑菇生成的lua脚本都是明文,玩家要修改剧本,暂时只能通过读懂并修改lua剧本的方式进行。 Mod插件使用 Github: https://github.com/Binarizer/Plugin-DD2.git 分支=Mortal 插件下载https://pan.baidu.com/s/1kgjeC3KIM92mFueF2EPUow?pwd=9ii2 找到Mod插件本体,解压后将其放到游戏目录,注意不是把解压出的那个文件夹,而是要将里面内容,即BepinEx目录、winhttp.dll、doorstop文件放在游戏可执行文件Mortal.exe同级目录里。完成后是这样的:LegendOfMortal/BepinEx/...

插件附带BepinEx插件管理器,而插件本身是BepinEx/plugin/FunctionalPlugin_Binarizer.dll这一个文件。其他插件一般也是如此打包的。所以若有文件冲突,就只copy里面对应的插件dll即可,当然全覆盖也可以,管理器的文件几乎没区别。 在游戏目录下建立一个Mods文件夹,作为Mod文件的根目录。目前插件是写死了读取该目录。

这个目录下的每个文件夹对应一个Mod,所以可以支持不同Mod间一键切换。
切换Mod需使用插件配置文件,位于BepinEx/config/binarizer.plugin.mortal.cfg中,如图所示:

只要留空就可以取消Mod,运行原版游戏。若用英文逗号分隔则会同时加载多个Mod,遇到文件冲突则写在前面的优先。 **进阶:若使用configurationManager.dll,则可以通过F1菜单直接改配置。 **Mod插件原理:写钩子勾住游戏程序加载文本、图片、音乐的接口,将其换成自己的函数,就可以跳过官方打包好的资源,而去加载你自己的外部资源。任何游戏的读取外部资源Mod制作都是这个原理,官方直接提供Mod接口也是该原理。相比直接改dll的方式,比较不依赖官方更新,只要官方没动钩子涉及的代码,插件就会一直生效,而游戏更新通常都是资源更新,代码变化极少。 全量Mod架构
这是一个典型的全量Mod示例,分成4个部分。该Mod是6月14日初版剧情和数据配表,且取消了骰子。

1. DataTable,策划数值配表,为json格式。例如:敌人的数值配置、秘籍道具的配置、各种事件的触发器...等等一切游戏数值。 **然而目前未支持读回游戏,ScriptableObject通用写法没找到。倒是改确定的几种很简单,预定优先支持Mission触发器。 2. LuaEquivanlent,本插件原创的lua等价脚本,原因是改ScriptableObject不好走通,所以把一部分常用的直接换成了lua 3. story,游戏剧本,需要手动解包。改剧情主要就是改它,参考b站各种魔攻视频。官方lua是小蘑菇生成的,但我们只能手改。 4. StringTable.csv,本地化表,就是完整的全游戏文本,key-value对应。如果找解包文本则看这个最完整、最快。网盘附带的是简体版。 5. Portrait,这里没展示,跟其他换图框架大差不差,但图片命名强关联剧本。 6. Music,音乐和音效,还没做,预定移植下侠之道插件,可以支持玩家自己给剧情配音。 **以上除story外均支持运行时导出,需要打开插件配置里的Enable Export。目前F3导luaEq,F4导本地化,F5导图片,F6导策划表 增量Mod制作 我们实际应该做的是增量模式Mod,改了哪几个文件,就只往Mod目录放哪些文件。若对应的原版资源不存在,则添加;原版存在,则替换。 **IT行业里这叫做 CUID,中文=增删查改 比如你只改了后宫剧本ch7_5_6_3.lua,那么你的mod只放这一个文件即可。头像、音乐也应是如此。 插件本身自带多Mod同时加载能力,非常方便各个不相干的增量Mod的自由组合,比如A头像+B剧本+C剧本+D配音,等等。 而如果都是全量,那多个Mod一起使用非常容易冲突,造成不可预料的问题。 修改文本 即修改本地化表格StringTable.csv,里面两列,左边key,右边对应语言的文字。 luamanager.GetStoryText("Foobar") 剧本里用这条语句,底层将对应key转换成当前所选语言的对应文字。 **任何多语言游戏都有类似机制,称为Localization
注意游戏导出来的表是所有文本,而增量Mod不要全部丢进去,应只添加有区别的。

如图,只从全量StringTable表里挪了两行过来,效果是只有这两行被改掉,其他文本还会是原版的。 ** csv可以用Excel打开,但微软Excel默认不支持utf8会乱码,需要通过导入手选utf8编码。本人习惯直接VSCode装csv插件。 **整张表可以当成翻译表丢给翻译公司,比如翻成日语、英语等。当前时代,喂给AI也可以做初步处理。完事拿回来替换,游戏就会整个变成另一种语言。 **网盘上传了一个调用GoogleTranslate的py工具,可以批量将中文表翻译成英文。小改一下代码即可翻成别的语言。 策划配表
即游戏里所有的数值配置。导出文件位于全量Mod的DataTable中,以类名做为目录名,格式为Json。本作没有用传统的excel配表,而是使用了ScriptableObject,后简称so。这个有利有弊,so在Unity编辑器配置起来的确方便,但玩家制作Mod就很费劲了。

上图举例一项so的数据配置,是某个战斗时敌人数值。如果是Excel或者文本,玩家还可能修改后直接读回去,但so会麻烦许多,目前插件还不支持回读。 **暗黑地牢2也是同样的问题,做的时候一时爽,结果该架构导致UGC内容无从制作,基本等同放弃Mod能力,发售后折腾到现在都做不出官方Mod支持功能。暗黑地牢1的火爆,有九成是靠玩家自制mod,2代用这套配表系统构建游戏约等于自断生路。 插件提供了导出策划配表功能,需要打开导出选项后,进游戏按F6。注意导出时机需要该资源已被加载到内存,否则导出的资源会不全。 解包剧本
下载AssetStudio,打开游戏的Mortal_Data文件夹,找到所有container为story/chineseTraditional的TextAsset文件,全选导出。

导出后的文件默认无后缀,为方便插件识别和IDE关联,需对所有文件添加.lua后缀。网盘自带rename.py,将文件夹拖上去即可(要装个python)
**关于为何要手动解包:剧本文件读取用的接口是 Resources.Load,此接口不依赖代码和场景,凡是Resources下的资源都会打进去,运行时无法访问到所有的文本资源,也就无从一键导出。好处是玩家也可以随意扩展、添加新的剧本文件。 定位剧本
游戏里或文本里看到一句话,如何搜索到其对应的剧本?

1. 去StringTable里搜对应文本,将其key记下来

比如这里就是D_6_4_3_2_01_503_025
2. 全局搜索story文件夹,看这个key是在哪个剧本里引用到的

之后就随便改了。
修改剧本
典型的剧本如图所示

大致分为几类:
1. 主线剧情,以ch_开头(即章节Chapter),比如开局剧情ch_1_1.lua,整个替换这个文件成其他剧本是最简单的看剧情方式。 2. 支线剧情,以S开头(SubMission子任务),比如大师兄刺金剧情 3. 修炼地点分支,以地点的英文名开头,比如Alchemy_XXX就是炼丹房 4. 会议选择项,以事件名_option为标识,会显示一个框让你点各种事件 内容则为lua脚本,这里不可能讲lua教程,简单介绍几个函数,剩下的请自行探索: ● 脚本跳转指令 luamanager.SetNextScript("alchemy_special_01_01") luamanager.Init() 执行后跳转到NextScript参数的lua里继续执行 ● 设置角色对话头像 runwait(characters.LoadCharacterAsset("player")) stage.show{character=characters.Get("player"), portrait=characters.GetPortrait("player", "normal"), fromPosition="L2", toPosition="L2", facing="right", fadeDuration=0, moveDuration=0, useDefaultSettings=false} characters.Focus("player")
参数为角色代码和情绪代码,可以去导出的头像库看头像到底是谁。角色情绪有30几种,如果在头像库放了对应的图片,插件就会自动替换之,如图所示

哪怕原始官方图没有这张,也可以添加新的。
FromPosition/ToPositon/Facing,顾名思义,头像朝向、位置、动画等设置,可以自己试试改。 Focus,其他背景变暗,只有Focus的头像是亮的且放到最前面。 注意,跳转前需要把场景当前正在显示的角色头像隐藏掉,为如下语句 stage.hide{character=characters.Get("player"), fadeDuration=1, useDefaultSettings=false} 否则下个场景还会保留前个场景最后的角色头像,类似卡住。
**插件提供了导出头像功能,需要打开导出选项,进游戏按F5。目前未使用协程,会占用大量内存导致崩溃,但多导几次总会导完。
● 设置旗标 statmodifymanager.SetFlag("P_Secret_01", 1) Flag是各种判定的依据。很多官方未开放剧本,都可以通过往剧本里强设旗标来过条件。 **当然,你也可以把条件判定的语句给删掉,比如直接local xxx_result = 1这样 另外官方未提供lua读取旗标能力,本插件为此开放了lua接口 local flagValue = ext.GetFlag("P_Secret_01") 获取到的值为整数,可以用来做判断。 ● 增减各种属性 statmodifymanager.Player("literacy", 3, "", 1) wait(statmodifymanager.GetDisplayTime("literacy")) 比如这个就是玩家的学问+3,第四个参数为1会显示出来,否则隐藏;下面的语句则是显示多少秒。 插件同样提供了查询属性的函数,类似这样:ext.GetPlayerStat('literacy') ● 开启选项菜单 setmenudialog(menudialogs.Options) local option1_1 = {} option1_1[1] = "O_1_1_Break_007" option1_1[2] = "O_1_1_Break_008" option1_1[3] = "O_1_1_Break_009" local choice_option1_1 = choose(option1_1) menudialogs.Options.SetActive(false) 该段会拉起选项窗口,样式由setmenudialog的参数决定。打工图片选项、对话文本选项,都一样是这套代码。选项按钮的string参数为字符key,需要去StringTable.csv查询或添加新的key-value。 返回的值为int,需要在下面自己写分支判定。也可以忽略,就是一些游戏的无效选项了。 ● 命运轮盘 略,个人不喜欢这个系统,而其代码特征又很像选项,所以就找GPT4帮忙替换成了选项。想看就自己摸索吧,应该需要和DataTable/DiceResultData里的值对应。 ● 流程判定 分为Condition、Switch、Position3种。 1. Condition,返回值bool local condition_1_result = checkpointmanager.Condition("Ch_1_2_3_001") 多用于判定选项是否可选。 2. Switch,返回值int local switch1_1_result = checkpointmanager.Switch("XXXXX") 多用于判定当前数值满足哪种情况的逻辑跳转。 3. Position,返回值string local position_1_result = checkpointmanager.Position("XXX") 本作多用于选择练功/打工地点时选择随机触发哪项事件,返回的string就是下个lua剧本。 剧本里这几种条件判定被引用次数均很多,可以去DataTable的ConditionResultData/SwitchResultData/PositionResultData表格里找到底怎么配置,其中“_devNote”是作者给的注释(骰子大师、知天命等mod就是通过这个打出的条件文字)。但目前插件直接改策划表回读尚未支持,之后会介绍lua的等价写法,通过lua来替换这类判定条件。 实操示例A:让修罗场结局里的所有女主都出场。
修罗场结局通过上文的剧本查找方式,可以定位到处于 ch7_5_6_3.lua,代码如图

注意剧本里出现了多个Switch,红框是第一个,对应表格类型是SwitchResultData。依次查DataTable,顺藤摸瓜,可知是各种“是否攻略某女主"的Flag。

我们只要在最前面将5女主的旗标均设为1,即可全部触发;或者选择几个写进去,即是b站视频的效果了。

去除命运轮盘
交由 ChatGPT 写的 python 代码,把官方story文件夹拖上去,即可将全部剧本里的命运轮盘脚本替换为选择菜单脚本。

以上是生成replace_dice.py时某次prompt截图,需要多迭代几次测试。
AI写的代码原理是正则表达式匹配替换,虽然python跟正则我都不会,但AI会就等于我会。 **适合用AI做的事情:比较通用、不那么偏的、常见的算法需求,比如文本替换、代码改语言等 配表等价Lua 由于策划表全部是ScriptableObject,无法成系统的改,而本作最上层是lua驱动,所以插件提供了将某些配表类型转换为lua的功能,这样就可以由玩家自定义修改了,从而完全绕过ScriptableObject系统。 **该转换的插件代码也是由GPT辅助写的 目前仅支持上述 Condition、Switch、Position三类,通过F3即可导出等价版Lua,玩家可以自行修改。
举个例子,这是原版策划表配置:

而转换为Lua后则只有一行

个人觉得lua版本代码更直观一点,和剧本其他函数也更类似。
总之,将对应的lua脚本做修改,放到对应的LuaEquivalent/XXX/下面,就能替换官方的配表。 也可以选择将剧本里的checkpointmanager.XXX()直接替换成等价的lua,但会比较累。 **注意,这样改成lua后,骰子大师、知天命等插件会判不准条件,因为该判定会被lua绕过去。 实操示例B:用固定事件(比如示例A的修罗场 ch7_5_6_3)代替打工触发的判定(比如锻造)
1. 搜索锻造场对应lua。如何找这里不做赘述,文件是forge_001.lua,选项分支在这里

这里其实直接改成 var2 = 'ch_7_5_6_3'就可以了。但如果不改这个文件,也可以通过修改Position达到一样的事情,找到LuaEquivalent/Forge_01.lua
**读代码可知这是一套加权随机算法,从很多选项里找到一个事件返回

我们将其全部删除,替换为这样

跟直接替换剧本里的var2计算,达到的效果是一样的,等于跳过了so机制。进入游戏测试,点击打铁,就会进入修罗场结局了。
b站本人发的几个视频基本都是这个原理,所以你会发现游戏所处日期根本对不上。
如图所示,将mp3文件和Story/XXX的语句对应,丢进 Mods/你的Mod/Voice 下面

播放该条对话时,就会播放相应的mp3了。 **原理:NAudio,开源的C#音频组件,轻量好用,但是只支持Windows平台