Commit Graph

8 Commits

Author SHA1 Message Date
51ee57e556 实例创建接口(Python 层)+ 后端透传新参数
## ① mcfold/instances.py —— Python 提供「建实例」接口

用户定向(2026-09-26):
> 这个文件夹内容也是 python 层创建的,而不是标准游戏目录。
> 即 python 提供创建实例的接口(含 mod/插件的实例,内部有对应目录,
> 插件 mod 放在目录中就会被加载)。

所以实例是**我们定义的目录**,不是一份 MC 客户端安装:

    create_instance(name, version=, loader=, mods=[], force=False)  → Path
    list_instances()                                    → [实例摘要]
    find_content_root(version, loader)                  → 共享只读那层

- 实例内部:`instance.json`(自述)+ **mods/ shaderpacks/ resourcepacks/
  datapacks/**(★ agent 的接口面,放进去即生效)+ config/ saves/ logs/
  options.txt(MC/加载器自己写的运行时产物)
- **不在实例里建 versions/libraries/assets** —— 那些属于内容根(只读共享)。
  放进实例会让「同一版本多实例」变成多份副本,也把「实例私有/版本公有」
  的边界糊掉。
- 实例名白名单 `^[A-Za-z0-9._-]{1,64}$`(它要进路径)
- 已存在时**报错而非默默复用**,并提示「换名字 = 装不同 mod 的方式」

★ 这与已有的三层口径一致(mcview/lib/versions.cjs:59/61):
  store/<loader>-<version>/ · instances/<name>/ · runtime/<name>/

## ② base.py 三个修正

**(a) cwd 缺省改为 game_dir** —— 版本隔离漏洞:
    渲染进程的 cwd 原是仓库根,而 Sodium 等会往 cwd 写 config/
    ⇒ 实测 config/sodium-mixins.properties 落进了 mcfold 仓库根。
    这些产物属于**实例**。已改为 `cwd=self.cwd or self.game_dir`。

**(b) 命名空间按 client jar 形态自动判** —— 原先硬编码 `named`:
    官方 obf jar + 加载器 ⇒ intermediary(mod 的 mixin 能解析)
    client-named.jar      ⇒ named(仅裸启动可用)
    实测:named 下 42 个 mod 的 464 处 mixin 全部找不到目标。

**(c) 新增 names_file 参数** —— 把 names.json 路径透给后端
    (B 方案的 extern 参数在 Python 侧的可入口)。
2026-09-26 10:18:26 +08:00
652a460405 加 .gitignore,并把 28 个运行期产物移出版本库
## 为什么

35534b1 把 logs/*.log.gz(10 个)与 __pycache__/*.pyc(18 个)
一起提交了,而仓库**根本没有 .gitignore** —— 那不是手误,是缺一道门。

已 `git rm --cached` 那 28 个(磁盘文件保留,只是不再跟踪)。

## 覆盖三类

① 运行期产物:logs/ *.log.gz __pycache__/ .pytest_cache/ …
② **加载器/mod 的运行时配置**:config/ shaderpacks/ options.txt saves/
   datapacks/ resourcepacks/
   ★ 这类属于**实例**,不属于仓库。实测踩过:Sodium 往**进程 cwd**
     写了 config/sodium-mixins.properties,落进了仓库根。
     根因已修(base.py:cwd=self.cwd or self.game_dir,让运行时产物
     落进实例目录),这里再兜一层。
③ 构建缓存:.mcfold-tools/ build/ dist/ *.egg-info/
2026-09-26 10:17:14 +08:00
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
a1ce560c05 修复渲染联测的两个阻断缺陷(之前 5/7 → 现 7/7)
## ① natives 必须平铺,不能保留包路径

natives-linux jar 里的路径是 `linux/x64/org/lwjgl/<module>/lib*.so`,
而 LWJGL 经 java.library.path / LD_LIBRARY_PATH 按**文件名**找库 ——
嵌套一层的目录它找不到。实测:

  UnsatisfiedLinkError: Failed to locate library: liblwjgl.so
    at RenderSystem.<clinit> → Tesselator.<clinit>

build.sh 原来直接 `unzip -d $OUT/natives`(保留包路径)⇒ 产物不可用。
改为解到临时目录再 `find -name '*.so' -exec cp` 平铺。
(已用新逻辑重铺 store/vanilla-1.20.1/.mcfold/natives,7 个 .so 在根下。)

## ② api_notes 应为 staticmethod

35534b1 把世代分派改成类级调用后,`api_notes` 仍是实例方法,
测试里 `Gen_1_20.api_notes()` 直接 TypeError。它不读 self ⇒ 一律 staticmethod。
这也是它能"不启动后端就做世代对照"的前提。

## ③ classpath.txt 的语义写进注释

它**只含第三方 libraries**;client-named.jar 与 classes/ 由使用方拼接
(build.sh:88 自己拼,测试/后端从各自路径取)。
在 build.sh 里注明,避免下一个人以为它是"完整运行 classpath"。

## 验证

  ./run_tests.sh                    → 62 项全绿
  .mcfold classpath + 平铺 natives  → 渲染联测 **7/7 全绿**
    (injected=49 chunks=31 colors=11714 / placed=1003716 chunks=25 colors=4540)
2026-09-26 06:53:30 +08:00
35534b1564 feat: get_block_info —— 方块信息查询(Java 侧回答,Python 只路由)
新增 cmd=blockinfo(两代后端编译通过、实测):
  返回 block 名(含 mod 方块)、stateId、属性、isAir/isSolid、
  光照/硬度、贴图缺失的程序化判据、是否来自 mod。

★ 修掉一个真实缺陷(附录 D 的语义区分落到了实处):
  未加载区块原先返回 void_air(known=true),实测发现两个 API 都不可用:
    - ClientLevel.hasChunk()      反编译实测**恒返回 true**
    - ChunkSource.hasChunk()      走 getChunk(...,false),而 ClientChunkCache
      覆写后在该参数下返回 emptyChunk(占位,非 null)⇒ 也恒 true
  正解:getChunk(..., requireChunk=**false**) 判 null
    (反编译:return requireChunk ? emptyChunk : null;)
  ⇒ 现在未加载明确回 known=false + reason=chunk_not_loaded

实测(1.20.1):
  草地 → minecraft:grass_block props={'snowy':'false'} missing=False
  原木 → minecraft:oak_log       props={'axis':'y'}
  空气 → minecraft:air
  远处 → known=false reason=chunk_not_loaded        ← 与空气区分

实测(1.21.1 + mod):
  mts:mtsofficialpack.fancydirt_block   mod=True tex=mtsofficialpack:block/fancydirt
  createbigcannons:steel_block          mod=True tex=createbigcannons:block/steel_block
  ⇒ mod 方块同样能查到正确贴图名与 fromMod 标记

顺带修 gen_1_21 两处世代差异(ChunkStatus 换包、bridge 编译顺序)。
测试:69 项全绿(新增 test_get_block_info 6 条判据)
2026-09-26 00:24:29 +08:00
16f2e04fff 架构重构:差异全部下移到 Java,Python 只做路由
用户口径:差异放在 Java 层,Python 相当于路由。

下移到 Java(新增 cmd=setblocks):
  Python 只发 RLE([[stateId,count],...]),Java 用官方 API 完成
  调色板组装 / 位打包 / 官方序列化 / 入 cache / 渲染。

★ 意外收益:走 ClientboundLevelChunkPacketData(chunk).getReadBuffer()
  + replaceWithPacketData(...) 这条官方路径,两代签名完全一致
  ⇒ 原先的 Storage.chunks 反射注入**整段删掉**。

新增 cmd=capacity:世界尺寸(sectionsCount/minSection/defaultBiome)
  由运行时回答,Python 不再硬编码 24/-4(1.16 时代是 16/0)。

Python 侧删除(防回归,见 test_import_surface):
  render_bridge.py(旧桥,无世代概念)
  render._pack/_long_array/_encode_paletted/_encode_chunk/_varint
  anvil.encode_block_section/encode_chunk_payload/save_chunk_to_payload
  合计 454→374 行(render.py)、300→177 行(anvil.py)

保留在 Python(都是「不随版本变」的知识):
  RLE 压缩(rle_encode/rle_decode,具名可测)
  存档读取(region/NBT)
  方块名→id 的路由(查询在 Java)
  NDJSON 协议

测试:62 项单元 + 6 项渲染联测全绿
  新测试反映新架构:RLE 边界、运行时容量、世代分派、新路径渲染
  test_anvil 里原有 8 项协议位打包测试已删(那已下移到 Java,
  由两代后端各自的编译验证覆盖)

新增 scaffold/ARCHITECTURE.md(分层原则 + 加新世代步骤 + 防回归清单)
2026-09-25 19:21:45 +08:00
253146cffe gen_1_21 实测启动成功 + provision 工具
决定性验证:gen_1_21 后端在 1.21.1 上收到 {"event":"ready"}
  ⇒ 世代分派架构成立:同一套 Python 层,两个独立编译的 Java 后端

运行时发现第 12 处世代差异(编译期看不出):
  1.20.x  registryAccess 是可变 LayeredRegistryAccess → 事后反射替换
  1.21.x  registryAccess 是 final RegistryAccess$Frozen → 反射抛
          "Can not set final field"
  处理:gen_1_21 改为先载注册表 → 经 CommonListenerCookie.receivedRegistries
        传入 → 再构造 listener。比 1.20 的反射做法更干净。

新增 mcfold/provision.py(从官方 manifest 装任意版本,版本无关):
  - 跳过音频资源:1.21.1 实测 3911 条里跳过 3744 条
  - natives 两种形态都认(1.21 起是独立库条目,非 classifiers)
  - 实测 1.21.1:274 文件 / 200MB / 32s / 0 失败
2026-09-25 18:35:28 +08:00
8c7d5b9d38 世代分派体系:Python 层 switch + 各世代独立编译的 Java 后端
用户定向:在 python 层写 switch,调用不同 java 后端(而非一个文件里 switch)。

新增:
  mcfold/backends/{__init__,base,gen_1_20,gen_1_21}.py  后端体系与分派
  scaffold/java/gen_1_20/  对 1.20.x client jar 编译
  scaffold/java/gen_1_21/  对 1.21.1 client jar 编译(✅ 实测编译通过)
  scaffold/java/VERSION-DIFFS.md  实测差异全表

判据:DeltaTracker 类是否存在(1.21 引入,正是 render 签名变化的根源)
  ⇒ 比硬编码版本号稳

实测差异共 11 处(21 个检查点里 13 个跨世代完全一致):
  setLevel / GameRenderer.render / countRendered* / createPackRepository /
  ResourceLocation 构造 / enableDataFixerOptimizations(移除) / User 构造 /
  ChunkStatus 包路径 / ClientPacketListener 构造(参数个数语义都变) /
  loadEffect(变 private)

为什么必须分后端:DeltaTracker 在 1.20.x jar 中不存在,
两代调用无法共存于一份源码(实测编译报 cannot find symbol)。

1.21.1 反混淆已验证(29MB named jar,DeltaTracker 3 处)
2026-09-25 18:21:08 +08:00