|
|
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 |
|
|
|
e6f8c0e596
|
build.sh 七步:加 remap 与名字表(加载器路线所需)
## 新增两步(原五步 → 七步)
**[6/7] 反射名次表** —— 调 gen_names.py 生成
names.json(named 形态)与 names-intermediary.json(intermediary 形态)
**[7/7] remap named → intermediary** —— 三步:
7a. 复合出 named→intermediary 的 SRG(gen_intermediary_srg.py)
7b. 造一个 **intermediary 形态的 client jar** ← 继承查找的输入,缺它不行
7c. 从它生成继承映射(-H),再用 -h 把我们的 classes remap 过去
## 为什么必须 remap
实 mod(Iris/Sodium 等)的 mixin 注解写的是 **intermediary** 名且不带 refmap。
跑 named 时 42 个 mod 的 464 处 mixin 全部找不到目标(实测)。
⇒「mod 要 intermediary」与「我们的产物是 named」在同一 JVM 互斥,
解法是把**我们的产物** remap 过去(非通用数据由 extern 名次表解决)。
## gen_intermediary_srg.py 的两个实测坑(不处理会出错且症状误导)
① **描述符必须转**。mappings.srg 的 MD 行两侧 desc **都是 official 形式**:
MD: enn/Z ()Laki; net/.../Minecraft/getResourcePackRepository ()Laki;
左 desc 必须转成 named、右转成 intermediary。
只改类名不改 desc 的症状:**类改名成功了、方法却没改** → NoSuchMethodError。
② **必须生成继承映射**。SpecialSource 按 owner+name+desc 精确匹配、
**不查继承链**;而 MC 里大量方法继承自父类/接口
(如 ClientPacketCache.registryAccess 实际声明在 RegistryAccess)。
不带它漏 ~60 处(实测:60 → 12,且剩余 12 全是 JDK 继承方法、
record component、和我们自己那个不该改名的桥类)。
## 另修三处
- **SS_CP 提到全局**:原来在第 3 步的 if 里定义,而第 7 步也要用它 ——
若 client-named.jar 已是最新,第 3 步不进入 if ⇒ 变量为空。
- **LoaderBoot 纳入编译**:launcher_fabric 从没被 build.sh 编译过,
运行期报 ClassNotFoundException: LoaderBoot。
- **common/ 纳入编译**:mcfold/McfoldNames.java 在 common 下,
漏了它 ⇒ remap 后产物里没有这个类 ⇒ NoSuchFieldException(实测踩过)。
- **natives 平铺**(见上一批提交):jar 内是 linux/x64/org/lwjgl/<mod>/lib*.so,
而 LWJGL 按**文件名**找库 ⇒ 保留包路径会 UnsatisfiedLinkError。
## 产物清单(落在内容根 .mcfold/)
classes/ 裸启动 / named
classes-intermediary.jar 加载器 / intermediary
client-named.jar · client-intermediary.jar
names.json · names-intermediary.json
named2intermediary.srg · inheritance.map
classpath.txt · natives/(平铺)· fabric/(8 库)
|
2026-09-26 10:18:08 +08:00 |
|
|
|
c3a57256ac
|
McfoldBridge 改反射 + 新增 Sodium tracker 通知与 hideGui
三件事,其中后两件是实测查出来的。
## ① McfoldBridge:「同包直调」在 intermediary 下失效 ⇒ 改反射
原来把桥类放进 `net.minecraft.client.renderer` 包,靠**同包**访问
GameRenderer 的 package-private 方法。这个技巧只在 **named** 下成立:
named 桥与 GameRenderer 同在 net.minecraft.client.renderer ⇒ 同包 ✅
intermediary GameRenderer → net.minecraft.class_757(包变成 net.minecraft)
桥仍在 net.minecraft.client.renderer ⇒ 异包 ❌
⇒ IllegalAccessError: McfoldBridge tried to access method
'void net.minecraft.class_757.method_34538(...)'
★ **包名本身也被 remap**,所以「靠包名对齐」切换命名空间就断了。
反射 + 语义名(McfoldNames)与命名空间无关,是唯一两态都通的形态。
(gen_1_21 本来就为 loadEffect 用了反射,现在两处统一。)
## ② Sodium 通知:装载地形渲染的 mod 现在能看见了(根因三层,逐层实测)
**症状**:装了 Sodium 后画面只有天空、chunks=0,而 blockinfo 能正确读出
方块 ⇒ 数据在,只是它不知道。
**三层根因(每层都有实测数据)**:
┌ ① mixin 永不命中
│ Sodium 挂在 replaceWithPacketData 内 @At(INVOKE onChunkLoaded, AFTER)
│ 而 1.20.1 该方法**内部不调** onChunkLoaded(在 getChunk/drop 里各有一处)
│ ⇒ 它的 ChunkTracker 从未被告知任何区块
│ 【修】notifyRenderTracker() 补通知
│
├ ② 反射签名错
│ ChunkTrackerHolder.get 的参数是 **ClientLevel** 不是 Level
│ 第三参数是**位掩码 int** 不是 ChunkStatus 对象(签名 (III)V)
│
└ ③ ★ status 值错(真正的那道坎)
Sodium 的语义:
FLAG_HAS_BLOCK_DATA = 1 FLAG_HAS_LIGHT_DATA = 2 FLAG_ALL = 3
而 ChunkTracker.updateMerged 的 2×2 邻域判定是
`四邻 status 按位与 == 3` (字节码 :54 iand / :71 iconst_3)
⇒ 传 1:1&1&1&1=1 ≠ 3 ⇒ 全部判成"凑不齐 2×2"
⇒ chunkReady 恒 0、loadQueue 恒空、画面无地形
传 3:3&3&3&3=3 == 3 ⇒ 25 个 2×2 组就绪 ⇒ 重建队列有 25 项
(我们确实有光照数据:注入时跑了 light.runLightUpdates())
**实测对比**(同一场景 7×7=49 区块):
传 1: chunkStatus=49 chunkReady=0 loadQueue=0 colors=4748
传 3: chunkStatus=49 chunkReady=25 loadQueue=25 colors=113546
★ 走反射 + 类不存在时静默跳过 ⇒ 不装 Sodium 时零影响。
这不是"给某个 mod 打补丁":注入路径缺了"区块已就绪"广播,
补上它对所有依赖该事件的 mod 都有效。
**同时广播官方的 level.onChunkLoaded(pos)** —— 与网络包路径等价的那一步。
## ③ hideGui:截图场景关掉 HUD
走**官方选项** `options.hideGui`(= 游戏里 F1),不改渲染代码 ——
GameRenderer.render 里就是 `if (!options.hideGui) gui.render(...)`。
为什么要关:截图只关心画面,HUD 是噪声,而且它给 distinctColors
掺一大块固定像素(实测污染判据)。
两个世代后端都加了 hideGui;Python 侧 set_view(hide_gui=...)。
|
2026-09-26 10:17:48 +08:00 |
|
|
|
d4ba586ac7
|
反射名的 extern 机制(B 方案):19 处字符串反射 → 0
## 问题
Java 后端要访问 MC 的**私有字段/方法**(Minecraft.resourceManager 等),
只能走反射。而反射参数是**字符串**:
Minecraft.class.getDeclaredField("resourceManager")
remap 工具只改常量池里的**符号引用**,**改不了字符串字面量** ⇒
把产物从 named remap 到 intermediary 时,`Xxx.class` 都对了,
`"resourceManager"` 原样留着 ⇒ 运行期 NoSuchFieldException(实测撞到过)。
## 做法:语义名 → 物理名,表由 extern 参数传入
源码写**语义路径**(跨命名空间稳定):
McfoldNames.field(Minecraft.class, "net/minecraft/client/Minecraft/resourceManager")
- named 模式(无表):退化为取末段 ⇒ "resourceManager" ✅
- intermediary 模式:表里查 ⇒ "field_1745" ✅
⇒ **同一份 class 在两种命名空间下都能跑**,切换不必重编。
这是「mod 要 intermediary」与「我们的产物本来是 named」能共存的**前提**。
## 表的来源(scaffold/gen_names.py)
扫源码里的语义引用 → 经两份映射复合解析:
named ──(mappings.srg 反查)──> official ──(intermediary tiny)──> intermediary
运行时按此序找表:
1. -Dmcfold.names=<path> ★ 显式 extern 参数(调用方只传路径)
2. <class 同级>/names.json
3. 都无 ⇒ named 模式
★ 调用方(Python)**不需要知道任何 MC 名字**,只传一个路径 ——
命名空间知识全部留在 jar 与构建产物里。这就是「接口统一、非通用数据走 extern」。
## 实测踩坑(都在 gen_names.py 里记了原因)
① 查表必须**带描述符**:同一个 obf 名(如 fjq/b)有十几个重载,
只按 (kind, owner, name) 会取到第一个重载 ——
症状:reloadShaders(Lala;)V 被解成 method_3207()V(无参),运行期 NoSuchMethodException。
② 字段要单建一张 (owner,name) 表:tiny 的 FIELD 行带字段类型描述符,
而 srg 的 FD 行没有 ⇒ 拿 None 去查永远不命中(7 个字段全漏)。
③ **不能靠首字母大小猜字段/方法**,必须在 srg 里实查 kind。
④ MC 里存在**同名字段与方法**(如 ClientPacketListener.registryAccess
既有 FD fex/B 又有 MD fex/r)⇒ 用 McfoldNames.field/method 的**调用形式**
判定 kind,与运行期用法一致。
## 验证
- 名字表 11/11 解析成功(gen_1_20)
- 两世代后端字符串反射归零(gen_1_20 10 处、gen_1_21 9 处)
- 加载器路线端到端跑通(见后续提交)
|
2026-09-26 10:17:29 +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 |
|
|
|
330867c6df
|
版本隔离守卫(新套件立即抓出两处真实回流)
## 修掉渲染联测的嵌套实例目录
`test_render_bridge.py` 把实例写成 `<store>/instances/test-mcfold`。
而权威口径(mcview/lib/versions.cjs:59/61)是:
contentRoot = BASE/store/<key> 共享只读(jar/libraries/assets)
instanceDir = BASE/instances/<name> 实例独占(mods/config/options/saves)
⇒ 那是「一个版本的内容根里长出了实例态」。而版本隔离要防的崩因正是
「多实例共用 mods」—— 内层目录会把它重新引回来,而那条路径看起来完全合法。
改为 `BASE/instances/test-mcfold`。
## 新增 tests/test_version_isolation.py(4 项,默认套件里跑)
★ 它**不需要 JVM**(纯路径断言),所以必须落在默认测试里 ——
放进 `--with-render` 才跑的套件等于没守卫(大多数人不会加那个参数)。
✓ 三层同级(store/ · instances/ · runtime/ 是 BASE 下的兄弟)
✓ 内容根干净(无 instances/ 嵌套、无 mods/config/options.txt/saves)
✓ 测试实例归位(落在 instances/ 层,不在任何内容根下)
✓ 内容根无实例配置(查内容,不只查存在)
## ★ 守卫首次运行就抓出第二处(我此前漏掉的)
```
✗ 内容根被实例化:
- store/vanilla-1.21.1/instances/(内容根里嵌套了实例层)
```
内容是 `instances/default/{options.txt,downloads/log.json}`(生成态,
正式 `instances/default` 已存在)⇒ 已删。两个内容根现已干净。
⇒ 这条说明:**「我检查过了」不等于「有一条会自己跑的检查」** ——
我上一轮手工核实过 1.20.1 那处并修掉,却没发现 1.21.1 那处;
守卫一运行,两处一次全暴露。
## 验证
./run_tests.sh → 66 项
./run_tests.sh --with-render → **80 项全绿**
|
2026-09-26 07:23:54 +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 |
|
|
|
6827f4d0e1
|
实测 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 ❌
|
2026-09-25 19:55:13 +08:00 |
|
|
|
d0fb5517a0
|
修复 gen_1_21 渲染:1.21 的 gameLoadFinished 门禁(第 13 处世代差异)
症状:gen_1_21 启动成功、数据注入成功(loadedChunks=49),
但 chunksRendered=0、画面停在 Mojang 加载屏(distinctColors≈199)。
根因(1.21 GameRenderer.render 内部,编译期看不出):
boolean ok = minecraft.isGameLoadFinished();
if (ok && renderLevel && level != null) renderLevel(...);
该标志只在 reloadResourcePacks 回调(onResourceLoadFinished)里置 true。
我们不跑异步 reload(会死锁)⇒ 恒 false ⇒ 世界永不渲染。
1.20.x 无此门禁 ⇒ gen_1_20 从未受影响。
修法:gen_1_21 的 McfoldBridge 加 markGameLoadFinished(反射置位),
在 boot 里 shader 编译后调用 —— 差异留在 Java 侧。
★ 教训:这类差异不崩不报错,只是画面不对 ⇒ 判据必须用
chunksRendered/distinctColors,不能只看进程活着。
顺带修:tick 异常不再静默吞掉(try{tick()}catch(ignore){} → 打印第一帧异常)
新增诊断字段:screenNow/overlayNow/levelNow/playerNow/noRender
实测:gen_1_21 现在 chunksRendered=25、has_world_content=True
(草地/天空/太阳/HUD 正常)
|
2026-09-25 19:45:24 +08:00 |
|
|
|
bea69b2556
|
mod 方块渲染(零加载器):注册链路实测打通
关键洞察:方块**渲染**只需要「注册表里有名字」+「资源包有模型」,
与方块行为无关 ⇒ 不必等 Fabric 加载器。
实测(已跑通):
frozen 置 false + unregisteredIntrusiveHolders 恢复非 null
注册占位方块 → BLOCK 1003→1004
BlockState 手动 add 进 BLOCK_STATE_REGISTRY → 24135→24136
state id = 24135,parseForBlock 可解析(渲染路径通)
★ 两个必须都处理的字段:只解冻 frozen 会抛
"This registry can't create intrusive holders"
★ BlockState 不会自动注册(state id 会是 0),需手动 IdMapper.add
边界已诚实标注(scaffold/MOD-BLOCKS.md):
外观 ✅ / state id ✅ / 变体属性 ⚠️ / 行为 ❌(需加载器)
渲染类型(半透明)⚠️ 未验证 / 方块实体 ⚠️ 未验证
Iris 光影必须加载器,此路不覆盖
|
2026-09-25 19:33:42 +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 |
|
|
|
34ed70ab53
|
修复视距不生效 + 接入统一测试
视距(用户报「无法调整视距?」)——根因是三处不同步:
1. options.renderDistance 本地设置(原先只设了这个)
2. options.setServerRenderDistance 服务端下发上限(getEffectiveRenderDistance = min(两者))
3. ClientChunkCache.updateViewRadius storage 半径
第三处最关键:官方只在收到服务端半径包时才调它(ClientPacketListener:2316),
我们不连服 ⇒ Storage.inRange 用旧半径判定,超出区块被**静默丢弃**。
另一个前置:加载半径必须跟随视距。视距 12 只喂 3 圈数据,
画面看起来就像没生效(最容易误判的坑)。
实测(同相机同存档):
视距 4 → 注入 81 渲染 55 色彩 15226
视距 12 → 注入 595 渲染 447 色彩 18477
画面差异显著:RD=12 能看到延伸到地平线的森林与完整海岸线。
(sample-render-distance-{4,12}.png)
测试:新增 tests/test_anvil.py(13 项,存档/NBT/编码)
tests/test_render_bridge.py(3 项,桥协议+合成渲染+真实存档渲染)
run_tests.sh 改为默认跑单元测试、--with-render 跑联测
合计 61 项全绿(原 48 + 13)
|
2026-09-25 17:47:58 +08:00 |
|
|
|
391d452c85
|
P9:Python 包装层 —— 真实存档 → 官方渲染器 → 出图
新增两模块:
mcfold/anvil.py 存档读取(region/NBT)+ 区块编解码工具
mcfold/render_bridge.py 桥:常驻 Java 渲染器进程,NDJSON 协议
mcfold/render.py 对外入口:WorldRenderer(存档→画面)
scaffold/McfoldRenderServer.java Java 侧服务端(常驻,NDJSON)
实测(真实存档 mc-test,DataVersion 3465=1.20.1):
injected=81 unknownBlocks=0 uniqueSpecs=111
loadedChunks=81 chunksRendered=60 distinctColors=11585
画面:草地/沙滩/树/水/天空/太阳/HUD(sample-real-save-render.png)
★ 分工落在实处(附录 D 的结论变成代码):
存档 palette 是**方块名**(minecraft:oak_log[axis=y]),网络载荷要 **state id**,
名字→id 的权威映射只有客户端注册表有 ⇒ 新增 resolve 命令,
Java 侧用 BlockStateParser.parseForBlock 解析(实测 111 种全中,0 unknown)
踩坑(本轮新发现):
1. level.dat 有两种布局(8字节头+zlib / 直接 gzip)—— 实测本机是后者
2. section 索引 ≠ 绝对 Y:存档用 -4..19,载荷用 0..23(少这步 → readerIndex 越界)
3. 注入前必须 set_viewport,否则 ClientChunkCache.inRange 丢弃区块
4. setPos 不会更新实体区块归属,要 setPosRaw + updateViewCenter
5. 生物群系只影响着色,用统一 plains 不影响「能渲染出世界」
6. camera 回包要用 JsonArray,字符串会被 Python tuple() 按字符拆开
性能:启动 66s(GL+资源重载),之后每帧渲染 ~1s(含 100 帧 tick)
|
2026-09-25 17:33:41 +08:00 |
|
|
|
541fe232a8
|
P8 完成:世界数据 → 官方渲染器 → 真实出图
不起完整客户端、不连服、不登录,只初始化 MC 渲染栈并喂入我们自己造的世界数据。
已验证(全绿):
countRenderedChunks=29 distinct colors=1450
画面:草地/原木/树叶/天空/HUD/英文文字(sample-world-render.png)
打通的关卡:
- 反混淆(client-named.jar)+ GL 离屏 + new Minecraft(cfg) 不进主循环
- 资源同步重载:22 listener(ModelManager/FontManager/Dispatcher)
★ 不能 join reloadResourcePacks(等主循环→死锁),直接调各 listener reload()
- 注册表从客户端自带 data/ 加载(dimension_type=4)
- 区块注入:LevelChunk + setBlockState → ClientChunkCache.Storage.chunks
+ LightEngine.queueSectionData(全亮)→ allChanged()
- 每帧 mc.tick() 推进网格重建队列(不跑则 countRenderedChunks=0)
踩坑记录(10 条,README 详列):
LoadingOverlay/ReceivingLevelScreen 遮挡、FolderData 参数序、
assetIndex 用落盘名、reloadShaders 包私有、读像素必须绑 FBO、
判据用 distinct colors 而非 non-black
更正附录 D:state_id=1003 → note_block 不是误映射(note_block 有 800 状态)
—— 真客户端给完整状态,是 1.20.1 上 mod 方块语义的唯一权威来源
|
2026-09-25 17:01:49 +08:00 |
|
|
|
e223be2571
|
P8: scaffold —— 反混淆 + MC 渲染栈离屏初始化 + 真渲染出图
实测链路(全绿):
1. 官方 ProGuard 映射 → SRG → SpecialSource 重映射 → client-named.jar
2. 反编译确认 MC 渲染器挂在 Minecraft 单例上,不能独立 new
3. 权威序列:tryDetectVersion → Bootstrap → RenderSystem → new Minecraft(cfg)
4. 渲 3 帧 + glReadPixels → PNG,画面是 Mojang 加载屏(真渲染,非清屏)
三个坑(实测证据):
- 漏 tryDetectVersion → Game version not set
- FolderData 参数顺序错 → icons/ 404
- assetIndex 用 id(5) 而非落盘名(1.20.1) → 资源全找不到
下一步:ClientLevel 构造只需 listener.registryAccess(),
ClientChunkCache.replaceWithPacketData 接收 map_chunk 原始字节
(与 chunk.py 同格式)⇒ 这就是数据喂渲染器的落点。
|
2026-09-25 15:39:41 +08:00 |
|
|
|
5038c7f679
|
feat(mcfold): P7 实例层(传入文件夹→拉起实例→读mod注册方块表→连服,含端到端验证)+ 统一测试入口
|
2026-09-25 14:36:21 +08:00 |
|
|
|
b0182de8c7
|
feat(mcfold): P6 世界模型(多区块聚合/负坐标定位/unloaded与air语义分明/mod降级统计)
|
2026-09-25 14:33:26 +08:00 |
|
|
|
85ad7e580e
|
feat(mcfold): P5 mod jar 解析(三种加载器+多加载器诊断+方块名提取,含诚实边界标注)
|
2026-09-25 14:32:39 +08:00 |
|
|
|
40c5739cae
|
feat(mcfold): P4 区块解析(位打包/三种调色板分支/注册表显式降级)+ prismarine 真实数据交叉验证
|
2026-09-25 14:25:34 +08:00 |
|
|
|
a4914b932e
|
feat(mcfold): 最小客户端 + 跨实现互操作测试(Python 客户端握手 Node 服务端成功)
|
2026-09-25 14:12:06 +08:00 |
|
|
|
fc5a1aae91
|
feat(mcfold): 声明式 protodef 解释器(原生类型+阶段作用域+switch/mapper/bitfield),真实握手字节验证通过
|
2026-09-25 14:10:40 +08:00 |
|
|
|
22cdee35e3
|
feat(mcfold): P1 按实例文件夹解析版本(inheritsFrom 归一化 + 加载器判定)+ 回归测试
|
2026-09-25 14:06:55 +08:00 |
|