"""运行时属性探针:向**运行中的官方客户端**问方块属性(含 mod)。 ## 为什么必须有这一层(实测结论,不是推测) 寻路需要两个属性:**能不能站/穿**(solid)与**能不能挖**(destroySpeed)。 我曾想用「方块名字 → 语义表」的办法覆盖 mod,实测覆盖率只有 **4.7%**: Create 643 个方块 → 显式表命中 9、命名规则推测 21、完全不识 613 ⇒ 手写表**追不上** 64 个 mod / 1465 个方块。 但这两个属性**运行时全都有**:Java 侧 `blockinfo` 已经在读 `BuiltInRegistries.BLOCK`(**含 mod 运行时注册的方块**),并返回 isSolid / destroySpeed / lightEmission / isAir / hasBlockEntity / block(名字) ⇒ 正确做法是**问**,不是猜。 ## 这个模块做什么 批量地把「世界坐标 → 方块属性」问出来,喂给寻路与感知层。 注意这需要 JVM(细渲染那一侧),所以它**不是常驻**的: - 寻路在 vanilla 世界里走本机 `blocks.json`(快、无 JVM) - 寻路在 mod 世界里**问运行时**(准、要 JVM) 两者接口一致,由 `SolidSource` 抽象掉差异。 ## 缓存:这是能不能用的关键 问一次要起 JVM(秒级)。所以: - 同一批坐标**一次问完**(`probe_many`),不逐格往返; - 结果按 `(state_id, 世代)` 缓存 —— 因为属性只依赖 state,不依赖坐标。 """ from __future__ import annotations from typing import Any, Iterable # ---------- 属性来源抽象 ---------- class SolidSource: """「某格是否实心 / 可挖」的来源。 ## 为什么要抽象 - `RegistrySolidSource`:读本机注册表(vanilla 快;**mod 认不出**) - `RuntimeSolidSource`:问运行中的官方 JVM(**含 mod**,准但要 JVM) 寻路只依赖 `is_solid(state_id) / destroy_speed(state_id)`, 所以两个来源可以互换 —— 差别只在**覆盖率**与**成本**。 """ def is_solid(self, state_id: int) -> bool | None: """None = 不知道(**不当成空气**)。""" raise NotImplementedError def destroy_speed(self, state_id: int) -> float | None: """挖方块速度(0 = 挖不动,如基岩)。None = 不知道。""" return None def name_of(self, state_id: int) -> str | None: return None @property def source(self) -> str: return "abstract" def coverage_note(self) -> str: return "" class RegistrySolidSource(SolidSource): """本机注册表(vanilla `blocks.json`)。快,但**没有 mod**。""" def __init__(self, registry): self._reg = registry def is_solid(self, state_id: int) -> bool | None: if self._reg is None: return None return self._reg.is_solid(state_id) def destroy_speed(self, state_id: int) -> float | None: reg = self._reg if reg is None: return None b = reg.by_name.get(reg.name_of(state_id) or "") if not b: return None return b.get("hardness") def name_of(self, state_id: int) -> str | None: if self._reg is None: return None return self._reg.name_of(state_id) @property def source(self) -> str: return "registry" def coverage_note(self) -> str: if self._reg is None: return "没有注册表 ⇒ 一切判定都是「不知道」" return (f"本机注册表 {len(self._reg.blocks)} 个方块 / " f"maxStateId {self._reg.max_state_id};" "**不含 mod 方块** ⇒ mod 的 state id 一律「不知道」") class RuntimeSolidSource(SolidSource): """问运行中的官方 JVM(`blockinfo` 已经读运行时注册表,含 mod)。 ## 前提 需要一个活着的 `WorldRenderer`(JVM 已在跑)。这是**按需**的: 细渲染本来就常在用(M8/M9),所以边际成本很低; 但单独为了问属性而起 JVM 就不划算 ⇒ 优先用 `RegistrySolidSource`。 ## 为什么「问」比「表」好 - 覆盖 100%(含所有 mod、所有版本) - 属性由游戏自己的注册表给出,与实际碰撞**一致** - 不用维护表、不用猜 """ def __init__(self, renderer, *, cache: dict[int, dict[str, Any]] | None = None): self._r = renderer self._cache: dict[int, dict[str, Any]] = cache if cache is not None else {} self.queried = 0 def _info(self, x: int, y: int, z: int) -> dict[str, Any] | None: try: return self._r.get_block_info(x, y, z) except Exception: return None def probe(self, x: int, y: int, z: int) -> dict[str, Any]: """问一格(结果按 state_id 缓存 —— 属性只依赖 state)。""" info = self._info(x, y, z) if not info: return {} if not info.get("known"): # 未加载:这是**事实**,不是「不知道属性」 return {"known": False, "reason": info.get("reason", "")} sid = info.get("stateId") if sid is None: return {} hit = self._cache.get(sid) if hit is None: hit = { "known": True, "name": info.get("block"), "isSolid": info.get("isSolid"), "isAir": info.get("isAir"), "destroySpeed": info.get("destroySpeed"), "lightEmission": info.get("lightEmission"), "hasBlockEntity": info.get("hasBlockEntity"), } self._cache[sid] = hit self.queried += 1 return hit def is_solid(self, state_id: int) -> bool | None: hit = self._cache.get(state_id) return None if hit is None else hit.get("isSolid") def destroy_speed(self, state_id: int) -> float | None: hit = self._cache.get(state_id) return None if hit is None else hit.get("destroySpeed") def name_of(self, state_id: int) -> str | None: hit = self._cache.get(state_id) return None if hit is None else hit.get("name") @property def source(self) -> str: return "runtime" def coverage_note(self) -> str: return (f"运行时注册表(已探测 {self.queried} 个 state," f"缓存 {len(self._cache)} 个)⇒ **含 mod**,覆盖 100%") # ---------- 批量探测 ---------- def attach_runtime( world, source: RuntimeSolidSource, positions: Iterable[tuple[int, int, int]], ) -> dict[str, Any]: """把一批坐标的属性问出来,写进 `world` 的 `_solid_cache`。 ★ **批量**是能不能用的关键:一次问一格要一次 NDJSON 往返, 问一万格就一万次往返。批量问完再统一查表,快得多。 """ cache = getattr(world, "solid_cache", None) if cache is None: cache = {} world.solid_cache = cache # type: ignore[attr-defined] ok = miss = 0 for (x, y, z) in positions: hit = source.probe(x, y, z) sid = None try: sid = world.block_at(x, y, z)["state_id"] except Exception: pass if hit.get("known") and sid is not None: cache[sid] = hit ok += 1 else: miss += 1 return {"probed": ok, "unknown": miss, "cached_states": len(cache), "source": source.source}