新增 mcfold/actions.py(16 个操作:look/move/move_and_look/look_at/ jump/sneak/sprint/select_slot/use_item/swing/dig/place/interact_block/ attack/use_entity/interact_entity_at + chat/command) 新增 mcfold/textcomp.py(1.20.3+ 的 NBT 文本组件渲染,查 language.json) ★★ 最关键发现:teleport_confirm 是必需前置包 1.19+ 的 position 带 teleportId,必须回 confirm,否则服务端**默默忽略所有方块操作**。 症状是「操作没反应、零报错」,与权限/物品/位置问题无法区分。 定位过程:先猜了 5 轮(包名/字段顺序/face 类型/sequence/位置与面)都不对; 最后拿 minecraft-protocol 对照,发现它也失败 ⇒ 排除我的包有问题; 再用 data get entity 排除权限与物品 ⇒ 补 confirm 后立刻成功。 其他真 bug: - packet_interact/swing/animation 在 1.21.1 都不存在(应是 use_entity/arm_animation) - dig 原写成 block_place + 多余 varint(应为独立 packet_block_dig)→ 断连 - use_entity(mouse=0) 也带 hand 字段,我漏了 → 少 1 字节 - 背包字段是 itemId(驼峰),我写 item_id → 永远判「空手」 - window0 的 slot 36..44 才是热键栏 - 聊天 NBT 组件三重漏(空串/键名/空键参数)→ 现在正确渲染人话 判据:126 项全通过;5 项变异全被抓。核心判据「16 个包能被官方 protodef 精确解回(0 余字节)」—— 唯一能离线证明「包写对了」的办法
145 lines
5.0 KiB
Python
145 lines
5.0 KiB
Python
"""1.20.3+ 的聊天是 **NBT 文本组件**(不是纯字符串)。
|
||
|
||
## 为什么值得单独一个模块
|
||
|
||
服务端回的 `system_chat` 里 `content` 是一棵组件树:
|
||
|
||
```json
|
||
{"translate": "commands.give.success.single",
|
||
"with": [{"": 1}, {"...": "block.minecraft.dirt"}, {"...": "mcfold_act"}]}
|
||
```
|
||
|
||
我先前只取 `text` 字段 ⇒ 拿到 `""`(空串),
|
||
agent 看到的聊天全是空的 —— **看起来「服务端没回话」,其实是解析漏了**。
|
||
再往前一步,我连 `translate` 都只当字符串原样吐出
|
||
(`commands.give.success.single`),agent 读到的不是人话。
|
||
|
||
## 本模块做两件事
|
||
|
||
1. **渲染**:把组件树摊平成可读文本(`Gave 1 Dirt to mcfold_act`)
|
||
2. **诚实降级**:取不到就**说取不到**(保留原值),绝不返回空串 ——
|
||
空串会让 agent 以为「什么也没发生」。
|
||
|
||
## 翻译表
|
||
|
||
`minecraft-data` 的 `language.json`(6881 条)**本机就有**(客户端 jar 也带),
|
||
所以 `translate` 键可以直接查表变人话,不必联网。
|
||
查不到时保留键名并标注 `⟨?⟩` —— 键名本身仍有诊断价值。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import functools
|
||
import json
|
||
import os
|
||
from typing import Any
|
||
|
||
|
||
LANG_ROOT = ('/home/newqqagent/plugin_data/mc/runtime/node_modules/'
|
||
'minecraft-data/minecraft-data/data/pc')
|
||
|
||
|
||
@functools.lru_cache(maxsize=8)
|
||
def _lang(version: str) -> dict[str, str]:
|
||
p = os.path.join(LANG_ROOT, version, "language.json")
|
||
if not os.path.exists(p):
|
||
return {}
|
||
with open(p, "r", encoding="utf-8") as f:
|
||
return json.load(f)
|
||
|
||
|
||
def render_text(comp: Any, version: str = "1.21.1", depth: int = 0) -> str:
|
||
"""把 NBT 文本组件渲染成可读字符串。
|
||
|
||
★ 永不返回空串来「代替」看不懂的内容:
|
||
看不懂就保留原文/键名,让 agent 知道有东西但我们没解析出来。
|
||
"""
|
||
if depth > 12:
|
||
return "⟨嵌套过深⟩"
|
||
if comp is None:
|
||
return ""
|
||
if isinstance(comp, str):
|
||
return comp
|
||
if isinstance(comp, list):
|
||
return "".join(render_text(c, version, depth + 1) for c in comp)
|
||
if not isinstance(comp, dict):
|
||
return str(comp)
|
||
|
||
lang = _lang(version)
|
||
out = ""
|
||
|
||
# 1) translate:查语言表,并把 with 里的参数按 %s 填回去
|
||
key = comp.get("translate")
|
||
if isinstance(key, str):
|
||
tmpl = lang.get(key)
|
||
if tmpl is None:
|
||
# 查不到模板 ⇒ 保留键名(比丢掉强:键名能定位问题)
|
||
out = f"⟨{key}⟩"
|
||
else:
|
||
args = comp.get("with")
|
||
if isinstance(args, list):
|
||
vals = [render_text(a, version, depth + 1) for a in args]
|
||
try:
|
||
out = tmpl % tuple(vals) if vals else tmpl
|
||
except (TypeError, ValueError):
|
||
# 占位符个数对不上时不要崩,如实拼接
|
||
out = tmpl + " " + " ".join(vals)
|
||
else:
|
||
out = tmpl
|
||
|
||
# 2) text:字面量
|
||
elif "text" in comp:
|
||
out = str(comp.get("text") or "")
|
||
|
||
# 2b) 空键 "":1.20.3+ 的 NBT 里,简单参数/数字就用空字符串作键
|
||
# (我用 {"": 1} 的形状存 1)。不读它就会把 %s 填成空,
|
||
# 表现为「Gave Dirt to 」——看着像翻译失败,其实是参数没取到。
|
||
elif "" in comp:
|
||
out = str(comp.get(""))
|
||
|
||
# 3) 没有文本意图,但有 hoverEvent.contents.id ⇒ 物品/实体名
|
||
elif "hoverEvent" in comp:
|
||
hv = comp.get("hoverEvent") or {}
|
||
c = hv.get("contents") or {}
|
||
ident = c.get("id") or c.get("value")
|
||
if isinstance(ident, str):
|
||
out = _humanize(ident)
|
||
else:
|
||
out = ""
|
||
|
||
# 4) 兜底:有子节点就拼子节点(如上面的 hoverEvent+with 形态)
|
||
else:
|
||
out = ""
|
||
|
||
# extra / with 里的子组件接在后面(vanilla 的拼接规则)
|
||
for key2 in ("extra",):
|
||
kids = comp.get(key2)
|
||
if isinstance(kids, list):
|
||
out += "".join(render_text(k, version, depth + 1) for k in kids)
|
||
|
||
return out
|
||
|
||
|
||
def _humanize(ident: str) -> str:
|
||
"""`minecraft:stone` → `Stone`;`item.minecraft.dirt` → `Dirt`。"""
|
||
s = ident.split(":")[-1]
|
||
for pre in ("block.", "item.", "entity.", "item.minecraft."):
|
||
if s.startswith(pre):
|
||
s = s[len(pre):]
|
||
break
|
||
s = s.replace("_", " ").strip()
|
||
return s[:1].upper() + s[1:] if s else ident
|
||
|
||
|
||
def flatten_with(args: Any, version: str = "1.21.1") -> list[str]:
|
||
"""把 translate 的 `with` 参数列表摊平成字符串列表。
|
||
|
||
★ 单独一个函数是因为这几步容易出隐蔽错:
|
||
1. 数字参数存成 `{"": 1}`(空键)
|
||
2. 物品参数是 `{"translate": "block.minecraft.dirt"}`
|
||
3. 也可能直接是字符串
|
||
我先前把 1 漏了 ⇒ 语言模板的 %s 全填成空。
|
||
"""
|
||
if not isinstance(args, list):
|
||
return []
|
||
return [render_text(a, version, 1) for a in args]
|