世代分派体系: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:
Binary file not shown.
Binary file not shown.
109
mcfold/backends/__init__.py
Normal file
109
mcfold/backends/__init__.py
Normal file
@ -0,0 +1,109 @@
|
||||
"""Java 后端分派 —— 按 MC 版本的 API 世代选择对应的渲染服务实现。
|
||||
|
||||
为什么用「世代」而不是「版本号」
|
||||
--------------------------------
|
||||
Java 是**编译期绑定**的:1.21 的 `render(DeltaTracker, boolean)` 里
|
||||
DeltaTracker 在 1.20.1 的 jar 中不存在,所以两代的调用**无法写在同一份源码里**
|
||||
(实测:世代 B 的调用对 1.20.1 jar 编译直接报 "cannot find symbol")。
|
||||
|
||||
⇒ 每个世代必须有**独立编译**的 Java 后端,各自对着自己版本的 client jar 编译。
|
||||
Python 这层负责选后端 —— 这是「加 case 支持新版本」的落点。
|
||||
|
||||
实测世代边界(来自官方 client_mappings 逐版本对比)
|
||||
--------------------------------------------------
|
||||
| 方法 | 1.20.x | 1.21+ |
|
||||
|---------------------|-------------------|--------------------------|
|
||||
| setLevel | (ClientLevel) | (ClientLevel, Reason) |
|
||||
| GameRenderer.render | (float,long,bool) | (DeltaTracker, bool) |
|
||||
| 渲染计数 | countRendered-* | 同(1.20.2 起改名) |
|
||||
|
||||
判据:**DeltaTracker 类是否存在**(1.21 引入的时间抽象,正是签名变化的根源)。
|
||||
这比硬编码版本号稳 —— 官方改版本号不影响判据。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import zipfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
# 世代的权威判据:这个类出现在哪个 version,就是那个世代
|
||||
GEN_MARKERS = {
|
||||
"gen_1_21": ["net/minecraft/client/DeltaTracker.class"],
|
||||
"gen_1_20": [], # 兜底(无判据类 ⇒ 最老的世代)
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Gen:
|
||||
"""一个 API 世代。"""
|
||||
|
||||
name: str
|
||||
min_version: str # 该世代起始版本(文档用)
|
||||
markers: tuple[str, ...] # 判据类(任一存在即属于本世代)
|
||||
backend: str # Python 后端模块名
|
||||
java_dir: str # Java 源码目录名
|
||||
|
||||
|
||||
GENERATIONS: list[Gen] = [
|
||||
Gen(
|
||||
name="gen_1_21",
|
||||
min_version="1.21",
|
||||
markers=tuple(GEN_MARKERS["gen_1_21"]),
|
||||
backend="gen_1_21",
|
||||
java_dir="gen_1_21",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def detect_generation(client_jar: str | Path) -> Gen | None:
|
||||
"""从 client jar 判世代(看判据类是否存在)。找不到则返回 None(属最老世代)。"""
|
||||
try:
|
||||
with zipfile.ZipFile(client_jar) as z:
|
||||
names = set(z.namelist())
|
||||
except Exception:
|
||||
return None
|
||||
for g in GENERATIONS:
|
||||
if any(m in names for m in g.markers):
|
||||
return g
|
||||
return None
|
||||
|
||||
|
||||
def detect_from_store(store: str | Path) -> Gen | None:
|
||||
"""从 store 目录找 client jar 并判世代。"""
|
||||
store = Path(store)
|
||||
for jar in sorted(store.glob("versions/*/*.jar")):
|
||||
g = detect_generation(jar)
|
||||
if g:
|
||||
return g
|
||||
return None
|
||||
|
||||
|
||||
def backend_class(client_jar: str | Path | None = None,
|
||||
generation: str | None = None):
|
||||
"""选中后端类。
|
||||
|
||||
★ 这是「Python 层 switch」的落点:加新世代只需在下面加一条 elif。
|
||||
"""
|
||||
if generation is None:
|
||||
generation = backend_for(client_jar) if client_jar else "gen_1_20"
|
||||
if generation == "gen_1_21":
|
||||
from .gen_1_21 import Gen_1_21
|
||||
return Gen_1_21
|
||||
from .gen_1_20 import Gen_1_20
|
||||
return Gen_1_20
|
||||
|
||||
|
||||
def backend_for(client_jar: str | Path) -> str:
|
||||
"""判世代并返回后端模块名。"""
|
||||
g = detect_generation(client_jar)
|
||||
if g is None:
|
||||
return "gen_1_20" # 无判据类 ⇒ 最老世代
|
||||
return g.backend
|
||||
|
||||
|
||||
def list_generations() -> list[tuple[str, str, str]]:
|
||||
"""(世代名, 版本区间, Java 目录) —— 供诊断/文档用。"""
|
||||
return [
|
||||
("gen_1_20", "1.20.x(实测 1.20.1/1.20.2)", "java/gen_1_20"),
|
||||
("gen_1_21", "1.21+", "java/gen_1_21"),
|
||||
]
|
||||
BIN
mcfold/backends/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
mcfold/backends/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
mcfold/backends/__pycache__/base.cpython-312.pyc
Normal file
BIN
mcfold/backends/__pycache__/base.cpython-312.pyc
Normal file
Binary file not shown.
BIN
mcfold/backends/__pycache__/gen_1_20.cpython-312.pyc
Normal file
BIN
mcfold/backends/__pycache__/gen_1_20.cpython-312.pyc
Normal file
Binary file not shown.
BIN
mcfold/backends/__pycache__/gen_1_21.cpython-312.pyc
Normal file
BIN
mcfold/backends/__pycache__/gen_1_21.cpython-312.pyc
Normal file
Binary file not shown.
BIN
mcfold/backends/__pycache__/gen_a_1_20.cpython-312.pyc
Normal file
BIN
mcfold/backends/__pycache__/gen_a_1_20.cpython-312.pyc
Normal file
Binary file not shown.
350
mcfold/backends/base.py
Normal file
350
mcfold/backends/base.py
Normal file
@ -0,0 +1,350 @@
|
||||
"""渲染后端基类 —— 世代无关的协议与生命周期。
|
||||
|
||||
设计(用户 2026-09-25 定向):
|
||||
Python 层做 switch 选择后端;每个世代一个**独立编译**的 Java 服务端。
|
||||
原因:Java 是编译期绑定的,世代间的签名差异(DeltaTracker 等)无法写在同一份源码里。
|
||||
|
||||
本基类承载所有世代共用的部分:
|
||||
- NDJSON 通道(子进程 + 行分隔 JSON)
|
||||
- 命令集(chunk / camera / resolve / render / registry / debug / view)
|
||||
- base64 载荷编解码
|
||||
子类只需声明世代特征(Java 目录、判据类、API 差异说明)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
class BackendError(RuntimeError):
|
||||
"""后端不可用或渲染失败。"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenderResult:
|
||||
"""一次渲染的结果。
|
||||
|
||||
distinct_colors 是本项目实测最锐利的判据:
|
||||
空屏/纯色 1-2 色 / 加载屏 ~199 / 简单地形 ~1450 / 真实存档 >10000
|
||||
(non-black 会被红底加载屏骗过 —— 实测踩过)
|
||||
"""
|
||||
|
||||
image: bytes | None = None
|
||||
path: str | None = None
|
||||
width: int = 0
|
||||
height: int = 0
|
||||
chunks_rendered: int = 0
|
||||
distinct_colors: int = 0
|
||||
camera: tuple[float, float, float] = (0.0, 0.0, 0.0)
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
raw: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def has_world_content(self) -> bool:
|
||||
return self.distinct_colors > 400 and self.chunks_rendered > 0
|
||||
|
||||
|
||||
class RenderBackend:
|
||||
"""所有世代后端共用的实现。子类覆写类属性即可。"""
|
||||
|
||||
generation: str = "base"
|
||||
min_mc_version: str = ""
|
||||
max_mc_version: str = ""
|
||||
java_dir: str = ""
|
||||
markers: tuple[str, ...] = ()
|
||||
main_class: str = "McfoldRenderServer"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
java: str = "java",
|
||||
classpath: str | list[str] | None = None,
|
||||
game_dir: str | None = None,
|
||||
assets: str | None = None,
|
||||
asset_index: str | None = None,
|
||||
java_args: list[str] | None = None,
|
||||
startup_timeout: float = 240.0,
|
||||
cwd: str | None = None,
|
||||
):
|
||||
self.java = java
|
||||
if classpath is None:
|
||||
classpath = os.environ.get("MCFOLD_RENDER_CLASSPATH")
|
||||
if isinstance(classpath, (list, tuple)):
|
||||
classpath = os.pathsep.join(classpath)
|
||||
self.classpath = classpath
|
||||
self.game_dir = game_dir
|
||||
self.assets = assets
|
||||
self.asset_index = asset_index
|
||||
self.startup_timeout = startup_timeout
|
||||
self.cwd = cwd
|
||||
self.java_args = java_args or [
|
||||
"-Xmx1200M", "-Xms512M", "-Djava.awt.headless=true",
|
||||
]
|
||||
|
||||
self._proc: subprocess.Popen | None = None
|
||||
self._next_id = 1
|
||||
self._pending: dict[int, tuple[threading.Event, dict]] = {}
|
||||
self._lock = threading.Lock()
|
||||
self._stderr_tail: list[str] = []
|
||||
|
||||
# ---------------- 生命周期 ----------------
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
return self._proc is not None and self._proc.poll() is None
|
||||
|
||||
def start(self) -> "RenderBackend":
|
||||
if self.running:
|
||||
return self
|
||||
if not self.classpath:
|
||||
raise BackendError(
|
||||
f"未提供 classpath。设置 MCFOLD_RENDER_CLASSPATH,"
|
||||
f"或先跑 scaffold/build.sh 编译 {self.generation} 后端。"
|
||||
)
|
||||
cmd = [self.java, *self.java_args, "-cp", self.classpath, self.main_class]
|
||||
if self.game_dir:
|
||||
cmd += ["--gameDir", self.game_dir]
|
||||
if self.assets:
|
||||
cmd += ["--assets", self.assets]
|
||||
if self.asset_index:
|
||||
cmd += ["--assetIndex", self.asset_index]
|
||||
|
||||
env = dict(os.environ)
|
||||
nat = env.get("MCFOLD_NATIVES_DIR")
|
||||
if nat:
|
||||
env["LD_LIBRARY_PATH"] = nat + os.pathsep + env.get("LD_LIBRARY_PATH", "")
|
||||
|
||||
try:
|
||||
self._proc = subprocess.Popen(
|
||||
cmd, cwd=self.cwd, env=env,
|
||||
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
text=False, bufsize=0,
|
||||
)
|
||||
except FileNotFoundError as e:
|
||||
raise BackendError(f"无法启动 java: {e}") from e
|
||||
|
||||
threading.Thread(target=self._read_loop, daemon=True).start()
|
||||
threading.Thread(target=self._stderr_loop, daemon=True).start()
|
||||
|
||||
ready = self._await_event("ready", self.startup_timeout)
|
||||
if not ready.get("_ok"):
|
||||
self.stop()
|
||||
tail = "".join(self._stderr_tail[-6:])
|
||||
raise BackendError(
|
||||
f"{self.generation} 后端启动失败:{ready.get('error') or '超时'}"
|
||||
+ (f"\n{tail}" if tail else "")
|
||||
)
|
||||
return self
|
||||
|
||||
def stop(self, timeout: float = 10.0) -> None:
|
||||
p = self._proc
|
||||
if p is None:
|
||||
return
|
||||
try:
|
||||
if p.poll() is None:
|
||||
try:
|
||||
self._send_raw({"cmd": "shutdown"})
|
||||
p.wait(timeout=timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
p.terminate()
|
||||
try:
|
||||
p.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
p.kill()
|
||||
except Exception:
|
||||
try:
|
||||
p.kill()
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._proc = None
|
||||
|
||||
def __enter__(self):
|
||||
return self.start()
|
||||
|
||||
def __exit__(self, *exc):
|
||||
self.stop()
|
||||
|
||||
# ---------------- 数据注入 ----------------
|
||||
|
||||
def feed_chunk(self, cx: int, cz: int, chunk_data: bytes) -> dict:
|
||||
return self._request({"cmd": "chunk", "cx": cx, "cz": cz,
|
||||
"data": self._b64(chunk_data)})
|
||||
|
||||
def feed_chunks(self, chunks: list[tuple[int, int, bytes]]) -> dict:
|
||||
return self._request({
|
||||
"cmd": "chunks",
|
||||
"items": [{"cx": cx, "cz": cz, "data": self._b64(d)} for cx, cz, d in chunks],
|
||||
}, timeout=300.0)
|
||||
|
||||
def set_viewport(self, cx: int, cz: int, radius: int | None = None) -> dict:
|
||||
"""视野中心 —— 必须在注入前设好,否则区块被 ClientChunkCache 丢弃(实测)。"""
|
||||
req: dict[str, Any] = {"cmd": "viewport", "cx": cx, "cz": cz}
|
||||
if radius is not None:
|
||||
req["radius"] = radius
|
||||
return self._request(req)
|
||||
|
||||
# ---------------- 视角与视觉 ----------------
|
||||
|
||||
def set_camera(self, x: float, y: float, z: float,
|
||||
yaw: float = 0.0, pitch: float = 0.0) -> dict:
|
||||
return self._request({"cmd": "camera", "x": x, "y": y, "z": z,
|
||||
"yaw": yaw, "pitch": pitch})
|
||||
|
||||
def set_view(self, **kw) -> dict:
|
||||
"""视觉设置。各世代支持的项见子类 api_notes()。
|
||||
|
||||
世代无关的项:camera_type / render_distance / fov
|
||||
原版画质(所有世代都有):graphics_mode / ambient_occlusion /
|
||||
mipmap_levels / gamma / entity_shadows / clouds / particles
|
||||
后处理链:post_effect(原版 PostChain,非第三方光影包)
|
||||
"""
|
||||
req: dict[str, Any] = {"cmd": "view"}
|
||||
mapping = {
|
||||
"camera_type": "cameraType", "render_distance": "renderDistance",
|
||||
"fov": "fov", "graphics_mode": "graphicsMode",
|
||||
"ambient_occlusion": "ambientOcclusion", "mipmap_levels": "mipmapLevels",
|
||||
"gamma": "gamma", "entity_shadows": "entityShadows",
|
||||
"clouds": "clouds", "particles": "particles",
|
||||
"post_effect": "postEffect",
|
||||
}
|
||||
for py, js in mapping.items():
|
||||
if py in kw and kw[py] is not None:
|
||||
req[js] = kw[py]
|
||||
return self._request(req)
|
||||
|
||||
# ---------------- 查询 ----------------
|
||||
|
||||
def registry_info(self) -> dict:
|
||||
return self._request({"cmd": "registry"})
|
||||
|
||||
def lookup_state(self, state_id: int) -> dict:
|
||||
return self._request({"cmd": "lookup", "stateId": state_id})
|
||||
|
||||
def resolve_blocks(self, specs: list[str]) -> list[int]:
|
||||
"""方块名(+属性) → state id。名字→id 的权威映射在客户端注册表。"""
|
||||
if not specs:
|
||||
return []
|
||||
return self._request({"cmd": "resolve", "blocks": specs})["ids"]
|
||||
|
||||
def debug(self) -> dict:
|
||||
return self._request({"cmd": "debug"})
|
||||
|
||||
# ---------------- 渲染 ----------------
|
||||
|
||||
def render(self, out: str | None = None, frames: int = 60,
|
||||
width: int | None = None, height: int | None = None) -> RenderResult:
|
||||
req: dict[str, Any] = {"cmd": "render", "frames": frames}
|
||||
if out:
|
||||
req["out"] = out
|
||||
if width:
|
||||
req["width"] = width
|
||||
if height:
|
||||
req["height"] = height
|
||||
resp = self._request(req, timeout=600.0)
|
||||
img = resp.pop("image", None)
|
||||
cam = resp.get("camera") or (0.0, 0.0, 0.0)
|
||||
if not isinstance(cam, (list, tuple)):
|
||||
cam = (0.0, 0.0, 0.0)
|
||||
return RenderResult(
|
||||
image=base64.b64decode(img) if img else None,
|
||||
path=resp.get("path"),
|
||||
width=resp.get("width", 0),
|
||||
height=resp.get("height", 0),
|
||||
chunks_rendered=resp.get("chunksRendered", 0),
|
||||
distinct_colors=resp.get("distinctColors", 0),
|
||||
camera=tuple(float(c) for c in cam[:3]),
|
||||
warnings=resp.get("warnings", []),
|
||||
raw=resp,
|
||||
)
|
||||
|
||||
# ---------------- 内部 ----------------
|
||||
|
||||
@staticmethod
|
||||
def _b64(b: bytes) -> str:
|
||||
return base64.b64encode(b).decode("ascii")
|
||||
|
||||
def _request(self, req: dict, timeout: float = 180.0) -> dict:
|
||||
if not self.running:
|
||||
raise BackendError("后端未运行(先 start())")
|
||||
with self._lock:
|
||||
rid = self._next_id
|
||||
self._next_id += 1
|
||||
req["id"] = rid
|
||||
ev = threading.Event()
|
||||
self._pending[rid] = (ev, {})
|
||||
self._send_raw(req)
|
||||
if not ev.wait(timeout):
|
||||
self._pending.pop(rid, None)
|
||||
raise BackendError(f"后端无响应({timeout}s):cmd={req.get('cmd')}")
|
||||
_, resp = self._pending.pop(rid)
|
||||
if not resp.get("_ok", True):
|
||||
raise BackendError(resp.get("error", "未知错误"))
|
||||
return resp
|
||||
|
||||
def _await_event(self, kind: str, timeout: float) -> dict:
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
if not self.running:
|
||||
code = self._proc.poll() if self._proc else "?"
|
||||
return {"_ok": False, "error": f"进程退出(code={code})"}
|
||||
box = self._pending.pop(-1, None)
|
||||
if box is not None:
|
||||
ev, payload = box
|
||||
if payload.get("_event") == kind:
|
||||
return {"_ok": True, **payload}
|
||||
self._pending[-1] = (ev, payload) # 不是目标事件,放回
|
||||
time.sleep(0.05)
|
||||
return {"_ok": False, "error": f"等待 {kind} 超时"}
|
||||
|
||||
def _send_raw(self, obj: dict) -> None:
|
||||
if not self._proc or self._proc.stdin is None:
|
||||
raise BackendError("stdin 不可用")
|
||||
self._proc.stdin.write((json.dumps(obj) + "\n").encode("utf-8"))
|
||||
self._proc.stdin.flush()
|
||||
|
||||
def _read_loop(self) -> None:
|
||||
assert self._proc and self._proc.stdout
|
||||
for raw in self._proc.stdout:
|
||||
line = raw.decode("utf-8", "replace").strip()
|
||||
if not line.startswith("{"):
|
||||
continue
|
||||
try:
|
||||
msg = json.loads(line)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if msg.get("event") == "ready":
|
||||
ev = threading.Event()
|
||||
box = dict(msg); box["_event"] = "ready"
|
||||
self._pending[-1] = (ev, box)
|
||||
ev.set()
|
||||
continue
|
||||
rid = msg.get("id")
|
||||
if rid in self._pending:
|
||||
ev, box = self._pending[rid]
|
||||
box.update(msg)
|
||||
ev.set()
|
||||
|
||||
def _stderr_loop(self) -> None:
|
||||
assert self._proc and self._proc.stderr
|
||||
for raw in self._proc.stderr:
|
||||
line = raw.decode("utf-8", "replace").rstrip()
|
||||
self._stderr_tail.append(line)
|
||||
if len(self._stderr_tail) > 300:
|
||||
del self._stderr_tail[:150]
|
||||
|
||||
# ---------------- 元信息 ----------------
|
||||
|
||||
def api_notes(self) -> dict:
|
||||
"""本世代的 API 特征(子类覆写)。"""
|
||||
return {}
|
||||
|
||||
def describe(self) -> str:
|
||||
return (f"{self.generation} [{self.min_mc_version}..{self.max_mc_version}] "
|
||||
f"java_dir={self.java_dir}")
|
||||
37
mcfold/backends/gen_1_20.py
Normal file
37
mcfold/backends/gen_1_20.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""1.20.x 渲染后端。
|
||||
|
||||
对应 Java 源码:scaffold/java/gen_1_20/
|
||||
编译目标:1.20.x 的 client jar(重映射为可读名后)
|
||||
|
||||
API 特征(实测):
|
||||
setLevel(ClientLevel)
|
||||
GameRenderer.render(float partialTick, long nanoTime, boolean renderLevel)
|
||||
LevelRenderer.countRenderedChunks()
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import RenderBackend
|
||||
|
||||
|
||||
class Gen_1_20(RenderBackend):
|
||||
"""1.20.x 系列(含 1.20 / 1.20.1 / 1.20.2 / 1.20.3 / 1.20.4 / 1.20.5 / 1.20.6)。"""
|
||||
|
||||
generation = "gen_1_20"
|
||||
# ★ 诚实标注:仅实测过 1.20.1 与 1.20.2(以及 1.20.1 的 mappings 对比)。
|
||||
# 1.16~1.19 未验证 —— 不要因为「看起来同代」就宣称支持。
|
||||
verified_versions = ("1.20.1", "1.20.2")
|
||||
min_mc_version = "(未验证)"
|
||||
max_mc_version = "1.20.6" # 1.21 起换世代(由 DeltaTracker 判据确定)
|
||||
|
||||
# 该世代的 Java 源码目录(相对 scaffold/java/)
|
||||
java_dir = "java/gen_1_20"
|
||||
# 判据类(用于自动识别)
|
||||
markers: tuple[str, ...] = ()
|
||||
|
||||
def api_notes(self) -> dict:
|
||||
return {
|
||||
"setLevel": "Minecraft.setLevel(ClientLevel)",
|
||||
"render": "GameRenderer.render(float, long, boolean)",
|
||||
"count": "LevelRenderer.countRenderedChunks()",
|
||||
"deltaTracker": False,
|
||||
}
|
||||
36
mcfold/backends/gen_1_21.py
Normal file
36
mcfold/backends/gen_1_21.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""1.21+ 渲染后端。
|
||||
|
||||
对应 Java 源码:scaffold/java/gen_1_21/
|
||||
编译目标:1.21.x 的 client jar(重映射为可读名后)
|
||||
|
||||
API 特征(实测,与世代 A 的差异):
|
||||
setLevel(ClientLevel, ReceivingLevelScreen$Reason) ← 多一个参数
|
||||
GameRenderer.render(DeltaTracker, boolean) ← 签名整个换了
|
||||
Minecraft.getTimer() → DeltaTracker ← 1.21 引入
|
||||
LevelRenderer.countRenderedSections() ← 1.20.2 起改名
|
||||
|
||||
★ 这些差异**无法**写在与 1.20 相同的源码里:
|
||||
DeltaTracker 在 1.20.x 的 jar 中不存在 ⇒ 编译期即失败。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from .base import RenderBackend
|
||||
|
||||
|
||||
class Gen_1_21(RenderBackend):
|
||||
"""1.21+ 系列。"""
|
||||
|
||||
generation = "gen_1_21"
|
||||
min_mc_version = "1.21"
|
||||
max_mc_version = ""
|
||||
java_dir = "java/gen_1_21"
|
||||
markers = ("net/minecraft/client/DeltaTracker.class",)
|
||||
|
||||
def api_notes(self) -> dict:
|
||||
return {
|
||||
"setLevel": "Minecraft.setLevel(ClientLevel, ReceivingLevelScreen$Reason)",
|
||||
"render": "GameRenderer.render(DeltaTracker, boolean)",
|
||||
"count": "LevelRenderer.countRenderedSections()",
|
||||
"deltaTracker": True,
|
||||
"source": "Minecraft.getTimer()",
|
||||
}
|
||||
@ -25,7 +25,8 @@ from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from . import anvil
|
||||
from .render_bridge import RenderBridge, RenderBridgeError, RenderResult
|
||||
from .backends import backend_class, detect_generation
|
||||
from .backends.base import BackendError, RenderBackend, RenderResult
|
||||
|
||||
# 存档区块在网络载荷里的 section 数(1.20.1 worldHeight 384 / 16 = 24)
|
||||
SECTION_COUNT = 24
|
||||
@ -182,7 +183,7 @@ def parse_save_chunk(cx: int, cz: int, nbt: dict) -> SaveChunk:
|
||||
class WorldRenderer:
|
||||
"""世界 → 画面。持有一个常驻 Java 渲染器进程。"""
|
||||
|
||||
def __init__(self, bridge: RenderBridge, instance: Any | None = None):
|
||||
def __init__(self, bridge: RenderBackend, instance: Any | None = None):
|
||||
self.bridge = bridge
|
||||
self.instance = instance
|
||||
self._spec_cache: dict[str, int] = {}
|
||||
@ -194,7 +195,8 @@ class WorldRenderer:
|
||||
@classmethod
|
||||
def open(cls, instance: Any = None, *, store: str | None = None,
|
||||
classpath: str | None = None, start: bool = True,
|
||||
java: str = "java") -> "WorldRenderer":
|
||||
java: str = "java", client_jar: str | None = None,
|
||||
generation: str | None = None) -> "WorldRenderer":
|
||||
"""从 instance(mcfold.instance.Instance)或 store 目录打开渲染器。
|
||||
|
||||
classpath 缺省取环境变量 MCFOLD_RENDER_CLASSPATH;
|
||||
@ -218,7 +220,10 @@ class WorldRenderer:
|
||||
asset_index = names[-1] if names else None
|
||||
game_dir = game_dir or str(store_p / "instances" / "default")
|
||||
|
||||
bridge = RenderBridge(
|
||||
# ★ Python 层 switch:按 client jar 的世代选后端
|
||||
# (1.21+ 的 DeltaTracker 在 1.20.x jar 中不存在 ⇒ 必须独立编译的后端)
|
||||
cls = backend_class(client_jar=client_jar, generation=generation)
|
||||
bridge = cls(
|
||||
java=java, classpath=classpath,
|
||||
game_dir=game_dir, assets=assets, asset_index=asset_index,
|
||||
)
|
||||
|
||||
@ -213,14 +213,47 @@ class RenderBridge:
|
||||
return self._request({"cmd": "camera", "x": x, "y": y, "z": z,
|
||||
"yaw": yaw, "pitch": pitch})
|
||||
|
||||
def set_view(self, *, camera_type: str = "THIRD_PERSON_BACK",
|
||||
def set_view(self, *, camera_type: str | None = "THIRD_PERSON_BACK",
|
||||
render_distance: int | None = None,
|
||||
fov: float | None = None) -> dict:
|
||||
req: dict[str, Any] = {"cmd": "view", "cameraType": camera_type}
|
||||
fov: float | None = None,
|
||||
graphics_mode: str | None = None,
|
||||
ambient_occlusion: bool | None = None,
|
||||
mipmap_levels: int | None = None,
|
||||
gamma: float | None = None,
|
||||
entity_shadows: bool | None = None,
|
||||
clouds: str | None = None,
|
||||
particles: str | None = None,
|
||||
post_effect: str | None = None) -> dict:
|
||||
"""视觉设置。
|
||||
|
||||
光影能力边界(实测,见 scaffold/README.md):
|
||||
- 原版画质选项(graphics_mode/AO/mipmap/gamma/云/实体阴影): 支持
|
||||
- 原版后处理链(post_effect,如 minecraft:shaders/post/invert.json): 支持
|
||||
- 第三方光影包(SEUS/BSL 等): 不支持(需 Iris/OptiFine 加载器改渲染管线)
|
||||
"""
|
||||
req: dict[str, Any] = {"cmd": "view"}
|
||||
if camera_type:
|
||||
req["cameraType"] = camera_type
|
||||
if render_distance is not None:
|
||||
req["renderDistance"] = render_distance
|
||||
if fov is not None:
|
||||
req["fov"] = fov
|
||||
if graphics_mode is not None:
|
||||
req["graphicsMode"] = graphics_mode
|
||||
if ambient_occlusion is not None:
|
||||
req["ambientOcclusion"] = ambient_occlusion
|
||||
if mipmap_levels is not None:
|
||||
req["mipmapLevels"] = mipmap_levels
|
||||
if gamma is not None:
|
||||
req["gamma"] = gamma
|
||||
if entity_shadows is not None:
|
||||
req["entityShadows"] = entity_shadows
|
||||
if clouds is not None:
|
||||
req["clouds"] = clouds
|
||||
if particles is not None:
|
||||
req["particles"] = particles
|
||||
if post_effect is not None:
|
||||
req["postEffect"] = post_effect
|
||||
return self._request(req)
|
||||
|
||||
# ---------------- 渲染 ----------------
|
||||
|
||||
Reference in New Issue
Block a user