脚本加载与回滚
基本信息
| 属性 | 值 |
|---|---|
| 脚本扩展名 | .zs(ZenScript),另支持 .zip 打包 |
| 脚本引擎 | 外部依赖 com.github.GTNewHorizons:ZenScript:1.0.2-GTNH(dependencies.gradle:2,shadowImplementation) |
| 运行时包 | minetweaker/runtime/(14 个文件)+ runtime/providers/(6 个) |
| 日志文件 | logs/minetweaker.log(MineTweakerMod.java:101) |
| 编解码 | UTF-8(MTTweaker.java:134) |
| 调试开关 | MTTweaker.DEBUG 硬编码 false(MTTweaker.java:35) |
边界说明:ZenScript 词法/语法/运算符的实现位于外部仓
GTNewHorizons/ZenScript,不在本仓。本仓src/main/java下没有stanhebben包;buildSrc/src/main/java/stanhebben/zenscript/仅有注解与文档生成器(ZenScriptDoclet等)的副本。因此本页只描述 MineTweaker 侧如何调用引擎与如何管理脚本来源,ZenScript 语言本身的语法细节无法在本仓核实。
三级脚本来源与级联顺序
构造阶段只装一个来源(MineTweakerMod.java:104-109):scripts/ 目录(不存在则 mkdirs)。
服务器启动时替换为三级级联(MineTweakerMod.java:163-172):
| 构造参数 | 来源 | 变量 |
|---|---|---|
| 第 1 个 | IMC 传入(其他模组用 addMineTweakerScript 推送) |
scriptsIMC = new ScriptProviderCustom("intermod")(:107) |
| 第 2 个 | 游戏根目录 scripts/ |
scriptsGlobal(:108) |
| 第 3 个 | 世界存档目录下 scripts/(不存在则 mkdir) |
scriptsLocal(:164-167) |
级联实际按参数顺序的逆序执行。ScriptProviderCascade.MyScripterator 构造时 currentIndex = providers.length - 1(ScriptProviderCascade.java:42),advance() 里 currentIndex-- 逐级回退(:66-78)。即:
世界存档 scripts/ → 游戏根 scripts/ → IMC intermod
客户端建连时只有两级(无世界存档,MineTweakerMod.java:183-184):
游戏根 scripts/ → IMC intermod
服务器停止时回退为单来源 scriptsGlobal(MineTweakerMod.java:193)。
同名组去重
级联迭代器用 Set<String> executed 按 getGroupName() 去重(ScriptProviderCascade.java:33、:57-58、:72),先到先得:世界存档里的同名组会屏蔽全局同名组。MTTweaker.load 内部还有第二层同机制的 executed 集合(MTTweaker.java:117、:123-124)。
各 provider 的组名定义:
| 迭代器 | getGroupName() |
源码行 |
|---|---|---|
ScriptIteratorDirectory |
子目录名 | ScriptIteratorDirectory.java:41-42 |
ScriptIteratorSingle |
.zs 文件名(含扩展) |
ScriptIteratorSingle.java:30-31 |
ScriptIteratorZip |
zip 文件名去扩展名 | ScriptIteratorZip.java:48-49 |
目录与打包规则
ScriptProviderDirectory.getScripts()(ScriptProviderDirectory.java:28-56):
| 条目 | 处理 | 源码行 |
|---|---|---|
| 子目录 | 递归为 ScriptIteratorDirectory |
:33-34 |
*.zs |
ScriptIteratorSingle |
:35-36 |
*.zip |
ScriptIteratorZip,失败则 logError("Could not load ...") |
:37-42 |
| 其他 | 忽略 | — |
结果多于 1 个时按 getName() 字典序排序(ScriptProviderDirectory.java:45-56)。
zip 内部只收 scripts/ 前缀且以 .zs 结尾的条目(ScriptIteratorZip.java:39),条目名会剥掉 scripts/ 前缀(:63-64)。zip 根目录下没有 scripts/ 目录的包会被完全忽略,这是打包脚本包时最常见的坑。
单 provider 内部的目录也会再过滤一次 .zs(ScriptIteratorDirectory.java:69)。
加载流程
MTTweaker.load()(MTTweaker.java:111-179):
ZenModule.loadedClasses.clear()—— 清空引擎全局类表(:113)。ScriptProviderMemory.collect(scriptProvider)把全部脚本文本快照成scriptData,供后续分发给客户端(:116)。- 逐组迭代,跳过已执行组名(
:119-124)。 - 每组建独立
HashMap<String, byte[]> classes与IEnvironmentGlobal(:126-127)。 - 逐文件词法/语法分析,类名由
ZenModule.extractClassName(filename)从文件名推导(:137)。 compileScripts(...)编译后new ZenModule(classes, ...)并module.getMain().run()(:163-167)。
错误处理分级
单文件错误被捕获并记录,不中断整组加载:
| 异常 | 日志 | 源码行 |
|---|---|---|
IOException |
Could not load <name>: <msg> |
MTTweaker.java:143 |
ParseException |
Error parsing <file>:<line> -- <explanation> |
:146-148 |
其他 Exception |
Error loading <name>: <toString> |
:150 |
编译/执行阶段整组包在 catch (Throwable) 里,记 Error executing <group>: <msg>(:168-170)。
所有日志经 GlobalRegistry.MyErrorLogger 转发到 MineTweakerAPI.logError / logWarning / logInfo(GlobalRegistry.java:145-193),并最终落到 logs/minetweaker.log(FileLogger,api/logger/FileLogger.java,UTF-8,会剥离 § 格式码)。
动作应用与回滚
所有脚本副作用必须包成 IUndoableAction 交给 MineTweakerAPI.apply(...)(MTTweakerMod.java:101 之后各 apply 调用即此路径),由 MTTweaker.apply 统一处理(MTTweaker.java:49-71):
- 先
logInfo(action.describe())。 - 若该动作在
wereStuck集合中(上次回滚失败),打印WAS STUCK并跳过apply()(:54-60)。 - 否则若
getOverrideKey()命中stuckOverridable,把旧的卡死动作移出(:62-65),然后action.apply()。 - 追加进
actions列表。
rollback()(MTTweaker.java:78-104)逆序遍历 actions:
canUndo()为真 →logInfo(describeUndo())并undo()。- 为假 → 记 3 行日志(
[Stuck] 1/2/3),加入stuck与wereStuck;若getOverrideKey()非 null 还登记进stuckOverridable(:88-100)。
actions.clear() 后返回 stuck 列表。加载结束时若 wereStuck 非空,会把每条卡死动作再打一遍 Stuck: <describe>(MTTweaker.java:174-178)。
「卡死」动作指无法撤销的修改(如改方块硬度、改翻译),重载后仍留在游戏里 —— 这正是 game.lock() 存在的原因,见Minecraft 层 API。