From c3a57256ac3bf2515f00e021b49f121aac79b9f4 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sat, 26 Sep 2026 10:17:48 +0800 Subject: [PATCH] =?UTF-8?q?McfoldBridge=20=E6=94=B9=E5=8F=8D=E5=B0=84=20+?= =?UTF-8?q?=20=E6=96=B0=E5=A2=9E=20Sodium=20tracker=20=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E4=B8=8E=20hideGui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三件事,其中后两件是实测查出来的。 ## ① 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=...)。 --- .../java/gen_1_20/McfoldRenderServer.java | 317 +++++++++++++++++- .../client/renderer/McfoldBridge.java | 33 +- .../java/gen_1_21/McfoldRenderServer.java | 95 +++++- .../client/renderer/McfoldBridge.java | 19 +- 4 files changed, 425 insertions(+), 39 deletions(-) diff --git a/scaffold/java/gen_1_20/McfoldRenderServer.java b/scaffold/java/gen_1_20/McfoldRenderServer.java index 2af936c..960dfcf 100644 --- a/scaffold/java/gen_1_20/McfoldRenderServer.java +++ b/scaffold/java/gen_1_20/McfoldRenderServer.java @@ -59,6 +59,8 @@ import java.util.Base64; import java.util.Optional; import java.util.OptionalInt; import java.util.UUID; +import java.lang.reflect.Method; +import mcfold.McfoldNames; /** * mcfold 渲染服务:常驻进程,NDJSON(行分隔 JSON) 协议,由 Python 侧驱动。 @@ -240,6 +242,26 @@ public class McfoldRenderServer { }); light.setLightEnabled(new ChunkPos(cx, cz), true); light.runLightUpdates(); + // ★★★ 补一步官方广播:ClientLevel.onChunkLoaded(pos) + // + // 背景(2026-09-26 实测):装了 Sodium(或任何靠「区块加载 + // 事件」得知区块存在的渲染替换件)时,区块**画不出来**: + // 画面只有天空、chunks=0、它的重建队列恒空, + // 而 blockinfo 能正确读出方块 ⇒ 数据在,只是它不知道。 + // + // 根因:Sodium 的 mixin 挂在 + // replaceWithPacketData 内部 @At(INVOKE onChunkLoaded, AFTER) + // 而 1.20.1 的 replaceWithPacketData **内部并不调** onChunkLoaded + //(它在 getChunk / drop 里各有一处,是别的路径)⇒ 注入点永不命中。 + // + // 为什么这一步是**官方调用**而不是给某个 mod 打补丁: + // onChunkLoaded 是「区块已就绪」的公开广播点 + // (染色缓存 + 实体存储启动), + // 我们绕过网络包直接塞数据 ⇒ 补上这一步才与官方路径等价。 + // 幂等:官方实现只做广播;Sodium 的 onChunkStatusAdded + // 先读已有 status,相同即 return(实测字节码 :13/:31) + // ⇒ 重复调安全。 + announceChunkLoaded(cx, cz); // ★ 必须触发网格重建 —— 否则渲染器不知道该 section 有数据(实测) mc.levelRenderer.allChanged(); JsonObject r = new JsonObject(); @@ -275,10 +297,12 @@ public class McfoldRenderServer { var shaper = mc.getBlockRenderer().getBlockModelShaper(); Object missing = null; try { - var mmF = Minecraft.class.getDeclaredField("modelManager"); + var mmF = McfoldNames.field(Minecraft.class, + "net/minecraft/client/Minecraft/modelManager"); mmF.setAccessible(true); - Object mm = mmF.get(mc); - missing = mm.getClass().getMethod("getMissingModel").invoke(mm); + Object mm = mmF.get(mc); + missing = McfoldNames.method(mm.getClass(), + "net/minecraft/client/resources/model/ModelManager/getMissingModel").invoke(mm); } catch (Throwable t) { } var lookup = BuiltInRegistries.BLOCK.asLookup(); JsonArray rows = new JsonArray(); @@ -294,9 +318,10 @@ public class McfoldRenderServer { ro.addProperty("isMissing", baked == missing); ro.addProperty("modelClass", baked.getClass().getSimpleName()); try { - Object icon = baked.getClass().getMethod("getParticleIcon", + Object icon = McfoldNames.method(baked.getClass(), + "net/minecraft/client/resources/model/BakedModel/getParticleIcon", com.mojang.serialization.Codec.class).invoke(baked, - net.minecraft.client.renderer.texture.TextureAtlasSprite.class == null ? null : null); + (Object) null); } catch (Throwable ignore) { } } catch (Throwable t) { ro.addProperty("error", String.valueOf(t)); @@ -462,6 +487,16 @@ public class McfoldRenderServer { level.getChunkSource().updateViewRadius(rd); mc.levelRenderer.allChanged(); } + // ── HUD(截图场景下应关掉)── + // ★ 用**官方选项**而非改渲染代码: + // GameRenderer.render 里 `if (!options.hideGui) gui.render(...)` + // (字节码 :559 getfield hideGui / :562 ifeq) + // ⇒ 这就是官方 F1 的同一条路径,改一个 bool 即可。 + // 为什么要关:截图只关心画面,HUD 是噪声,而且它会给 + // distinctColors 掺一大块固定像素(实测污染判据)。 + if (req.has("hideGui")) { + mc.options.hideGui = req.get("hideGui").getAsBoolean(); + } if (req.has("fov")) { mc.options.fov().set(req.get("fov").getAsInt()); } @@ -508,8 +543,9 @@ public class McfoldRenderServer { r.addProperty("ok", true); JsonObject cur = new JsonObject(); try { - java.lang.reflect.Field pf = net.minecraft.client.renderer.GameRenderer.class - .getDeclaredField("postEffect"); + java.lang.reflect.Field pf = McfoldNames.field( + net.minecraft.client.renderer.GameRenderer.class, + "net/minecraft/client/renderer/GameRenderer/postEffect"); pf.setAccessible(true); cur.addProperty("postEffect", pf.get(mc.gameRenderer) != null ? "active" : "none"); } catch (Throwable ignore) { @@ -640,6 +676,7 @@ public class McfoldRenderServer { } // viewCenter r.addProperty("fogStart", String.valueOf(mc.options.renderDistance().get())); + probeSodium(id, r); emit(id, r); break; } @@ -702,11 +739,244 @@ public class McfoldRenderServer { return r; } + /** + * 广播「区块已就绪」(等价于网络包路径里会发生的那一步)。 + * + * ★ 为什么单独一个方法:两个世代后端都要用,且它是**加载器共存的关键** —— + * 渲染替换件(Sodium 等)靠这个事件发现区块,缺了它们就永远看不见地形。 + * 实测症状:画面只有天空 + chunks=0 + 它的重建队列恒空, + * 而 blockinfo 能读出方块(数据在,只是没人通知)。 + */ + static void announceChunkLoaded(int cx, int cz) { + // ① 官方广播:对 vanilla 路径无害(只做染色缓存 + 实体存储启动), + // 但让**依赖该事件的 mod** 能正常感知区块。 + try { + level.onChunkLoaded(new ChunkPos(cx, cz)); + } catch (Throwable t) { + System.err.println("[mcfold] onChunkLoaded(" + cx + "," + cz + ") 失败: " + t); + } + // ② 渲染替换件的 tracker(实测:Sodium 靠这个才知道区块存在) + notifyRenderTracker(cx, cz); + } + + /** + * 告诉「渲染替换件」的区块 tracker:这块地已就绪。 + * + * ★ 为什么需要它(2026-09-26 实测,见 debug 里的 sodium* 字段) + * + * 症状:装了 Sodium 后画面只有天空、chunks=0、它的重建队列恒空, + * 而 blockinfo 能正确读出方块 ⇒ **数据在,只是它不知道**。 + * + * 根因:Sodium 的 mixin 挂在 + * replaceWithPacketData 内部 @At(INVOKE onChunkLoaded, AFTER) + * 而 1.20.1 的 replaceWithPacketData **内部并不调** onChunkLoaded + * (它在 getChunk / drop 里各有一处,属别的路径)⇒ 注入点永不命中。 + * 我们直接调 onChunkLoaded 也救不了:mixin 只在 replaceWithPacketData + * **执行期间**生效。 + * + * ★ 为什么这不算“绑死 Sodium”: + * - ChunkTrackerHolder 是它的 **public interface**(mixin 提供的公开钩子); + * - 全部走反射,**类不存在时静默跳过** ⇒ 不装 Sodium 时零影响。 + * - 将来别的渲染器若有同名钩子,改这一处即可。 + */ + static void notifyRenderTracker(int cx, int cz) { + try { + Class holder = Class.forName( + "me.jellysquid.mods.sodium.client.render.chunk.map.ChunkTrackerHolder"); + // ★ 参数类型必须是 **ClientLevel**(不是它的父类 Level)—— + // 签名是 get(class_638);传 Level.class 会 NoSuchMethodException + // (实测踩过:'get(net.minecraft.world.level.Level)' 找不到)。 + Object tracker = holder.getMethod("get", ClientLevel.class).invoke(null, level); + if (tracker == null) return; + // ★ 第三参数是**位掩码 int**(实测签名 (III)V)。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) + // ⇒ **必须报 3**:只报 1 会让所有区块判成"凑不齐 2×2", + // 结果 chunkReady 恒 0、loadQueue 恒空、画面无地形 + // (实测:传 1 → chunkStatus=49 但 chunkReady=0)。 + // 我们确实有光照数据 —— 注入时就跑了 light.runLightUpdates()。 + int flagAll = sodiumFlagAll(); + tracker.getClass() + .getMethod("onChunkStatusAdded", int.class, int.class, int.class) + .invoke(tracker, cx, cz, flagAll); + } catch (ClassNotFoundException noTracker) { + // 没装 Sodium(或其他无此钩子的渲染器)⇒ 正常,静默 + } catch (Throwable t) { + // 装了但调用失败 ⇒ 报出来(否则会默默退回“无地形”) + System.err.println("[mcfold] notifyRenderTracker(" + cx + "," + cz + "): " + t); + } + } + + /** Sodium 的 FLAG_ALL(有方块 + 有光照);取不到时退回 3。 */ + static int sodiumFlagAll() { + try { + Class cs = sodiumClass().getClassLoader() + .loadClass("me.jellysquid.mods.sodium.client.render.chunk.map.ChunkStatus"); + return cs.getField("FLAG_ALL").getInt(null); + } catch (Throwable t) { + return 3; + } + } + static void levelRendererAllChanged() { mc.levelRenderer.allChanged(); } /** tick + 渲染各帧(区块网格重建队列靠 tick 推进) */ + /** [chunkReady, loadQueue] 的当前大小;未装 Sodium 则 null。 */ + static long[] sodiumTrackerSnapshot() { + var holder = sodiumClass(); + if (holder == null) return null; + try { + Class h = holder.getClassLoader() + .loadClass("me.jellysquid.mods.sodium.client.render.chunk.map.ChunkTrackerHolder"); + Object tracker = h.getMethod("get", + net.minecraft.client.multiplayer.ClientLevel.class).invoke(null, level); + if (tracker == null) return new long[]{-1, -1, -1, -1}; + long[] out = new long[4]; + String[] names = {"chunkReady", "loadQueue", "unloadQueue", "chunkStatus"}; + for (int i = 0; i < 4; i++) { + var f = tracker.getClass().getDeclaredField(names[i]); + f.setAccessible(true); + Object v = f.get(tracker); + out[i] = ((Number) v.getClass().getMethod("size").invoke(v)).longValue(); + } + return out; + } catch (Throwable t) { + return null; + } + } + + /** 本次渲染期间 [chunkReady 增量, loadQueue 增量]。 */ + static final long[] SODIUM_DELTA = new long[2]; + + /** 诊断:Sodium 接管区块渲染后,它的真实状态(不装 Sodium 则全部 absent)。 + * + * ★ 为什么需要它:Sodium 用 mixin 换掉了 LevelRenderer 的地形渲染, + * 而 vanilla 的 `countRenderedChunks()` **在 Sodium 下永远返回 0** + * (它数的是 vanilla 的 section 网格,Sodium 不走那条路)。 + * 实测症状:`chunks=0` + `hasRenderedAllChunks=true` + 画面无地形 + * —— 而 `blockinfo` 表明数据已在区块里。 + * ⇒ 光看 vanilla 计数会误判成「没数据」,实为「换了渲染器」。 + */ + static void probeSodium(int id, JsonObject r) { + r.addProperty("sodiumLoaded", sodiumClass() != null); + var swr = sodiumInstance(); + if (swr == null) { r.addProperty("sodiumState", "absent"); return; } + try { + Method m = swr.getClass().getMethod("getVisibleChunkCount"); + r.addProperty("sodiumVisibleChunks", String.valueOf(m.invoke(swr))); + Method c = swr.getClass().getMethod("isTerrainRenderComplete"); + r.addProperty("sodiumTerrainComplete", String.valueOf(c.invoke(swr))); + // ★ 直接读 ChunkTracker 的内部集合:这是「区块到底有没有进 tracker」 + // 的权威证据(visibleChunks=0 可能只是可见性判定为假)。 + try { + Class holder = sodiumClass().getClassLoader() + .loadClass("me.jellysquid.mods.sodium.client.render.chunk.map.ChunkTrackerHolder"); + Object tracker = holder.getMethod("get", + net.minecraft.client.multiplayer.ClientLevel.class).invoke(null, level); + if (tracker != null) { + for (String fl : new String[]{"chunkStatus", "chunkReady", "loadQueue"}) { + var ff = tracker.getClass().getDeclaredField(fl); + ff.setAccessible(true); + Object v = ff.get(tracker); + // 不依赖 instanceof(fastutil 的类型在 remap 后名字会变)⇒ 用反射 size() + int sz = -1; + try { sz = (Integer) v.getClass().getMethod("size").invoke(v); } + catch (Throwable ignore) { } + r.addProperty("sodiumTracker_" + fl, sz); + } + Method mc2 = tracker.getClass().getMethod("getReadyChunks"); + Object rc = mc2.invoke(tracker); + try { r.addProperty("sodiumReadyChunks", + String.valueOf(rc.getClass().getMethod("size").invoke(rc))); } + catch (Throwable ignore) { r.addProperty("sodiumReadyChunks", "n/a"); } + } + } catch (Throwable t) { + r.addProperty("sodiumTrackerProbeError", String.valueOf(t)); + } + r.addProperty("sodiumDeltaChunkReady", SODIUM_DELTA[0]); + r.addProperty("sodiumDeltaLoadQueue", SODIUM_DELTA[1]); + r.addProperty("sodiumUnloadQueue", sodiumTrackerSnapshot() == null ? -1 + : sodiumTrackerSnapshot()[2]); + r.addProperty("sodiumNeighboursAfter", sodiumTrackerSnapshot() == null ? -1 + : sodiumTrackerSnapshot()[3]); + Method dbg = swr.getClass().getMethod("getChunksDebugString"); + Object s = dbg.invoke(swr); + r.addProperty("sodiumChunksDebug", s == null ? "null" : s.toString()); + // 重建队列里还剩多少活? + var f = swr.getClass().getDeclaredField("renderSectionManager"); + f.setAccessible(true); + Object rsm = f.get(swr); + if (rsm != null) { + for (String fl : new String[]{"rebuildLists", "needsUpdate", "lastUpdatedFrame"}) { + try { + var ff = rsm.getClass().getDeclaredField(fl); + ff.setAccessible(true); + Object v = ff.get(rsm); + if (fl.equals("rebuildLists") && v instanceof java.util.Map m2) { + JsonObject q = new JsonObject(); + for (var en : m2.entrySet()) { + Object de = en.getValue(); + int sz = de instanceof java.util.Collection c2 ? c2.size() : -1; + q.addProperty(String.valueOf(en.getKey()), sz); + } + r.add("sodiumRebuildQueue", q); + } else { + r.addProperty("sodium_" + fl, String.valueOf(v)); + } + } catch (NoSuchFieldException ignore) { } + } + } + } catch (Throwable t) { + r.addProperty("sodiumProbeError", String.valueOf(t)); + } + // 本次渲染期间 setupTerrain 被调了几次(快照法:渲染前后相减) + } + + /** SodiumWorldRenderer 的 Class(未装 Sodium 则 null)。 */ + static Class sodiumClass() { + try { + return Class.forName("me.jellysquid.mods.sodium.client.render.SodiumWorldRenderer"); + } catch (Throwable t) { + return null; + } + } + + /** 当前 SodiumWorldRenderer 单例(未装 / 未初始化则 null)。 */ + static Object sodiumInstance() { + var c = sodiumClass(); + if (c == null) return null; + try { + return c.getMethod("instance").invoke(null); + } catch (Throwable t) { + return null; + } + } + + /** 探针:Sodium 的 setupTerrain/processChunkEvents 到底有没有被调(计数)。 */ + static final java.util.concurrent.atomic.AtomicLong SODIUM_SETUP_TERRAIN = + new java.util.concurrent.atomic.AtomicLong(); + static final java.util.concurrent.atomic.AtomicLong SODIUM_PROCESS_EVENTS = + new java.util.concurrent.atomic.AtomicLong(); + + /** + * 探针:Sodium 的 setupTerrain 到底有没有被它的 hook 调到。 + * + * ★ 为什么需要它(实测困境): + * Sodium 的 mixin 挂 LevelRenderer.setupRender(private), + * 而 setupRender 只在 renderLevel 内部被调(字节码 :484), + * 我们**确实**调了 renderLevel —— 按理 setupTerrain 该被调到, + * 但 chunkReady 恒 0。 + * ⇒ 缺一个「它被调了几次」这个数,否则无法区分: + * (a) hook 没走到 (b) 走到了但参数无效 (c) 走到了但内部失败 + * + * 做法:在调用**之前**记录现状(已调次数),渲染后由 debug 报差值。 + * 只观测,不驱动 —— 驱动它要构造 Frustum,属于接管渲染循环。 + */ static void tickFrames(int n) { MainTarget target = (MainTarget) mc.getMainRenderTarget(); for (int f = 0; f < n; f++) { @@ -723,7 +993,15 @@ public class McfoldRenderServer { } static void render(Integer id, int frames, String out) throws Exception { + // ★ 观测:渲染**前**的 tracker 快照(用来判断 processChunkEvents 有没有被调) + long[] before = sodiumTrackerSnapshot(); tickFrames(frames); + long[] after = sodiumTrackerSnapshot(); + if (after != null) { + // delta 放进静态字段,供 debug 读取 + SODIUM_DELTA[0] = after[0] - before[0]; + SODIUM_DELTA[1] = after[1] - before[1]; + } MainTarget target = (MainTarget) mc.getMainRenderTarget(); int w = mc.getWindow().getWidth(), h = mc.getWindow().getHeight(); @@ -811,8 +1089,9 @@ public class McfoldRenderServer { var layered = net.minecraft.client.multiplayer.ClientRegistryLayer.createRegistryAccess() .replaceFrom(net.minecraft.client.multiplayer.ClientRegistryLayer.REMOTE, new RegistryAccess.Frozen[]{loaded}); - var rf = ClientPacketListener.class.getDeclaredField("registryAccess"); - rf.setAccessible(true); + var rf = McfoldNames.field(ClientPacketListener.class, + "net/minecraft/client/multiplayer/ClientPacketListener/registryAccess"); + rf.setAccessible(true); rf.set(listener, layered); Holder dimHolder = loaded.registryOrThrow(Registries.DIMENSION_TYPE) @@ -961,13 +1240,14 @@ public class McfoldRenderServer { try { for (Class c = blockReg.getClass(); c != null; c = c.getSuperclass()) { try { - var f = c.getDeclaredField("frozen"); + var f = McfoldNames.field(c, "net/minecraft/core/MappedRegistry/frozen"); f.setAccessible(true); f.setBoolean(blockReg, false); } catch (NoSuchFieldException ignore) { } - try { - var f = c.getDeclaredField("unregisteredIntrusiveHolders"); - f.setAccessible(true); + try { + var f = McfoldNames.field(c, + "net/minecraft/core/MappedRegistry/unregisteredIntrusiveHolders"); + f.setAccessible(true); if (f.get(blockReg) == null) f.set(blockReg, new java.util.IdentityHashMap<>()); } catch (NoSuchFieldException ignore) { } } @@ -1070,11 +1350,13 @@ public class McfoldRenderServer { // 再手工驱动 listener(不能 join Minecraft.reloadResourcePacks,会死锁) java.util.List packs = new java.util.ArrayList<>(); for (var pk : repo.getSelectedPacks()) packs.add(pk.open()); - var rmF = Minecraft.class.getDeclaredField("resourceManager"); + var rmF = McfoldNames.field(Minecraft.class, + "net/minecraft/client/Minecraft/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"); + var resF = McfoldNames.field( + net.minecraft.server.packs.resources.ReloadableResourceManager.class, + "net/minecraft/server/packs/resources/ReloadableResourceManager/resources"); resF.setAccessible(true); resF.set(rm, new net.minecraft.server.packs.resources.MultiPackResourceManager( net.minecraft.server.packs.PackType.CLIENT_RESOURCES, packs)); @@ -1127,7 +1409,8 @@ public class McfoldRenderServer { /** 同步驱动各 reload listener(不能 join Minecraft 的 reloadResourcePacks:等主循环 → 死锁)*/ static void reloadResources(Minecraft mc) throws Exception { - var rmF = Minecraft.class.getDeclaredField("resourceManager"); + var rmF = McfoldNames.field(Minecraft.class, + "net/minecraft/client/Minecraft/resourceManager"); rmF.setAccessible(true); var rm = (net.minecraft.server.packs.resources.ReloadableResourceManager) rmF.get(mc); mc.getResourcePackRepository().reload(); diff --git a/scaffold/java/gen_1_20/net/minecraft/client/renderer/McfoldBridge.java b/scaffold/java/gen_1_20/net/minecraft/client/renderer/McfoldBridge.java index 961e835..b317285 100644 --- a/scaffold/java/gen_1_20/net/minecraft/client/renderer/McfoldBridge.java +++ b/scaffold/java/gen_1_20/net/minecraft/client/renderer/McfoldBridge.java @@ -2,6 +2,7 @@ package net.minecraft.client.renderer; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceProvider; +import mcfold.McfoldNames; /** * 包私有成员桥。 @@ -10,14 +11,42 @@ import net.minecraft.server.packs.resources.ResourceProvider; * 我们不跑主循环、不连服,所以要用同包类直接调。 * reloadShaders —— 官方由匿名内部类 GameRenderer$1(资源重载 listener)调 * loadEffect —— 官方由 onResourceManagerReload / 药水效果改 + * + * ★★ 为什么这里必须用反射,而不是同包直调(2026-09-26 实测) + * + * 「把桥塞进 `net.minecraft.client.renderer` 包」这个技巧只在 **named** 命名空间下成立: + * + * named 桥在 net.minecraft.client.renderer + * GameRenderer 也在 net.minecraft.client.renderer ⇒ 同包 ✅ + * intermediary 桥仍在 net.minecraft.client.renderer(我们自己的类不被改名) + * GameRenderer → **net.minecraft.class_757**(包变成 net.minecraft) + * ⇒ 异包 ❌ + * ⇒ `IllegalAccessError: McfoldBridge tried to access method + * 'void net.minecraft.class_757.method_34538(...)'` + * + * ⇒ **包名本身也被 remap**,所以「靠包名对齐」的做法切换命名空间就断了。 + * 反射 + 语义名(见 McfoldNames)与命名空间无关,是唯一两态都通的形态。 */ public class McfoldBridge { + public static void reloadShaders(GameRenderer gr, ResourceProvider provider) { - gr.reloadShaders(provider); + try { + McfoldNames.method(GameRenderer.class, + "net/minecraft/client/renderer/GameRenderer/reloadShaders", + ResourceProvider.class).invoke(gr, provider); + } catch (Exception e) { + throw new RuntimeException("reloadShaders 调用失败", e); + } } /** 载入一个原版后处理链(如 minecraft:shaders/post/invert.json)。 */ public static void loadEffect(GameRenderer gr, ResourceLocation id) { - gr.loadEffect(id); + try { + McfoldNames.method(GameRenderer.class, + "net/minecraft/client/renderer/GameRenderer/loadEffect", + ResourceLocation.class).invoke(gr, id); + } catch (Exception e) { + throw new RuntimeException("loadEffect 调用失败", e); + } } } diff --git a/scaffold/java/gen_1_21/McfoldRenderServer.java b/scaffold/java/gen_1_21/McfoldRenderServer.java index 1678c24..0a43ebc 100644 --- a/scaffold/java/gen_1_21/McfoldRenderServer.java +++ b/scaffold/java/gen_1_21/McfoldRenderServer.java @@ -59,6 +59,7 @@ import java.util.Base64; import java.util.Optional; import java.util.OptionalInt; import java.util.UUID; +import mcfold.McfoldNames; /** * mcfold 渲染服务:常驻进程,NDJSON(行分隔 JSON) 协议,由 Python 侧驱动。 @@ -240,6 +241,12 @@ public class McfoldRenderServer { }); light.setLightEnabled(new ChunkPos(cx, cz), true); light.runLightUpdates(); + // ★★★ 补一步官方广播(见 gen_1_20 同处的完整说明): + // 渲染替换件(Sodium 等)靠「区块加载事件」得知区块存在; + // 我们绕过网络包直接塞数据 ⇒ 缺这一步它们永远看不见地形 + // (实测:画面只有天空、chunks=0、它的重建队列恒空, + // 而 blockinfo 能读出方块 ⇒ 数据在,只是没人通知) + announceChunkLoaded(cx, cz); // ★ 必须触发网格重建 —— 否则渲染器不知道该 section 有数据(实测) mc.levelRenderer.allChanged(); JsonObject r = new JsonObject(); @@ -275,10 +282,12 @@ public class McfoldRenderServer { var shaper = mc.getBlockRenderer().getBlockModelShaper(); Object missing = null; try { - var mmF = Minecraft.class.getDeclaredField("modelManager"); + var mmF = McfoldNames.field(Minecraft.class, + "net/minecraft/client/Minecraft/modelManager"); mmF.setAccessible(true); - Object mm = mmF.get(mc); - missing = mm.getClass().getMethod("getMissingModel").invoke(mm); + Object mm = mmF.get(mc); + missing = McfoldNames.method(mm.getClass(), + "net/minecraft/client/resources/model/ModelManager/getMissingModel").invoke(mm); } catch (Throwable t) { } var lookup = BuiltInRegistries.BLOCK.asLookup(); JsonArray rows = new JsonArray(); @@ -294,9 +303,10 @@ public class McfoldRenderServer { ro.addProperty("isMissing", baked == missing); ro.addProperty("modelClass", baked.getClass().getSimpleName()); try { - Object icon = baked.getClass().getMethod("getParticleIcon", + Object icon = McfoldNames.method(baked.getClass(), + "net/minecraft/client/resources/model/BakedModel/getParticleIcon", com.mojang.serialization.Codec.class).invoke(baked, - net.minecraft.client.renderer.texture.TextureAtlasSprite.class == null ? null : null); + (Object) null); } catch (Throwable ignore) { } } catch (Throwable t) { ro.addProperty("error", String.valueOf(t)); @@ -463,6 +473,15 @@ public class McfoldRenderServer { level.getChunkSource().updateViewRadius(rd); mc.levelRenderer.allChanged(); } + // ── HUD(截图场景下应关掉)── + // ★ 用**官方选项**而非改渲染代码: + // GameRenderer.render 里 `if (!options.hideGui) gui.render(...)` + // ⇒ 这就是官方 F1 的同一条路径,改一个 bool 即可。 + // 为什么要关:截图只关心画面,HUD 是噪声,而且它会给 + // distinctColors 掺一大块固定像素(实测污染判据)。 + if (req.has("hideGui")) { + mc.options.hideGui = req.get("hideGui").getAsBoolean(); + } if (req.has("fov")) { mc.options.fov().set(req.get("fov").getAsInt()); } @@ -509,8 +528,9 @@ public class McfoldRenderServer { r.addProperty("ok", true); JsonObject cur = new JsonObject(); try { - java.lang.reflect.Field pf = net.minecraft.client.renderer.GameRenderer.class - .getDeclaredField("postEffect"); + java.lang.reflect.Field pf = McfoldNames.field( + net.minecraft.client.renderer.GameRenderer.class, + "net/minecraft/client/renderer/GameRenderer/postEffect"); pf.setAccessible(true); cur.addProperty("postEffect", pf.get(mc.gameRenderer) != null ? "active" : "none"); } catch (Throwable ignore) { @@ -709,6 +729,49 @@ public class McfoldRenderServer { return r; } + /** + * 广播「区块已就绪」(等价于网络包路径里会发生的那一步)。 + * + * ★ 加载器共存的关键:渲染替换件(Sodium 等)靠这个事件发现区块, + * 缺了它们就永远看不见地形(画面只有天空、chunks=0、它的队列恒空, + * 而 blockinfo 能读出方块 —— 数据在,只是没人通知)。 + */ + static void announceChunkLoaded(int cx, int cz) { + // ① 官方广播(对 vanilla 路径无害) + try { + level.onChunkLoaded(new ChunkPos(cx, cz)); + } catch (Throwable t) { + System.err.println("[mcfold] onChunkLoaded(" + cx + "," + cz + ") 失败: " + t); + } + // ② 渲染替换件的 tracker(完整说明见 gen_1_20 同处) + try { + Class holder = Class.forName( + "me.jellysquid.mods.sodium.client.render.chunk.map.ChunkTrackerHolder"); + // ★ 参数类型必须是 ClientLevel(签名 get(class_638)),传 Level.class 会 NoSuchMethodException + Object tracker = holder.getMethod("get", + net.minecraft.client.multiplayer.ClientLevel.class).invoke(null, level); + if (tracker == null) return; + // ★ 第三参数是位掩码 int(实测签名 (III)V): + // FLAG_HAS_BLOCK_DATA=1 / FLAG_HAS_LIGHT_DATA=2 / FLAG_ALL=3 + // 而 updateMerged 的 2×2 判定要求 == 3 ⇒ 必须报 FLAG_ALL, + // 否则所有区块判成"凑不齐 2×2" ⇒ chunkReady 恒 0、无地形 + // (实测:传 1 → chunkStatus=49 但 chunkReady=0) + int flagAll; + try { + Class cs = sodiumClass().getClassLoader().loadClass( + "me.jellysquid.mods.sodium.client.render.chunk.map.ChunkStatus"); + flagAll = cs.getField("FLAG_ALL").getInt(null); + } catch (Throwable t) { flagAll = 3; } + tracker.getClass() + .getMethod("onChunkStatusAdded", int.class, int.class, int.class) + .invoke(tracker, cx, cz, flagAll); + } catch (ClassNotFoundException noTracker) { + // 没装该渲染器 ⇒ 正常,静默 + } catch (Throwable t) { + System.err.println("[mcfold] notifyRenderTracker(" + cx + "," + cz + "): " + t); + } + } + static void levelRendererAllChanged() { mc.levelRenderer.allChanged(); } @@ -1006,12 +1069,13 @@ public class McfoldRenderServer { try { for (Class c = blockReg.getClass(); c != null; c = c.getSuperclass()) { try { - var f = c.getDeclaredField("frozen"); + var f = McfoldNames.field(c, "net/minecraft/core/MappedRegistry/frozen"); f.setAccessible(true); f.setBoolean(blockReg, false); } catch (NoSuchFieldException ignore) { } - try { - var f = c.getDeclaredField("unregisteredIntrusiveHolders"); + try { + var f = McfoldNames.field(c, + "net/minecraft/core/MappedRegistry/unregisteredIntrusiveHolders"); f.setAccessible(true); if (f.get(blockReg) == null) f.set(blockReg, new java.util.IdentityHashMap<>()); } catch (NoSuchFieldException ignore) { } @@ -1117,11 +1181,13 @@ public class McfoldRenderServer { // 再手工驱动 listener(不能 join Minecraft.reloadResourcePacks,会死锁) java.util.List packs = new java.util.ArrayList<>(); for (var pk : repo.getSelectedPacks()) packs.add(pk.open()); - var rmF = Minecraft.class.getDeclaredField("resourceManager"); + var rmF = McfoldNames.field(Minecraft.class, + "net/minecraft/client/Minecraft/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"); + var resF = McfoldNames.field( + net.minecraft.server.packs.resources.ReloadableResourceManager.class, + "net/minecraft/server/packs/resources/ReloadableResourceManager/resources"); resF.setAccessible(true); resF.set(rm, new net.minecraft.server.packs.resources.MultiPackResourceManager( net.minecraft.server.packs.PackType.CLIENT_RESOURCES, packs)); @@ -1176,7 +1242,8 @@ public class McfoldRenderServer { /** 同步驱动各 reload listener(不能 join Minecraft 的 reloadResourcePacks:等主循环 → 死锁)*/ static void reloadResources(Minecraft mc) throws Exception { - var rmF = Minecraft.class.getDeclaredField("resourceManager"); + var rmF = McfoldNames.field(Minecraft.class, + "net/minecraft/client/Minecraft/resourceManager"); rmF.setAccessible(true); var rm = (net.minecraft.server.packs.resources.ReloadableResourceManager) rmF.get(mc); mc.getResourcePackRepository().reload(); diff --git a/scaffold/java/gen_1_21/net/minecraft/client/renderer/McfoldBridge.java b/scaffold/java/gen_1_21/net/minecraft/client/renderer/McfoldBridge.java index 81d56c5..5ad0404 100644 --- a/scaffold/java/gen_1_21/net/minecraft/client/renderer/McfoldBridge.java +++ b/scaffold/java/gen_1_21/net/minecraft/client/renderer/McfoldBridge.java @@ -2,6 +2,7 @@ package net.minecraft.client.renderer; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.ResourceProvider; +import mcfold.McfoldNames; /** * 包私有成员桥。 @@ -24,8 +25,8 @@ public class McfoldBridge { */ public static void markGameLoadFinished(net.minecraft.client.Minecraft mc) { try { - java.lang.reflect.Field f = net.minecraft.client.Minecraft.class - .getDeclaredField("gameLoadFinished"); + java.lang.reflect.Field f = McfoldNames.field(net.minecraft.client.Minecraft.class, + "net/minecraft/client/Minecraft/gameLoadFinished"); f.setAccessible(true); f.setBoolean(mc, true); } catch (Throwable t) { @@ -33,7 +34,13 @@ public class McfoldBridge { } } public static void reloadShaders(GameRenderer gr, ResourceProvider provider) { - gr.reloadShaders(provider); + try { + McfoldNames.method(GameRenderer.class, + "net/minecraft/client/renderer/GameRenderer/reloadShaders", + ResourceProvider.class).invoke(gr, provider); + } catch (Exception e) { + throw new RuntimeException("reloadShaders 调用失败", e); + } } /** @@ -47,9 +54,9 @@ public class McfoldBridge { */ public static void loadEffect(GameRenderer gr, ResourceLocation id) { try { - java.lang.reflect.Method m = GameRenderer.class.getDeclaredMethod("loadEffect", ResourceLocation.class); - m.setAccessible(true); - m.invoke(gr, id); + McfoldNames.method(GameRenderer.class, + "net/minecraft/client/renderer/GameRenderer/loadEffect", + ResourceLocation.class).invoke(gr, id); } catch (Exception e) { throw new RuntimeException("loadEffect 不可用(1.21+ 为 private)", e); }