- 游戏开发
- 插件系统
【免费下载链接】tModLoader
A mod to make and play Terraria mods. Supports Terraria 1.4 (and earlier) installations
tModPorter 是 tModLoader 官方仓库中随发行版一并发布的迁移辅助工具,它的唯一职责是帮助 Mod 开发者把因 tModLoader / Terraria API 变更而无法编译的旧代码自动改写为符合新 API 的代码。本文以仓库内 release_extras/tModPorter/README.md 为骨架,结合其源码实现,完整说明 tModPorter 的使用前置条件、运行方式、输出行为、备份机制与底层重写原理,读完后你可以安全、正确地用它完成一次 Mod 升级。
tModPorter 是什么:只修编译错误的"保守迁移器"
tModPorter 的目标很明确——帮助 Mod 跟上 tModLoader API 的变化(原文档第一句即为此定位)。它的设计哲学与普通"一键升级工具"不同,有三条核心原则:
- 只修编译错误:工具只处理导致编译失败的代码,不主动重构你风格良好的既有代码;
- 无错不动:如果项目没有编译错误,运行后不会产生任何改动;
- 随时可安全运行:因为改动是"最小干预"式的,你可以在迁移过程中的任意时刻反复运行它。
从源码看,这一原则被贯彻到了执行层面。入口 Program.cs 捕获所有异常并打印,随后等待按键退出;核心处理类 tModPorter.cs 在整个处理流程中不断上报进度,最终只统计changedDocs并汇报"变了几个文件、花了多长时间"。整个工具不会主动删除、移动或合并你的代码逻辑,只做语义层面的符号改写。
使用前置条件:先把 .csproj 升级到 1.4 格式
原文档明确列出了运行 tModPorter 之前必须满足的条件,缺一不可:
.csproj 必须是 1.4 格式。判断标准是文件中包含类似下面这一行:
<Import Project="..\tModLoader.targets" />如果你的 Mod 还是旧版(1.3.x 时代)的项目格式,需要先在tML Mod 开发菜单(tModLoader mod development menu)里使用升级按钮把 .csproj 升级到 1.4 格式,再运行 tModPorter。
.csproj 应位于 ModSources 文件夹中。这是 tModLoader 约定的 Mod 源码目录,确保能被 tModLoader 的构建目标(
tModLoader.targets)正确解析引用。先打开 Visual Studio 检查项目:确认项目没有任何"未解析引用"(unresolved references)警告后再运行工具。这一点尤其重要——从实现上看,tModPorter.cs 通过
MSBuildWorkspace加载项目,如果项目本身引用缺失,工作区加载阶段就会失败并直接抛出异常,根本无法进入重写环节。具备 .NET 运行环境:tModPorter 基于 Roslyn 编译平台实现。仓库中的 tModPorter.csproj 声明
TargetFramework为net10.0,依赖Microsoft.CodeAnalysis.CSharp.Workspaces、Microsoft.CodeAnalysis.Workspaces.MSBuild、Microsoft.Build.Locator以及UTF.Unknown等包,因此运行机器上需要有可用的 .NET SDK/运行时。Linux 启动脚本 tModPorter.sh 会优先使用DOTNET_ROOT,否则回退到~/.dotnet,找不到dotnet时会给出明确提示并退出。
运行方式:拖放 .csproj,或从命令行传入路径
原文档提供了两种交互方式,源码也完全支持:
- 拖放方式:把
.csproj文件直接拖到tModPorter.bat上运行; - 命令行窗口方式:先启动
tModPorter.bat,再把.csproj拖进命令行窗口回车。
对应的启动脚本位于仓库 release_extras/tModPorter/tModPorter.bat(Windows)与 tModPorter.sh(Linux/macOS)。两者最终都会调用同一入口——Windows 脚本通过../start-tModLoader.bat -tModPorter %*把参数透传给 tModLoader 主程序,Linux 脚本则直接执行$DOTNET_PATH tModLoader.dll -tModPorter "$@"并把输出同时写入tModLoader-Logs/tModPorter.log。也就是说,tModPorter 实际上是作为 tModLoader 的一个内置命令行子命令(-tModPorter)存在的。
入口 Program.cs 的路径解析逻辑值得注意:
- 取命令行最后一个参数作为项目路径;
- 自动把路径扩展名强制替换为
.csproj(即使你传了.sln或没有扩展名); - 如果该路径不存在,会进入循环交互提示:"Enter the path to the .csproj of the mod you want to port:",直到输入一个真实存在的
.csproj路径。
处理过程中,控制台会实时显示进度:[####------] Pass 1, 3/15之类的进度条与文件计数(见 Program.cs)。Linux 下若没有终端(如双击脚本),启动脚本会自动尝试用konsole、gnome-terminal、xterm或 macOS 的 Terminal.app 打开新窗口运行。
执行过程:多 Pass 语义重写与进度汇报
真正干活的是 tModPorter.cs 中的Process方法,它实现了一套多 Pass 迭代重写循环:
- 加载项目后,先删除项目
obj目录(避免陈旧生成缓存干扰 Roslyn 语义分析,删除失败仅警告不中断,见 tModPorter.cs); - 每一 Pass 内,所有文档并行执行一次重写(
Task.Run并发处理各.cs文件); - 由于更新语义模型(semantic model)是最昂贵的操作,一旦某个文档语法树被改写,它的语义模型就失效了——所以只要有任何文档发生变化,本 Pass 就结束,下一 Pass 只重跑"本轮被改过的文档",直到某轮没有任何文档再变化;
- 全部文档稳定后,再对"从未变化过的文档"补跑一轮,检查跨文档依赖(例如一个文件里对另一文件内部符号的引用),确保不留死角;
- 每轮 Pass 都会汇报
Pass N, X/Y形式的进度。
这个循环保证了重写是"收敛"的:不会出现 A 改完、B 又基于旧语义改一遍导致连锁失效的情况,也解释了为什么 README 说"可以随时安全运行"——每一轮都是全量语义重算后的稳定输出。
备份机制:.bak 文件与 Git 检测
原文档最后一条规则是:tModPorter 会为每个被修改的文件生成.bak备份,除非.csproj的某个父目录中存在.git文件夹。
源码实现印证了这条规则(tModPorter.cs 与 tModPorter.cs):
MakeBackups默认值由IsUnderGit(projectPath)决定——IsUnderGit会从 .csproj 所在目录逐级向上递归查找.git目录(tModPorter.cs);- 生成备份时,若
xxx.cs.bak已存在,自动递增为xxx.cs.bak2、xxx.cs.bak3……绝不会覆盖旧备份; - 备份采用"先改名再写新文件"的策略:原文件被移动为
.bak,随后以原路径写入改写后的新内容。
对使用 Git 管理源码的 Mod 来说,改动历史由版本控制系统负责,无需额外备份;对没有 Git 的目录,.bak文件就是你回退的最后保障。
另一个细节是文件编码保护:写入前会用CharsetDetector(UTF.Unknown库)探测原文件编码,置信度低于 95% 时给出警告,随后以探测到的编码原样写回(tModPorter.cs),避免中文注释等非 UTF-8 内容被写坏。
底层原理:Roslyn 语法树 + 语义模型 + 重写器管线
tModPorter 不是简单的文本替换,而是基于 Roslyn(.NET 编译器平台)的语法树级语义重写。核心结构在 Config.cs,它按固定顺序串联了 7 个重写器:
| 重写器 | 职责 |
|---|---|
HookRewriter | 改写 Mod 钩子方法(override)的签名、参数、返回类型、访问修饰符,并同步改写方法体内对base.XXX(...)的调用参数 |
RenameRewriter | 依据重命名表批量重命名类型、方法、字段、命名空间 |
MemberTypeRewriter | 改写成员(字段/属性)的类型 |
MemberUseRewriter | 改写成员访问的使用方式(如把方法调用改属性、把布尔字段改 ID 常量等) |
InvokeRewriter | 改写方法调用(如Item.NewItem、SoundEngine.PlaySound等签名变化的调用) |
RecipeRewriter | 改写合成配方(Recipe)相关 API |
HookGenRewriter | 处理 HookGen 生成代码的重写 |
所有重写器继承自 BaseRewriter.cs,它基于CSharpSyntaxRewriter遍历语法树,并通过GetSemanticModelAsync()取得语义模型来判断每个符号的真实类型、是否为无效符号(IInvalidOperation)或已过时(IsObsolete)——这正是"只修编译错误"的实现根基:只有语义层面解析失败(invalid)或标记为 obsolete 的调用才会被重写(见 BaseRewriter.cs)。
以 HookRewriter.cs 为例,钩子签名改写会做三件事:按基类新签名重排参数列表、修正返回类型、补齐override与访问修饰符;若钩子在新版本中已删除,还会在方法上附加一行注释说明替代方案(HookRemoved)。改写表本身集中在 Config.ModLoader.cs 与 Config.Terraria.cs 两个分部类中,你可以直接查看当前版本 tModPorter 支持的完整迁移清单。
迁移效果示例:从重命名表看典型改动
为了直观理解 tModPorter 会帮你做什么,这里从源码中的重写表摘录几类典型迁移(完整清单见 Config.ModLoader.cs 与 Config.Terraria.cs):
1. 字段命名规范化(小写 → 大写属性风格)
// 旧写法(1.3.x 时代) public class MyItem : ModItem { public override void SetDefaults() { item.width = 32; // item 字段 item.maxStack = 99; } }tModPorter 会把ModItem.item重命名为Item、ModNPC.npc重命名为NPC、ModPlayer.player重命名为Player、ModProjectile.projectile重命名为Projectile(对应 Config.ModLoader.cs 的RenameInstanceField表),同时把Terraria.Item.modItem、Terraria.NPC.modNPC等反向字段也一并更新。
2. 类型整体改名
// ModWorld → ModSystem public class MyWorld : ModSystem { ... } // ModHotKey → ModKeybind public class MyKeybind : ModKeybind { ... } // ModMountData → ModMount public class MyMount : ModMount { ... }对应 Config.ModLoader.cs 的RenameType表,继承关系、引用处的类型标识都会被同步改写。
3. 钩子方法改名 + 注释引导
// NPCLoot → OnKill,PreNPCLoot → PreKill public override void OnKill(NPC npc) { ... } // 若某个钩子在新版本中被移除,会自动加上提示注释: // Note: Removed. Spawn the treasure bag alongside other loot via npcLoot.Add(ItemDropRule.BossBag(type))对应 Config.ModLoader.cs 的RenameMethod与HookRemoved机制。注意HookRewriter在管线中被刻意放在RenameRewriter之前(Config.cs 的注释),因为类型改名会改变钩子签名匹配,顺序颠倒会导致参数重命名被跳过。
4. 方法调用 → 属性(Terraria.Tile 大量案例)
// 旧:tile.active() → 新:tile.HasTile // 旧:tile.lava() → 新:tile.LiquidType == LiquidID.Lava // 旧:tile.slope() → 新:tile.Slope // 旧:tile.wire2() → 新:tile.BlueWire对应 Config.Terraria.cs 的GetterSetterToProperty/GetterToProperty表——这是 1.4 重构中改动面最大的部分,手改极易遗漏,交给工具处理最稳妥。
5. 伤害/暴击字段 → 伤害类 API
// 旧:player.meleeDamage *= 1.1f; // 新:player.GetDamage(DamageClass.Melee) += 0.1f; // 旧:item.melee = true; // 新:item.DamageType = DamageClass.Melee;对应 Config.Terraria.cs 的DamageTypeField与DamageModifier规则,还会附带 "Consider MeleeNoSpeed" 之类的建议注释。
质量保障:自动测试与"编译验证"闭环
tModPorter 的可靠性并非空口无凭,仓库内置了完整的自动测试工程 tModPorter.Tests:
- AutomaticTest.cs 会把测试数据源中的每个
.cs文件反复执行RewriteOnce,直到输出稳定,与期望输出比对; - AutomaticTest.cs 的
ExpectedModCompiles测试会加载Expected.csproj并调用 Roslyn 编译,强制要求迁移后的产物能够编译通过——这正是工具核心目标的自动化验证; ProjectWideRefactor测试则模拟真实的多文档项目级重写场景,覆盖跨文件依赖(AutomaticTest.cs)。
测试数据位于 tModPorter.Tests/TestData,里面是成对组织的"迁移前源码 / 期望输出",如果你想精确了解某个 API 会被改写成什么样,直接翻阅对应测试数据是最权威的方式。
使用建议与注意事项
综合原文档与源码,给出如下实操建议:
- 先升级 .csproj 再运行:1.4 格式(含
<Import Project="..\tModLoader.targets" />)是硬性前提,旧格式项目会直接加载失败; - 运行前保证项目可解析:在 Visual Studio 里确认没有未解析引用警告;源码会在
EnsureTypesResolved阶段对无法解析的类型直接抛错并提示检查TargetFramework与项目引用(HookRewriter.cs); - 有 Git 就用 Git:工具本身已按 Git 存在与否自动决定是否生成
.bak,但强烈建议在运行前先提交一次基线,便于逐条 review 改动; - 运行后仍需人工 review:tModPorter 只解决编译错误。它会在无法自动改写的地方插入注释(如 "Suggestion: ..."),这些注释指向的新 API 用法需要你按注释与官方迁移指南手工补完逻辑,例如被移除的
Mod.Properties、ModTile.torch(改用TileID.Sets.Torch)、NPCSpawnInfo.PlanteraDefeated(改用NPC.downedPlantBoss && Main.hardMode)等场景(见 Config.ModLoader.cs); - 可反复运行:多 Pass 收敛式设计保证工具幂等安全,一次迁移后如果又改了代码引发新错误,再次运行即可。
小结
tModPorter 是 tModLoader 生态中"API 迁移"这一环节的官方解决方案:以 .csproj 为输入单元,以 Roslyn 语义分析保证只改编译错误,以多 Pass 循环保证收敛,以.bak/Git 双轨机制保证可回退。对 Mod 作者而言,每次 tModLoader 大版本更新后的升级流程可以简化为:升级 .csproj → 运行 tModPorter → 按注释人工补完 → 编译验证,其中机械性的重命名与签名改写全部交给工具完成。如果你正在维护一个经历了 1.3 → 1.4 迁移的 Mod,README.md 给出的这四条规则,加上 Config.ModLoader.cs 与 Config.Terraria.cs 里的完整迁移表,就是你最值得收藏的参考资料。
- 游戏开发
- 插件系统
【免费下载链接】tModLoader
A mod to make and play Terraria mods. Supports Terraria 1.4 (and earlier) installations
相关推荐
Sketch批量重命名利器 - Rename It插件
Sketch批量重命名利器 Rename It插件 在设计工作流程中,保持文件和图层的良好组织性是提高效率的关键步骤。Rename It是一款专为Sketch设
游戏开发插件系统nxadm/tail 文件跟踪库全解析:从 ChangeLog 看 Go 日志跟随库的关键演进与核心实现
nxadm/tail 文件跟踪库全解析:从 ChangeLog 看 Go 日志跟随库的关键演进与核心实现 导读:本文以 Cilium 仓库所 vendored
云原生网络服务网格可观测性网络安全eBPF终极指南:如何使用tModLoader解锁Terraria的无限可能
终极指南:如何使用tModLoader解锁Terraria的无限可能 tModLoader作为Terraria游戏最强大的开源模组加载器,为玩家打开了通往无限创
游戏开发插件系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考