架构重构:差异全部下移到 Java,Python 只做路由

用户口径:差异放在 Java 层,Python 相当于路由。

下移到 Java(新增 cmd=setblocks):
  Python 只发 RLE([[stateId,count],...]),Java 用官方 API 完成
  调色板组装 / 位打包 / 官方序列化 / 入 cache / 渲染。

★ 意外收益:走 ClientboundLevelChunkPacketData(chunk).getReadBuffer()
  + replaceWithPacketData(...) 这条官方路径,两代签名完全一致
  ⇒ 原先的 Storage.chunks 反射注入**整段删掉**。

新增 cmd=capacity:世界尺寸(sectionsCount/minSection/defaultBiome)
  由运行时回答,Python 不再硬编码 24/-4(1.16 时代是 16/0)。

Python 侧删除(防回归,见 test_import_surface):
  render_bridge.py(旧桥,无世代概念)
  render._pack/_long_array/_encode_paletted/_encode_chunk/_varint
  anvil.encode_block_section/encode_chunk_payload/save_chunk_to_payload
  合计 454→374 行(render.py)、300→177 行(anvil.py)

保留在 Python(都是「不随版本变」的知识):
  RLE 压缩(rle_encode/rle_decode,具名可测)
  存档读取(region/NBT)
  方块名→id 的路由(查询在 Java)
  NDJSON 协议

测试:62 项单元 + 6 项渲染联测全绿
  新测试反映新架构:RLE 边界、运行时容量、世代分派、新路径渲染
  test_anvil 里原有 8 项协议位打包测试已删(那已下移到 Java,
  由两代后端各自的编译验证覆盖)

新增 scaffold/ARCHITECTURE.md(分层原则 + 加新世代步骤 + 防回归清单)
This commit is contained in:
JianFeeeee
2026-09-25 19:21:45 +08:00
parent 253146cffe
commit 16f2e04fff
20 changed files with 687 additions and 862 deletions

View File

@ -37,6 +37,8 @@ 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;
@ -180,6 +182,66 @@ public class McfoldRenderServer {
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;
@ -283,6 +345,28 @@ public class McfoldRenderServer {
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);
@ -317,8 +401,12 @@ public class McfoldRenderServer {
// 而这个名字→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);
// ★ 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) {

View File

@ -37,6 +37,8 @@ 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;
@ -180,6 +182,66 @@ public class McfoldRenderServer {
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;
@ -283,6 +345,28 @@ public class McfoldRenderServer {
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);
@ -317,8 +401,12 @@ public class McfoldRenderServer {
// 而这个名字→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);
// ★ 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) {
@ -453,6 +541,10 @@ public class McfoldRenderServer {
}
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();