【免费下载链接】universal-modder
Point Claude at any game. Skills, tools and the fal MCP that let Claude Code mod almost any PC game you own: recon, reverse engineering, fal-generated art/3D/audio, in-game testing, showcase videos.
本文基于 universal-modder 仓库知识库中的实战记录(字段笔记),完整还原了在 Schedule I 0.4.7 IL2CPP 测试分支上,通过 MelonLoader 0.7.3 + HarmonyX 编写两个 C# Harmony Mod(
IncreasedStackLimit-Latest与PocketPlug)的全过程:从环境搭建、IL2CPP 逆向阅读、堆叠上限系统剖析,到手机应用、指南针等 QoL 功能的运行时 uGUI 构建,再到以 MelonLoader 日志 + 游戏内截图为"神谕"的验证闭环,以及 12 条可直接复用的 symptom→cause→fix 实战陷阱。读完本文,你将掌握在 Unity IL2CPP 游戏上实施 managed-patch 路线的完整方法论,并了解如何用 Harmony 补丁 + Il2CppInterop 直接字段写入来修改"读取定义字段而非实例 getter"的硬编码逻辑。
前置背景:为何选择 managed-patch 路线
Schedule I 0.4.7 测试分支是IL2CPP 后端的 Unity 游戏(Steam 3164500,beta 分支,buildid 25698382,版本号 0.4.7f9),而对应的alternate-beta分支是Mono后端(0.4.7f6)。两条分支的 C# 逻辑相同,Mono 版反编译后近乎原始源码,因此被用作可读性参考。按 unity.md 引擎手册 的识别方法:IL2CPP 安装存在GameAssembly.dll与<Game>_Data/il2cpp_data/Metadata/global-metadata.dat,逻辑已被编译为原生代码,Mod 必须通过生成的互操作程序集工作。
两个 Mod 选择了managed-patch(托管补丁)路线:Harmony 补丁 + 通过 Il2CppInterop 程序集直接写入字段,不使用 AssetBundle,所有 UI 均以 uGUI 在运行时构建。这条路线的选择理由与 mod-any-game 技能 中"加载器 API 无法覆盖目标时使用托管代码补丁"的判定一致——S1API(ifBars 的社区 API 库)在 0.4.7 当时处于损坏状态(issue #305),无法依赖,因此完全绕开它自研。
本仓库中关于"神谕"(Oracle)验证方法的专题笔记 oracles-how-agents-know-a-mod-works.md 概括了本案例采用的验证哲学:运行中的游戏本身就是神谕,对代码的阅读不是;并强调"诚实列出未验证项"。
环境搭建(Setup)
游戏与启动方式
| 项目 | 值 |
|---|---|
| 游戏 | Schedule I,Steam,Linux,经 GE-Proton11-6 运行 |
| 测试分支 | beta:IL2CPP,0.4.7f9(buildid 25698382) |
| 可读参考分支 | alternate-beta:Mono,0.4.7f6,与匹配的 IL2CPP 构建 C# 相同,反编译近乎源码 |
加载器:MelonLoader 0.7.3
MelonLoader 0.7.3(2026-05-14 发布)以version.dll代理方式安装。Steam 启动选项:
WINEDLLOVERRIDES="version=n,b" %command%与 unity.md 引擎手册 中"Proton/Linux 下 MelonLoader 需设置WINEDLLOVERRIDES="version=n,b""的说明一致。首次 IL2CPP 启动时,MelonLoader 会用 Cpp2IL 与 Il2CppInterop 生成MelonLoader/Il2CppAssemblies,耗时约 30 秒;日志中出现的Failed to restore 885 methods / 3 fields是无害的,可忽略。
构建环境
- .NET SDK 10,目标框架
net6.0。 - 需要引用的程序集:
MelonLoader/net6/{MelonLoader,0Harmony,Il2CppInterop.Runtime,Il2CppInterop.Common}.dll(加载器与互操作核心);MelonLoader/Il2CppAssemblies/*.dll(由首次启动生成的游戏互操作程序集)。
- 反编译工具:
ilspycmd 11.1。按 reverse-engineering 技能 的说明,Unity IL2CPP 应先用 Cpp2IL/Il2CppDumper 处理GameAssembly.dll+global-metadata.dat,得到类型、字段、方法签名和供 ILSpy 阅读的 dummy DLL;方法体为原生代码,需要时再用 Ghidra/IDA 配合生成的脚本命名函数。
无 Steam 的实验室(lab)
- 使用
umu-run,设置GAMEID=umu-3164500与PROTONPATH=<GE-Proton>,并在游戏目录放置steam_appid.txt。Steam 必须处于运行状态。 - 实验室环境下的存档落在
Saves/TempPlayer/而非你的 SteamID 目录——这是预期行为。
S1API 状态
S1API(ifBars)在 0.4.7 当时已损坏(S1API#305 问题)。这两个 Mod 不依赖它,这正是本案例能独立成行的原因之一。
游戏内幕:必须学会的引擎事实
堆叠上限(Stack limits)
堆叠上限存储于物品定义上:
ScheduleOne.Core.Items.Framework.BaseItemDefinition.StackLimit是一个public int(0.4.7 中它移动到了ScheduleOne.Core.dll)。BaseItemInstance.StackLimit只是读取_definition.StackLimit。- 关键发现:多个系统直接读取定义字段本身——
DeliveryInstance、DeliveryShop.WillCartFitInVehicle、商店Cart、Supplier死投递点、SpecialCustomerLeader。因此旧 Mod 只补丁实例 getter 的做法,会让这些系统仍然停留在原版尺寸。写入定义字段才能覆盖一切。
写入位置:遍历Registry.GetAllItems()。Registry是PersistentSingleton,在Awake中被填充。需要后置(Postfix)Registry.AddToRegistry(ItemDefinition item)以捕获运行时新增的定义。
新产品混合(product mixes)通过Object.Instantiate(DefaultWeed)创建(Meth、Cocaine、Shroom 同理),它们会复制模板的当前StackLimit,因此必须记住原始值,否则倍数会复合叠加。
0.4.7f9 的原版堆叠上限:只有 1、10、20 三种取值,共 227 个物品。
物品分类:使用定义类而非EItemCategory枚举桶(后者太粗糙):
ProductDefinition(Weed、Meth、Cocaine、Shroom 子类)继承自PropertyItemDefinition,后者同时是混合器类,因此先检查产品。- 其他类:
PackagingDefinition、QualityItemDefinition、SeedDefinition、SoilDefinition、AdditiveDefinition、SporeSyringeDefinition、ShroomSpawnDefinition、BuildableItemDefinition。
弹药没有自己的类型:通过ItemDefinition.Equippable上的Equippable_RangedWeapon.Magazine找到;0.4.7f9 上对应的是shotgunshell。枪械与近战武器同理:检查 equippable 上的Equippable_RangedWeapon/Equippable_MeleeWeapon。
指南针(Compass)
CompassManager(单例)为每个任务条目创建一个Element:QuestEntry.CreateCompassElement使用ParentQuest.IconPrefab。Element.DistanceLabel是一个 TMPText子对象。游戏只在50 米内填充距离文本,使用UnitsUtility.FormatShortDistance,其显示遵循公制/英制设置。- 交易是
Contract任务(Contract.Contracts)。Contract.Customer是NetworkObject,因此要对其调用GetComponent<NPC>()。 - 联系人应用中显示的肖像来自
NPC.MugshotSprite(RelationCircle.HeadshotImg)。 - Element 子对象是
QuestElement(Clone),包含Text与任务图标克隆。 - Element 容器
Quests是800×30 且带RectMask2D:任何绘制在标记下方的内容都会被裁剪,除非设置RectMask2D.padding(本案例使用(0,-60,0,0))。
手机应用(Phone apps)
- 原生应用是
App<T> : PlayerSingleton<T>。无法从 IL2CPP Mod 中继承该泛型(注入泛型派生类型不现实),因此像 S1API 的 PhoneApp 那样手工构建应用:- 在
HomeScreen.transform.parent/AppsCanvas下添加页面。 - 克隆
HomeScreen.appIconPrefab到appIconContainer,再将其加入homeScreen.appIcons与homeScreen.uiPanel。 - 通过重复调用
App.SetOpen实现开关:AppsCanvas.SetIsOpen、HomeScreen.SetIsOpen(!open)、Phone.ActiveApp = page。 - 挂接
Phone.closeApps与GameInput.RegisterExitListener;用DelegateSupport.ConvertDelegate<GameInput.ExitDelegate>转换委托。
- 在
- AppsCanvas 是1201×655(横屏)。竖屏应用是 655×1201 的页面旋转90°;原生 Messages 应用同样如此。
- 从代码打开手机:先调用
GameplayMenu.Open()再SetScreen(EGameplayScreen.Phone);单靠Phone.SetIsOpen只改变状态。 NotificationsManager.SendNotification(title, subtitle, sprite, duration, sound)显示游戏自带的通知 toast。
金钱系统(Money)
MoneyManager.ChangeCashBalance(delta, visualize, sound)修改现金;CreateOnlineTransaction(name, unit, qty, note)是带RunLocally的 ServerRpc,会修改银行余额并追加到ledger。- 分类账(ledger)仅限当前会话,不保存。
- 每周存款上限是
ATM.WeeklyDepositLimit = 10000(一个const),仅在ATMInterface内部强制。ATM.WeeklyDepositSum(static)一旦达到 10000 还会触发Quest_CleanCash。 - 在 IL2CPP 上,在线余额的 SyncVar 属性无法从 C# 直接使用,需调用
sync___get_value_onlineBalance()。
经销商(Dealers)
Dealer.AllPlayerDealers、Dealer.Cash、Dealer.SetCash。NPC.MSGConversation.CreateSendableMessage(text)添加玩家消息选项,带onSent(一个Il2CppSystem.Action)与ShouldShowCheck回调。
构建步骤(Build steps)
- 将 MelonLoader 0.7.3 安装进 IL2CPP 安装目录并启动一次以生成互操作程序集。
- 克隆 Mod 仓库并运行:
dotnet build src -c Release -p:GameDir="<Schedule I>" -p:DeployToGame=true- 从 Steam 启动。设置存放于
UserData/<Mod>.cfg。
验证:神谕与开发命令文件
按 oracles-how-agents-know-a-mod-works.md 的方法论,本案例的神谕是MelonLoader 日志(MelonLoader/Latest.log)加一个由 Mod 读取的开发命令文件。该命令文件仅在UserData/PocketPlug.dev存在时生效,命令包括:
| 命令 | 作用 |
|---|---|
load | 调用LoadManager.StartGame(SaveGames[0]) |
phone open、app <id> | 打开手机、打开指定应用 |
click <app> <path> | 调用按钮的onClick |
shot | 调用ScreenCapture.CaptureScreenshot,即使游戏窗口在另一个 Wayland 工作区也能工作 |
dump <name> | 写出带矩形、旋转、颜色、字体的变换树 |
fakedeal | 在真实 NPC 上添加指南针元素 |
money | 记录现金与银行余额 |
已在游戏中验证(Verified in game):
- 堆叠上限:227 个物品中 133 个被提高;霰弹枪子弹是唯一保持不变的可堆叠物品;开关与实时配置重载均正常。
- 设置应用的开关与 +/- 按钮会写入配置并立即重新应用。
- 银行应用的存款与取款守恒(现金 1542→1442→1542,银行 0→100→0)。
- 通知能正常显示。
- 指南针标记显示绿环肖像、名字与游戏单位距离(如 "45ft"),已通过截图确认。
用户实测(Play-tested by the user):堆叠上限与银行应用(期间发现并修复了一个重复 bug,见陷阱 4)。
尚未在游戏中验证(Not yet verified):真实站点的就绪提醒、交易提醒、经销商转账(存档中无已招募经销商或活跃交易)、无尽滑板、以及实体 ATM 上的无上限补丁。
12 条陷阱(Gotchas):symptom → cause → fix
- froggy 原版 IncreasedStackLimit"能用"但送货忽略它。原因:它只后置补丁了实例 getter,而送货、购物车、死投递代码直接读取
BaseItemDefinition.StackLimit。修复:写入定义字段。 - 提高某些堆叠上限会引发 bug。(用户反馈而非本团队复现)弹药超过原版上限后行为异常。修复:绝不碰武器、弹药(从
RangedWeapon.Magazine检测)或上限为 1 的物品,且绝不降低任何上限。 - 自定义手机应用打开后滚动列表为空。原因:应用页面旋转 90°,
RectMask2D在画布空间裁剪,把所有内容都裁掉了。修复:改用模板Mask(一个 alpha 为 1、showMaskGraphic = false的Image)。 - 银行应用在真实点击时复制了金钱,尽管
onClick.Invoke()单独调用一次是对的。原因:被点击的 uGUIButton会成为选中对象,游戏的 Submit 输入随后再次触发onClick。修复:在 Mod 的按钮上设置navigation.mode = None,并对金钱操作加 0.3 秒防护。 NPC.MugshotSprite对无外观数据的 NPC 抛 NullReferenceException,且异常发生在 IL2CPP getter 内部。修复:用 try/catch 包裹。- 指南针标记下的名字与距离不可见。原因:30 像素高的
Quests容器带有RectMask2D。修复:向下扩展padding。 - Harmony 堆叠的
[HarmonyPatch]特性不会给出多个目标。修复:用TargetMethods()围绕每个ATMInterface方法遮盖每周总和。由于上限是 const,无法直接补丁;Mod 临时将ATM.WeeklyDepositSum换成巨大的负数,再恢复真实值并补上后续存款。 - MelonPreferences 在 Wine 下编辑后不重载
MelonPreferences.cfg。修复:category.SetFilePath(own file),约每秒轮询一次其 mtime,并调用LoadFromFile。 - 开发命令文件在 Wine 下执行了两次,因为删除滞后于读取。修复:记住文件的最后写入时间,跳过已处理过的文件。
- Steam 启动的 Proton 前缀
compatdata/3164500在首次 Steam 启动前不存在。全新的实验室前缀会在Saves/TempPlayer/下保存。在两者之间移动存档对主机无碍,因为PlayerManager.TryGetPlayerData会先为主机加载Player_0。 - 实验室是 Mono,但其中的旧 Mod 是 IL2CPP 构建(它引用了
Il2CppInterop与Il2CppScheduleOne.*)。后端不匹配的 Mod 会在加载时失败,因此信任已安装 Mod 前务必检查其引用。 - 每 2 秒出现约 200 ms 的规律性卡顿。原因:对七种站点/花盆类型使用
Object.FindObjectsOfType<T>()轮询,在此 IL2CPP 场景中每次遍历约耗时 190 ms。修复:改为遍历Property.OwnedProperties[i].BuildableItems,缓存每项的TryCast类型一次,每帧只检查 12 项。该问题由开发用性能探针发现(记录超过 150 ms 的帧与超过 2 ms 的特性)。修复后:加载后零卡顿。
资源与成本
- 图标:Lucide 的 SVG(ISC 协议),用
rsvg-convert渲染,再用 Pillow 合成到渐变方块上(PocketPlug 仓库的scripts/make-icons.py)。 - 指南针环与圆形遮罩:程序化绘制,无生成艺术。
- 成本与时间:约两个工作日会话,含侦察、两个 Mod 与游戏内测试循环;每次重启到进游戏的循环约 2 分钟。
开放问题(Open questions)
- Verification 一节中尚未验证的功能。
- 联机行为:两个 Mod 仅限单人。堆叠上限需要每个客户端配置一致。
ATMInterface.ProcessTransaction是一个协程,掩码就位后它是否还需要额外处理:它读取的是真实值,因此应当无需改动。
延伸阅读
- 本字段笔记在知识库索引中的位置:INDEX.md(
Schedule I条目,engineunity-il2cpp,routemanaged-patch,statusworking)。 - Unity 引擎通用手册:unity.md——含 IL2CPP 与 Mono 的识别、MelonLoader/BepInEx 安装与 Proton 启动选项、Harmony 补丁类型说明。
- 逆向工具链:reverse-engineering——IL2CPP 的 Cpp2IL/Il2CppDumper 流程。
- 验证方法论:oracles-how-agents-know-a-mod-works.md。
- 仓库知识库规则:knowledge/README.md 与 CONTRIBUTING.md。
【免费下载链接】universal-modder
Point Claude at any game. Skills, tools and the fal MCP that let Claude Code mod almost any PC game you own: recon, reverse engineering, fal-generated art/3D/audio, in-game testing, showcase videos.
相关推荐
免费 PhotoGIMP:GIMP 的 Photoshop 风格布局补丁安装指南
免费 PhotoGIMP:GIMP 的 Photoshop 风格布局补丁安装指南 PhotoGIMP 是一个由社区维护的免费 GIMP 界面补丁,面向习惯 Ad
bbport 文件 Mod 与 XML 补丁:CUSA03173 的 Mod 管理、补丁编译与 TAA 锐化实战
bbport 文件 Mod 与 XML 补丁:CUSA03173 的 Mod 管理、补丁编译与 TAA 锐化实战 bbport 项目为《血源诅咒》(Bloodb
universal-modder 的 Unity 游戏 Mod 实战指南:从引擎识别、BepInEx/Harmony 到 IL2CPP 攻防
universal modder 的 Unity 游戏 Mod 实战指南:从引擎识别、BepInEx/Harmony 到 IL2CPP 攻防 本文以 Unity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考