实测 Fabric 客户端可启动 + 加载器后端设计文档

用户判断:mod 支持分 fabric/forge,且不同版本还有差异 ⇒ 各有独立后端。
实测支持此结论,且差异比方法签名更深一层。

实测成功(关键):
  Fabric Loader 0.19.5 完整启动 1.20.1 客户端
  'Loading Minecraft 1.20.1 with Fabric Loader 0.19.5' + Mixin 子系统 + 12 个资源图集
  ★ 必须剔除 1.20.1 自带的 asm 9.2/9.6,改用 Fabric 的 9.10.1
    (否则 Knot.<clinit> 抛 duplicate ASM classes)

架构结论:后端身份是 (加载器, API 世代) 二元组
  因为 Fabric/Forge 下**我们不拥有启动流程**(Loader 起游戏+跑主循环),
  与我们「不跑主循环+手动喂数据」的模型根本冲突。
  两条可行路线已写入 scaffold/LOADERS.md(A: 作为 mod 被加载;
  B: 只借 Fabric 的注册表与资源包,保留我们的渲染循环)——均待实测。

已实现(零加载器,两个 vanilla 后端编译通过、待实测):
  cmd=mods:Java 侧自己扫 jar/注册占位方块/挂资源包
  覆盖:外观 ✅ state id ✅ 变体 ⚠️ 行为 ❌ Iris ❌
This commit is contained in:
JianFeeeee
2026-09-25 19:55:13 +08:00
parent d0fb5517a0
commit 6827f4d0e1
3 changed files with 381 additions and 0 deletions

81
scaffold/LOADERS.md Normal file
View File

@ -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.<clinit> 抛 "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/<ns>/blockstates/*.json`
→ 解冻 BLOCK 注册表 → 注册占位方块 + state → jar 挂为资源包。
覆盖范围(诚实标注):
方块**外观** ✅ / state id ✅ / 变体属性 ⚠️ / 行为 ❌ / Iris 光影 ❌

View File

@ -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<String> seen = new java.util.LinkedHashSet<>();
while (en.hasMoreElements()) {
String name = en.nextElement().getName();
// 方块名来源:assets/<ns>/blockstates/<name>.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<net.minecraft.server.packs.PackResources> 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");

View File

@ -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<String> seen = new java.util.LinkedHashSet<>();
while (en.hasMoreElements()) {
String name = en.nextElement().getName();
// 方块名来源:assets/<ns>/blockstates/<name>.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<net.minecraft.server.packs.PackResources> 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");