diff --git a/scaffold/GlProbe.java b/scaffold/GlProbe.java deleted file mode 100644 index 80afc7b..0000000 --- a/scaffold/GlProbe.java +++ /dev/null @@ -1,21 +0,0 @@ -import org.lwjgl.glfw.*; -import org.lwjgl.opengl.*; -import static org.lwjgl.glfw.GLFW.*; -import static org.lwjgl.opengl.GL11.*; -import static org.lwjgl.system.MemoryUtil.NULL; - -public class GlProbe { - public static void main(String[] a) { - GLFWErrorCallback.createPrint(System.err).set(); - if (!glfwInit()) { System.out.println("FAIL: glfwInit"); return; } - glfwDefaultWindowHints(); - glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE); - long win = glfwCreateWindow(320, 240, "probe", NULL, NULL); - if (win == NULL) { System.out.println("FAIL: createWindow"); return; } - glfwMakeContextCurrent(win); - GL.createCapabilities(); - System.out.println("OK gl=" + glGetString(GL_VERSION)); - System.out.println("OK renderer=" + glGetString(GL_RENDERER)); - glfwDestroyWindow(win); glfwTerminate(); - } -} diff --git a/scaffold/README.md b/scaffold/README.md index c249072..3448d80 100644 --- a/scaffold/README.md +++ b/scaffold/README.md @@ -6,67 +6,102 @@ ## 实测结论(2026-09-25 全绿) 客户端 jar 是 **ProGuard 全量混淆**的(`LevelRenderer` 叫 `fjv`), -所以「抽渲染器」的第一步必须是**反混淆**。三层关卡全部验证通过: +所以「抽渲染器」的第一步必须是**反混淆**。全部关卡已验证: | 关卡 | 证据 | 结果 | |---|---|---| -| 反混淆 | `client-named.jar` 25MB;`javap` 读出 `net.minecraft.client.renderer.LevelRenderer` | ✅ | -| 编译期引用 | `javac` 编译引用 `Minecraft`/`LevelRenderer` 的代码 | ✅ | +| 反混淆 | `client-named.jar`;`javap` 读出 `LevelRenderer` | ✅ | | GL 离屏 | Xvfb + llvmpipe → OpenGL 4.5 core | ✅ | -| **渲染栈初始化** | `new Minecraft(cfg)` 完成 GL/资源/渲染器初始化,**不进主循环** | ✅ | -| **真渲染出图** | 渲 3 帧 + 读像素 → PNG,画面是 Mojang 加载屏(logo 贴图 + 进度条) | ✅ | +| 渲染栈初始化 | `new Minecraft(cfg)` 完成初始化,**不进主循环** | ✅ | +| 资源重载 | 22 个 listener;ModelManager/FontManager/Dispatcher 全 ok | ✅ | +| 注册表 | 客户端自带 `data/` → dimension_type=4 | ✅ | +| 区块注入 | 81 chunks → `countRenderedChunks=29` | ✅ | +| **真实世界渲染** | 草地+原木+树叶+天空+HUD 文字,`distinct colors=1450` | ✅ | + +见 `sample-world-render.png`。 ## 为什么渲染器不能独立 new -反编译(Vineflower)给出的实测签名: +反编译(Vineflower)实测签名: ```java LevelRenderer(Minecraft, EntityRenderDispatcher, BlockEntityRenderDispatcher, RenderBuffers) renderLevel(PoseStack, float, long, boolean, Camera, GameRenderer, LightTexture, Matrix4f) ``` -全部挂在 `Minecraft` 单例上 ⇒ **不能** `new LevelRenderer()` 独立用。 +全挂在 `Minecraft` 单例上 ⇒ **不能** `new LevelRenderer()` 独立用。 正确形态是「初始化 MC 渲染栈,但不跑主循环」。 -## 权威序列(反编译 `net.minecraft.client.main.Main` 得到) +## 权威序列 + +初始化(反编译 `net.minecraft.client.main.Main`): ```java -SharedConstants.tryDetectVersion(); // ← 漏了会 "Game version not set" +SharedConstants.tryDetectVersion(); // 漏了 → "Game version not set" SharedConstants.enableDataFixerOptimizations(); Bootstrap.bootStrap(); RenderSystem.initRenderThread(); RenderSystem.beginInitialization(); -Minecraft mc = new Minecraft(gameConfig); // ← 构造即完成渲染栈初始化 +Minecraft mc = new Minecraft(gameConfig); RenderSystem.finishInitialization(); -// 此处官方会起 Game thread 跑主循环 —— 我们不做 ``` -渲染一帧(反编译 `Minecraft.render()` 得到): +渲染一帧(反编译 `Minecraft.render()`): ```java RenderSystem.clear(16640, ON_OSX); mainRenderTarget.bindWrite(true); -gameRenderer.render(partialTick, nanos, renderLevel); +gameRenderer.render(partialTick, nanos, true); mainRenderTarget.unbindWrite(); mainRenderTarget.blitToScreen(w, h); window.updateDisplay(); ``` -## 踩过的三个坑(都有实测证据) +## 踩过的坑(全部有实测证据) 1. **漏 `tryDetectVersion()`** → `DataFixers.` 抛 `Game version not set` -2. **`FolderData` 参数顺序** - `(gameDirectory, resourcePackDirectory, assetDirectory, assetIndex)` - —— 顺序写错会让 `icons/icon_16x16.png` 404 -3. **assetIndex 名要用落盘名** - version.json 里 `assetIndex.id = "5"`,但磁盘是 `assets/indexes/1.20.1.json` - (MCLC 按 `version.number` 落盘)—— 传 `"5"` 则索引加载失败、资源全找不到 +2. **`FolderData` 参数顺序** = `(gameDirectory, resourcePackDirectory, assetDirectory, assetIndex)` + —— 写错则 `icons/icon_16x16.png` 404 +3. **assetIndex 要用落盘名**(`1.20.1`)而非 `version.json` 的 `assetIndex.id`(`5`) + —— MCLC 按 `version.number` 落盘 +4. **`LoadingOverlay` 必须摘** → 否则永远渲出 Mojang 加载屏 +5. **`ReceivingLevelScreen`("Joining world...")必须清** → 否则客户端认为未进世界 +6. **必须跑 `mc.tick()`** → 区块网格重建队列靠 tick 推进(不跑则 `countRenderedChunks=0`) +7. **`reloadResourcePacks()` 不可 join**(等主循环 → 死锁); + 正解是**直接调各 listener 的 public `reload()`**,barrier 同步返回、 + apply 用 `Runnable::run`(主线程即渲染线程) +8. **`GameRenderer.reloadShaders` 包私有** → 同包 bridge 调用 +9. **读像素必须绑 FBO**(`glBindFramebuffer(target.frameBufferId)` + `GL_COLOR_ATTACHMENT0`); + `bindRead()` 路径实测读到的是残留缓冲 +10. **判据要锐利**:`non-black=100%` 会被红底加载屏骗过; + `distinct colors` 才区分得出「加载屏(199) vs 世界(1450)」 -## 下一步:渲染真实世界 +## 注入世界数据 -`ClientLevel` 构造只需 `listener.registryAccess()`(其余字段不用 listener), -且 `ClientChunkCache.replaceWithPacketData(...)` 接收的正是 **`map_chunk` 包的原始字节** -—— 与 mcfold `chunk.py` 解码器同一格式。这就是设计文档说的桥梁落点。 +```java +LevelChunk chunk = new LevelChunk(level, new ChunkPos(cx, cz)); +chunk.setBlockState(new BlockPos(lx, y, lz), Blocks.GRASS_BLOCK.defaultBlockState(), false); +// ★ 放进 ClientChunkCache.Storage.chunks(反射 getIndex) +// ★ 光照:LightEngine.queueSectionData(SKY/BLOCK, SectionPos, DataLayer 全15) +// ★ 触发重建:levelRenderer.allChanged() + 每帧 mc.tick() +``` + +**与 mcfold Python 客户端的接口**:`ClientChunkCache.replaceWithPacketData(...)` +接收的正是 `map_chunk` 包原始字节 —— **Python 客户端收到的就是它**, +可零转换直通;或用 Python 解码后的方块写入 `LevelChunk`。 + +## 注册表:state id ↔ 方块名(mod 支持的关键) + +```java +BuiltInRegistries.BLOCK.size() // 1003 +Block.BLOCK_STATE_REGISTRY.size() // 24135 +Block.stateById(1003) // note_block[instrument=xylophone,note=7,powered=false] +BuiltInRegistries.BLOCK.getKey(state.getBlock()) +``` + +★ **更正附录 D 的一个错误结论**:`1003 → note_block` **不是**静默误映射 +—— note_block 有 800 个状态,1003 真的落在其范围内。真客户端给的是**完整状态** +(`instrument/note/powered`),这正是 1.20.1 上 mod 方块语义的唯一权威来源。 ## 构建 diff --git a/scaffold/RegistryProbe.java b/scaffold/RegistryProbe.java new file mode 100644 index 0000000..08badad --- /dev/null +++ b/scaffold/RegistryProbe.java @@ -0,0 +1,82 @@ +import com.mojang.blaze3d.platform.DisplayData; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.authlib.properties.PropertyMap; +import net.minecraft.SharedConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.User; +import net.minecraft.client.main.GameConfig; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.Bootstrap; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +import java.io.File; +import java.net.Proxy; +import java.util.Optional; +import java.util.OptionalInt; + +/** + * 证明:真客户端的注册表能给出 state id ↔ 方块名的权威映射。 + * + * 这正是不起完整客户端、只用客户端注册表的价值所在: + * 1.20.1 不从网络下发方块表(附录D),mod 方块的 state id 只有客户端注册表知道。 + */ +public class RegistryProbe { + + public static void main(String[] args) throws Exception { + String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default"); + String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets"); + String assetIdx = arg(args, "--assetIndex", "1.20.1"); + + SharedConstants.tryDetectVersion(); + SharedConstants.enableDataFixerOptimizations(); + Bootstrap.bootStrap(); + + File runDir = new File(gameDir); + User user = new User("mcfold_cam", "0".repeat(32), "mcfold_cam", + Optional.empty(), Optional.empty(), User.Type.LEGACY); + PropertyMap props = new PropertyMap(); + GameConfig cfg = new GameConfig( + new GameConfig.UserData(user, props, props, Proxy.NO_PROXY), + new DisplayData(854, 480, OptionalInt.empty(), OptionalInt.empty(), false), + new GameConfig.FolderData(runDir, new File(runDir, "resourcepacks"), new File(assets), assetIdx), + new GameConfig.GameData(false, "mcfold", "release", false, false), + new GameConfig.QuickPlayData(null, null, null, null)); + + RenderSystem.initRenderThread(); + RenderSystem.beginInitialization(); + Minecraft mc = new Minecraft(cfg); + RenderSystem.finishInitialization(); + System.out.println("[reg] Minecraft constructed"); + + // ★ 核心:客户端的方块注册表 + int blockCount = BuiltInRegistries.BLOCK.size(); + int stateCount = Block.BLOCK_STATE_REGISTRY.size(); + System.out.println("[reg] BLOCK registry size = " + blockCount); + System.out.println("[reg] BLOCK_STATE registry size = " + stateCount); + + // ★ 关键验证:mineflayer 会静默误映射的那些越界 id,客户端怎么答 + int[] probes = {0, 1, 1002, 1003, 1502, 30000, stateCount - 1}; + for (int id : probes) { + BlockState st = Block.stateById(id); + ResourceLocation key = BuiltInRegistries.BLOCK.getKey(st.getBlock()); + System.out.println("[reg] state_id=" + id + " -> " + key + " [" + st + "]"); + } + + // ★ 列出几个方块名(证明可读) + System.out.println("[reg] sample blocks:"); + for (int i = 0; i < 5; i++) { + ResourceLocation k = BuiltInRegistries.BLOCK.getKey(BuiltInRegistries.BLOCK.byId(i)); + System.out.println(" id=" + i + " -> " + k); + } + + System.out.println("[reg] DONE"); + System.exit(0); + } + + private static String arg(String[] a, String key, String def) { + for (int i = 0; i < a.length - 1; i++) if (a[i].equals(key)) return a[i + 1]; + return def; + } +} diff --git a/scaffold/RenderProbe.java b/scaffold/RenderProbe.java deleted file mode 100644 index 6bc48e8..0000000 --- a/scaffold/RenderProbe.java +++ /dev/null @@ -1,105 +0,0 @@ -import com.mojang.blaze3d.pipeline.MainTarget; -import com.mojang.blaze3d.platform.DisplayData; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.authlib.properties.PropertyMap; -import net.minecraft.SharedConstants; -import net.minecraft.client.Minecraft; -import net.minecraft.client.User; -import net.minecraft.client.main.GameConfig; -import net.minecraft.server.Bootstrap; -import org.lwjgl.glfw.GLFW; -import org.lwjgl.opengl.GL11; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.net.Proxy; -import java.nio.ByteBuffer; -import java.util.Optional; -import java.util.OptionalInt; - -/** - * mcfold render probe —— 起 MC 渲染栈,渲一帧,读像素写 PNG。 - * 全程:不连服、不登录、不进主循环、不 gameplay。 - */ -public class RenderProbe { - - public static void main(String[] args) throws Exception { - String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default"); - String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets"); - String assetIdx = arg(args, "--assetIndex", "1.20.1"); - String out = arg(args, "--out", "/tmp/scaffold-frame.png"); - int width = Integer.parseInt(arg(args, "--width", "854")); - int height = Integer.parseInt(arg(args, "--height", "480")); - - SharedConstants.tryDetectVersion(); - SharedConstants.enableDataFixerOptimizations(); - Bootstrap.bootStrap(); - - File runDir = new File(gameDir); - User user = new User("mcfold_cam", "0".repeat(32), "mcfold_cam", - Optional.empty(), Optional.empty(), User.Type.LEGACY); - PropertyMap props = new PropertyMap(); - - GameConfig cfg = new GameConfig( - new GameConfig.UserData(user, props, props, Proxy.NO_PROXY), - new DisplayData(width, height, OptionalInt.empty(), OptionalInt.empty(), false), - new GameConfig.FolderData(runDir, new File(runDir, "resourcepacks"), new File(assets), assetIdx), - new GameConfig.GameData(false, "mcfold", "release", false, false), - new GameConfig.QuickPlayData(null, null, null, null) - ); - - RenderSystem.initRenderThread(); - RenderSystem.beginInitialization(); - Minecraft mc = new Minecraft(cfg); - RenderSystem.finishInitialization(); - - System.out.println("[probe] constructed. w=" + mc.getWindow().getWidth() - + " h=" + mc.getWindow().getHeight()); - - // ★ 渲染一帧:照抄 Minecraft.render() 的实测序列(反编译 nout2/.../Minecraft.java:1110-1140) - MainTarget target = (MainTarget) mc.getMainRenderTarget(); - for (int frame = 0; frame < 3; frame++) { - RenderSystem.clear(16640, Minecraft.ON_OSX); - target.bindWrite(true); - mc.gameRenderer.render(0.0F, 0L, true); - target.unbindWrite(); - target.blitToScreen(mc.getWindow().getWidth(), mc.getWindow().getHeight()); - mc.getWindow().updateDisplay(); - System.out.println("[probe] frame " + frame + " rendered"); - } - - // ★ 读像素:从 GL 后台缓冲直接读(比截屏更可靠,且不依赖 X) - GL11.glReadBuffer(GL11.GL_BACK); - int w = mc.getWindow().getWidth(), h = mc.getWindow().getHeight(); - ByteBuffer buf = org.lwjgl.BufferUtils.createByteBuffer(w * h * 4); - GL11.glReadPixels(0, 0, w, h, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf); - - BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); - // GL 原点在左下,PNG 在左上,需翻转 - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int i = ((h - 1 - y) * w + x) * 4; - int r = buf.get(i) & 0xFF, g = buf.get(i + 1) & 0xFF, b = buf.get(i + 2) & 0xFF; - img.setRGB(x, y, (r << 16) | (g << 8) | b); - } - } - ImageIO.write(img, "png", new File(out)); - System.out.println("[probe] wrote " + out + " (" + w + "x" + h + ")"); - - // 统计非黑像素,证明真的画了东西(而非空缓冲) - long nonBlack = 0; int total = w * h; - for (int p : img.getRGB(0, 0, w, h, null, 0, w)) if ((p & 0xFFFFFF) != 0) nonBlack++; - System.out.println("[probe] non-black pixels = " + nonBlack + "/" + total - + " (" + (100.0 * nonBlack / total) + "%)"); - - GLFW.glfwTerminate(); - System.out.println("[probe] DONE"); - System.exit(0); - } - - private static String arg(String[] a, String key, String def) { - for (int i = 0; i < a.length - 1; i++) if (a[i].equals(key)) return a[i + 1]; - return def; - } -} diff --git a/scaffold/Scaffold.java b/scaffold/Scaffold.java deleted file mode 100644 index b1d255d..0000000 --- a/scaffold/Scaffold.java +++ /dev/null @@ -1,91 +0,0 @@ -import com.mojang.blaze3d.platform.DisplayData; -import com.mojang.blaze3d.systems.RenderSystem; -import com.mojang.authlib.properties.PropertyMap; -import net.minecraft.SharedConstants; -import net.minecraft.client.Minecraft; -import net.minecraft.client.User; -import net.minecraft.client.main.GameConfig; -import net.minecraft.server.Bootstrap; - -import java.io.File; -import java.net.Proxy; -import java.util.OptionalInt; - -/** - * mcfold scaffold —— 只起 MC 的**渲染栈**,不进游戏主循环。 - * - * 依据(反编译 net.minecraft.client.main.Main 的实测序列): - * Bootstrap.bootStrap() - * RenderSystem.initRenderThread(); beginInitialization(); - * new Minecraft(gameConfig) ← 构造即完成 GL/资源/渲染器初始化 - * RenderSystem.finishInitialization() - * 之后 Main 才起 Game thread 跑主循环 —— 我们**不**走那步。 - */ -public class Scaffold { - - public static void main(String[] args) throws Exception { - String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default"); - String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets"); - // ★ 索引名必须是 assets/indexes/.json 中真实存在的那个。 - // version.json 的 assetIndex.id 是 "5",但 MCLC 落盘用 version.number(1.20.1) - // —— 传 "5" 会导致索引加载失败,所有资源/icons 全部找不到(实测踩过)。 - String assetIdx = arg(args, "--assetIndex", "1.20.1"); - int width = Integer.parseInt(arg(args, "--width", "1280")); - int height = Integer.parseInt(arg(args, "--height", "720")); - - System.out.println("[scaffold] bootstrap..."); - // ★ 官方 Main 的实测序列:tryDetectVersion 必须在 bootStrap 之前, - // 否则 DataFixers. 抛 "Game version not set" - SharedConstants.tryDetectVersion(); - SharedConstants.enableDataFixerOptimizations(); - Bootstrap.bootStrap(); - System.out.println("[scaffold] bootstrap ok, mcVersion=" + SharedConstants.getCurrentVersion().getName()); - - File runDir = new File(gameDir); - File assetsDir = new File(assets); - - User user = new User("mcfold_cam", "0".repeat(32), "mcfold_cam", - java.util.Optional.empty(), java.util.Optional.empty(), User.Type.LEGACY); - PropertyMap props = new PropertyMap(); - - GameConfig cfg = new GameConfig( - new GameConfig.UserData(user, props, props, Proxy.NO_PROXY), - new DisplayData(width, height, OptionalInt.empty(), OptionalInt.empty(), false), - new GameConfig.FolderData( - // ★ 顺序是 (gameDirectory, resourcePackDirectory, assetDirectory, assetIndex) - // 反着写会把 assets 当游戏目录,导致资源全找不到(实测踩过:icons/icon_16x16.png 404) - runDir, new File(runDir, "resourcepacks"), assetsDir, assetIdx), - new GameConfig.GameData( - false, // demo - "mcfold", // version - "release", // versionType - false, // disableMultiplayer - false), // disableChat - new GameConfig.QuickPlayData(null, null, null, null) - ); - - System.out.println("[scaffold] RenderSystem init..."); - RenderSystem.initRenderThread(); - RenderSystem.beginInitialization(); - - System.out.println("[scaffold] new Minecraft(cfg) ..."); - Minecraft mc = new Minecraft(cfg); - - RenderSystem.finishInitialization(); - System.out.println("[scaffold] OK Minecraft constructed"); - - // 检查渲染栈关键对象是否就绪(不启主循环) - System.out.println("[scaffold] window=" + mc.getWindow().getWidth() + "x" + mc.getWindow().getHeight()); - System.out.println("[scaffold] levelRenderer=" + (mc.levelRenderer != null ? "present" : "null")); - System.out.println("[scaffold] gameRenderer=" + (mc.gameRenderer != null ? "present" : "null")); - System.out.println("[scaffold] level=" + (mc.level != null ? "present" : "null (не connected)")); - - System.out.println("[scaffold] DONE (no main loop)"); - } - - private static String arg(String[] a, String key, String def) { - for (int i = 0; i < a.length - 1; i++) - if (a[i].equals(key)) return a[i + 1]; - return def; - } -} diff --git a/scaffold/WorldRender.java b/scaffold/WorldRender.java new file mode 100644 index 0000000..e56d319 --- /dev/null +++ b/scaffold/WorldRender.java @@ -0,0 +1,344 @@ +import com.mojang.blaze3d.pipeline.MainTarget; +import com.mojang.blaze3d.platform.DisplayData; +import com.mojang.blaze3d.systems.RenderSystem; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.PropertyMap; +import net.minecraft.SharedConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.User; +import net.minecraft.client.main.GameConfig; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.client.telemetry.TelemetryEventSender; +import net.minecraft.client.telemetry.WorldSessionTelemetryManager; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.resources.RegistryDataLoader; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.Bootstrap; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.util.profiling.InactiveProfiler; +import net.minecraft.world.Difficulty; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.dimension.DimensionType; +import org.lwjgl.opengl.GL11; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.net.Proxy; +import java.time.Duration; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.UUID; + +/** + * 世界渲染器 —— 把世界数据喂给真客户端渲染器并出图。 + * + * 实测要点(2026-09-25): + * 1. 不跑主循环 ⇒ 渲染栈手动初始化(tryDetectVersion/Bootstrap/RenderSystem/new Minecraft) + * 2. 注册表从客户端自带 data/ 加载(RegistryDataLoader + vanilla pack) + * 3. shader 需显式编译(GameRenderer.reloadShaders 包私有 → 同包 bridge) + * 4. LoadingOverlay 必须摘掉,否则永远渲出加载屏 + * 5. 区块靠 LevelChunk + setBlockState 注入 + setSectionDirty 触发网格重建 + * 6. 读像素必须绑 FBO(glBindFramebuffer(frameBufferId)),GL_BACK 读到的是残留 + */ +public class WorldRender { + + public static void main(String[] args) throws Exception { + String gameDir = arg(args, "--gameDir", "/home/newqqagent/mcview/instances/default"); + String assets = arg(args, "--assets", "/home/newqqagent/mcview/store/vanilla-1.20.1/assets"); + String assetIdx = arg(args, "--assetIndex", "1.20.1"); + String out = arg(args, "--out", "/tmp/scaffold-world.png"); + int W = Integer.parseInt(arg(args, "--width", "854")); + int H = Integer.parseInt(arg(args, "--height", "480")); + + SharedConstants.tryDetectVersion(); + SharedConstants.enableDataFixerOptimizations(); + Bootstrap.bootStrap(); + + File runDir = new File(gameDir); + User user = new User("mcfold_cam", "0".repeat(32), "mcfold_cam", + Optional.empty(), Optional.empty(), User.Type.LEGACY); + PropertyMap props = new PropertyMap(); + GameConfig cfg = new GameConfig( + new GameConfig.UserData(user, props, props, Proxy.NO_PROXY), + new DisplayData(W, H, OptionalInt.empty(), OptionalInt.empty(), false), + new GameConfig.FolderData(runDir, new File(runDir, "resourcepacks"), new File(assets), assetIdx), + new GameConfig.GameData(false, "mcfold", "release", false, false), + new GameConfig.QuickPlayData(null, null, null, null)); + + RenderSystem.initRenderThread(); + RenderSystem.beginInitialization(); + Minecraft mc = new Minecraft(cfg); + RenderSystem.finishInitialization(); + System.out.println("[wr] Minecraft ok"); + + // ── 资源:字体/贴图/模型 ── + // createReload 的 future 不该在主线程 join(会死锁),但字体加载是同步的, + // 这里用官方 FontManager.reload 直接同步加载字体(修「文字成方框」) + // ★ 资源重载:modelManager/fontManager/textureManager 全是 reload listener, + // 不跑重载则方块模型/字体/贴图全空(地形渲不出来)。 + // 驱动方式照抄 LoadingOverlay:轮询 isDone(),不能 join(会死锁)。 + reloadResources(mc); + System.out.println("[wr] resources reloaded"); + + // ── listener ── + Connection conn = new Connection(PacketFlow.CLIENTBOUND); + GameProfile profile = new GameProfile(UUID.randomUUID(), "mcfold_cam"); + WorldSessionTelemetryManager telemetry = new WorldSessionTelemetryManager( + TelemetryEventSender.DISABLED, false, Duration.ZERO, ""); + ClientPacketListener listener = new ClientPacketListener(mc, null, conn, null, profile, telemetry); + + // ── 注册表:从客户端自带 data/ 加载 ── + var packs = net.minecraft.server.packs.repository.ServerPacksSource.createPackRepository( + new File(runDir, "datapacks").toPath()); + packs.reload(); + ResourceManager dataRm = new net.minecraft.server.packs.resources.MultiPackResourceManager( + net.minecraft.server.packs.PackType.SERVER_DATA, + java.util.List.of(packs.getPack("vanilla").open())); + java.util.List> all = new java.util.ArrayList<>(); + all.addAll(RegistryDataLoader.WORLDGEN_REGISTRIES); + all.addAll(RegistryDataLoader.DIMENSION_REGISTRIES); + RegistryAccess.Frozen loaded = RegistryDataLoader.load(dataRm, listener.registryAccess(), all); + var layered = net.minecraft.client.multiplayer.ClientRegistryLayer.createRegistryAccess() + .replaceFrom(net.minecraft.client.multiplayer.ClientRegistryLayer.REMOTE, + new RegistryAccess.Frozen[]{loaded}); + java.lang.reflect.Field rf = ClientPacketListener.class.getDeclaredField("registryAccess"); + rf.setAccessible(true); + rf.set(listener, layered); + System.out.println("[wr] registries loaded: dimension_type=" + + loaded.registryOrThrow(Registries.DIMENSION_TYPE).size()); + + Holder dimHolder = loaded.registryOrThrow(Registries.DIMENSION_TYPE) + .getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, new ResourceLocation("overworld"))); + + // ── ClientLevel ── + var levelData = new ClientLevel.ClientLevelData(Difficulty.NORMAL, false, false); + ClientLevel level = new ClientLevel(listener, levelData, Level.OVERWORLD, dimHolder, + 8, 8, () -> InactiveProfiler.INSTANCE, mc.levelRenderer, false, 0L); + mc.setLevel(level); + + var player = new net.minecraft.client.player.LocalPlayer(mc, level, listener, + new net.minecraft.stats.StatsCounter(), new net.minecraft.client.ClientRecipeBook(), false, false); + // 站在区块中心上方 + player.setPos(8.0, 70.0, 8.0); + mc.player = player; + mc.setCameraEntity(player); + mc.gameMode = new net.minecraft.client.multiplayer.MultiPlayerGameMode(mc, listener); + System.out.println("[wr] level + player ok"); + + // ── shader 编译(包私有,走同包 bridge)── + net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager()); + System.out.println("[wr] shaders ok: " + (RenderSystem.getShader() != null)); + + // ── LoadingOverlay 必须摘掉 ── + mc.setOverlay(null); + // ★ "Joining world..." 是 ReceivingLevelScreen;不清它客户端始终认为未进世界 + mc.setScreen(null); + + // ── 注入区块 ── + int injected = injectWorld(level, mc); + System.out.println("[wr] injected chunks=" + injected); + + // ── 第三人称(跳过 renderItemInHand)── + mc.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK); + + // ── 渲染(每帧先 tick:区块网格重建队列靠 tick 推进)── + MainTarget target = (MainTarget) mc.getMainRenderTarget(); + for (int f = 0; f < 60; f++) { + try { mc.tick(); } catch (Throwable t) { if (f == 0) System.out.println("[wr] tick err: " + t); } + RenderSystem.clear(16640, Minecraft.ON_OSX); + target.bindWrite(true); + mc.gameRenderer.render(0.0F, (long) f * 16_000_000L, true); + target.unbindWrite(); + target.blitToScreen(mc.getWindow().getWidth(), mc.getWindow().getHeight()); + mc.getWindow().updateDisplay(); + } + System.out.println("[wr] rendered 20 frames"); + + // ── 诊断:区块与网格状态 ── + System.out.println("[wr] hasRenderedAllChunks=" + mc.levelRenderer.hasRenderedAllChunks() + + " countRenderedChunks=" + mc.levelRenderer.countRenderedChunks()); + var got = level.getChunkSource().getChunk(0, 0, net.minecraft.world.level.chunk.ChunkStatus.FULL, false); + System.out.println("[wr] getChunk(0,0)=" + (got != null ? got.getClass().getSimpleName() : "null") + + " block(0,64,0)=" + (got != null ? got.getBlockState(new BlockPos(0,64,0)) : "-")); + System.out.println("[wr] isChunkCompiled(0,64,0)=" + mc.levelRenderer.isChunkCompiled(new BlockPos(0,64,0))); + + save(target, W, H, out); + System.exit(0); + } + + /** + * 同步驱动各 reload listener。 + * + * 为什么不用 Minecraft.reloadResourcePacks(): + * 它的 future 靠主循环 tick 推进(LoadingOverlay),我们不跑主循环 ⇒ 死锁。 + * 为什么不用 createReload(): + * prepare 阶段仍走真后台池,实测 120s 超时。 + * 正解:直接调各 listener 的 public reload(),barrier 同步返回, + * 且 apply 用 Runnable::run 保证在渲染线程执行。 + */ + private static void reloadResources(Minecraft mc) throws Exception { + var rmF = Minecraft.class.getDeclaredField("resourceManager"); + rmF.setAccessible(true); + var rm = (net.minecraft.server.packs.resources.ReloadableResourceManager) rmF.get(mc); + var packs = mc.getResourcePackRepository(); + packs.reload(); + + // 同步 barrier:prepare 完成后立刻交出,不排队 + var barrier = new net.minecraft.server.packs.resources.PreparableReloadListener.PreparationBarrier() { + @Override public java.util.concurrent.CompletableFuture wait(T t) { + return java.util.concurrent.CompletableFuture.completedFuture(t); + } + }; + var prof = InactiveProfiler.INSTANCE; + // prepare 用当前线程(同步跑),apply 也用当前线程(渲染线程) + java.util.concurrent.Executor sync = Runnable::run; + + var all = reloadListeners(mc); + System.out.println("[wr] listeners=" + all.size()); + for (var listener : all) { + String name = listener.getName(); + System.out.println("[wr] -> " + name + " ..."); + try { + var fut = listener.reload(barrier, rm, prof, prof, sync, sync); + boolean done = false; + long dl = System.currentTimeMillis() + 30_000; + while (!done && System.currentTimeMillis() < dl) { + done = fut.isDone(); + if (!done) Thread.sleep(10); + } + if (!done) { System.out.println("[wr] TIMEOUT " + name); continue; } + fut.join(); + System.out.println("[wr] ok: " + name); + } catch (Throwable e) { + System.out.println("[wr] FAILED " + name + ": " + e); + } + } + } + + /** 取出注册过的 listener(private 字段) */ + @SuppressWarnings("unchecked") + private static java.util.List reloadListeners(Minecraft mc) throws Exception { + var rmF = Minecraft.class.getDeclaredField("resourceManager"); + rmF.setAccessible(true); + var rm = rmF.get(mc); + for (var f : rm.getClass().getDeclaredFields()) { + if (java.util.List.class.isAssignableFrom(f.getType())) { + f.setAccessible(true); + Object v = f.get(rm); + if (v instanceof java.util.List l && !l.isEmpty() + && l.get(0) instanceof net.minecraft.server.packs.resources.PreparableReloadListener) { + return (java.util.List) l; + } + } + } + throw new IllegalStateException("reload listeners not found"); + } + + /** 造地形并注入:区块 + 光照 + 网格重建 */ + private static int injectWorld(ClientLevel level, Minecraft mc) throws Exception { + int radius = 4; // 9x9 区块 = 144x144 格 + int yBottom = level.getMinBuildHeight(); + var cache = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource(); + var light = cache.getLightEngine(); + + // 反射拿 Storage.chunks / getIndex(包私有) + var storageF = net.minecraft.client.multiplayer.ClientChunkCache.class.getDeclaredField("storage"); + storageF.setAccessible(true); + Object storage = storageF.get(cache); + var chunksF = storage.getClass().getDeclaredField("chunks"); + chunksF.setAccessible(true); + var chunks = (java.util.concurrent.atomic.AtomicReferenceArray) chunksF.get(storage); + var getIndex = storage.getClass().getDeclaredMethod("getIndex", int.class, int.class); + getIndex.setAccessible(true); + System.out.println("[wr] storage chunks.length=" + chunks.length()); + + int count = 0; + for (int cx = -radius; cx <= radius; cx++) { + for (int cz = -radius; cz <= radius; cz++) { + LevelChunk chunk = new LevelChunk(level, new ChunkPos(cx, cz)); + for (int lx = 0; lx < 16; lx++) { + for (int lz = 0; lz < 16; lz++) { + int wx = cx * 16 + lx, wz = cz * 16 + lz; + for (int y = yBottom; y < 63; y++) + chunk.setBlockState(new BlockPos(lx, y, lz), Blocks.STONE.defaultBlockState(), false); + chunk.setBlockState(new BlockPos(lx, 63, lz), Blocks.DIRT.defaultBlockState(), false); + chunk.setBlockState(new BlockPos(lx, 64, lz), Blocks.GRASS_BLOCK.defaultBlockState(), false); + if ((wx % 7 == 0 && wz % 7 == 0) && (wx != 0 || wz != 0)) { + for (int y = 65; y < 72; y++) + chunk.setBlockState(new BlockPos(lx, y, lz), Blocks.OAK_LOG.defaultBlockState(), false); + for (int dx = -2; dx <= 2; dx++) + for (int dz = -2; dz <= 2; dz++) + if (lx+dx >= 0 && lx+dx < 16 && lz+dz >= 0 && lz+dz < 16 + && Math.abs(dx) + Math.abs(dz) <= 2) + chunk.setBlockState(new BlockPos(lx+dx, 72, lz+dz), + Blocks.OAK_LEAVES.defaultBlockState(), false); + } + } + } + // ★ 核心:直接放进 ClientChunkCache.Storage.chunks + int idx = (int) getIndex.invoke(storage, cx, cz); + chunks.set(idx, chunk); + count++; + + // ★ 光照:全亮(否则地形全黑)。section y 0..19 覆盖 build height + for (int sy = level.getMinSection(); sy < level.getMaxSection(); sy++) { + var sky = new net.minecraft.world.level.chunk.DataLayer(); + for (int x = 0; x < 16; x++) + for (int y = 0; y < 16; y++) + for (int z = 0; z < 16; z++) sky.set(x, y, z, 15); + light.queueSectionData(net.minecraft.world.level.LightLayer.SKY, + net.minecraft.core.SectionPos.of(cx, sy, cz), sky); + light.queueSectionData(net.minecraft.world.level.LightLayer.BLOCK, + net.minecraft.core.SectionPos.of(cx, sy, cz), sky); + } + light.setLightEnabled(new ChunkPos(cx, cz), true); + chunk.setLightCorrect(true); + } + } + int upd = light.runLightUpdates(); + System.out.println("[wr] light updates=" + upd); + + // 触发区块网格重建 + mc.levelRenderer.allChanged(); + System.out.println("[wr] allChanged done"); + return count; + } + + private static void save(MainTarget target, int W, int H, String out) throws Exception { + org.lwjgl.opengl.GL30.glBindFramebuffer(org.lwjgl.opengl.GL30.GL_FRAMEBUFFER, target.frameBufferId); + GL11.glReadBuffer(org.lwjgl.opengl.GL30.GL_COLOR_ATTACHMENT0); + var buf = org.lwjgl.BufferUtils.createByteBuffer(W * H * 4); + GL11.glReadPixels(0, 0, W, H, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf); + org.lwjgl.opengl.GL30.glBindFramebuffer(org.lwjgl.opengl.GL30.GL_FRAMEBUFFER, 0); + + BufferedImage img = new BufferedImage(W, H, BufferedImage.TYPE_INT_RGB); + for (int y = 0; y < H; y++) + for (int x = 0; x < W; x++) { + int i = ((H - 1 - y) * W + x) * 4; + img.setRGB(x, y, ((buf.get(i) & 0xFF) << 16) | ((buf.get(i+1) & 0xFF) << 8) | (buf.get(i+2) & 0xFF)); + } + ImageIO.write(img, "png", new File(out)); + java.util.Set colors = new java.util.HashSet<>(); + for (int p : img.getRGB(0, 0, W, H, null, 0, W)) colors.add(p & 0xFFFFFF); + System.out.println("[wr] wrote " + out + " distinct colors=" + colors.size()); + } + + private static String arg(String[] a, String key, String def) { + for (int i = 0; i < a.length - 1; i++) if (a[i].equals(key)) return a[i + 1]; + return def; + } +} diff --git a/scaffold/net/minecraft/client/renderer/McfoldBridge.java b/scaffold/net/minecraft/client/renderer/McfoldBridge.java new file mode 100644 index 0000000..ffa9663 --- /dev/null +++ b/scaffold/net/minecraft/client/renderer/McfoldBridge.java @@ -0,0 +1,14 @@ +package net.minecraft.client.renderer; + +import net.minecraft.server.packs.resources.ResourceProvider; + +/** + * 包私有成员桥(GameRenderer.reloadShaders 是 package-private)。 + * 官方由匿名内部类 GameRenderer$1 在资源重载时调用; + * 我们不跑主循环 ⇒ 用同包类直接调用,跳过整个 reload 基础设施。 + */ +public class McfoldBridge { + public static void reloadShaders(GameRenderer gr, ResourceProvider provider) { + gr.reloadShaders(provider); + } +} diff --git a/scaffold/sample-world-render.png b/scaffold/sample-world-render.png new file mode 100644 index 0000000..e7b15b1 Binary files /dev/null and b/scaffold/sample-world-render.png differ