Files
agentmc/scaffold/java/gen_1_21/McfoldRenderServer.java
JianFeeeee 6827f4d0e1 实测 Fabric 客户端可启动 + 加载器后端设计文档
用户判断:mod 支持分 fabric/forge,且不同版本还有差异 ⇒ 各有独立后端。
实测支持此结论,且差异比方法签名更深一层。

实测成功(关键):
  Fabric Loader 0.19.5 完整启动 1.20.1 客户端
  'Loading Minecraft 1.20.1 with Fabric Loader 0.19.5' + Mixin 子系统 + 12 个资源图集
  ★ 必须剔除 1.20.1 自带的 asm 9.2/9.6,改用 Fabric 的 9.10.1
    (否则 Knot.<clinit> 抛 duplicate ASM classes)

架构结论:后端身份是 (加载器, API 世代) 二元组
  因为 Fabric/Forge 下**我们不拥有启动流程**(Loader 起游戏+跑主循环),
  与我们「不跑主循环+手动喂数据」的模型根本冲突。
  两条可行路线已写入 scaffold/LOADERS.md(A: 作为 mod 被加载;
  B: 只借 Fabric 的注册表与资源包,保留我们的渲染循环)——均待实测。

已实现(零加载器,两个 vanilla 后端编译通过、待实测):
  cmd=mods:Java 侧自己扫 jar/注册占位方块/挂资源包
  覆盖:外观 ✅ state id ✅ 变体 ⚠️ 行为 ❌ Iris ❌
2026-09-25 19:55:13 +08:00

933 lines
48 KiB
Java
Raw 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.

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.BlockBehaviour;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.core.Registry;
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":"<b64>","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":"<b64 png>"}
*
* 启动就绪时主动发 {"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();
// ★ 必须触发网格重建 —— 否则渲染器不知道该 section 有数据(实测)
mc.levelRenderer.allChanged();
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 "mods": {
// ★ mod 支持全在 Java 侧完成(用户 2026-09-25 定向):
// Java 自己扫 mods 目录、自己读 jar、自己注册方块、自己加载资源包。
// Python 只传目录路径。
String dir = req.has("dir") ? req.get("dir").getAsString() : null;
if (dir == null) {
JsonObject r = new JsonObject();
r.addProperty("ok", true);
r.addProperty("mods", 0);
r.addProperty("note", "未提供 dir");
emit(id, r);
break;
}
JsonObject res = loadMods(dir);
emit(id, res);
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, ResourceLocation.parse(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.countRenderedSections());
r.addProperty("screenNow", mc.screen != null ? mc.screen.getClass().getSimpleName() : "null");
r.addProperty("overlayNow", mc.getOverlay() != null ? mc.getOverlay().getClass().getSimpleName() : "null");
r.addProperty("levelNow", mc.level != null);
r.addProperty("playerNow", mc.player != null);
r.addProperty("cameraEntity", mc.getCameraEntity() != null ? "set" : "null");
r.addProperty("noRender", mc.noRender);
r.addProperty("hasRenderedAllChunks", mc.levelRenderer.hasRenderedAllSections());
var got = cc.getChunk(pcx, pcz, net.minecraft.world.level.chunk.status.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<BlockEntityTagOutput>(消费这个接口本身),不是该接口的方法签名。
// 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 t) {
if (f == 0) { System.err.println("[mcfold] tick 异常(第1帧): " + t); t.printStackTrace(System.err); }
}
RenderSystem.clear(16640, Minecraft.ON_OSX);
target.bindWrite(true);
// ★ 世代差异(1.21+):render(DeltaTracker, boolean)
mc.gameRenderer.render(mc.getTimer(), 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 {
// ★ 1.21 实测:setLevel 会挂 ReceivingLevelScreen;
// 若渲染前它有残留,画面就是加载屏(distinctColors 停在 ~199)。
mc.setScreen(null);
mc.setOverlay(null);
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<Integer> 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.countRenderedSections());
r.addProperty("hasRenderedAllChunks", mc.levelRenderer.hasRenderedAllSections());
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();
// ★ 世代差异(1.21+):enableDataFixerOptimizations() 已被移除(实测映射表无此方法)
Bootstrap.bootStrap();
File runDir = new File(gameDir);
runDir.mkdirs();
// ★ 世代差异(1.21+):User(String, UUID, String, Optional, Optional, Type)
// 1.20.x 是 (String, String sessionId, String, ...)
User user = new User("mcfold_cam", java.util.UUID.nameUUIDFromBytes("mcfold_cam".getBytes()),
"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");
// ★★ 世代差异(1.21+):注册表经 CommonListenerCookie.receivedRegistries 传入,
// 并在 ClientPacketListener 构造时赋给 **final** 字段
// (实测:1.20.x 是可变 LayeredRegistryAccess,可事后反射替换;
// 1.21 是 final RegistryAccess$Frozen,反射会抛
// "Can not set final ... field")。
// ⇒ 本世代的正确顺序:**先加载注册表,再构造 cookie + listener**。
// 这比 1.20 的反射注入更干净。
var packs = net.minecraft.server.packs.repository.ServerPacksSource.createPackRepository(
new File(runDir, "datapacks").toPath(),
new net.minecraft.world.level.validation.DirectoryValidator(p -> false));
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<RegistryDataLoader.RegistryData<?>>();
all.addAll(RegistryDataLoader.WORLDGEN_REGISTRIES);
all.addAll(RegistryDataLoader.DIMENSION_REGISTRIES);
RegistryAccess.Frozen base = net.minecraft.client.multiplayer.ClientRegistryLayer
.createRegistryAccess().compositeAccess();
RegistryAccess.Frozen loaded = RegistryDataLoader.load(dataRm, base, all);
System.err.println("[mcfold] registries loaded: dimension_type="
+ loaded.registryOrThrow(Registries.DIMENSION_TYPE).size());
Connection conn = new Connection(PacketFlow.CLIENTBOUND);
GameProfile profile = new GameProfile(UUID.randomUUID(), "mcfold_cam");
WorldSessionTelemetryManager telemetry = new WorldSessionTelemetryManager(
TelemetryEventSender.DISABLED, false, Duration.ZERO, "");
var cookie = new net.minecraft.client.multiplayer.CommonListenerCookie(
profile,
telemetry,
loaded, // ← 注册表从这里进
net.minecraft.world.flag.FeatureFlags.DEFAULT_FLAGS,
null, null, null,
java.util.Map.of(), null, false, java.util.Map.of(),
net.minecraft.server.ServerLinks.EMPTY);
ClientPacketListener listener = new ClientPacketListener(mc, conn, cookie);
System.err.println("[mcfold] listener ok, registryAccess dims="
+ listener.registryAccess().registryOrThrow(Registries.DIMENSION_TYPE).size());
Holder<DimensionType> dimHolder = loaded.registryOrThrow(Registries.DIMENSION_TYPE)
.getHolderOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, ResourceLocation.parse("minecraft: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);
// ★ 世代差异(1.21+):setLevel(ClientLevel, ReceivingLevelScreen$Reason)
// 1.20.x 只有一个参数。这是本文件与 gen_1_20 的差异之一。
// 枚举值实测:NETHER_PORTAL / END_PORTAL / OTHER(没有 LEVEL_LOAD)
// 我们不是经传送门进入世界 ⇒ OTHER
mc.setLevel(level, net.minecraft.client.gui.screens.ReceivingLevelScreen.Reason.OTHER);
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());
// ★★ 世代差异(1.21+,编译期看不出):
// GameRenderer.render(DeltaTracker,boolean) 里有门禁
// boolean ok = minecraft.isGameLoadFinished();
// if (ok && renderLevel && level != null) renderLevel(...);
// 而 gameLoadFinished 只在 **reloadResourcePacks 的回调**
// (onResourceLoadFinished)里被置 true。
// 我们不跑异步 reload(会死锁)⇒ 该位恒 false ⇒ 世界永不渲染
// (症状:画面停在 Mojang 加载屏,distinctColors≈199,chunksRendered=0)。
// 1.20.x 无此门禁,所以 gen_1_20 不受影响。
net.minecraft.client.renderer.McfoldBridge.markGameLoadFinished(mc);
mc.setOverlay(null);
mc.setScreen(null);
mc.options.setCameraType(net.minecraft.client.CameraType.THIRD_PERSON_BACK);
System.err.println("[mcfold] boot done");
}
/**
* 加载 mod:扫 jar → 注册占位方块 → 挂资源包(外观)。
*
* ★ 全部在 Java 侧完成(用户 2026-09-25 定向):Java 自己读 jar、自己注册、
* 自己挂资源包。Python 只传目录路径。
*
* 原理(见 scaffold/MOD-BLOCKS.md):方块**渲染**只需要
* (a) 注册表里有这个名字(拿 state id)
* (b) 资源包里有它的 blockstates/models/textures
* 与方块行为无关 ⇒ 零 mod 加载器也能正确显示。
*/
static JsonObject loadMods(String dir) {
JsonObject out = new JsonObject();
File d = new File(dir);
File[] jars = d.listFiles((f, n) -> n.endsWith(".jar"));
if (jars == null || jars.length == 0) {
out.addProperty("ok", true);
out.addProperty("mods", 0);
out.addProperty("blocksRegistered", 0);
return out;
}
// ── 1. 解冻 BLOCK 注册表(frozen=false + 恢复 intrusiveHolders)──
// 两个字段都必须处理:只清 frozen 会抛 "can't create intrusive holders"
Object blockReg = BuiltInRegistries.BLOCK;
try {
for (Class<?> c = blockReg.getClass(); c != null; c = c.getSuperclass()) {
try {
var f = c.getDeclaredField("frozen");
f.setAccessible(true);
f.setBoolean(blockReg, false);
} catch (NoSuchFieldException ignore) { }
try {
var f = c.getDeclaredField("unregisteredIntrusiveHolders");
f.setAccessible(true);
if (f.get(blockReg) == null) f.set(blockReg, new java.util.IdentityHashMap<>());
} catch (NoSuchFieldException ignore) { }
}
} catch (Throwable t) {
System.err.println("[mcfold] 解冻失败: " + t);
}
int mods = 0, blocksAdded = 0, skipped = 0;
JsonArray detail = new JsonArray();
for (File jar : jars) {
JsonObject m = new JsonObject();
m.addProperty("jar", jar.getName());
int n = 0, skip = 0;
try (java.util.zip.ZipFile zf = new java.util.zip.ZipFile(jar)) {
var en = zf.entries();
java.util.Set<String> seen = new java.util.LinkedHashSet<>();
while (en.hasMoreElements()) {
String name = en.nextElement().getName();
// 方块名来源:assets/<ns>/blockstates/<name>.json(权威,见 P5 实测)
java.util.regex.Matcher mm = java.util.regex.Pattern
.compile("^assets/([^/]+)/blockstates/(.+)\\.json$").matcher(name);
if (!mm.matches()) continue;
String ns = mm.group(1), blk = mm.group(2);
if (!seen.add(ns + ":" + blk)) continue;
// ★ 世代差异(1.21+):ResourceLocation 构造变 private → 静态工厂
ResourceLocation rl = ResourceLocation.fromNamespaceAndPath(ns, blk);
try {
if (BuiltInRegistries.BLOCK.containsKey(rl)) { skip++; continue; }
// ★ 世代差异(1.21+):Properties.copy(Block) 已移除,只有 of()
Block b = new Block(BlockBehaviour.Properties.of());
Registry.register(BuiltInRegistries.BLOCK, rl, b);
// ★ BlockState 不会自动进 BLOCK_STATE_REGISTRY(实测 state id 会是 0)
Block.BLOCK_STATE_REGISTRY.add(b.defaultBlockState());
n++;
} catch (Throwable t) {
skip++;
}
}
mods++;
} catch (Throwable t) {
m.addProperty("error", String.valueOf(t));
}
m.addProperty("blocksRegistered", n);
m.addProperty("skipped", skip);
blocksAdded += n;
skipped += skip;
detail.add(m);
}
// ── 2. 挂资源包(外观:blockstates/models/textures)──
// 把 mod jar 作为资源包加入并选中,然后重载资源管理器。
int packsAdded = 0;
try {
var repo = mc.getResourcePackRepository();
java.nio.file.Path rpDir = mc.getResourcePackDirectory();
for (File jar : jars) {
File dst = new File(rpDir.toFile(), "mcfold-" + jar.getName());
if (!dst.isFile()) java.nio.file.Files.copy(jar.toPath(), dst.toPath(),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
repo.reload();
var sel = new java.util.LinkedHashSet<>(repo.getSelectedIds());
for (String packId : repo.getAvailableIds()) {
if (packId.contains("mcfold-")) { sel.add(packId); packsAdded++; }
}
repo.setSelected(sel);
// 用新 pack 列表重建 resource manager(照官方 createReload 内部做法),
// 再手工驱动 listener(不能 join Minecraft.reloadResourcePacks,会死锁)
java.util.List<net.minecraft.server.packs.PackResources> packs = new java.util.ArrayList<>();
for (var pk : repo.getSelectedPacks()) packs.add(pk.open());
var rmF = Minecraft.class.getDeclaredField("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");
resF.setAccessible(true);
resF.set(rm, new net.minecraft.server.packs.resources.MultiPackResourceManager(
net.minecraft.server.packs.PackType.CLIENT_RESOURCES, packs));
reloadResources(mc);
net.minecraft.client.renderer.McfoldBridge.reloadShaders(mc.gameRenderer, mc.getResourceManager());
mc.levelRenderer.allChanged();
} catch (Throwable t) {
out.addProperty("packError", String.valueOf(t));
System.err.println("[mcfold] 资源包挂载失败: " + t);
}
out.addProperty("ok", true);
out.addProperty("mods", mods);
out.addProperty("blocksRegistered", blocksAdded);
out.addProperty("skipped", skipped);
out.addProperty("packsAdded", packsAdded);
out.add("detail", detail);
return out;
}
/** 同步驱动各 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 <T> java.util.concurrent.CompletableFuture<T> 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<net.minecraft.server.packs.resources.PreparableReloadListener> 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<net.minecraft.server.packs.resources.PreparableReloadListener>) 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();
}
}