Files
agentmc/mcfold/textcomp.py
JianFeeeee 1a6f197572 M5:操作层(真能改世界)+ 16 项判据
新增 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 余字节)」—— 唯一能离线证明「包写对了」的办法
2026-09-26 12:44:08 +08:00

145 lines
5.0 KiB
Python
Raw 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.

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