Files
agentmc/mcfold/backends/__init__.py
JianFeeeee 35534b1564 feat: get_block_info —— 方块信息查询(Java 侧回答,Python 只路由)
新增 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 条判据)
2026-09-26 00:24:29 +08:00

144 lines
5.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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"),
]