用户要求:Create 与 Aeronautics 会引入大量原版没有的方块与危险区,
寻路必须一起优化。用户裁定:危险区用「表 + 命名推测并标记」,
动态机械用「警告 + 提高代价」。
## ★ 关键实测结论(本轮最重要)
我先按「给 mod 方块建语义表」做,实测覆盖率:
Create 643 个方块 → 显式表命中 9、命名规则推测 21、完全不识 613
覆盖率 4.7%
⇒ **手写表是死路**(64 mod / 1465 方块永远追不上)。
⇒ 但寻路真正需要的属性,**运行时全都有**:
Java 侧 blockinfo 已在读 BuiltInRegistries.BLOCK(**含 mod 运行时注册的方块**),
并返回 isSolid / destroySpeed / lightEmission / isAir / hasBlockEntity。
⇒ 正确做法是**问**,不是猜。
## 改动
新增 mcfold/solid.py:
- SolidSource 抽象:RegistrySolidSource(本机,快但无 mod)
vs RuntimeSolidSource(问 JVM,**含 mod**,准但要 JVM)
- 按 **state_id** 缓存(属性只依赖 state,不依赖坐标)
- attach_runtime() 批量探测(一次问一格要一次往返,批量才可用)
mcfold/pathfind.py:Terrain 改为「**先运行时缓存,再本机注册表**」;
can_dig 用 destroySpeed 判可挖(基岩官方约定是 -1 = 不可破坏),
不再靠名字猜「基岩挖不动」
新增 mcfold/modblocks.py(危险/会动这部分的表,配套覆盖率报告):
- 语义取���:ground/passable/hazard/breakable/solid/dynamic/carrier/unknown
- 三级来源:显式表 → 命名规则(**标记 inferred**)→ **如实说不认识**
- ★ vanilla 与 mod **分开算覆盖率**:vanilla 语义由注册表给,
把它算成「未知」会让人误以为「连石头都认不出」
WorldState:加 solid_cache 字段
## 判据
test_pathfind 19 → 24 项,新增 5 项:
- mod 方块优先用运行时 isSolid(本机注册表确实认不出)
- 不可破坏吃 destroySpeed(含基岩 -1 约定)
- 两边都无数据 ⇒ 不知道(**不谎称可走**)
- 覆盖率报告如实(且区分 mod/vanilla)
- 语义查询永远返回带 inferred 标记的结构
★ 写判据时踩了两个坑(都是 fixture 形状错,不是代码错):
1. `Chunk.sections[i]` 对应 y = MIN_Y + 16*i,MIN_Y=-64
⇒ 我按「第 0 个 section = y=0」写,查 y=0 拿到的是空气
2. 同一 section 内 16 格**只能一个值** ⇒ mod 方块与空气落同一 section 时,
空气格会变成 mod 方块(看着像代码 bug,其实是 fixture 形状问题)
⇒ 教训:**判据的 fixture 形状必须与被测代码的真实输入结构一致**。
226 项全通过。
228 lines
8.9 KiB
Python
228 lines
8.9 KiB
Python
"""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("<none>", 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
|