决定性验证:gen_1_21 后端在 1.21.1 上收到 {"event":"ready"}
⇒ 世代分派架构成立:同一套 Python 层,两个独立编译的 Java 后端
运行时发现第 12 处世代差异(编译期看不出):
1.20.x registryAccess 是可变 LayeredRegistryAccess → 事后反射替换
1.21.x registryAccess 是 final RegistryAccess$Frozen → 反射抛
"Can not set final field"
处理:gen_1_21 改为先载注册表 → 经 CommonListenerCookie.receivedRegistries
传入 → 再构造 listener。比 1.20 的反射做法更干净。
新增 mcfold/provision.py(从官方 manifest 装任意版本,版本无关):
- 跳过音频资源:1.21.1 实测 3911 条里跳过 3744 条
- natives 两种形态都认(1.21 起是独立库条目,非 classifiers)
- 实测 1.21.1:274 文件 / 200MB / 32s / 0 失败
116 lines
4.1 KiB
Python
116 lines
4.1 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",
|
||
"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):
|
||
"""选中后端类。
|
||
|
||
★ 这是「Python 层 switch」的落点:加新世代只需在下面加一条 elif。
|
||
"""
|
||
if generation is None:
|
||
generation = backend_for(client_jar) if client_jar else "gen_1_20"
|
||
if generation == "gen_1_21":
|
||
return Gen_1_21
|
||
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"),
|
||
]
|