世代分派体系:Python 层 switch + 各世代独立编译的 Java 后端

用户定向:在 python 层写 switch,调用不同 java 后端(而非一个文件里 switch)。

新增:
  mcfold/backends/{__init__,base,gen_1_20,gen_1_21}.py  后端体系与分派
  scaffold/java/gen_1_20/  对 1.20.x client jar 编译
  scaffold/java/gen_1_21/  对 1.21.1 client jar 编译(✅ 实测编译通过)
  scaffold/java/VERSION-DIFFS.md  实测差异全表

判据:DeltaTracker 类是否存在(1.21 引入,正是 render 签名变化的根源)
  ⇒ 比硬编码版本号稳

实测差异共 11 处(21 个检查点里 13 个跨世代完全一致):
  setLevel / GameRenderer.render / countRendered* / createPackRepository /
  ResourceLocation 构造 / enableDataFixerOptimizations(移除) / User 构造 /
  ChunkStatus 包路径 / ClientPacketListener 构造(参数个数语义都变) /
  loadEffect(变 private)

为什么必须分后端:DeltaTracker 在 1.20.x jar 中不存在,
两代调用无法共存于一份源码(实测编译报 cannot find symbol)。

1.21.1 反混淆已验证(29MB named jar,DeltaTracker 3 处)
This commit is contained in:
JianFeeeee
2026-09-25 18:21:08 +08:00
parent 34ed70ab53
commit 8c7d5b9d38
27 changed files with 1460 additions and 55 deletions

View File

@ -0,0 +1,56 @@
# 世代差异清单(实测,非推断)
来源:官方 `client_mappings.txt` 逐版本对比(1.20.1 / 1.20.2 / 1.21.1),
以及两侧源码对各自 client jar 的实际编译结果。
## 判据
**`net.minecraft.client.DeltaTracker` 类是否存在** ⇒ 1.21+。
这是 1.21 引入的时间抽象,也正是 `GameRenderer.render` 签名变化的根源。
比硬编码版本号稳(官方改版本号不影响判据)。
实现:`mcfold/backends/__init__.py:detect_generation()`
## 差异全表(8 处,覆盖 21 个检查点中的 13 个「完全一致」之外的部分)
| # | API | 1.20.x | 1.21+ | 处理 |
|---|---|---|---|---|
| 1 | `Minecraft.setLevel` | `(ClientLevel)` | `(ClientLevel, ReceivingLevelScreen$Reason)` | gen_1_21 传 `Reason.OTHER` |
| 2 | `GameRenderer.render` | `(float,long,boolean)` | `(DeltaTracker, boolean)` | gen_1_21 用 `mc.getTimer()` |
| 3 | 区块计数 | `countRenderedChunks()` | `countRenderedSections()` | 各世代用各自名字 |
| 4 | 同理 | `hasRenderedAllChunks()` | `hasRenderedAllSections()` | 同上 |
| 5 | `ServerPacksSource.createPackRepository` | `(Path)` | `(Path, DirectoryValidator)` | gen_1_21 构造 validator |
| 6 | `ResourceLocation` 构造 | `new ResourceLocation(s)` | `ResourceLocation.parse(s)` | 静态工厂 |
| 7 | `SharedConstants.enableDataFixerOptimizations` | 有 | **已移除** | gen_1_21 不调 |
| 8 | `User` 构造 | `(String,String sessionId,...)` | `(String,UUID,...)` | 传 UUID |
| 9 | `ChunkStatus` | `world.level.chunk.ChunkStatus` | `world.level.chunk.status.ChunkStatus` | 包路径变了 |
| 10 | `ClientPacketListener` 构造 | `(Minecraft,Screen,Connection,ServerData,GameProfile,Telemetry)` | `(Minecraft,Connection,CommonListenerCookie)` | ★ 参数个数与语义都变 |
| 11 | `GameRenderer.loadEffect` | package-private(同包可调) | **private**(同包也调不到) | gen_1_21 用反射 |
★ 关键结论:**差异集中在 11 处,而不是「每个版本一套」**。
21 个检查点里 13 个(如 `tick`/`allChanged`/`replaceWithPacketData`/
`updateViewCenter`/`getChunkSource`/`PackRepository.*`)跨世代**完全一致**。
## 为什么必须分后端(而不是一个文件里 switch)
Java 编译期绑定签名。实测:
```
世代 B 的调用对 1.20.1 jar 编译:
error: cannot find symbol class DeltaTracker
```
`DeltaTracker` 在 1.20.x 的 jar 里不存在 ⇒ 两代的调用**无法共存于一份源码**。
所以每个世代独立编译,Python 层做分派。
## 加新世代的做法(用户 2026-09-25 定向)
1. `scaffold/java/gen_<ver>/` 建目录,拷上一代源码
2. 按上表改差异点(对着 **javap 输出**改,不要凭印象写 API 名)
3. `mcfold/backends/gen_<ver>.py` 声明 `markers`(判据类)
4. `mcfold/backends/__init__.py` 的 `GENERATIONS` 加一条
5. 各自对**对应版本**的 named jar 编译
⚠️ 教训(本轮踩过):`Reason.LEVEL_LOAD`、`DirectoryValidator.NONE`
都是凭印象写出来的、实际不存在的 API;`loadEffect` 的可见性也没验证。
**改之前先 `javap`。**

View File

@ -0,0 +1,645 @@
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.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 "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 "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)
var reg = mc.level.registryAccess().lookupOrThrow(Registries.BLOCK);
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<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 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<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.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<RegistryDataLoader.RegistryData<?>>();
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<DimensionType> 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 <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();
}
}

View File

@ -0,0 +1,23 @@
package net.minecraft.client.renderer;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceProvider;
/**
* 包私有成员桥。
*
* GameRenderer 里几个关键方法不是 public,官方只在客户端内部调用;
* 我们不跑主循环、不连服,所以要用同包类直接调。
* reloadShaders —— 官方由匿名内部类 GameRenderer$1(资源重载 listener)调
* loadEffect —— 官方由 onResourceManagerReload / 药水效果改
*/
public class McfoldBridge {
public static void reloadShaders(GameRenderer gr, ResourceProvider provider) {
gr.reloadShaders(provider);
}
/** 载入一个原版后处理链(如 minecraft:shaders/post/invert.json)。 */
public static void loadEffect(GameRenderer gr, ResourceLocation id) {
gr.loadEffect(id);
}
}

View File

@ -0,0 +1,676 @@
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.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 "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 "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)
var reg = mc.level.registryAccess().lookupOrThrow(Registries.BLOCK);
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("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 ignore) {}
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 {
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");
Connection conn = new Connection(PacketFlow.CLIENTBOUND);
GameProfile profile = new GameProfile(UUID.randomUUID(), "mcfold_cam");
// 初始注册表:官方静态层(1.21 的 cookie 需要它,随后会被真实注册表替换)
net.minecraft.core.RegistryAccess.Frozen listenerRegistryAccess =
net.minecraft.client.multiplayer.ClientRegistryLayer.createRegistryAccess()
.compositeAccess();
WorldSessionTelemetryManager telemetry = new WorldSessionTelemetryManager(
TelemetryEventSender.DISABLED, false, Duration.ZERO, "");
// ★ 世代差异(1.21+):ClientPacketListener(Minecraft, Connection, CommonListenerCookie)
// 1.20.x 是 (Minecraft, Screen, Connection, ServerData, GameProfile, TelemetryManager)。
// ★★ 这是本世代最大的一处变化 —— 参数个数与语义都不同。
var cookie = new net.minecraft.client.multiplayer.CommonListenerCookie(
profile,
telemetry,
listenerRegistryAccess, // 后面注册表加载后会被替换
net.minecraft.world.flag.FeatureFlags.DEFAULT_FLAGS,
null, // serverBrand
null, // serverData
null, // postDisconnectScreen
java.util.Map.of(), // serverCookies
null, // chatState
false, // strictErrorHandling
java.util.Map.of(), // customReportDetails
net.minecraft.server.ServerLinks.EMPTY);
ClientPacketListener listener = new ClientPacketListener(mc, conn, cookie);
// ★ 世代差异(1.21+):createPackRepository(Path, DirectoryValidator)
// DirectoryValidator 没有 NONE 常量,构造要 PathMatcher;
// 传一个「全部拒绝」的 matcher 最安全(我们不加载外来符号链接数据包)
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 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<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());
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 <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();
}
}

View File

@ -0,0 +1,37 @@
package net.minecraft.client.renderer;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceProvider;
/**
* 包私有成员桥。
*
* GameRenderer 里几个关键方法不是 public,官方只在客户端内部调用;
* 我们不跑主循环、不连服,所以要用同包类直接调。
* reloadShaders —— 官方由匿名内部类 GameRenderer$1(资源重载 listener)调
* loadEffect —— 官方由 onResourceManagerReload / 药水效果改
*/
public class McfoldBridge {
public static void reloadShaders(GameRenderer gr, ResourceProvider provider) {
gr.reloadShaders(provider);
}
/**
* 载入一个原版后处理链。
*
* ★ 世代差异(1.21+):loadEffect 从 package-private 变成了 **private**
* (实测 javap:1.20.1 是 `void loadEffect(...)`,1.21 是 `private void`),
* 同包也调不到 ⇒ 只能用反射。
* 1.21 新增了 public togglePostEffect() 但那是「切换内置效果」,
* 不支持任意 post chain id,所以仍走反射。
*/
public static void loadEffect(GameRenderer gr, ResourceLocation id) {
try {
java.lang.reflect.Method m = GameRenderer.class.getDeclaredMethod("loadEffect", ResourceLocation.class);
m.setAccessible(true);
m.invoke(gr, id);
} catch (Exception e) {
throw new RuntimeException("loadEffect 不可用(1.21+ 为 private)", e);
}
}
}