Voidkit 套件的操作审计模组 —— CoreProtect 风格的方块 / 容器 / 实体操作记录, 专为科技整合包(机械动力 Create / Mekanism / AE2)设计。
项目还在测试版(beta),遇到任何问题都欢迎来群里找我们。
群名:【MC-玩了X年服务器终于不想玩了】
一键加群 → 498700382
这是 Voidkit 的 Bug 维修群。 装不上、启动崩、模块不生效、配置改了不生效、和别的 mod 冲突 —— 都来群里说。 报 bug 请附上:版本线(如 1.21.1 Forge)、服务端日志、相关配置。
从 Voidkit 套件独立发布,但仍依赖
voidkit_core作为前置模组。 纯服务端模组,无客户端 jar。
三条版本线挂在同一个 release 下,按文件名后缀区分 —— 下载前先看清自己的环境:
| Minecraft | 加载器 | 文件 |
|---|---|---|
| 1.21.1 | NeoForge 21.1.x | voidkit_inspector-0.1.0-beta.1+mc1.21.1-neoforge.jar |
| 1.21.1 | Forge 52.1.x | voidkit_inspector-0.1.0-beta.1+mc1.21.1-forge.jar |
| 1.20.1 | Forge 47.4.x | voidkit_inspector-0.1.0-beta.1+mc1.20.1-forge.jar |
→ KokuuWork/voidkit-inspector Releases
⚠️ 必须同时安装voidkit_core,且 core 的版本线要与本 jar 一致。 缺 core 时 mod 会被加载器静默跳过 —— 服务端照常启动,只是没有审计, 非常难发现。core 从KokuuWork/voidkit的 release 下载对应的那一个 (NeoForge 版与 Forge 版的 jar 内含不同加载器 API,不能互相顶替)。
普通查插件在科技包里会翻车:一条自动化产线刷几百万圆石、产几万铁锭,全记进数据库, 结果真正的破坏行为被淹没在海量噪音里,查不出来。Inspector 从架构上解决了两件事:
① 记录面:来源分类(该记的记) 先把「是谁干的」认出来,再决定记不记 —— 机械臂 / 管道 / 假玩家驱动的操作自动识别、默认不记, 一条 Create 产线刷几百万圆石进不了库,那一下真正的破坏行为才查得出来。
② 恢复面:智能回滚五层(不只是「查」,还能「退回去」) 无脑回滚在科技包里必然翻车:补回一个 Create 传动轴,应力网络照样断;补回 AE2 线缆, ME 网络照样瘫。所以回滚做了五层设计(见下文),核心原则是 宁可不回滚,也不留下半扇门 / 半条产线。
| Minecraft | 加载器 | 分支 | jar |
|---|---|---|---|
| 1.21.1 | NeoForge 21.1.x | main |
voidkit_inspector-<版本>+mc1.21.1-neoforge.jar |
| 1.21.1 | Forge 52.1.x | mc/1.21.1-forge |
voidkit_inspector-<版本>+mc1.21.1-forge.jar |
| 1.20.1 | Forge 47.4.x | mc/1.20.1 |
voidkit_inspector-<版本>+mc1.20.1-forge.jar |
- 本分支
main对应 1.21.1 + NeoForge;其它版本线见上表。 - 所有版本线的 jar 挂在同一个 release 下,按文件名后缀区分,勿混用。
- Inspector jar 必须与
voidkit_core同版本线(mods.toml有强制依赖检查)。
科技整合包里绝大多数方块 / 容器变化是自动化产线干的,不是玩家。Inspector 的灵魂是 来源闸门 —— 三类来源,各有各的默认处置:
| 来源类别 | 判定方式 | 默认 |
|---|---|---|
PLAYER 真人玩家 |
有网络连接的真 ServerPlayer |
记 |
AUTOMATION 自动化 |
官方 FakePlayer 类、或 ServerPlayer 但无网络连接(模组自造假玩家) |
不记 |
NATURAL 自然 |
树叶腐烂 / 流体 / 重力方块 / 生物 | 不记 |
OTHER 无法归类 |
兜底 | 不记 |
⚠️ 不能用connection == null当唯一判据:官方FakePlayer自带一个FakeConnection,connection 不是 null,必须先判Platform.isFakePlayer拦掉。 判定已收口到 core 的platform层 —— NeoForge / Forge 有官方类,Fabric 没有。
强制记录名单(alwaysLogSources,# 前缀):即便来源类别被关也照记 ——
苦力怕爆炸、TNT、火蔓延、岩浆流动、末影人搬方块这些「没有真人玩家但要追责」的事件。
自动化来源名取方块实体命名空间,日志里能直接看出 #create / #mekanism / #ae2。
| 事件 | 默认 | 动作码 |
|---|---|---|
| 玩家破坏方块(含方块实体 NBT) | 记 | BREAK |
| 玩家放置方块 | 记 | PLACE |
| 玩家点火 / 放岩浆 | 记 | PLACE_FIRE |
| 容器拿取 / 放入(开合快照 diff) | 记 | CONTAINER_TAKE / CONTAINER_PUT |
| 玩家击杀实体 | 记 | KILL |
| 苦力怕 / TNT / 凋灵 / 恶魂爆炸 | 记(走强制名单) | BREAK |
| 末影人搬方块 | 记(走强制名单) | PLACE |
| 整链回滚执行 | 记(审计回滚本身) | ROLLBACK |
| 玩家丢出 / 捡起物品 | 不记(太吵) | DROP / PICKUP |
| 自动化产线的一切操作 | 不记 | — |
| 树叶腐烂 / 流体 | 不记 | — |
| 机制 | 做法 |
|---|---|
| 写入 | 主线程只做 O(1) 入队;唯一写线程批量事务落库(WAL);队列满自动丢弃,绝不卡 TPS |
| 查询 | 独立查询线程池 + 各自读连接,与写入并行,不占主线程、不卡磁盘 |
| 雪崩闸门 | 单 tick 入队上限 maxEventsPerTick(2000)+ 单来源每秒上限 maxPerSourcePerSecond(500) |
| 存储 | 嵌入式 SQLite 零配置单文件;方块 / 物品 / 维度 / 玩家名全字典化,行小而密 |
| 分页 | 复合索引 + keyset 分页,几十万条翻页毫秒级(不像老插件查一次遍历整表) |
| 完整 NBT | 模组方块实体、带组件的物品都存全量 NBT |
对应 voidkit issue #8。这是 Inspector 从「查插件」变成「能退回去的关键」。
无脑回滚必然翻车,所以先把每条记录分类,给不同类别不同策略。 判定依据完全来自审计记录本身(不依赖世界状态):
| 类别 | 判据 | 默认处置 |
|---|---|---|
NORMAL 普通方块 |
无方块实体 NBT,且命名空间不在机器名单 | 直接还原 |
RESTORE_NBT 容器 / 含数据方块 |
带 NBT 且 NBT 的 id 是 minecraft: 命名空间 |
还原方块 + 恢复 NBT |
MACHINE 机器 / 装置 |
带 NBT 但命名空间是模组的(Create / Mek / AE2…),或无 NBT 但命名空间在机器名单(线缆 / 传动件 / 管道) | 默认跳过,--include-machines 才动 |
补回一个 Create 传动轴,应力网络照样断 —— 所以机器默认不碰。
对「属于更大结构」的方块,要么整结构一起回滚,要么整结构跳过,绝不回滚一半:
- 双格方块成对:门(
half=upper/lower)、床(part=head/foot)占两格。 两半的记录都在同一批里才回滚;缺另一半的保守跳过 —— 宁可不回滚, 也不留下半扇门 / 半张床。 - 模组机器连通体:被跳过的机器 / 线缆按 6 向邻接聚类成连通体(同命名空间或同方块实体
类型才算连通),预览按连通体聚合输出(例如「create 连通体 ×3(共 17 格,已整体跳过)」),
让服主看清整结构规模而不是逐格噪音。
--include-machines时整连通体一起回滚。
聚类 / 配对都是纯函数,不碰世界状态、不碰主线程,可直接 JUnit 测试。
违规破坏往往不是一次动作,而是一条链:拆箱子 → 拿东西 → 放 TNT → 炸。 单条记录只能回答「谁动了这个方块」,回答不了「这次破坏的全貌」。
聚类规则(同一玩家):
- 时间断链:相邻两次操作间隔超过
chainIdleMinutes(默认 5 分钟) - 空间断链:新事件与链上最近事件距离超过
chainRadiusBlocks(默认 32 格) - 维度断链:换维度必然是新链(跨维度谈距离没有意义)
每条链带 chain:<玩家>:<链id> 编号,可一键回滚整条链。
preview 与 rollback 用同一批数据(都受 maxRollbackEvents / maxRollbackRadius 约束),
预览有效期 rollbackPreviewTtlSeconds(默认 5 分钟),超时必须重新预览 —— 防「预览小的、执行大的」。
执行按 rollbackBlocksPerSecond(默认 2000)分帧推进,永不卡主线程。
回滚把世界从 A 改成 B,要「撤销」就得知道 A 长什么样 —— 而审计库存的是「玩家干了什么」, 不是「回滚那一刻世界什么样」,且两次操作之间可能隔着一次重启。
所以每次回滚执行时,把每个目标的回滚前原始状态落进 SQLite 的 rollback_journal 表,
跨重启可用。约束:
- 每个回滚只能撤销一次(journal 侧
UPDATE … WHERE undone=0原子抢占) - 只允许撤销
rollbackUndoWindowMinutes(默认 30 分钟)内执行的回滚 —— 间隔越久,回滚后世界上的新改动越多,撤销越容易误伤合法改动 - 撤销本身也进审计(
UNDO来源 +ROLLBACK动作码),谁撤的、撤了多少,一清二楚
命令前缀 /inspect,别名 /coi、/ins。权限节点统一 voidkit.inspector。
| 命令 | 说明 |
|---|---|
/inspect |
切换检查模式(左键 / 右键点击方块查看历史,再输一次关闭) |
/inspect on / off |
显式开关检查模式 |
/inspect here |
查看准星对准方块的历史 |
/inspect lookup u:玩家 r:半径 t:1h a:place b:石头 |
参数查询,结果弹箱子 GUI |
/inspect lookup ... --text |
退回文字输出(方便控制台 / 复制) |
/inspect page |
翻页(keyset 分页) |
/inspect status |
数据库状态(队列深度 / 已写 / 丢弃 / 待审) |
/inspect purge <天> |
清理 N 天前的记录(分批删,不长时间锁库) |
/inspect preview <filters> |
回滚预览:按分类 / 结构 / 连通体聚合输出,不改世界 |
/inspect rollback <filters> |
执行回滚(需先 preview 且未过期) |
/inspect rollback --include-machines |
连模组机器一起回滚(默认跳过) |
/inspect rollback --undo [jobId] |
撤销最近一次回滚(缺省 = 最近一次) |
/inspect chain <玩家> [filters] |
列出该玩家的操作链 |
/inspect rollback chain:<玩家>:<链id> |
整链回滚 |
查询参数:u: 玩家、r: 半径、t: 时间(1h/1d)、a: 动作(place/break/open/kill)、
b: 方块/物品 id。
命令分查询与改数据两类,节点分开 —— 给版主「查」的权限,不等于让他能回滚世界或清库。
| 节点 | 覆盖的命令 | 说明 |
|---|---|---|
voidkit.inspector |
/inspect /inspect on|off /inspect here /inspect lookup /inspect page /inspect chain /inspect status |
只读:查看历史 |
voidkit.inspector.rollback |
/inspect preview /inspect rollback |
会改世界(回滚) |
voidkit.inspector.purge |
/inspect purge <天> |
会删历史数据 |
- 三个命令名等价:
/inspect(别名/coi/ins),别名逐条容错——整合包里撞名只跳过这一个,不连累另外两个。 ⚠️ 升级注意:voidkit.inspector.rollback/voidkit.inspector.purge是新增节点。 本次之前所有子命令都只由voidkit.inspector把守,如果你的服依赖 「有voidkit.inspector就能回滚」,升级后需要额外授予这两个。 这是刻意的:查看历史与改写世界不该是同一个权限。- 没装
voidkit_perm时按原版 OP 判定(只有 OP 能用)。审计权限敏感, 刻意不并入voidkit.admin—— 应该单独授予,而不是「给了管理总权限就顺带拿到审计与回滚」。 ⚠️ 「权限不足」时 Brigadier 报的是Unknown command,报错完全看不出是requires没通过。玩家说「命令不存在」时,先查权限节点,而不是查注册逻辑。here/on|off/lookup/page/chain/rollback/preview都需要玩家实体: 在控制台、RCON、命令方块里执行会得到「仅玩家可用」(RCON 读不到这条,因为 RCON 只在命令返回 0 之外才回包 —— 见下)。status与purge没有这个限制。
四层,换加载器只碰 platform/:
org/voidkit/inspector/
├── InspectorFeature.kt 业务主体:判定 + 落库 + 命令 + API(零加载器 import)
├── Source.kt 来源分类,经 core 的 Platforms.current 判自动化
├── InspectorDb.kt SQLite:队列 / 批量写 / 保留期清理
├── RollbackJob.kt 回滚执行器(分帧 + 预览复用)
├── RollbackPolicy.kt 第 1 层:分类感知
├── StructureSense.kt 第 2 层:结构感知(双格 + 机器连通体)
├── ChainCluster.kt 第 3 层:因果链聚类
├── UndoJournal.kt 第 5 层:回滚快照与撤销
├── AuditMenu.kt 检查模式 GUI
└── platform/
└── neoforge/ ← 唯一 import 加载器 API 的地方
├── InspectorEvents.kt 12 个事件适配,只解包转调
└── VoidkitInspectorNeoForge.kt @Mod 入口
Source.kt 不用自己造工厂 —— 它直接复用 core 的 Platforms.current.isFakePlayer
/ isUnconnectedPlayer,因为 core 已经把加载器差异收口了。
| 端点 | 说明 |
|---|---|
GET /api/inspector |
状态:各记录开关、保留天数、队列深度、已写 / 丢弃 / 待审、检查模式人数 |
GET /api/inspector/lookup?world=&x=&y=&z=&r=&actor=&action=&material=&limit= |
面板查询,返回 nextCursor + 行数组(含 hasNbt 标记) |
落 voidkit/inspector/config.json(JSONC,带完整中文注释)。核心几项:
| 配置 | 默认 | 说明 |
|---|---|---|
databaseFile |
inspector/inspector.db |
SQLite 文件路径,相对 voidkit/ 根目录 |
retentionDays |
30 | 记录保留天数,0 = 永久 |
logPlayer |
true |
记录真人玩家 |
logAutomation |
false |
记录自动化来源 —— 科技包数据量头号来源,默认关 |
logNatural |
false |
记录自然来源 —— 几乎全是噪音 |
alwaysLogSources |
#creeper #tnt #fire #lava #enderman #wither #ghast #lightning |
强制记录名单(关类别也照记) |
logItemDropPickup |
false |
记录丢 / 捡物品 —— 太吵 |
blockBlacklist / itemBlacklist / worldBlacklist |
空 | 黑名单,即便真人操作也不记 |
maxEventsPerTick |
2000 | 单 tick 入队上限,防雪崩闸门 |
maxPerSourcePerSecond |
500 | 单来源每秒上限,兜底防「模组冒充真人」刷爆库 |
queueCapacity |
100000 | 写队列容量,满了丢弃而非阻塞主线程 |
queryThreads |
2 | 查询线程池大小 |
rollbackEnabled |
true |
智能回滚总开关 |
maxRollbackEvents / maxRollbackRadius |
10000 / 50 | 单次回滚的事件数 / 半径上限,防手滑 |
rollbackMachineNamespaces |
create mekanism ae2 appliedenergistics2 immersiveengineering thermal |
机器命名空间名单 |
rollbackPreviewTtlSeconds |
300 | 预览有效期 |
rollbackUndoEnabled |
true |
回滚撤销总开关 |
rollbackUndoWindowMinutes |
30 | 撤销时间窗 |
chainIdleMinutes / chainRadiusBlocks |
5 / 32 | 因果链的时间 / 空间断链阈值 |
inspector/
├── config.json 带中文注释的全部配置
└── inspector.db 审计记录 + 回滚 journal(SQLite WAL)
数据只在 voidkit/ 下,不散落。
./fetch-core.sh # 从 voidkit Release 下载 voidkit_core jar到 libs/
# 默认按当前分支自动推断版本线(MC 版本 + 加载器)
./build.sh build # →build/libs/voidkit_inspector-<版本>+mc<版本>-<加载器>.jar
./build.sh runServer # 开发服务端(build/devserver/,已预置离线+创造+OP+RCON)
./build.sh runClient # 开发客户端(直连 127.0.0.1,自动以 VoidkitTester 身份进服)三条版本线的构建配置各不相同(代码逐行相同,差的只有构建与平台层):
| 分支 | MC / 加载器 | 构建插件 | Gradle | 发布 jar 来自 | 运行时映射 | JDK |
|---|---|---|---|---|---|---|
main |
1.21.1 NeoForge 21.1.x | ModDevGradle | 8.12.1 | jar |
官方映射,不reobf | 21 |
mc/1.21.1-forge |
1.21.1 Forge 52.1.x | ForgeGradle 7 + jarjar |
9.3.1 | jarJar |
官方映射,不 reobf | 21(配置期还解析 25) |
mc/1.20.1 |
1.20.1 Forge 47.4.x | ModDevGradle legacyforge |
8.12.1 | reobfJar |
SRG,要重混淆 | 17 |
-
core 前置是
compileOnly libs/下的 jar(由fetch-core.sh下载), 不是组合构建 —— Inspector 只需要 core 的 12 个 API 符号,不需要源码级联编译。 -
全 Kotlin,注释讲「为什么」;零 mixin;中文注释。
-
SQLite 驱动 jarJar 进本 jar(
strictly '[3.46,)'只锁下界,让 FML 仲裁)。 开发运行时runServer从 classes 加载、jarJar 不生效,所以additionalRuntimeClasspath补了 SQLite 原生库与 Kotlin 运行时 —— 发布 jar 里不要重复补,会和 core 的 jarJar 撞成两个同名模块,JPMS 直接ResolutionException。 -
测试:
./gradlew test——ChainCluster/RollbackPolicy/StructureSense都是纯数据计算,不需要起 MC 服务端。 三条版本线实测均 39 个测试全绿(build/test-results/test/*.xml)。 -
🚨
libs/里的 core jar 必须与当前分支的 MC 版本一致。libs/*.jar被.gitignore排除,切分支后它往往还留着上一条线的 core,拿错线的症状分两种:- Gradle 路径:拿 1.21.1 的 core 去编译 1.20.1 线 →
Cannot inline bytecode built with JVM target 21 into ... JVM target 17; - 离线脚本路径更隐蔽:脚本固定用 JDK 21 +
-jvm-target 21,字节码版本检查被 绕过去,只会看到 core 的包「unresolved reference」,极易误判成 classpath 缺件。
各线 core 从 kokuucore 同名分支的
core/build/devlibs/取:+mc1.21.1-neoforge(main)/+mc1.21.1-forge(mc/1.21.1-forge)/+mc1.20.1-forge(mc/1.20.1)。tools/offline-test.sh启动时会校验并在不匹配时告警。 - Gradle 路径:拿 1.21.1 的 core 去编译 1.20.1 线 →
-
Gradle 走不通时的备用测试路径:
./tools/offline-test.sh(三条版本线都支持,实测各 39 个测试全绿,与./gradlew test结果一致)。 它绕开 Gradle,直接用kotlin-compiler-embeddable+ JUnit Launcher 编译并跑测试。 当createMinecraftArtifacts卡在 NeoForm Runtime 下载、--offline也救不回来时用它。 脚本会自动认出当前是哪条线(1.20.1 / 1.21.1 NeoForge / 1.21.1 ForgeGradle 7), 并自动挑选对应的 MC 制品与 Kotlin 编译器版本 —— 三条线的 classpath 形状完全不同, 手拼必踩。注意:脚本里的-jvm-target 21与 JDK 21 只是编译/运行工具链, 与发布 jar 的 jvm-target 无关(那个由 Gradle toolchain 负责)。
比脚本 E2E 更强的验证:起真客户端进真服,真手放/破方块,然后直接读审计库。
./gradlew runServer # 先起,等日志出现 "Done (…)" + "审计数据库就绪"
./gradlew runClient # 原版 1.21.1 客户端,自动连 127.0.0.1(离线账号 VoidkitTester)build/devserver/ 的配置由 Gradle 的 syncDevServerConfig 在启动前写入
(离线 + 创造 + 超平坦 + VoidkitTester 给 level 4 + RCON 25575)。
不能手工改 server.properties / ops.json —— 服务端启动时会把它们读进来再写回去,
手改必被覆盖。
验证命令链路不必手敲键盘,走 RCON 更可靠:
python tools/rcon.py "coi status"
python tools/rcon.py "coi here"
python tools/rcon.py "coi preview actor=VoidkitTester"requires 判定(core 的 VanillaPermissionProvider.admin
对无玩家实体的命令源直接放行),所以它能证明「命令注册 + 参数解析 + 执行」都正常,
但证不了玩家权限。验玩家权限必须回真客户端里敲。
原版 RCON 只在命令返回值 > 0 时才发响应包,返回 0(失败)时一个字节都不回。 所以 RCON 只能验「有没有输出」,不能验「成功还是失败」——
tools/rcon.py靠超时兜底并打印「命令无输出」而不是抛栈。
顺带记一笔最容易误判的现象:权限不足时 Brigadier 报的是
Unknown command, 完全看不出是requires没通过。加新命令时若「玩家说命令不存在」, 先确认.requires(...)的权限节点,而不是查注册逻辑。
- core 必须放
gameDirectory/mods/,不能靠additionalRuntimeClasspath: 后者只提供类,FML 仍认为该 mod 未安装,报Mod voidkit_inspector requires voidkit_core … Currently, voidkit_core is not installed。runServer会扫mods/,放进去才被当合法 mod 加载。 - jarJar 与
additionalRuntimeClasspath只能二选一:core jarJar 里已含一份kotlin-stdlib,classpath 上再补一份,JPMS 看到两个同名模块直接拒绝解析 ——ResolutionException: Module neoforge.coremods reads more than one module named kotlin.stdlib。additionalRuntimeClasspath只补SQLite 原生库。 RunModel只能挂sourceSet,不能挂外部 jar(ModModel只有getModModSourceSets());清空内置 mod 用loadedMods.set([] as Set), Groovy 的SetProperty没有clear()。
写新版本线时几乎必然会再遇到这几个,每一个都是编译过、真机才炸:
-
1.20.x↔ 1.21.x 的数据模型断层(不是 API 改名,是模型换了):
用途 1.21.x 1.20.1 物品附加数据 DataComponents/stack.componentsNBT stack.tag保存方块实体 be.saveWithFullMetadata(registry)be.saveWithFullMetadata()还原方块实体 BlockEntity.loadStatic(pos, state, tag, registry)loadStatic(pos, state, tag)从 NBT 还原物品 ItemStack.parse(registry, tag).orElse(null)ItemStack.of(tag)同类物品判定 isSameItemSameComponentsisSameItemSameTagsGUI 物品名/lore stack.set(DataComponents.CUSTOM_NAME, …)+ItemLore手写 NBT display子标签 +Component.Serializer.toJson改动集中在
Nbt.kt/AuditMenu.kt/RollbackJob.kt/UndoJob.kt四个文件, 业务层与平台层都不用动 —— 这就是四层架构的回报。 -
reobfJar的产物目录不是build/libs/? 恰恰相反:ModDevGradle 下reobfJar产build/libs/(发布态,SRG 名),jar产build/devlibs/(开发态,官方名)。 装错目录 = 装错形态,正式服直接类找不到。 -
reobfJar不继承jar上设的archiveFileName,不显式配会退回默认名 (丢掉版本线信息,发布时三个版本的 jar 名字会撞)。 -
Forge 的
mods.toml每个[[dependencies.*]]必须显式mandatory = true—— NeoForge 默认填 true,Forge 不填就没有。另外 loader modId 必须是forge, 且没有type = "required"(那是 NeoForge 的字段)。 -
@Mod类不能用构造器注入(Forge 52 不支持,只能无参构造器), 且ServerAboutToStartEvent是游戏总线事件,挂到mod 总线上会抛IllegalArgumentException: … not valid for this bus—— NeoForge 的 mod 总线 不转发游戏事件,与直觉相反。 -
Forge 52 的服务端日志不打印模组清单(NeoForge 才打),E2E 判据别照抄 NeoForge 那套。
-
E2E 判据只用ASCII 锚点(如
voidkit_inspector\(1\)):服务端日志中文编码不固定, 同版本下 GBK / UTF-8 都实测到,grep 中文会随机假失败。 -
Windows 上起 Forge 服务端要用
win_args.txt,不能选unix_args.txt—— 后者classpath 用:分隔,Windows 的 java 认;,会在 boot layer 就炸InvalidPathException: Illegal char <:>,且错误发生在 mod 加载之前,极易误判成模组有问题。
tools/e2e-*.sh各自对应一条版本线,用真实服务端 + 发布态jar 跑一遍:
SERVER_DIR=<已装好的服务端目录> ./tools/e2e-<版本线>-server.sh判据不是「服务端起来了」—— mods.toml 写错时 mod 会被静默跳过,Done 照样出现。
真正的判据是core 的模块统计里 voidkit_inspector(1) + voidkit/inspector/inspector.db
的 WAL 有实际写入(证明事件桥真的被调用、jarJar 的 SQLite 原生库可用)。
E2E 脚本只能验「模组活着」,验不了「审计记的对不对」。后者必须起真客户端进真服、 亲手放几个方块,然后直接读库:
$ python - <<'PY'
import sqlite3; …
# 汇总:[('PLACE','PLAYER',42), ('BREAK','PLAYER',10)]
# 最近:#52 (-1,-60,-9) PLACE minecraft:smooth_stone src=PLAYER
# actor:VoidkitTester / b85366b837e637cda6b5442553385c92 / kind=0
PY
这一条同时证明了四件事:事件桥被调用、source=PLAYER 分类正确、
玩家 uuid 与名字都落库、SQLite 的 WAL 真的在写。