"""mod 方块的**语义表**:让寻路/感知认识 vanilla 之外的世界。 ## 为什么需要它 Create 这样的 mod 引入了三类格子寻路没见过的方块(实测分类 643 个方块): | 类别 | Create 的例子 | 对寻路意味着什么 | |---|---|---| | **可站** | `create:track`、`create:fake_track` | 地面(`boundingBox` 多半是 block,但要确认) | | **可拆** | `create:*_box`(32)、`cogwheel`(6)、`shaft`(6) | 障碍,但**能挖**(不是天然岩壁) | | **会伤** | `create:blaze_burner`、`lit_blaze_burner` | 致命区,必须**绕开**,不能当空地 | 而 `blocks.json` 里**没有这些**(vanilla 只有 1060 个方块), `modjar` 只能给出**名字**(从 blockstates/ 提取),给不出**语义**。 ⇒ 所以需要一张「名字 → 语义」的表。三个来源,按可信度排序: 1. **显式表**(本文件 + 用户可扩展)—— 最可信 2. **命名规则兜底**(`*burner*` 里有 blaze ⇒ 危险)—— 会猜 3. **都查不到** → **如实说「不认识」**,不猜 ## 关键纪律:推测必须标记 规则匹配出来的语义是**推测**(`inferred: True`)。使用者必须能看到它, 因为「以为知道」与「知道」在这个场景里差别很大: 按推测的语义走过去,可能正好走进燃烧室。 ⇒ `semantics_of()` 永远返回 `(语义, 是否推测, 依据)`,不返回裸字符串。 """ from __future__ import annotations import fnmatch from dataclasses import dataclass from typing import Any # ---------- 语义取值(与 pathfind 的用法对齐) ---------- #: 可站立(能当地面) GROUND = "ground" #: 可穿过(身体能过) PASSABLE = "passable" #: 致命/有害 ⇒ 既不能当地面也不能穿过 HAZARD = "hazard" #: 障碍但**可挖** BREAKABLE = "breakable" #: 天然障碍(挖不动,如基岩) SOLID = "solid" #: 会动的机械(路径经过时要警告) DYNAMIC = "dynamic" #: 移动载体(能载人,不当普通地面) CARRIER = "carrier" #: 完全不认识 UNKNOWN = "unknown" @dataclass(frozen=True) class Semantics: """一个方块的寻路语义。`inferred=True` 表示这是**推测**。""" name: str kind: str inferred: bool = False reason: str = "" def as_dict(self) -> dict[str, Any]: return {"name": self.name, "kind": self.kind, "inferred": self.inferred, "reason": self.reason} # ---------- 显式表(可由用户扩展/覆盖) ---------- #: 精确匹配表:方块名(不带命名空间)→ 语义。 #: 键用不带命名空间的名字,因为不同 mod 常用同名不同空间的方块。 EXPLICIT: dict[str, str] = { # --- Create:可站 --- "track": GROUND, "fake_track": GROUND, "gantry_shaft": GROUND, # --- Create:会伤 --- "blaze_burner": HAZARD, "lit_blaze_burner": HAZARD, "deployer": DYNAMIC, "hand_roll": GROUND, "sand_basin": GROUND, "cannon": DYNAMIC, # --- Create:可拆的机械结构 --- "box": BREAKABLE, "andesite_belt_funnel": BREAKABLE, "brass_belt_funnel": BREAKABLE, "belt_funnel": BREAKABLE, "gantry_carriage": CARRIER, "contraption_carriage": CARRIER, "carriage_wheel": BREAKABLE, } #: 通配表:`*burner*` → 语义。用于兜住同类的新方块。 PATTERNS: list[tuple[str, str]] = [ ("*blaze_burner*", HAZARD), ("*burner*", HAZARD), ("*_saw*", DYNAMIC), ("*_drill*", DYNAMIC), ("*_cannon*", DYNAMIC), ("*cannon*", DYNAMIC), ("*flame_burner*", HAZARD), ("*lava*", HAZARD), ("*_track*", GROUND), ("*_carriage*", CARRIER), ("*_box*", BREAKABLE), ("*cogwheel*", BREAKABLE), ("*_shaft*", BREAKABLE), ("*_bearing*", BREAKABLE), ("*_gear*", BREAKABLE), ("*_piston*", DYNAMIC), ] #: vanilla 命名空间 —— 这些方块的语义由**注册表**给出(见 mcfold.solid), #: 不走本表。本表只管「注册表里没有的」= mod 方块。 VANILLA_NS = "minecraft" def semantics_of(block_name: str | None, table: dict[str, str] | None = None, patterns: list[tuple[str, str]] | None = None, *, in_registry=None) -> Semantics: """方块名 → 语义。**永不返回裸字符串**,推测会标记。 参数 ---- block_name 带或不带命名空间都行(`create:track` 与 `track` 等价) table 覆盖显式表(可由用户/配置文件扩展) patterns 覆盖通配表 in_registry 可调用:给 state 判定是否「注册表里有」(有则语义交给注册表) ★ 为什么加 `in_registry`:vanilla 方块(`minecraft:stone`)的 solid/硬度**注册表里就有**(mcfold.solid),不该被算成「未知」—— 早先我的覆盖率报告把 `minecraft:stone` 记成 unknown, 会让人误以为「连石头都认不出」。这两类「不知道」含义不同: · 注册表里没有 ⇒ 需要 mod 语义表 · 注册表有、但语义(危险/会动)没有 ⇒ 需要外部标注 """ if not block_name: return Semantics("", UNKNOWN, False, "没有方块名") ns = block_name.split(":")[0] if ":" in block_name else VANILLA_NS bare = block_name.split(":")[-1] if ":" in block_name else block_name t = EXPLICIT if table is None else {**EXPLICIT, **table} pats = PATTERNS if patterns is None else patterns # ① vanilla:语义来自注册表,不查本表 if ns == VANILLA_NS and bare not in t: return Semantics(block_name, UNKNOWN, False, "vanilla 方块:语义由注册表给出,不走 mod 语义表") # ① 显式表 —— 可信 if bare in t: return Semantics(block_name, t[bare], False, "显式表") # ② 通配规则 —— **推测** for pat, kind in pats: if fnmatch.fnmatch(bare, pat): return Semantics(block_name, kind, True, f"命名规则 {pat}") # ③ 不认识 —— **如实说** return Semantics(block_name, UNKNOWN, False, "表与规则都没命中") # ---------- 汇总:把一批方块分类(诊断用) ---------- def classify_all(names: list[str], table: dict[str, str] | None = None, patterns: list[tuple[str, str]] | None = None, ) -> dict[str, list[str]]: """把方块名列表按语义分组(用于「这个 mod 引入了什么」这类问题)。""" out: dict[str, list[str]] = {} for n in names: s = semantics_of(n, table, patterns) out.setdefault(s.kind, []).append(n) return {k: sorted(v) for k, v in sorted( out.items(), key=lambda kv: -len(kv[1]))} def coverage_report(names: list[str], table: dict[str, str] | None = None, patterns: list[tuple[str, str]] | None = None, ) -> dict[str, Any]: """覆盖率报告:多少认出了、多少是推测、多少完全不认识。 ★ 这个报告的价值:它让「工具包在 mod 环境下的能力边界」变成**可读的**, 而不是让人以为「认不出 = 方块不存在」。 """ total = len(names) # ★ vanilla 与 mod 要**分开算**:vanilla 的语义由注册表给(mcfold.solid), # 把它算成「未知」会让人误以为「连石头都认不出」—— 那是两码事。 mod_names = [n for n in names if (n.split(":")[0] if ":" in n else VANILLA_NS) != VANILLA_NS] vanilla_names = [n for n in names if n not in mod_names] explicit, inferred, unknown = [], [], [] for n in mod_names: s = semantics_of(n, table, patterns) if s.kind == UNKNOWN: unknown.append(n) elif s.inferred: inferred.append(n) else: explicit.append(n) m = len(mod_names) or 1 return { "total": total, "mod_total": len(mod_names), "vanilla": {"count": len(vanilla_names), "note": "vanilla 方块不走本表(语义由注册表给)"}, "explicit": {"count": len(explicit), "kinds": _kinds(explicit, table, patterns), "sample": explicit[:10]}, "inferred": {"count": len(inferred), "kinds": _kinds(inferred, table, patterns), "sample": inferred[:10], "note": "这些是**推测**(命名规则命中),不是确认的语义"}, "unknown": {"count": len(unknown), "sample": unknown[:10], "note": "表与规则都没命中 ⇒ 寻路会当普通方块处理(可能出错)"}, "coverage_pct": round(100.0 * (len(mod_names) - len(unknown)) / m, 1), "coverage_note": "覆盖率只对 **mod 方块**计算(vanilla 由注册表覆盖)", } def _kinds(names: list[str], table, patterns) -> dict[str, int]: c: dict[str, int] = {} for n in names: k = semantics_of(n, table, patterns).kind c[k] = c.get(k, 0) + 1 return c