用户口径:差异放在 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(分层原则 + 加新世代步骤 + 防回归清单)
178 lines
5.8 KiB
Python
178 lines
5.8 KiB
Python
"""从 MC 存档(anvil region)读真实区块数据。
|
||
|
||
为什么走存档而不走服务端:
|
||
用户口径是「只调用渲染能力,不参与游戏」——存档是**零网络、零登录、
|
||
零服务端**的真实世界数据源,且与官方客户端认同一套格式。
|
||
|
||
★ 本模块只做**读取**(Python 的职责):
|
||
region/*.mca → 解压区块 NBT → 取出 Sections[].block_states(palette + data)
|
||
|
||
编码/协议组装**不在这里** —— 那属于 Java 侧(用官方 API 完成),
|
||
因为历代改过协议格式(位宽、palette 分支、LongArray 编码),
|
||
在 Python 里复制一份就等于把版本差异搬回了 Python。
|
||
下游 render.py 只把读取结果压成 RLE,交给后端。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import gzip
|
||
import io
|
||
import struct
|
||
import zlib
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
from typing import Any, Iterator
|
||
|
||
SECTOR = 4096
|
||
|
||
|
||
class AnvilError(RuntimeError):
|
||
pass
|
||
|
||
|
||
# ---------------------------------------------------------------- NBT 读取
|
||
|
||
class NbtReader:
|
||
"""极简 NBT 读取器(只读,支持存档里的全部 tag 类型)。"""
|
||
|
||
def __init__(self, data: bytes):
|
||
self.d = data
|
||
self.i = 0
|
||
|
||
def u1(self) -> int:
|
||
v = self.d[self.i]; self.i += 1; return v
|
||
|
||
def i1(self) -> int:
|
||
v = struct.unpack_from(">b", self.d, self.i)[0]; self.i += 1; return v
|
||
|
||
def i2(self) -> int:
|
||
v = struct.unpack_from(">h", self.d, self.i)[0]; self.i += 2; return v
|
||
|
||
def u2(self) -> int:
|
||
v = struct.unpack_from(">H", self.d, self.i)[0]; self.i += 2; return v
|
||
|
||
def i4(self) -> int:
|
||
v = struct.unpack_from(">i", self.d, self.i)[0]; self.i += 4; return v
|
||
|
||
def i8(self) -> int:
|
||
v = struct.unpack_from(">q", self.d, self.i)[0]; self.i += 8; return v
|
||
|
||
def f4(self) -> float:
|
||
v = struct.unpack_from(">f", self.d, self.i)[0]; self.i += 4; return v
|
||
|
||
def f8(self) -> float:
|
||
v = struct.unpack_from(">d", self.d, self.i)[0]; self.i += 8; return v
|
||
|
||
def s(self) -> str:
|
||
n = self.u2()
|
||
v = self.d[self.i:self.i + n].decode("utf-8", "replace"); self.i += n
|
||
return v
|
||
|
||
def payload(self, t: int) -> Any:
|
||
if t == 1: return self.i1()
|
||
if t == 2: return self.i2()
|
||
if t == 3: return self.i4()
|
||
if t == 4: return self.i8()
|
||
if t == 5: return self.f4()
|
||
if t == 6: return self.f8()
|
||
if t == 7:
|
||
n = self.i4(); v = self.d[self.i:self.i + n]; self.i += n; return v
|
||
if t == 8: return self.s()
|
||
if t == 9:
|
||
it = self.u1(); n = self.i4()
|
||
return [self.payload(it) for _ in range(n)]
|
||
if t == 10:
|
||
out = {}
|
||
while True:
|
||
tt = self.u1()
|
||
if tt == 0: break
|
||
nm = self.s()
|
||
out[nm] = self.payload(tt)
|
||
return out
|
||
if t == 11:
|
||
n = self.i4()
|
||
v = list(struct.unpack_from(f">{n}i", self.d, self.i)); self.i += 4 * n
|
||
return v
|
||
if t == 12:
|
||
n = self.i4()
|
||
v = list(struct.unpack_from(f">{n}q", self.d, self.i)); self.i += 8 * n
|
||
return v
|
||
raise AnvilError(f"未知 NBT tag {t} @ {self.i}")
|
||
|
||
|
||
def nbt_read(data: bytes) -> dict:
|
||
r = NbtReader(data)
|
||
t = r.u1()
|
||
if t != 10:
|
||
raise AnvilError(f"根 tag 应为 Compound(10),得到 {t}")
|
||
r.s() # 根名(通常空串)
|
||
return r.payload(10)
|
||
|
||
|
||
def read_level_dat(save_dir: Path) -> dict:
|
||
"""读 level.dat。
|
||
|
||
★ 实测两种布局都存在:
|
||
- 官方启动器/服务端:8 字节头 + zlib
|
||
- 部分整合包(本机 mc-test 实测):文件直接就是 gzip
|
||
所以先看魔数,再决定偏移。
|
||
"""
|
||
raw = (save_dir / "level.dat").read_bytes()
|
||
if raw[:2] == b"\x1f\x8b": # 直接 gzip
|
||
body = gzip.decompress(raw)
|
||
elif len(raw) > 10 and raw[8:10] == b"\x1f\x8b": # 8 字节头 + gzip
|
||
body = gzip.decompress(raw[8:])
|
||
else: # 8 字节头 + zlib
|
||
body = zlib.decompress(raw[8:])
|
||
return nbt_read(body)
|
||
|
||
|
||
# ---------------------------------------------------------------- region 读取
|
||
|
||
@dataclass
|
||
class RegionFile:
|
||
"""一个 .mca 文件:读出指定区块的原始 NBT(已解压)。"""
|
||
|
||
path: Path
|
||
_buf: bytes = b""
|
||
|
||
def __post_init__(self):
|
||
self._buf = self.path.read_bytes()
|
||
if len(self._buf) < SECTOR * 2:
|
||
raise AnvilError(f"region 文件过小: {self.path}")
|
||
|
||
def chunk_nbt(self, cx: int, cz: int) -> bytes | None:
|
||
"""cx/cz 是**区块坐标**(世界坐标 >> 4)。"""
|
||
lx, lz = cx & 31, cz & 31
|
||
idx = (lx + lz * 32) * 4
|
||
off = struct.unpack_from(">I", self._buf, idx)[0]
|
||
if off == 0:
|
||
return None
|
||
sector = (off >> 8) & 0xFFFFFF
|
||
count = off & 0xFF
|
||
if sector == 0 or count == 0:
|
||
return None
|
||
start = sector * SECTOR
|
||
length = struct.unpack_from(">I", self._buf, start)[0]
|
||
comp = self._buf[start + 4]
|
||
blob = self._buf[start + 5:start + 4 + length]
|
||
if comp == 1:
|
||
return gzip.decompress(blob)
|
||
if comp == 2:
|
||
return zlib.decompress(blob)
|
||
if comp == 3:
|
||
return blob
|
||
if comp == 4:
|
||
import lzma
|
||
return lzma.decompress(blob)
|
||
raise AnvilError(f"未知压缩类型 {comp}")
|
||
|
||
|
||
def region_path(save_dir: Path, dim: str | None, rx: int, rz: int) -> Path:
|
||
dim_part = "" if not dim else f"{dim}/"
|
||
return save_dir / f"{dim_part}region" / f"r.{rx}.{rz}.mca"
|
||
|
||
|
||
def list_region_files(save_dir: Path, dim: str | None = None) -> list[Path]:
|
||
base = save_dir / (f"{dim}/region" if dim else "region")
|
||
return sorted(base.glob("r.*.*.mca")) if base.is_dir() else []
|