UnityAssetsPatcher:一个替换 Unity 资产文件的 Mod 安装器
前情提要
在玩一些 Unity 单机游戏的时候,我很喜欢打 Mod,不是一定要搞那种无限游戏道具,无限资源的 Mod,我玩的大多是些生存游戏,因此装上的也都是辅助类 Mod。
装 Unity 游戏 Mod,大致有两条路。第一条是接入像 BepInEx 这类的运行时框架,靠 Harmony 在游戏跑起来的时候去 patch 方法,灵活、可逆,是绝大多数 Mod 的首选。但问题是,有些游戏压根不适合这么干:要么带反作弊,运行时注入会被拦,一注入就会崩溃;要么 Mod 的需求本质上就是改一堆静态资源(换贴图、换音效、调几个配置值),上 BepInEx 这类大框架属于杀鸡用牛刀。
这时候就只剩下第二条路:直接改游戏目录下那些 .assets / .resource 二进制文件。
这条路的“标准工具”是 UABEA。用它打开 .assets,在字段树里翻到目标 asset,手动改几个值,保存。如果是 Mod 作者,就把修改过的文件压缩打包,传到群里或者是网盘。
为什么会有 UnityAssetsPatcher?
前情提要里提到了 UABEA,先用它举个改字段的简单例子:
用 UABEA 打开
resources.assets,找到类型是Camera、field of view是 60 的那个 asset,把field of view从 60 改成 90,保存。
听起来简单,对不对?可这套操作要是让用户自己来改,那就是一场灾难:
- 改错一个地方,游戏直接崩溃,还不知道错在哪。
- 没有备份,改坏了只能去 Steam 里验证游戏文件完整性。
(验证完整性吃不满硬盘读取,遇到大游戏你就偷着乐吧) - 没法卸载,想恢复原样全凭记忆
(或者重装游戏)。
既然让用户自己改这么不靠谱,Mod 作者干脆把改好的整个 .assets 文件发出来,让用户直接覆盖原文件不就行了?这确实是静态资源 Mod 的另一种主流做法。制作时要把资源导出来、在外部改好、再重新打包对齐字段布局,本来就够折腾;可真正要命的是它在分发和兼容上的死结:
- 文件太大,传输要命:一个
.assets/.resource动辄几十上百 MB,甚至上 GB,里面装的都是已经压缩过的纹理、模型、音频,再打 zip 也压不掉多少。分发一个 Mod 等于搬运一大坨游戏本体资源。要是 Mod 作者发的是速度巨慢要求还多的网盘,那…… - 强绑定游戏版本:换的是整个文件,意味着 Mod 和特定游戏版本、资源文件布局、asset 标识与字段结构死死绑在一起。游戏一更新,哪怕只动了几个 asset,整个文件都可能对不上。
- 极易崩溃:版本对不上、或文件结构有哪怕一点点出入,轻则 Mod 失效,重则游戏直接崩给你看,而用户根本无从排查,只能甩给你一句:为什么装上之后游戏一直崩溃?
- 没法和别的 Mod 共存:两个 Mod 都想覆盖同一个
.assets,后装的直接把先装的覆盖掉,然后游戏就会崩溃。
也就是说,手改字段对用户是灾难,直接换整个文件对分发和兼容性又是另一重灾难。两条路都走得磕磕绊绊。
被这两条路折腾够了,我就想:这玩意儿难道不能做成一个“安装器”吗? Mod 作者写一份配置文件,配置好 Mod,说清楚“改哪个文件、匹配哪个 asset、把哪个字段从什么值改成什么值”。用户运行工具,直接无脑一键安装,这不香吗?
于是就有了 UnityAssetsPatcher。
UnityAssetsPatcher 是什么
简单说,它就是我给 Unity 静态资源 Mod 写的一个安装器。
不是那种很重的 Mod 管理平台,也不是要替代 BepInEx。它解决的是一个很具体的问题:当一个 Mod 只是想改 .assets 里的几个字段、替换几个 asset、顺手复制几个 .resource 文件时,能不能别再让用户手动开 UABEA,也别再把整个游戏资源文件打包发出去?
我的想法是,把 Mod 作者脑子里的那份“安装说明”变成机器能读的 manifest.json。比如改哪个 .assets,用什么条件匹配目标 asset,把哪个字段从什么值改成什么值。用户这边就不用理解字段树长什么样了,打开工具,选 Mod 包,确认预览,然后安装。
写这篇文章的时候,项目刚到 v0.2.0。目前只发了 Windows win-x64,MIT 协议,NativeAOT 自包含单文件,不需要预装 .NET 运行时。解压后双击 UnityAssetsPatcher.exe 就能跑,界面是一个终端 TUI,支持简体中文和英文。
至于为什么还是终端程序……
一开始我也想过要不要做 GUI,但做着做着发现终端其实更适合这个阶段。它启动快、打包简单、远程给别人排查问题也方便,而且不会为了一个按钮、一块布局把时间全烧在 UI 上。
用起来是什么感觉
从用户视角看,流程很短:
- 去 GitHub Releases 下载压缩包;
- 解压,双击
UnityAssetsPatcher.exe; - 选
Install Mod,填 Mod 的 zip 路径; - 游戏目录可以手动填,也可以留空让它去 Steam 库里找;
- 看一眼预览,确认没问题再安装。
预览这一步是我故意做得很显眼的。工具会把接下来要改哪些文件、哪些 asset、哪些字段全列出来,确认前不会写文件、不会复制 payload、不会更新安装记录。假设这个 Mod 想偷偷改点不该改的东西,用户也能第一时间发现。
不过大部分人估计看都不看吧……那些 AI Agent 常年 danger-full-access 和 bypass-permissions-on 的小朋友们,你们好呀。
如果后面不想要这个 Mod 了,就回到主菜单选 Uninstall Mod。它会从安装时的备份里把原文件还原回去,同时清理安装时复制进去的 payload。
[!CAUTION] 别手动删
backup文件夹 卸载靠的就是安装时生成的备份和记录。这个文件夹删了,工具就没法知道该把哪些东西恢复到什么状态了。
Mod 作者要做什么
Mod 作者这边,核心工作就是写一份 manifest.json。
这份 manifest 不需要把整个 .assets 文件塞进去,只需要描述“我要怎么改”。比如一个最典型的字段修改,大概就是:匹配某个 asset,然后把 field of view 从 60 改成 90。
工具现在支持的变更主要有三种:
set:把字段值从from改成to;add:往数组字段里追加一个值,已经存在就不重复加;replaceAsset:用 Mod 包里的某个源 asset 整体替换目标 asset。
除此之外,还有一些实际做 Mod 时很常见的需求,比如复制 .resource 这类 payload 文件、做可选安装项、在 manifest 里用 $pathId 引用其他 asset。完整格式我放在了项目文档里,这里就不把文档再搬一遍了。
我更想强调的是,这种方式对分发很友好。你不用发一个几十上百 MB 的完整资源文件,只要发“差异”和必要的 payload。游戏更新后,如果字段值已经变了,工具也会在安装前拦下来,而不是硬把旧版本的修改糊上去。
现在工具还不完善,没法像 UABEA 一样能直接可视化预览资产树和里边的值,但是之后一定会越做越好的(肯定)!也许,还会加上 MCP Server,让 AI 帮你查找项目,并且做 Mod?
一些边边角角的防御
做这种工具很容易陷入一个误区:只要 Mod 包是“自己人”发的,就可以少检查一点。
但我不太想这么赌。用户下载 zip、拖进工具、点安装,这条链路里能出问题的地方太多了。于是我顺手把很多边界也堵上了。
比如路径遍历。manifest 里引用的目标文件不能是绝对路径,不能以 / 开头,不能带 .. 或 . 段,targets[].file 也只允许纯文件名。zip 解压后还会再检查一次,确保实际落点还在临时目录里面。
再比如 zip bomb。manifest 限 10MB,整体解压限 10GB,而且不是看 zip 里声明的大小,而是按实际解压出来的字节数累计。payload 文件复制时也不会覆盖已有文件,避免一不小心把用户游戏目录里本来就存在的东西顶掉。
还有符号链接。这个点一开始我其实没怎么注意,后来测试时越想越不对:如果目标文件是符号链接,直接 Move 可能会顺着链接写到别的地方去。最后的处理方式是识别到符号链接后走单独逻辑,避免把写入范围带出游戏目录。
这些东西单独看都挺碎,但合在一起就是我对这个工具的基本要求,当然,这不等于它是安全沙箱,也不代表可以无脑安装来源不明的 Mod 包。它只是尽量把安装器能管住的风险先管住。
NativeAOT 的小坑
发布时我选了 NativeAOT,原因很现实:单文件、启动快、不用装 .NET 运行时。对这种发给普通玩家用的小工具来说,“解压双击就能跑”比什么都重要。
代价也有,主要是 AOT 对反射不友好。于是 JSON 处理这块我就尽量绕开反射:
- 构造
JsonElement时,用Utf8JsonWriter手写 token,而不是偷懒用JsonSerializer.SerializeToElement; - manifest 读取主要靠
JsonDocument/JsonElement,没有搞一堆泛型反序列化; - 安装记录这种结构比较固定的东西,用
JsonSerializerContext源生成; - 发布配置里关掉
JsonSerializerIsReflectionEnabledByDefault,AOT 兼容性警告也直接当错误处理。
这部分代码肯定不如“直接序列化对象”优雅,但我宁愿写得笨一点,也不想发出去之后才发现某个反射路径在 AOT 下炸了。
写在最后
做 UnityAssetsPatcher 的初衷其实很朴素:我受够了“用 UABEA 手改 .assets、改完没法回滚、出问题只能重装游戏”这套流程。
如果 Mod 作者本来就知道要改哪个文件、哪个 asset、哪个字段,那这些信息就不该只停留在一段“请按如下步骤操作”的教程里。它完全可以变成一份 manifest,让工具去执行。人负责表达意图,机器负责重复、检查、备份和回滚。
回头看,写这个工具最花时间的也不是“能不能改字段”,而是怎么把“改游戏文件”这件危险的事做得没那么吓人。from 校验、写前备份、临时文件替换、安装回滚、卸载回滚、zip bomb 防御、路径检查……这些东西都不酷,也不适合拿来做宣传图,但少一个我都不放心。
目前 v0.2.0 还很早期:只发了 win-x64,还是纯交互式终端,没有非交互 CLI 参数,也没有 GUI。Steam 目录解析遇到多个匹配时也还得手动确认。这些后面我会慢慢补。
如果你也在折腾 Unity 游戏的静态资源 Mod,或者只是对它的实现细节感兴趣,可以去 GitHub 仓库 看看。提 issue、提建议、提 PR 都欢迎。如果你是 Mod 作者,最欢迎你拿一个真实 Mod 包来试试 manifest 有没有表达不了的场景;如果你只是玩家,也欢迎提那些“我看不懂 / 我不敢点 / 我不知道它会改什么”的体验问题。折腾这东西,说到底还是为了让自己和同好少受点罪,多玩会儿游戏嘛。