Files
agentmc/scaffold/java/common/mcfold/RenderCompat.java
JianFeeeee 547f4beb7e M1+M2:1.21.1 构建跑通 + 协议客户端进 play 阶段
M1(构建):
- build.sh 编译 CP 加入 $OUT/fabric/*.jar,解决 net.fabricmc.* 找不到
- gen_1_21 内联 Sodium 通知改为委托共用 RenderCompat,补 reportRenderCompat
- 1.21.1 全量七步构建产物齐备(classes-intermediary.jar / names*.json / srg / inheritance.map)

M2(协议):
- login_start 按协议数据发 UUID(1.21 必填;1.20 可选)
- login_acknowledged / serverbound 包 id 改为查表,不再写死 0x00
- send() 实现压缩帧格式(varint(dataLen)+数据,小包不压)
- configuration 双向协商:发 settings、回 select_known_packs、拒 resource_pack
- protodef 补 bitflags / entityMetadataLoop / count(ctx引用) / compareTo 路径表达式
- NBT 读取器由占位符改为真实现(anonymousNbt 支撑 registry_data)
- 坏包容错:长度前缀是帧层权威,解不开整包丢弃并布告,不断流

实测:login→configuration(11 注册表)→play,位置 (5.5,-60.0,-0.5),map_chunk×9
判据:protodef 6→13 项(含线上真实 entity_metadata 字节回归);全量 77 项通过
2026-09-26 12:15:48 +08:00

215 lines
9.3 KiB
Java
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.

package mcfold;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* 装载「地形渲染替换件」(Sodium 等)的桥。
*
* ## 为什么需要它(2026-09-26 实测,根因三层)
*
* **症状**:装了 Sodium 后画面只有天空、`chunksRendered=0`,
* 而 `blockinfo` 能正确读出方块 ⇒ **数据在,只是渲染器不知道**。
*
* **根因**:
* <pre>
* ① Sodium 的 mixin 挂在 replaceWithPacketData 内
* @At(INVOKE ClientLevel.onChunkLoaded, shift=AFTER)
* 而 1.20.1 / 1.21.x 的该方法**内部并不调** onChunkLoaded
* (它在 getChunk / drop 里各有一处,属别的路径)⇒ 注入点永不命中
* ⇒ 它的 ChunkTracker 从未被告知任何区块存在
*
* ② 补上通知后,ChunkTracker 拿到了区块,但仍不进渲染队列
*
* ③ ★ 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" ⇒ 队列恒空 ⇒ 无地形
* 传 3:3&3&3&3 = 3 ⇒ 2×2 组就绪 ⇒ 重建队列有内容
* (我们确实有光照数据 —— 注入时跑了 runLightUpdates())
* </pre>
*
* **实测对比**(同一场景 7×7=49 区块):
* <pre>
* 传 1: chunkStatus=49 chunkReady=0 loadQueue=0 colors=4748
* 传 3: chunkStatus=49 chunkReady=25 loadQueue=25 colors=113546
* </pre>
*
* ## 为什么这不算「给某个 mod 打补丁」
*
* 注入路径缺了「区块已就绪」这一步**广播**。补上它对所有
* 依赖该事件的 mod 都有效,而不只是 Sodium。
* 这里只对 Sodium 做了显式适配,是因为它的 mixin 钩子
* ({@code ChunkTrackerHolder})是 public 接口,且**类不存在时静默跳过** ——
* 不装它就零影响。
*
* ## 世代差异
*
* 1.20.1 与 1.21.1 的 API 位置不同(ChunkStatus 包路径、ChunkStatus 常量),
* 故本类只做**反射调用**,不直接引用 MC 类型 ⇒ 两世代共用一份。
*/
public final class RenderCompat {
private RenderCompat() {}
private static final String SODIUM_HOLDER =
"me.jellysquid.mods.sodium.client.render.chunk.map.ChunkTrackerHolder";
private static final String SODIUM_CHUNK_STATUS =
"me.jellysquid.mods.sodium.client.render.chunk.map.ChunkStatus";
// ── 探测 ───────────────────────────────────────────────
/** 该渲染替换件是否在(装了没装都能安全调)。 */
public static boolean available() {
return sodiumClassLoader() != null;
}
private static ClassLoader sodiumClassLoader() {
try {
Class.forName(SODIUM_HOLDER);
return RenderCompat.class.getClassLoader();
} catch (ClassNotFoundException notInstalled) {
return null;
}
}
/** Sodium 的 FLAG_ALL(有方块 + 有光照);取不到时退回 3。
*
* ★ 3 是硬要求:updateMerged 的 2×2 判定是 `四邻 & == 3`。
* 传 1(只有方块)会让所有区块判成"凑不齐" ⇒ 地形不出现。 */
public static int flagAll() {
try {
Class<?> cs = Class.forName(SODIUM_CHUNK_STATUS);
return cs.getField("FLAG_ALL").getInt(null);
} catch (Throwable t) {
return 3;
}
}
// ── 通知 ───────────────────────────────────────────────
/**
* 告诉渲染替换件:这块地已就绪(等价于网络包路径里会发生的那一步)。
*
* @param level 当前世界(必须是 ClientLevel —— 签名是 get(class_638),
* 传它的父类会 NoSuchMethodException,实测踩过)
* @return 是否成功通知(false = 没装该渲染器,属正常)
*/
public static boolean notifyChunkLoaded(Object level, int cx, int cz) {
try {
Class<?> holder = Class.forName(SODIUM_HOLDER);
// ★ 参数类型必须匹配:先按名字找 ClientLevel 的 Class,
// 再用它做 getMethod 的参数类型(编译期引不到 MC 类型,跨世代共用)
Class<?> levelClass = level.getClass();
Object tracker = holder.getMethod("get", levelClass).invoke(null, level);
if (tracker == null) return false;
// ★ 第三参数是**位掩码 int**(签名 (III)V),不是 ChunkStatus 对象
tracker.getClass()
.getMethod("onChunkStatusAdded", int.class, int.class, int.class)
.invoke(tracker, cx, cz, flagAll());
return true;
} catch (ClassNotFoundException notInstalled) {
return false; // 没装 ⇒ 正常,静默
} catch (Throwable t) {
System.err.println("[mcfold] RenderCompat.notifyChunkLoaded(" + cx + ","
+ cz + "): " + t);
return false;
}
}
// ── 诊断(供 debug 命令读)──────────────────────────────
/** [chunkStatus, chunkReady, loadQueue, chunkStatus 重复项] 的当前大小;没装则 null。 */
public static long[] trackerSnapshot(Object level) {
try {
Class<?> holder = Class.forName(SODIUM_HOLDER);
Object tracker = holder.getMethod("get", level.getClass()).invoke(null, level);
if (tracker == null) return new long[]{-1, -1, -1, -1};
long[] out = new long[4];
String[] names = {"chunkStatus", "chunkReady", "loadQueue", "unloadQueue"};
for (int i = 0; i < names.length; i++) {
Field f = tracker.getClass().getDeclaredField(names[i]);
f.setAccessible(true);
Object v = f.get(tracker);
// 不依赖 instanceof(fastutil 的类型在 remap 后名字会变)⇒ 反射 size()
out[i] = ((Number) v.getClass().getMethod("size").invoke(v)).longValue();
}
return out;
} catch (Throwable t) {
return null;
}
}
/** 它自报的调试串(如 "C: 0/600 D: 12":C=可见, D=距离);取不到则 null。 */
public static String chunksDebugString(Object level) {
try {
Class<?> swr = Class.forName(
"me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer");
Object inst = swr.getMethod("instance").invoke(null);
if (inst == null) return null;
Object s = inst.getClass().getMethod("getChunksDebugString").invoke(inst);
return s == null ? null : s.toString();
} catch (Throwable t) {
return null;
}
}
/** 它的地形是否已渲完(注意:队列空时它会答 true —— 不可单独作为判据)。 */
public static Boolean terrainComplete(Object level) {
try {
Class<?> swr = Class.forName(
"me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer");
Object inst = swr.getMethod("instance").invoke(null);
if (inst == null) return null;
return (Boolean) inst.getClass().getMethod("isTerrainRenderComplete").invoke(inst);
} catch (Throwable t) {
return null;
}
}
/** 可见区块数(vanilla 的 countRenderedChunks 在它之下恒为 0,不能用那个)。 */
public static Integer visibleChunks(Object level) {
try {
Class<?> swr = Class.forName(
"me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer");
Object inst = swr.getMethod("instance").invoke(null);
if (inst == null) return null;
return (Integer) inst.getClass().getMethod("getVisibleChunkCount").invoke(inst);
} catch (Throwable t) {
return null;
}
}
/** 重建队列 {INITIAL_BUILD, REBUILD, IMPORTANT_REBUILD} 的条数;取不到则 null。 */
public static int[] rebuildQueue(Object level) {
try {
Class<?> swr = Class.forName(
"me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer");
Object inst = swr.getMethod("instance").invoke(null);
if (inst == null) return null;
Field f = inst.getClass().getDeclaredField("renderSectionManager");
f.setAccessible(true);
Object rsm = f.get(inst);
if (rsm == null) return null;
Field rl = rsm.getClass().getDeclaredField("rebuildLists");
rl.setAccessible(true);
Object lists = rl.get(rsm);
int[] out = new int[3];
if (lists instanceof java.util.Map<?, ?> m) {
int i = 0;
for (Object v : m.values()) {
if (i < out.length) {
out[i] = v instanceof java.util.Collection<?> c ? c.size() : -1;
}
i++;
}
}
return out;
} catch (Throwable t) {
return null;
}
}
}