diff --git a/scaffold/LOADERS.md b/scaffold/LOADERS.md new file mode 100644 index 0000000..0a81dbb --- /dev/null +++ b/scaffold/LOADERS.md @@ -0,0 +1,81 @@ +# 加载器差异与后端设计(实测) + +用户判断(2026-09-25):**mod 支持分 fabric/forge,两者不同版本还有差异, +所以各应有一个独立后端。** —— 实测支持这个结论,且差异比"方法签名"更深一层。 + +## 实测:Fabric 客户端能启动(2026-09-25) + +命令(成功的形态): + +``` +# ★ 必须剔除 1.20.1 自带的 asm(9.2 + 9.6 两版并存),改用 Fabric 的 asm 9.10.1 +# 否则 Knot. 抛 "duplicate ASM classes found on classpath" +grep -v "/org/ow2/asm/" vanilla_libs.txt > base.txt +CP="$(paste -sd: base.txt):fabric-loader:asm*:sponge-mixin:intermediary:1.20.1.jar" +DISPLAY=:98 java -cp "$CP" -Djava.library.path=nativ \ + net.fabricmc.loader.impl.launch.knot.KnotClient --gameDir ... --assets ... --assetIndex 1.20.1 +``` + +实测输出(证明真的启动了): + +``` +[main/INFO]: Loading Minecraft 1.20.1 with Fabric Loader 0.19.5 +[main/INFO]: SpongePowered MIXIN Subsystem Version=0.8.7 ... Env=CLIENT +[Render thread/INFO]: Created: 1024x512x4 minecraft:textures/atlas/blocks.png-atlas +... 共 12 个资源图集 +``` + +Fabric 必需的 8 个库(从 meta API 的 profile 拿,**纯声明式**): +asm / asm-analysis / asm-commons / asm-tree / asm-util / +sponge-mixin / intermediary / fabric-loader + +## 为什么加载器必须各有独立后端(不只是版本差异) + +| | vanilla | fabric | forge | +|---|---|---|---| +| 主类 | **我们自己的** `McfoldRenderServer` | `KnotClient`(固定,不可替换) | ForgeWrapper → FML | +| 我们的代码形态 | 直接 `new Minecraft(cfg)` | **必须作为 mod 被加载** | 必须作为 coremod/mod | +| 命名空间 | Mojang official(已反混淆) | **intermediary**(运行时重映射) | SRG/TSRG | +| 起 Minecraft 实例 | 自己 new | 由 Loader 创建,我们**拿不到控制权** | 同 fabric | + +★ 关键:Fabric/Forge 下**我们不拥有启动流程** —— Loader 起游戏、进主菜单、 + 跑主循环。我们的「不跑主循环 + 手动喂世界数据」模型与它们**根本冲突**。 + +所以后端身份是 **(加载器, API 世代)** 二元组: + +``` +vanilla: + gen_1_20 已实测(渲染通过) + gen_1_21 已实测(渲染通过) +fabric: + gen_1_20 待设计(架构与 vanilla 不同,见下) + gen_1_21 待设计 +forge: + gen_1_20 待设计 + gen_1_21 待设计 +``` + +## Fabric 下的两条可行路线(都待实测) + +### 路线 A:作为 Fabric mod 被加载 +把我们的服务端打包成 fabric mod(`fabric.mod.json` + entrypoint), +由 Loader 在游戏初始化后调用我们。**问题**:Loader 会继续跑主循环, +我们要在 entrypoint 里接管渲染循环(或让它跑着、我们在 client tick 里注入数据)。 +改造量大,但能得到完整的 mod 生态(Iris 也在内)。 + +### 路线 B:只借 Fabric 的**注册表与资源包**,不跑其主循环 +用 Knot 启动(它会加载我们的 mod 并注册方块),但在我们的 mod entrypoint +里**阻止进入主菜单**(替换 Screen),然后按 vanilla 那套手动喂数据 + 渲染。 +—— 即保留我们已验证的渲染循环,只从 Fabric 借「mod 加载 + 注册表」。 + +路线 B 更贴近现有架构(渲染循环不用重写),且已验证的部分(反混淆后的 +渲染调用、setblocks、RLE 路由)能复用。 + +## 当前已具备的零加载器能力(不依赖上面两条) + +`cmd="mods"`(已在两个 vanilla 后端实现,编译通过、待实测): +Java 侧自己扫 mods 目录 → 读 jar 的 `assets//blockstates/*.json` +→ 解冻 BLOCK 注册表 → 注册占位方块 + state → jar 挂为资源包。 + +覆盖范围(诚实标注): + 方块**外观** ✅ / state id ✅ / 变体属性 ⚠️ / 行为 ❌ / Iris 光影 ❌ diff --git a/scaffold/java/gen_1_20/McfoldRenderServer.java b/scaffold/java/gen_1_20/McfoldRenderServer.java index 31ccf6f..a51cd9b 100644 --- a/scaffold/java/gen_1_20/McfoldRenderServer.java +++ b/scaffold/java/gen_1_20/McfoldRenderServer.java @@ -35,6 +35,9 @@ import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.LightLayer; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.core.Registry; +import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.DataLayer; import net.minecraft.world.level.chunk.LevelChunk; @@ -244,6 +247,23 @@ public class McfoldRenderServer { emit(id, r); break; } + case "mods": { + // ★ mod 支持全在 Java 侧完成(用户 2026-09-25 定向): + // Java 自己扫 mods 目录、自己读 jar、自己注册方块、自己加载资源包。 + // Python 只传目录路径。 + String dir = req.has("dir") ? req.get("dir").getAsString() : null; + if (dir == null) { + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("mods", 0); + r.addProperty("note", "未提供 dir"); + emit(id, r); + break; + } + JsonObject res = loadMods(dir); + emit(id, res); + break; + } case "camera": { double x = req.get("x").getAsDouble(), y = req.get("y").getAsDouble(), z = req.get("z").getAsDouble(); float yaw = req.has("yaw") ? req.get("yaw").getAsFloat() : 0f; @@ -659,6 +679,135 @@ public class McfoldRenderServer { System.err.println("[mcfold] boot done"); } + /** + * 加载 mod:扫 jar → 注册占位方块 → 挂资源包(外观)。 + * + * ★ 全部在 Java 侧完成(用户 2026-09-25 定向):Java 自己读 jar、自己注册、 + * 自己挂资源包。Python 只传目录路径。 + * + * 原理(见 scaffold/MOD-BLOCKS.md):方块**渲染**只需要 + * (a) 注册表里有这个名字(拿 state id) + * (b) 资源包里有它的 blockstates/models/textures + * 与方块行为无关 ⇒ 零 mod 加载器也能正确显示。 + */ + static JsonObject loadMods(String dir) { + JsonObject out = new JsonObject(); + File d = new File(dir); + File[] jars = d.listFiles((f, n) -> n.endsWith(".jar")); + if (jars == null || jars.length == 0) { + out.addProperty("ok", true); + out.addProperty("mods", 0); + out.addProperty("blocksRegistered", 0); + return out; + } + + // ── 1. 解冻 BLOCK 注册表(frozen=false + 恢复 intrusiveHolders)── + // 两个字段都必须处理:只清 frozen 会抛 "can't create intrusive holders" + Object blockReg = BuiltInRegistries.BLOCK; + try { + for (Class c = blockReg.getClass(); c != null; c = c.getSuperclass()) { + try { + var f = c.getDeclaredField("frozen"); + f.setAccessible(true); + f.setBoolean(blockReg, false); + } catch (NoSuchFieldException ignore) { } + try { + var f = c.getDeclaredField("unregisteredIntrusiveHolders"); + f.setAccessible(true); + if (f.get(blockReg) == null) f.set(blockReg, new java.util.IdentityHashMap<>()); + } catch (NoSuchFieldException ignore) { } + } + } catch (Throwable t) { + System.err.println("[mcfold] 解冻失败: " + t); + } + + int mods = 0, blocksAdded = 0, skipped = 0; + JsonArray detail = new JsonArray(); + for (File jar : jars) { + JsonObject m = new JsonObject(); + m.addProperty("jar", jar.getName()); + int n = 0, skip = 0; + try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(jar)) { + var en = zf.entries(); + java.util.Set seen = new java.util.LinkedHashSet<>(); + while (en.hasMoreElements()) { + String name = en.nextElement().getName(); + // 方块名来源:assets//blockstates/.json(权威,见 P5 实测) + java.util.regex.Matcher mm = java.util.regex.Pattern + .compile("^assets/([^/]+)/blockstates/(.+)\\.json$").matcher(name); + if (!mm.matches()) continue; + String ns = mm.group(1), blk = mm.group(2); + if (!seen.add(ns + ":" + blk)) continue; + ResourceLocation rl = new ResourceLocation(ns, blk); + try { + if (BuiltInRegistries.BLOCK.containsKey(rl)) { skip++; continue; } + Block b = new Block(BlockBehaviour.Properties.copy(Blocks.STONE)); + Registry.register(BuiltInRegistries.BLOCK, rl, b); + // ★ BlockState 不会自动进 BLOCK_STATE_REGISTRY(实测 state id 会是 0) + Block.BLOCK_STATE_REGISTRY.add(b.defaultBlockState()); + n++; + } catch (Throwable t) { + skip++; + } + } + mods++; + } catch (Throwable t) { + m.addProperty("error", String.valueOf(t)); + } + m.addProperty("blocksRegistered", n); + m.addProperty("skipped", skip); + blocksAdded += n; + skipped += skip; + detail.add(m); + } + + // ── 2. 挂资源包(外观:blockstates/models/textures)── + // 把 mod jar 作为资源包加入并选中,然后重载资源管理器。 + int packsAdded = 0; + try { + var repo = mc.getResourcePackRepository(); + java.nio.file.Path rpDir = mc.getResourcePackDirectory(); + for (File jar : jars) { + File dst = new File(rpDir.toFile(), "mcfold-" + jar.getName()); + if (!dst.isFile()) java.nio.file.Files.copy(jar.toPath(), dst.toPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + repo.reload(); + var sel = new java.util.LinkedHashSet<>(repo.getSelectedIds()); + for (String packId : repo.getAvailableIds()) { + if (packId.contains("mcfold-")) { sel.add(packId); packsAdded++; } + } + repo.setSelected(sel); + + // 用新 pack 列表重建 resource manager(照官方 createReload 内部做法), + // 再手工驱动 listener(不能 join Minecraft.reloadResourcePacks,会死锁) + java.util.List packs = new java.util.ArrayList<>(); + for (var pk : repo.getSelectedPacks()) packs.add(pk.open()); + var rmF = Minecraft.class.getDeclaredField("resourceManager"); + rmF.setAccessible(true); + var rm = (net.minecraft.server.packs.resources.ReloadableResourceManager) rmF.get(mc); + var resF = net.minecraft.server.packs.resources.ReloadableResourceManager.class + .getDeclaredField("resources"); + resF.setAccessible(true); + resF.set(rm, new net.minecraft.server.packs.resources.MultiPackResourceManager( + net.minecraft.server.packs.PackType.CLIENT_RESOURCES, packs)); + reloadResources(mc); + net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager()); + mc.levelRenderer.allChanged(); + } catch (Throwable t) { + out.addProperty("packError", String.valueOf(t)); + System.err.println("[mcfold] 资源包挂载失败: " + t); + } + + out.addProperty("ok", true); + out.addProperty("mods", mods); + out.addProperty("blocksRegistered", blocksAdded); + out.addProperty("skipped", skipped); + out.addProperty("packsAdded", packsAdded); + out.add("detail", detail); + return out; + } + /** 同步驱动各 reload listener(不能 join Minecraft 的 reloadResourcePacks:等主循环 → 死锁)*/ static void reloadResources(Minecraft mc) throws Exception { var rmF = Minecraft.class.getDeclaredField("resourceManager"); diff --git a/scaffold/java/gen_1_21/McfoldRenderServer.java b/scaffold/java/gen_1_21/McfoldRenderServer.java index 44570b3..bf05e9d 100644 --- a/scaffold/java/gen_1_21/McfoldRenderServer.java +++ b/scaffold/java/gen_1_21/McfoldRenderServer.java @@ -35,6 +35,9 @@ import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.LightLayer; import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.core.Registry; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.DataLayer; import net.minecraft.world.level.chunk.LevelChunk; @@ -244,6 +247,23 @@ public class McfoldRenderServer { emit(id, r); break; } + case "mods": { + // ★ mod 支持全在 Java 侧完成(用户 2026-09-25 定向): + // Java 自己扫 mods 目录、自己读 jar、自己注册方块、自己加载资源包。 + // Python 只传目录路径。 + String dir = req.has("dir") ? req.get("dir").getAsString() : null; + if (dir == null) { + JsonObject r = new JsonObject(); + r.addProperty("ok", true); + r.addProperty("mods", 0); + r.addProperty("note", "未提供 dir"); + emit(id, r); + break; + } + JsonObject res = loadMods(dir); + emit(id, res); + break; + } case "camera": { double x = req.get("x").getAsDouble(), y = req.get("y").getAsDouble(), z = req.get("z").getAsDouble(); float yaw = req.has("yaw") ? req.get("yaw").getAsFloat() : 0f; @@ -703,6 +723,137 @@ public class McfoldRenderServer { System.err.println("[mcfold] boot done"); } + /** + * 加载 mod:扫 jar → 注册占位方块 → 挂资源包(外观)。 + * + * ★ 全部在 Java 侧完成(用户 2026-09-25 定向):Java 自己读 jar、自己注册、 + * 自己挂资源包。Python 只传目录路径。 + * + * 原理(见 scaffold/MOD-BLOCKS.md):方块**渲染**只需要 + * (a) 注册表里有这个名字(拿 state id) + * (b) 资源包里有它的 blockstates/models/textures + * 与方块行为无关 ⇒ 零 mod 加载器也能正确显示。 + */ + static JsonObject loadMods(String dir) { + JsonObject out = new JsonObject(); + File d = new File(dir); + File[] jars = d.listFiles((f, n) -> n.endsWith(".jar")); + if (jars == null || jars.length == 0) { + out.addProperty("ok", true); + out.addProperty("mods", 0); + out.addProperty("blocksRegistered", 0); + return out; + } + + // ── 1. 解冻 BLOCK 注册表(frozen=false + 恢复 intrusiveHolders)── + // 两个字段都必须处理:只清 frozen 会抛 "can't create intrusive holders" + Object blockReg = BuiltInRegistries.BLOCK; + try { + for (Class c = blockReg.getClass(); c != null; c = c.getSuperclass()) { + try { + var f = c.getDeclaredField("frozen"); + f.setAccessible(true); + f.setBoolean(blockReg, false); + } catch (NoSuchFieldException ignore) { } + try { + var f = c.getDeclaredField("unregisteredIntrusiveHolders"); + f.setAccessible(true); + if (f.get(blockReg) == null) f.set(blockReg, new java.util.IdentityHashMap<>()); + } catch (NoSuchFieldException ignore) { } + } + } catch (Throwable t) { + System.err.println("[mcfold] 解冻失败: " + t); + } + + int mods = 0, blocksAdded = 0, skipped = 0; + JsonArray detail = new JsonArray(); + for (File jar : jars) { + JsonObject m = new JsonObject(); + m.addProperty("jar", jar.getName()); + int n = 0, skip = 0; + try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(jar)) { + var en = zf.entries(); + java.util.Set seen = new java.util.LinkedHashSet<>(); + while (en.hasMoreElements()) { + String name = en.nextElement().getName(); + // 方块名来源:assets//blockstates/.json(权威,见 P5 实测) + java.util.regex.Matcher mm = java.util.regex.Pattern + .compile("^assets/([^/]+)/blockstates/(.+)\\.json$").matcher(name); + if (!mm.matches()) continue; + String ns = mm.group(1), blk = mm.group(2); + if (!seen.add(ns + ":" + blk)) continue; + // ★ 世代差异(1.21+):ResourceLocation 构造变 private → 静态工厂 + ResourceLocation rl = ResourceLocation.fromNamespaceAndPath(ns, blk); + try { + if (BuiltInRegistries.BLOCK.containsKey(rl)) { skip++; continue; } + // ★ 世代差异(1.21+):Properties.copy(Block) 已移除,只有 of() + Block b = new Block(BlockBehaviour.Properties.of()); + Registry.register(BuiltInRegistries.BLOCK, rl, b); + // ★ BlockState 不会自动进 BLOCK_STATE_REGISTRY(实测 state id 会是 0) + Block.BLOCK_STATE_REGISTRY.add(b.defaultBlockState()); + n++; + } catch (Throwable t) { + skip++; + } + } + mods++; + } catch (Throwable t) { + m.addProperty("error", String.valueOf(t)); + } + m.addProperty("blocksRegistered", n); + m.addProperty("skipped", skip); + blocksAdded += n; + skipped += skip; + detail.add(m); + } + + // ── 2. 挂资源包(外观:blockstates/models/textures)── + // 把 mod jar 作为资源包加入并选中,然后重载资源管理器。 + int packsAdded = 0; + try { + var repo = mc.getResourcePackRepository(); + java.nio.file.Path rpDir = mc.getResourcePackDirectory(); + for (File jar : jars) { + File dst = new File(rpDir.toFile(), "mcfold-" + jar.getName()); + if (!dst.isFile()) java.nio.file.Files.copy(jar.toPath(), dst.toPath(), + java.nio.file.StandardCopyOption.REPLACE_EXISTING); + } + repo.reload(); + var sel = new java.util.LinkedHashSet<>(repo.getSelectedIds()); + for (String packId : repo.getAvailableIds()) { + if (packId.contains("mcfold-")) { sel.add(packId); packsAdded++; } + } + repo.setSelected(sel); + + // 用新 pack 列表重建 resource manager(照官方 createReload 内部做法), + // 再手工驱动 listener(不能 join Minecraft.reloadResourcePacks,会死锁) + java.util.List packs = new java.util.ArrayList<>(); + for (var pk : repo.getSelectedPacks()) packs.add(pk.open()); + var rmF = Minecraft.class.getDeclaredField("resourceManager"); + rmF.setAccessible(true); + var rm = (net.minecraft.server.packs.resources.ReloadableResourceManager) rmF.get(mc); + var resF = net.minecraft.server.packs.resources.ReloadableResourceManager.class + .getDeclaredField("resources"); + resF.setAccessible(true); + resF.set(rm, new net.minecraft.server.packs.resources.MultiPackResourceManager( + net.minecraft.server.packs.PackType.CLIENT_RESOURCES, packs)); + reloadResources(mc); + net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager()); + mc.levelRenderer.allChanged(); + } catch (Throwable t) { + out.addProperty("packError", String.valueOf(t)); + System.err.println("[mcfold] 资源包挂载失败: " + t); + } + + out.addProperty("ok", true); + out.addProperty("mods", mods); + out.addProperty("blocksRegistered", blocksAdded); + out.addProperty("skipped", skipped); + out.addProperty("packsAdded", packsAdded); + out.add("detail", detail); + return out; + } + /** 同步驱动各 reload listener(不能 join Minecraft 的 reloadResourcePacks:等主循环 → 死锁)*/ static void reloadResources(Minecraft mc) throws Exception { var rmF = Minecraft.class.getDeclaredField("resourceManager");