引擎自带一个 javassist agent 内核:不改游戏本体、不替换原引擎,只在原 transformer 之后追加自己的一层注入, 并把「模组网络通道 / 服务端点击钩子 / 方块新增同步 / 世界生成」开放给模组。
全部是 insertBefore(一处 insertAfter),方法体一行都没改。
| 侧 | 类 # 方法 | 插进去的代码 | 作用 |
|---|---|---|---|
| 服务端 | PlayerManager#handlePlayerClickPacket |
if (ModHooks.onServerClick(...)) return; | 让模组接管放/挖 |
| 服务端 | BlockManager#setBlock | KernelBlockSync.beforeServerSetBlock(...) | 记录被改的格子 |
| 服务端 | BlockManager#onUpdate | KernelBlockSync.flushServerBlockAdds($0) | 广播方块新增(引擎缺的就是这一环) |
| 服务端 | BlockOutputter#generateChunk末尾 | KernelWorldGen.decorate($0,$1,$2) | 区块生成时种矿脉 / 矮树 |
| 客户端 | BlockManager#register | KernelBlockSync.armClient($0) | 接入方块同步 |
| 客户端 | Manager#update(float) | KernelBlockSync.flushClientAdds() | 渲染线程落地(安全改 Scene2D) |
loadServer / loadClient / tickAll 覆盖掉。引擎原本只给模组 manager,没有任何发包/收包入口。
// 服务端 ModNet.bindServer(api); // api 就是 GameWorld ModNet.listen(300, 300, MyPacket.class, (conn, pkt) -> { ... }); ModNet.broadcast(new MyPacket(...)); // 服务端 → 所有客户端 ModNet.onServerClick((conn, pkt, id, cx, cy, lx, ly, type) -> { if (type != 1) return false; // 0=左键挖;返回 false = 交给引擎 ...自己放置... return true; // 跳过引擎硬编码的"放泥土" }); // 客户端 ModNet.bindClient(api); ModNet.register(300, 300, MyPacket.class); // 发送前必须注册 ModNet.send(new MyPacket(...));
101-104·106·107/组 100、151·152/150、201-203/200、253/250310/310(方块新增同步)300 SelectRequest、301 PlaceRequest(已废弃)、302 背包同步、303 合成请求instanceof 判类型开发\工具\新建Mod项目.bat,输入包名(小写字母+数字,如 planttools),
它会自动改包名/类名/服务声明并试编译一次。registerServer(ServerAPI api),客户端改
registerClient(ClientAPI api)。想在每 tick 做事用 ModLoader.registerTick(...)。编译打包.bat:编译 → 校验入口类 → 打包 → 自动拷到
mods\。然后重启服务端/客户端看日志里的 Mod registered on server: ...。src/META-INF/services/dev.YN.PlantGame.core.api.mod.IMod(写入口类的全名)。
改类名/包名后必须同步改它,否则 ServiceLoader 找不到。这些都是实测撞出来的,写 mod 前值得先看一眼。
ModLoader.tickAll() 跑在 GameWorld.onTick() 之后,而引擎的放/挖点击包是在
onTick() 里按到达顺序处理的。在自己的 tick 回调里写世界会越过这个顺序:
"右键放 → 同一 tick 左键挖"就变成"挖掉又被放回来"。放置请走内核点击钩子。
Chunk.setForegroundBlock 在方块数组还是 null 时直接 return:
不报错、不抛异常、也没日志。写完一定要 getBlockByBlockPos 读回校验。
isBackground 标记地形生成写背景层用的是 Chunk.setBackgroundBlock(),它只往数组里塞、从不设置
block.isBackground = true。判断"能不能挖到东西"要看前景层,别看这个标志。
客户端拿到矩形后会自己加 chunkIndex * chunkSize。传世界坐标的话
chunk(0,0) 恰好相等看不出问题,其它区块整个偏一个区块宽 —— 表现就是"有时有实体、有时没有"。
引擎画完世界/UI 后会 glDisable(GL_BLEND),而 ShapeRenderer 只在构造时开一次混合。
所以每次 shapes.begin() 前自己 glEnable(GL_BLEND),否则半透明矩形会变成实心
(踩过:遮罩把整屏刷成纯黑)。
MessageCodec.encode 查不到类型 id 会抛异常。如果这个包是在点击钩子里发的,
异常冒出去会被内核当成"钩子拒绝接管",引擎就会去放它默认的泥土 —— 表现是"我什么都没干,方块自己变了"。
client.World 用它们做相机缩放。
mod 想开面板可以用 I 之类没被占用的键,并用 Gdx.input.isKeyJustPressed 轮询
(别去抢游戏的 InputProcessor)。开发\工具\无头自测\ 里 6 个程序 + 一个 自测.bat,不需要人进服务器。
| 测试 | 验证什么 | 期望 |
|---|---|---|
KernelBlockSyncTest | 方块新增同步(引擎自己也放泥土那条路) | 地形阶段 0 个包;右键后恰好 1 个包;碰撞矩形是局部坐标 |
KernelOreGenTest | 矿脉生成 | 四种矿石都出现、数量 > 0、碰撞信息完整 |
KernelProductionTest | 掉落 / 合成台 / 消耗 / 防刷 | failures=0 |
KernelDigRaceTest | 放/挖顺序竞态 | restoredAfterDig=0、ghost=0、旧通道已失效 |
KernelClickTest | 服务端点击钩子接管放置 | 日志出现 [Hotbar] 点击放置 … 读回 … |
ClientModHarness / ServerModHarness | 假 API 走真实 mod 路径 | 见 自测.bat 的 6 步 |