新增 cmd=blockinfo(两代后端编译通过、实测):
返回 block 名(含 mod 方块)、stateId、属性、isAir/isSolid、
光照/硬度、贴图缺失的程序化判据、是否来自 mod。
★ 修掉一个真实缺陷(附录 D 的语义区分落到了实处):
未加载区块原先返回 void_air(known=true),实测发现两个 API 都不可用:
- ClientLevel.hasChunk() 反编译实测**恒返回 true**
- ChunkSource.hasChunk() 走 getChunk(...,false),而 ClientChunkCache
覆写后在该参数下返回 emptyChunk(占位,非 null)⇒ 也恒 true
正解:getChunk(..., requireChunk=**false**) 判 null
(反编译:return requireChunk ? emptyChunk : null;)
⇒ 现在未加载明确回 known=false + reason=chunk_not_loaded
实测(1.20.1):
草地 → minecraft:grass_block props={'snowy':'false'} missing=False
原木 → minecraft:oak_log props={'axis':'y'}
空气 → minecraft:air
远处 → known=false reason=chunk_not_loaded ← 与空气区分
实测(1.21.1 + mod):
mts:mtsofficialpack.fancydirt_block mod=True tex=mtsofficialpack:block/fancydirt
createbigcannons:steel_block mod=True tex=createbigcannons:block/steel_block
⇒ mod 方块同样能查到正确贴图名与 fromMod 标记
顺带修 gen_1_21 两处世代差异(ChunkStatus 换包、bridge 编译顺序)。
测试:69 项全绿(新增 test_get_block_info 6 条判据)
144 lines
5.3 KiB
Python
144 lines
5.3 KiB
Python
"""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,就是那个世代
|
||
__all__ = ["Gen_1_20", "Gen_1_21", "Gen", "GENERATIONS",
|
||
"detect_generation", "detect_from_store", "backend_class",
|
||
"LAUNCHERS", "list_launchers",
|
||
"backend_for", "list_generations"]
|
||
|
||
GEN_MARKERS = {
|
||
"gen_1_21": ["net/minecraft/client/DeltaTracker.class"],
|
||
"gen_1_20": [], # 兜底(无判据类 ⇒ 最老的世代)
|
||
}
|
||
|
||
|
||
from .gen_1_20 import Gen_1_20 # noqa: E402
|
||
from .gen_1_21 import Gen_1_21 # noqa: E402
|
||
|
||
|
||
@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,
|
||
launcher: str = ""):
|
||
"""选中后端类。
|
||
|
||
★ 这是「Python 层 switch」的落点:加新世代只需在下面加一条分支。
|
||
|
||
两个轴是**正交**的(用户 2026-09-25 定向):
|
||
世代(版本差异) × 启动架构(加载器差异)
|
||
- 世代决定 McfoldRenderServer 怎么调 MC API(见 VERSION-DIFFS.md)
|
||
- 启动架构决定「谁起游戏」:裸启动 = 我们自己的 main;
|
||
launcher="fabric" = 经 Fabric 加载器(能加载 mod / Iris,但不写我们的 mod)
|
||
正交 ⇒ 用动态子类组合,不写 N 个类。
|
||
"""
|
||
if generation is None:
|
||
generation = backend_for(client_jar) if client_jar else "gen_1_20"
|
||
base = Gen_1_21 if generation == "gen_1_21" else Gen_1_20
|
||
if not launcher:
|
||
return base
|
||
# 动态子类:只覆盖 launcher,其余沿用世代实现
|
||
name = f"{base.__name__}_{launcher}"
|
||
existing = globals().get(name)
|
||
if existing is not None:
|
||
return existing
|
||
cls = type(name, (base,), {"launcher": launcher})
|
||
globals()[name] = cls
|
||
return cls
|
||
|
||
|
||
def backend_for(client_jar: str | Path) -> str:
|
||
"""判世代并返回后端模块名。"""
|
||
g = detect_generation(client_jar)
|
||
if g is None:
|
||
return "gen_1_20" # 无判据类 ⇒ 最老世代
|
||
return g.backend
|
||
|
||
|
||
LAUNCHERS = [
|
||
("", "裸启动", "我们自己的 main;不加载 mod(零加载器方案可注册占位方块)"),
|
||
("fabric", "Fabric 加载器", "经 launcher_fabric.LoaderBoot;能加载 fabric mod / Iris,不写我们的 mod"),
|
||
]
|
||
|
||
|
||
def list_launchers() -> list[tuple[str, str, str]]:
|
||
"""(标识, 名称, 说明) —— 启动架构与世代正交。"""
|
||
return list(LAUNCHERS)
|
||
|
||
|
||
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"),
|
||
]
|