Files
agentmc/scaffold/LOADERS.md
JianFeeeee 8b1fc9289d Fabric 加载器路线跑通并落地验证(launcher="fabric",6/6 绿)
上个会话把 `launcher="fabric"` 写进了后端,却**没有留下任何可重跑的验证** ——
「经加载器能起来」这件事只活在对话里。本次把它变成可复算的。

## 机制(路线 C,不是 LOADERS.md 原来写的 A/B)

A/B 都假设「游戏由 Loader 起」。但 `Knot.init()` 是 public 的:它做完
「发现 mod → 建 KnotClassLoader → 应用 mixin → invokeEntrypoints(preLaunch)」
后**返回**,不进入官方主循环(那在 GameProvider.launch())。
⇒ `knot.loadIntoTarget("McfoldRenderServer")` 把我们的服务端注入同一个
KnotClassLoader —— **mod 生态与我们的控制权同时拿到,不必把服务改写成 mod**。

## 主要改动

### ① `RenderBackend._fabric_classpath()` —— 组装从用户手里移到后端

两个变换是实测必需、且用户不该自己拼的机械操作:
  - 剔除 1.20.1 自带的 asm(9.2 与 9.6 两版并存)—— 否则 Knot.<clinit> 抛
    "duplicate ASM classes found on classpath"
  - 追加 Fabric 的 8 个库(asm×5 / sponge-mixin / intermediary / fabric-loader)

库来源改为内容根 `.mcfold/fabric/`(原在 /tmp/fab,tmpfs 且违反版本隔离)。
实测 67 条 → 去 7 asm + 加 8 fabric = 68 条。

### ② `tests/test_loader_render.py`(新增,6 项)

  ✓ 启动架构与世代正交(动态子类,不写 N 个类)
  ✓ 两个架构可枚举
  ✓ classpath 组装(67→68:去 7 asm,加 8 fabric)
  ✓ 非 asm 库原样保留(去 asm 是外科手术,不是重写)
  ✓ **Fabric 加载器真启动并渲染**(KnotClassLoader / chunks=10..25 / colors=4391..4547)
  ✓ **Fabric 拒绝不兼容 mod** ← 反向对照:缺 fabric-api 的 Explorify 被拒

最后一条是**两条路线的可判差异**:零加载器是「无脑注册」,不知道 mod 要
fabric-api;Fabric 会。没有它,"加载器真的介入了"这句话就没有反面证据。

### ③ `run_tests.sh` —— 慢套件改为列表驱动 + 全跳过不再报绿

- SLOW 列表含 test_render_bridge / test_loader_render(默认不跑)
- ★ 全部跳过的套件**不算绿**:报"跳过 N 项"而非计入通过数 ——
  否则"没测"会被读成"测过了"。

## 验证

  ./run_tests.sh               → 62 项(2 个慢套件标 ⊘)
  ./run_tests.sh --with-render → **76 项全绿**
    (62 单元 + test_render_bridge 8 + test_loader_render 6)

## 仍未做

  Iris 光影真实加载(加载器已通,未验过一次 shader pack)
  forge / neoforge 后端(gen_1_20 / gen_1_21 均待设计)
2026-09-26 07:12:41 +08:00

133 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 加载器差异与后端设计(实测)
用户判断(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 待设计
```
## ✅ 已实测落地:路线 C(`Knot.init()` + `loadIntoTarget()`)
**上面的 A/B 都不是实际采用的 —— 两者都假设了「游戏由 Loader 起」,
而 `Knot.init()` 是 public 的,它做完 mod 加载 + mixin 后**返回**,
根本不需要进入官方主循环。**
```
LoaderBoot.main:
① Knot knot = new Knot(EnvType.CLIENT)
② ClassLoader cl = knot.init(args) ← 发现 mod / 建 KnotClassLoader / 应用 mixin
/ invokeEntrypoints(preLaunch)
**不**启动官方主循环(那在 GameProvider.launch())
③ knot.loadIntoTarget("McfoldRenderServer")
→ 把我们的服务端注入**同一个** KnotClassLoader(与 mod 共享 classloader 与 mixin)
④ 调用它的 main → 我们的渲染服务照常跑
```
★ 关键:**mod 生态与我们的控制权可以同时拿到** —— 不需要把服务改写成 mod。
### 实测结果(`tests/test_loader_render.py`,6/6 绿)
```
[loaderboot] KnotClassLoader = net.fabricmc.loader.impl.launch.knot.KnotClassLoader
[loaderboot] mod fabricloader 0.19.5 / java 21 / minecraft 1.20.1 / mixinextras 0.5.5
[loaderboot] 注入 McfoldRenderServer 到 KnotClassLoader
{"event":"ready"} {"ok":true,"sectionsCount":24,…}
渲染: chunks=10..25 colors=4391..4547
```
### 两个必需条件(缺一则我们的 named 代码在加载器下跑不起来)
1. classpath 放**反混淆后的** client jar(`client-named.jar`),不是官方 obf jar
2. `-Dfabric.gameMappingNamespace=named`
否则游戏被 remap 成 intermediary,堆栈里是 `enn.<init>`,
我们的代码抛 `NoClassDefFoundError: net/minecraft/client/Minecraft`(实测)。
### ★ 第三个必需条件:asm 必须只留一份
1.20.1 自带 asm **9.2 与 9.6 两版并存**,与 Fabric 的 9.10.1 同时在场会让
`Knot.<clinit>` 抛 `duplicate ASM classes found on classpath`。
⇒ 这两步(去 asm + 加 Fabric 8 库)已**从用户手里移到后端**:
`RenderBackend._fabric_classpath()` 自动完成(67 → 68 条:去 7 asm,加 8 fabric)。
Fabric 库放在内容根 `.mcfold/fabric/`(随版本隔离,不再散在 /tmp)。
### 可判差异:加载器 vs 零加载器
两条路线不是「高级/低级」,而是**能做的事不同**:
| | 零加载器(`cmd=mods`) | Fabric 加载器 |
|---|---|---|
| 方块外观 / state id | ✅ | ✅ |
| mod 的硬依赖校验 | ❌ 无脑注册 | ✅ **会拒绝** |
| mixin 变换 | ❌ | ✅ |
| Iris 光影 | ❌ | 待验 |
The negative control proves this: `Explorify`(依赖 `fabric-api`,缺失)在 Fabric 下被**拒绝** ——
而 vanilla 那条「无脑注册」路线根本无从知道这件事。
## 仍未做
```
Iris 光影加载 —— 加载器已通,但未验过一次真实 shader pack
forge / neoforge —— gen_1_20 / gen_1_21 均待设计
```
## 当前已具备的零加载器能力(不依赖加载器)
`cmd="mods"`(已在两个 vanilla 后端实现,实测通过):
Java 侧自己扫 mods 目录 → 读 jar 的 `assets/<ns>/blockstates/*.json`
→ 解冻 BLOCK 注册表 → 注册占位方块 + state → jar 挂为资源包。
覆盖范围(诚实标注):
方块**外观** ✅ / state id ✅ / 变体属性 ⚠️ / 行为 ❌ / Iris 光影 ❌