脚本加载与回滚

基本信息

属性 值
脚本扩展名 .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):

  1. ZenModule.loadedClasses.clear() —— 清空引擎全局类表(:113)。
  2. ScriptProviderMemory.collect(scriptProvider) 把全部脚本文本快照成 scriptData,供后续分发给客户端(:116)。
  3. 逐组迭代,跳过已执行组名(:119-124)。
  4. 每组建独立 HashMap<String, byte[]> classes 与 IEnvironmentGlobal(:126-127)。
  5. 逐文件词法/语法分析,类名由 ZenModule.extractClassName(filename) 从文件名推导(:137)。
  6. 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):

  1. 先 logInfo(action.describe())。
  2. 若该动作在 wereStuck 集合中(上次回滚失败),打印 WAS STUCK 并跳过 apply()(:54-60)。
  3. 否则若 getOverrideKey() 命中 stuckOverridable,把旧的卡死动作移出(:62-65),然后 action.apply()。
  4. 追加进 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。

相关条目