Files
agentmc/mcfold/modblocks.py
JianFeeeee 554ebc239a M10c:mod 环境下寻路 —— 问运行时而非猜名字(+ 语义表与覆盖率报告)
用户要求: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 项全通过。
2026-09-26 14:29:58 +08:00

228 lines
8.9 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.

"""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