import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; 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.FriendlyByteBuf; 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.LightLayer; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.DataLayer; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.dimension.DimensionType; import org.lwjgl.opengl.GL11; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStreamReader; import java.io.PrintStream; import java.net.Proxy; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Base64; import java.util.Optional; import java.util.OptionalInt; import java.util.UUID; /** * mcfold 渲染服务:常驻进程,NDJSON(行分隔 JSON) 协议,由 Python 侧驱动。 * * 设计约束(用户 2026-09-25): * - 不起完整客户端:不跑主循环、不连服、不登录、不作玩家 * - 我们的世界数据喂给官方渲染器出图 * * 协议(stdin/stdout 各一行一个 JSON): * → {"id":1,"cmd":"chunk","cx":0,"cz":0,"data":"","heightmaps":null} * → {"id":2,"cmd":"camera","x":8,"y":70,"z":8,"yaw":0,"pitch":0} * → {"id":3,"cmd":"render","frames":60,"out":"/tmp/a.png"} * ← {"id":3,"ok":true,"width":854,"height":480,"chunksRendered":29, * "distinctColors":1450,"image":""} * * 启动就绪时主动发 {"event":"ready"}。 * * ★ 关键实现点(全部实测得来,详见 scaffold/README.md): * 1. tryDetectVersion 必须先于 Bootstrap.bootStrap * 2. FolderData 顺序 (gameDir, resourcePackDir, assets, assetIndex),assetIndex 用落盘名 * 3. 资源必须手工同步重载(各 listener 的 public reload()),不能 join Minecraft 的 * 4. LoadingOverlay + ReceivingLevelScreen 都要清,否则渲出加载屏 * 5. 每帧 mc.tick() 才能推进区块网格重建队列 * 6. 读像素必须绑 FBO * 7. chunk 数据走官方 replaceWithPacketData(与网络包同路径) */ public class McfoldRenderServer { static final Gson GSON = new Gson(); static final PrintStream OUT = new PrintStream( new java.io.FileOutputStream(java.io.FileDescriptor.out), true, StandardCharsets.UTF_8); static Minecraft mc; static ClientLevel level; static int width = 854, height = 480; public static void main(String[] args) throws Exception { // stdout 只放协议 JSON,MC 的 INFO/WARN 全部赶到 stderr System.setOut(new PrintStream(new java.io.FileOutputStream(java.io.FileDescriptor.err), true)); 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"); width = Integer.parseInt(arg(args, "--width", "854")); height = Integer.parseInt(arg(args, "--height", "480")); try { boot(gameDir, assets, assetIdx); } catch (Throwable t) { emitError(null, "启动失败: " + t); t.printStackTrace(System.err); System.exit(1); } event("ready", null); BufferedReader in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); String line; while ((line = in.readLine()) != null) { line = line.trim(); if (line.isEmpty()) continue; JsonObject req; try { req = JsonParser.parseString(line).getAsJsonObject(); } catch (Exception e) { emitError(null, "非法 JSON: " + e.getMessage()); continue; } Integer id = req.has("id") ? req.get("id").getAsInt() : null; try { handle(id, req); } catch (Throwable t) { emitError(id, t.toString()); t.printStackTrace(System.err); } } System.exit(0); } // ---------------- 协议处理 ---------------- static void handle(Integer id, JsonObject req) throws Exception { String cmd = req.get("cmd").getAsString(); switch (cmd) { case "shutdown": { JsonObject r = new JsonObject(); r.addProperty("ok", true); emit(id, r); System.exit(0); } case "chunk": { int cx = req.get("cx").getAsInt(), cz = req.get("cz").getAsInt(); byte[] data = decode(req.get("data").getAsString()); var resp = applyChunk(cx, cz, data); JsonObject r = new JsonObject(); r.addProperty("ok", true); r.add("result", resp); emit(id, r); break; } case "chunks": { JsonArray items = req.getAsJsonArray("items"); int ok = 0, failed = 0; String firstErr = null; for (JsonElement el : items) { JsonObject it = el.getAsJsonObject(); try { applyChunk(it.get("cx").getAsInt(), it.get("cz").getAsInt(), decode(it.get("data").getAsString())); ok++; } catch (Throwable t) { failed++; if (firstErr == null) { firstErr = it.get("cx") + "," + it.get("cz") + ": " + t; } } } levelRendererAllChanged(); JsonObject r = new JsonObject(); r.addProperty("ok", true); r.addProperty("injected", ok); r.addProperty("failed", failed); if (firstErr != null) r.addProperty("firstError", firstErr); var cache2 = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource(); r.addProperty("loadedChunks", cache2.getLoadedChunksCount()); emit(id, r); break; } case "setblocks": { // ★ 世代无关:Python 只做路由,方块写入、调色板组装、入 cache // 全部由 Java 用官方 API 完成 —— 版本差异不落回 Python。 // 载荷:sections[].rle = [[stateId,count],...],按 (y,z,x) 线性序覆盖该 section 4096 格。 // 优势:协议位打包(历代改过)不再由 Python 复制;入 cache 走官方 // ClientboundLevelChunkPacketData(LevelChunk) → getReadBuffer(), // 签名两代一致,**无需反射**。 int cx = req.get("cx").getAsInt(), cz = req.get("cz").getAsInt(); LevelChunk chunk = new LevelChunk(level, new ChunkPos(cx, cz)); LevelChunkSection[] sections = chunk.getSections(); int placed = 0; long cells = 0; for (JsonElement se : req.getAsJsonArray("sections")) { JsonObject so = se.getAsJsonObject(); int si = so.get("index").getAsInt(); if (si < 0 || si >= sections.length) continue; LevelChunkSection sec = sections[si]; int x = 0, y = 0, z = 0; for (JsonElement re : so.getAsJsonArray("rle")) { JsonArray pair = re.getAsJsonArray(); int stateId = pair.get(0).getAsInt(); int count = pair.get(1).getAsInt(); BlockState st = stateId == 0 ? null : Block.stateById(stateId); for (int i = 0; i < count; i++) { if (st != null) { sec.setBlockState(x, y, z, st); placed++; } cells++; if (++x == 16) { x = 0; if (++z == 16) { z = 0; y++; } } } } } // 光照(无光照数据则全亮,否则地形全黑) var light = level.getChunkSource().getLightEngine(); for (int sy = level.getMinSection(); sy < level.getMaxSection(); sy++) { DataLayer sky = new DataLayer(); for (int lx = 0; lx < 16; lx++) for (int ly = 0; ly < 16; ly++) for (int lz = 0; lz < 16; lz++) sky.set(lx, ly, lz, 15); light.queueSectionData(LightLayer.SKY, net.minecraft.core.SectionPos.of(cx, sy, cz), sky); light.queueSectionData(LightLayer.BLOCK, net.minecraft.core.SectionPos.of(cx, sy, cz), sky); } // ★ 官方序列化 → 官方入口入 cache(两代签名一致) var pkt = new net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData(chunk); level.getChunkSource().replaceWithPacketData(cx, cz, pkt.getReadBuffer(), pkt.getHeightmaps(), new java.util.function.Consumer< net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput>() { @Override public void accept( net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput t) { } }); light.setLightEnabled(new ChunkPos(cx, cz), true); light.runLightUpdates(); JsonObject r = new JsonObject(); r.addProperty("ok", true); r.addProperty("placed", placed); r.addProperty("cells", cells); r.addProperty("sections", sections.length); r.addProperty("loadedChunks", level.getChunkSource().getLoadedChunksCount()); emit(id, r); break; } case "camera": { double x = req.get("x").getAsDouble(), y = req.get("y").getAsDouble(), z = req.get("z").getAsDouble(); float yaw = req.has("yaw") ? req.get("yaw").getAsFloat() : 0f; float pitch = req.has("pitch") ? req.get("pitch").getAsFloat() : 0f; // ★ 必须 setPosRaw + 手动更新 chunk 追踪,否则实体仍属旧区块 mc.player.setPosRaw(x, y, z); mc.player.xo = x; mc.player.yo = y; mc.player.zo = z; mc.player.setYRot(yaw); mc.player.setXRot(pitch); mc.player.yRotO = yaw; mc.player.xRotO = pitch; mc.setCameraEntity(mc.player); // 视野中心跟着相机走,否则目标区域区块被 ClientChunkCache 丢弃 level.getChunkSource().updateViewCenter((int)Math.floor(x) >> 4, (int)Math.floor(z) >> 4); mc.levelRenderer.allChanged(); tickFrames(5); JsonObject r = new JsonObject(); r.addProperty("ok", true); JsonArray c = new JsonArray(); c.add(x); c.add(y); c.add(z); r.add("camera", c); emit(id, r); break; } case "view": { if (req.has("cameraType")) { mc.options.setCameraType(net.minecraft.client.CameraType.valueOf(req.get("cameraType").getAsString())); } if (req.has("renderDistance")) { int rd = req.get("renderDistance").getAsInt(); // ★ 两处都要设:renderDistance 是本地设置; // serverRenderDistance 模拟服务端下发的视距上限 // (getEffectiveRenderDistance = min(两者)) mc.options.renderDistance().set(rd); mc.options.setServerRenderDistance(rd); // ★ 必须同步 ClientChunkCache 的 storage 半径, // 否则区块被 inRange 丢弃(官方靠服务端包触发 updateViewRadius) level.getChunkSource().updateViewRadius(rd); mc.levelRenderer.allChanged(); } if (req.has("fov")) { mc.options.fov().set(req.get("fov").getAsInt()); } // ── 原版画质选项(光影的基础层)── if (req.has("graphicsMode")) { mc.options.graphicsMode().set(net.minecraft.client.GraphicsStatus .valueOf(req.get("graphicsMode").getAsString())); mc.levelRenderer.allChanged(); // FANCY 会启用更多渲染层 } if (req.has("ambientOcclusion")) { mc.options.ambientOcclusion().set(req.get("ambientOcclusion").getAsBoolean()); mc.levelRenderer.allChanged(); } if (req.has("mipmapLevels")) { mc.options.mipmapLevels().set(req.get("mipmapLevels").getAsInt()); } if (req.has("gamma")) { mc.options.gamma().set(req.get("gamma").getAsDouble()); } if (req.has("entityShadows")) { mc.options.entityShadows().set(req.get("entityShadows").getAsBoolean()); } if (req.has("clouds")) { mc.options.cloudStatus().set(net.minecraft.client.CloudStatus .valueOf(req.get("clouds").getAsString())); } if (req.has("particles")) { mc.options.particles().set(net.minecraft.client.ParticleStatus .valueOf(req.get("particles").getAsString())); } // ── 原版后处理链(PostChain 基础设施)── if (req.has("postEffect")) { String eff = req.get("postEffect").getAsString(); if (eff.isEmpty() || eff.equals("none")) { mc.gameRenderer.shutdownEffect(); } else { // loadEffect 是包私有 → 走同包 bridge net.minecraft.client.renderer.McfoldBridge.loadEffect( mc.gameRenderer, new ResourceLocation(eff)); } } // 回包带上真实生效值,便于验证 JsonObject r = new JsonObject(); r.addProperty("ok", true); JsonObject cur = new JsonObject(); try { java.lang.reflect.Field pf = net.minecraft.client.renderer.GameRenderer.class .getDeclaredField("postEffect"); pf.setAccessible(true); cur.addProperty("postEffect", pf.get(mc.gameRenderer) != null ? "active" : "none"); } catch (Throwable ignore) { cur.addProperty("postEffect", "unknown"); } cur.addProperty("graphicsMode", mc.options.graphicsMode().get().name()); cur.addProperty("ambientOcclusion", mc.options.ambientOcclusion().get()); cur.addProperty("mipmapLevels", mc.options.mipmapLevels().get()); cur.addProperty("gamma", mc.options.gamma().get()); cur.addProperty("entityShadows", mc.options.entityShadows().get()); cur.addProperty("clouds", mc.options.cloudStatus().get().name()); cur.addProperty("renderDistance", mc.options.getEffectiveRenderDistance()); r.add("current", cur); emit(id, r); break; } case "capacity": { // ★ 世界尺寸由运行时提供 —— Python 不硬编码版本相关常量。 // 这些方法都在 LevelHeightAccessor 上(Level 实现),两代 API 一致。 JsonObject r = new JsonObject(); r.addProperty("ok", true); r.addProperty("sectionsCount", level.getSectionsCount()); r.addProperty("minSection", level.getMinSection()); r.addProperty("maxSection", level.getMaxSection()); r.addProperty("minBuildHeight", level.getMinBuildHeight()); r.addProperty("maxBuildHeight", level.getMaxBuildHeight()); r.addProperty("height", level.getHeight()); // 生物群系默认 id(不硬编码 1) try { var bio = mc.level.registryAccess().registryOrThrow(Registries.BIOME); var key = bio.getKey(bio.byId(1)); r.addProperty("defaultBiome", key != null ? key.toString() : "minecraft:plains"); } catch (Throwable t) { r.addProperty("defaultBiome", "minecraft:plains"); } emit(id, r); break; } case "registry": { JsonObject r = new JsonObject(); r.addProperty("ok", true); r.addProperty("blockCount", BuiltInRegistries.BLOCK.size()); r.addProperty("stateCount", Block.BLOCK_STATE_REGISTRY.size()); emit(id, r); break; } case "lookup": { int sid = req.get("stateId").getAsInt(); BlockState st = Block.stateById(sid); ResourceLocation key = BuiltInRegistries.BLOCK.getKey(st.getBlock()); JsonObject r = new JsonObject(); r.addProperty("ok", true); r.addProperty("stateId", sid); r.addProperty("block", key.toString()); r.addProperty("state", st.toString()); r.addProperty("inRange", sid >= 0 && sid < Block.BLOCK_STATE_REGISTRY.size()); emit(id, r); break; } case "fog": { // 关雾:fogStart/fogEnd 是 levelRenderer 内部状态; // 更简单可靠的是加大 renderDistance + 用 BLOCK 光照 JsonObject r = new JsonObject(); r.addProperty("ok", true); emit(id, r); break; } case "resolve": { // ★ 方块名(+属性) → state id。存档 palette 是名字,网络载荷要 id, // 而这个名字→id 的权威映射只有客户端注册表有。 // 入参: {"cmd":"resolve","blocks":["minecraft:oak_log[axis=y]", ...]} JsonArray arr = req.getAsJsonArray("blocks"); // ★ parseForBlock 要 HolderLookup(不是 Registry)。 // 用**静态注册表**的 asLookup():它是方块名→id 的权威来源 // (mod 方块也注册在这里),且两代 API 一致。 // 不用 mc.level.registryAccess() —— 1.21 上它来自 cookie, // 只含 worldgen 那批注册表,查 block 会报 "Registry not found"(实测)。 var reg = BuiltInRegistries.BLOCK.asLookup(); JsonArray out = new JsonArray(); int unknown = 0; for (JsonElement el : arr) { String spec = el.getAsString(); int sid; try { var res = net.minecraft.commands.arguments.blocks.BlockStateParser .parseForBlock(reg, spec, false); sid = Block.getId(res.blockState()); } catch (Throwable t) { sid = -1; unknown++; } out.add(sid); } JsonObject r = new JsonObject(); r.addProperty("ok", true); r.add("ids", out); r.addProperty("unknown", unknown); r.addProperty("total", arr.size()); emit(id, r); break; } case "debug": { JsonObject r = new JsonObject(); r.addProperty("ok", true); r.addProperty("playerPos", mc.player.getX() + "," + mc.player.getY() + "," + mc.player.getZ()); int pcx = (int)Math.floor(mc.player.getX()) >> 4, pcz = (int)Math.floor(mc.player.getZ()) >> 4; r.addProperty("playerChunk", pcx + "," + pcz); var cc = (net.minecraft.client.multiplayer.ClientChunkCache) level.getChunkSource(); r.addProperty("loadedChunks", cc.getLoadedChunksCount()); r.addProperty("chunksRendered", mc.levelRenderer.countRenderedChunks()); r.addProperty("hasRenderedAllChunks", mc.levelRenderer.hasRenderedAllChunks()); var got = cc.getChunk(pcx, pcz, net.minecraft.world.level.chunk.ChunkStatus.FULL, false); r.addProperty("chunkAtPlayer", got != null ? got.getClass().getSimpleName() : "null"); if (got != null) { // 扫描玩家所在列,找最高的非空气方块 int top = Integer.MIN_VALUE; for (int y = level.getMaxBuildHeight() - 1; y >= level.getMinBuildHeight(); y--) { var st = got.getBlockState(new BlockPos( (int)Math.floor(mc.player.getX()) & 15, y, (int)Math.floor(mc.player.getZ()) & 15)); if (!st.isAir()) { top = y; r.addProperty("topSolidY", y); r.addProperty("topSolidBlock", BuiltInRegistries.BLOCK.getKey(st.getBlock()).toString()); r.addProperty("topSolidProps", st.toString()); break; } } if (top == Integer.MIN_VALUE) r.addProperty("topSolidY", "none"); } // viewCenter r.addProperty("fogStart", String.valueOf(mc.options.renderDistance().get())); emit(id, r); break; } case "viewport": { // ★ viewCenter 决定 ClientChunkCache 收哪些区块(超范围会被丢弃) int cx = req.get("cx").getAsInt(), cz = req.get("cz").getAsInt(); if (req.has("radius")) { level.getChunkSource().updateViewRadius(req.get("radius").getAsInt()); } level.getChunkSource().updateViewCenter(cx, cz); JsonObject r = new JsonObject(); r.addProperty("ok", true); r.addProperty("viewCenter", cx + "," + cz); emit(id, r); break; } case "render": { int frames = req.has("frames") ? req.get("frames").getAsInt() : 60; String out = req.has("out") ? req.get("out").getAsString() : null; if (req.has("width")) width = req.get("width").getAsInt(); if (req.has("height")) height = req.get("height").getAsInt(); render(id, frames, out); break; } default: emitError(id, "未知命令: " + cmd); } } static JsonObject applyChunk(int cx, int cz, byte[] data) { // ★ 官方路径:ClientChunkCache.replaceWithPacketData // 内部:new LevelChunk + chunk.replaceWithPacketData(buf, heightmaps, consumer) // 与网络包 handleLevelChunkWithLight 完全同一路径 FriendlyByteBuf buf = new FriendlyByteBuf(io.netty.buffer.Unpooled.wrappedBuffer(data)); var cache = level.getChunkSource(); // 显式匿名类而非 lambda:lambda 参数类型推断在此处会失败(实测) net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput beOut = new net.minecraft.network.protocol.game.ClientboundLevelChunkPacketData.BlockEntityTagOutput() { @Override public void accept(BlockPos bp, net.minecraft.world.level.block.entity.BlockEntityType type, net.minecraft.nbt.CompoundTag tag) { } }; // ★ 参数是 Consumer(消费这个接口本身),不是该接口的方法签名。 // LevelChunk.replaceWithPacketData 内部会调 beOut.accept(...),这里直接空实现。 cache.replaceWithPacketData(cx, cz, buf, new net.minecraft.nbt.CompoundTag(), out -> {}); // 无光照数据 ⇒ 显式全亮,否则地形全黑 var light = cache.getLightEngine(); for (int sy = level.getMinSection(); sy < level.getMaxSection(); sy++) { DataLayer sky = new 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(LightLayer.SKY, net.minecraft.core.SectionPos.of(cx, sy, cz), sky); light.queueSectionData(LightLayer.BLOCK, net.minecraft.core.SectionPos.of(cx, sy, cz), sky); } light.setLightEnabled(new ChunkPos(cx, cz), true); int upd = light.runLightUpdates(); JsonObject r = new JsonObject(); r.addProperty("lightUpdates", upd); return r; } static void levelRendererAllChanged() { mc.levelRenderer.allChanged(); } /** tick + 渲染各帧(区块网格重建队列靠 tick 推进) */ static void tickFrames(int n) { MainTarget target = (MainTarget) mc.getMainRenderTarget(); for (int f = 0; f < n; f++) { try { mc.tick(); } catch (Throwable ignore) {} 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(); } } static void render(Integer id, int frames, String out) throws Exception { tickFrames(frames); MainTarget target = (MainTarget) mc.getMainRenderTarget(); int w = mc.getWindow().getWidth(), h = mc.getWindow().getHeight(); 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); java.util.Set colors = new java.util.HashSet<>(); for (int y = 0; y < h; y++) for (int x = 0; x < w; x++) { int i = ((h - 1 - y) * w + x) * 4; int rgb = ((buf.get(i) & 0xFF) << 16) | ((buf.get(i + 1) & 0xFF) << 8) | (buf.get(i + 2) & 0xFF); img.setRGB(x, y, rgb); colors.add(rgb); } ByteArrayOutputStream bos = new ByteArrayOutputStream(); ImageIO.write(img, "png", bos); byte[] png = bos.toByteArray(); if (out != null) ImageIO.write(img, "png", new File(out)); JsonObject r = new JsonObject(); r.addProperty("ok", true); r.addProperty("width", w); r.addProperty("height", h); r.addProperty("chunksRendered", mc.levelRenderer.countRenderedChunks()); r.addProperty("hasRenderedAllChunks", mc.levelRenderer.hasRenderedAllChunks()); r.addProperty("distinctColors", colors.size()); JsonArray cam = new JsonArray(); cam.add(mc.player.getX()); cam.add(mc.player.getY()); cam.add(mc.player.getZ()); r.add("camera", cam); if (out != null) r.addProperty("path", out); r.addProperty("image", Base64.getEncoder().encodeToString(png)); emit(id, r); } // ---------------- 启动 ---------------- static void boot(String gameDir, String assets, String assetIdx) throws Exception { SharedConstants.tryDetectVersion(); SharedConstants.enableDataFixerOptimizations(); Bootstrap.bootStrap(); File runDir = new File(gameDir); runDir.mkdirs(); 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(); mc = new Minecraft(cfg); RenderSystem.finishInitialization(); System.err.println("[mcfold] Minecraft constructed"); reloadResources(mc); System.err.println("[mcfold] resources reloaded"); 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); 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())); var 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}); var rf = ClientPacketListener.class.getDeclaredField("registryAccess"); rf.setAccessible(true); rf.set(listener, layered); Holder dimHolder = loaded.registryOrThrow(Registries.DIMENSION_TYPE) .getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, new ResourceLocation("overworld"))); var levelData = new ClientLevel.ClientLevelData(Difficulty.NORMAL, false, false); level = new ClientLevel(listener, levelData, Level.OVERWORLD, dimHolder, Math.max(8, mc.options.renderDistance().get()), 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); net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager()); mc.setOverlay(null); mc.setScreen(null); mc.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK); System.err.println("[mcfold] boot done"); } /** 同步驱动各 reload listener(不能 join Minecraft 的 reloadResourcePacks:等主循环 → 死锁)*/ 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); mc.getResourcePackRepository().reload(); 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; java.util.concurrent.Executor sync = Runnable::run; for (var listener : reloadListeners(rm)) { String name = listener.getName(); try { var fut = listener.reload(barrier, rm, prof, prof, sync, sync); long deadline = System.currentTimeMillis() + 60_000; while (!fut.isDone() && System.currentTimeMillis() < deadline) Thread.sleep(10); if (fut.isDone()) fut.join(); else System.err.println("[mcfold] TIMEOUT " + name); } catch (Throwable e) { System.err.println("[mcfold] FAILED " + name + ": " + e); } } } @SuppressWarnings("unchecked") static java.util.List reloadListeners(Object rm) throws Exception { 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"); } // ---------------- 工具 ---------------- static byte[] decode(String b64) { return Base64.getDecoder().decode(b64); } 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; } static synchronized void emit(Integer id, JsonObject obj) { if (id != null) obj.addProperty("id", id); OUT.println(GSON.toJson(obj)); OUT.flush(); } static void emitError(Integer id, String msg) { JsonObject o = new JsonObject(); o.addProperty("ok", false); o.addProperty("error", msg); emit(id, o); } static void event(String name, JsonObject payload) { JsonObject o = new JsonObject(); o.addProperty("event", name); if (payload != null) o.add("payload", payload); OUT.println(GSON.toJson(o)); OUT.flush(); } }