马鞍

马鞍

马鞍:一款用于实时编辑的数据包调试器

马鞍

马鞍:一个用于实时编辑的数据包调试器

一款 Fabric 模组,在 Minecraft 中嵌入了调试适配器协议服务器,使得数据包 .mcfunction 文件可以从 VS Code(或任何 DAP 客户端)进行调试——支持断点、单步执行、时间回溯,以及在函数运行时所处游戏状态的实时检查与编辑。

在 Minecraft 中,马鞍能让你驾驭原本自行其是的生物。这个模组则为数据包的执行装上了马鞍:在你想要的地方停下它(断点),让它后退(时间回溯),以及引导它(实时编辑)——而不是看着它狂奔而过。

  • Minecraft 26.2 / Fabric Loader ≥ 0.19.0 / Fabric API
  • Java 25(26.x 版本是未混淆的;构建脚本使用了无重映射的 net.fabricmc.fabric-loom 插件)
  • DAP 端点:127.0.0.1:16352——可通过 -Dsaddle.host / -Dsaddle.port 覆盖。除非设置了 -Dsaddle.allowRemote=true,否则拒绝非回环地址的绑定:调试端口允许未经身份验证的命令执行,因此只在受信任或隧道网络上暴露它。

快速开始

  1. 安装模组(以及 Fabric API)并启动一个世界或服务器。

  2. 从 Marketplace 安装 VS Code 扩展 Saddle — Minecraft Datapack Debugger(或在扩展视图中搜索 "Saddle")。如果要从源码构建,请执行:

       cd vscode-extension
       npx --yes @vscode/vsce package --allow-missing-repository
       code --install-extension saddle-debug-*.vsix
    
  3. 在 VS Code 中打开你的数据包文件夹,在 .mcfunction 文件中设置断点,然后运行 Attach to Minecraft (Saddle)(或按 F5)。调试控制台和游戏聊天都会告知你附加到了哪个世界。

  4. 在游戏中触发一个函数(通过 tick 函数或直接在调试控制台中触发)。

详细的编辑器端功能导览,请参阅 vscode-extension/README.md。

工作原理

  • 一个混入到 CommandFunction.fromLines 中的代码,会将每个解析后的命令入口包裹在一个带有其 (函数 ID, 源码行) 来源的装饰器中,并记录函数源码文本。宏($...)行在解析时被标记,并在 MacroFunction 实例化它们时被包裹,因此它们也是可调试的。当没有调试客户端附加时,每条命令的运行时开销仅为一次易变读取。
  • 在命中断点/单步执行/暂停时,服务器线程会在命令执行内部暂停——游戏在保留原版执行状态的情况下在函数中间冻结。暂停期间,线程会处理一个任务队列,因此需要游戏状态(变量、求值、内省)的 DAP 请求仍然可以在服务器线程上安全运行。
  • 断点路径会根据文件路径中的 data/<命名空间>/function/<路径>.mcfunction 段映射到函数 ID,因此任何工作区布局都可以无需配置地工作。在注释行或空行上设置的断点会向下移动到下一个可执行行。

DAP 支持

区域 请求
生命周期 initialize,launch/attach,configurationDone,disconnect
断点 setBreakpoints,breakpointLocations——支持普通命令和宏行;注释/空行会移动到下一个可执行行
执行 continue,next,stepIn,stepOut,pause
状态 threads,stackTrace,scopes,variables,setVariable,source
控制台 evaluate——执行任意命令,异步响应,因此命中断点的命令不会延迟堆栈视图;暂停时,它在隔离的 ExecutionContext 中执行。completions 提供 Brigadier 建议,因此调试控制台可以像游戏内聊天一样自动补全
悬停 evaluate(context: hover) 无需执行命令即可解析宏参数($(name))、实体选择器(@e[...])和坐标三元组(~ ~2 ~)
输出 游戏内聊天内容会作为 output 事件镜像到客户端:系统广播(/say、死亡、加入)、玩家聊天以及针对单个玩家的消息(/tellraw、/msg)
时间回溯 stepBack / reverseContinue 导航已执行命令的记录(环形缓冲区,-Dsaddle.ttd.steps,默认 20000)。在回溯时,会显示该时刻的堆栈、执行器、宏参数以及重建的记分板/存储状态——作用域会保留其实时名称,因此在当前和历史之间移动时,展开的行不会丢失,执行器作用域会带有一个 (time travel) 标记。向前单步会重播记录直到回到当前;continue 越过记录后会恢复实时执行。saddle/trace 返回最近的执行跟踪记录

变量(“寄存器与内存”)

每个栈帧在停止时都会暴露实时作用域,所有作用域都读取游戏状态:

  • 执行器 — 命令来源摘要(执行者、位置、旋转、维度)以及执行实体可编辑的 NBT 树的惰性展开。
  • 宏参数 — 当前宏帧的 $(...) 值。
  • 命令 — 即将运行的命令,每个实体选择器都解析为它当前匹配的实体(每个实体都可展开为实时 NBT),每个坐标三元组都解析为它所指向的方块。
  • 监视 — 用户固定的表达式(见下文),每次请求时都会重新实时解析。
  • 记分板 — 每个记分项及其分数;分数值可通过 setVariable 编辑。
  • 存储 — 每个命令存储 ID 作为一个可编辑的 NBT 树;叶节点值通过 setVariable 接受 SNBT。容器预览基于大小({400 条目}),因此浏览大型存储仍然很快;编辑会触发一个 invalidated 事件,以便客户端重新获取过时的行。

在 .mcfunction 文件中暂停时,悬停在 $(name)、@e[...] 或 1 2 3/~ ~2 ~ 上会内联显示相同的数据(通过 VS Code 扩展)。

监视与固定表达式

VS Code 的 WATCH 面板、固定的“监视”作用域(saddle/pin、saddle/unpin、saddle/pins)以及 Saddle Watch 视图都接受:

  • @e[type=pig] — 匹配的实体,可展开为实时 NBT
  • storage <id> [path] / entity <uuid> [path] / block <x> <y> <z> [path] — 目标处的实时(可编辑)NBT
  • score <objective> [holder] — 单个分数,或省略持有者时显示整个记分项(可编辑)
  • scoreboard / storage — 所有记分项 / 所有存储 ID
  • $(name) — 选定帧的宏参数;单独的坐标三元组解析为它们指向的方块

Saddle Watch(实时 + 可编辑,无需断点)

该扩展在“运行和调试”侧边栏中添加了一个 Saddle Watch 视图——一个集成了 WATCH 和 Variables 功能的监视面板,无需断点:

  • 实时:固定的表达式通过一个计时器(saddle.liveWatchRefreshInterval,默认 1 秒)刷新,通过无状态的 saddle/live {expression, path} 请求实现,该请求无论游戏是在运行还是暂停,都会在服务器线程上读取游戏状态——记分板实时增加,实体位置移动,存储随函数写入而更新。
  • 可编辑:由分数或 NBT 支持的行会显示一个内联铅笔图标;编辑通过 saddle/liveSet {expression, path, name, value} 进行并立即应用到实时游戏。
  • 原生样式:该视图使用 VS Code 自身的调试令牌主题渲染(等宽行,名称、数字、字符串使用 debugTokenExpression.* 颜色),因此看起来与 Variables/WATCH 面板完全相同。

为了只保留一个监视面板,可以隐藏内置面板:右键单击“运行和调试”侧边栏中的任意节标题,然后取消勾选 Watch——VS Code 会记住。(扩展无法移除或替换内置视图,并且内置的 WATCH 仅在调试器停止时重新求值;这两者都是平台限制。将 Saddle Watch 拖到 WATCH 原来的位置,布局也会固定。)

自定义请求

  • minecraft/getScoreboard,minecraft/setScore {objective, holder, value}
  • minecraft/getStorage {id?, path?},minecraft/listEntities {selector},minecraft/getEntity {uuid}(包含 NBT)
  • minecraft/getData / minecraft/setData {type: storage|entity|block, target, path, value} — 原版 /data 风格的访问;方块目标为 "x y z [dimension]"
  • minecraft/getBlock {pos, dimension?} — 方块状态加上方块实体 NBT

测试

# 仅首次:接受 EULA 并禁用 tick 看门狗
mkdir -p run
printf 'eula=true\n' > run/eula.txt
printf 'max-tick-time=-1\nonline-mode=false\npause-when-empty-seconds=0\n' > run/server.properties

./gradlew runServer                 # 终端 1
python3 scripts/dap_smoke_test.py   # 终端 2

该脚本会将 scripts/test-datapack 安装到世界中,执行 /reload,并端到端地演练完整的调试循环(108 项检查):断点、单步执行、时间回溯(后退/反向继续/历史状态重建)、宏断点和宏参数值、注释行偏移、实时变量读/写(记分板、存储 NBT、实体 NBT)、选择器/坐标解析、悬停求值、控制台补全、聊天输出镜像、实体/方块数据请求、求值与暂停。CI 在真实的专用服务器上运行相同的测试套件,推送 v* 标签会将 jar 和 vsix 发布为 GitHub Release。

注意事项

  • 保持模组和 VS Code 扩展的版本匹配:模组在附加时会发送一个 saddle/version 事件,当两者版本(主版本.次版本)不一致时扩展会发出警告。
  • 在专用服务器上设置 max-tick-time=-1:在断点处暂停会挂起服务器线程,否则会触发看门狗。
  • 在单人游戏中,如果游戏长时间保持暂停状态,内部客户端可能会断开连接。
  • 单步执行超过所有排队的命令的末尾后,会话将保持运行状态,直到下一次命中断点/暂停(例如,下一个 tick 函数命令)。

故障排除

  • 编辑似乎未生效 / data get 显示旧值。 首先检查附加公告:附加时,Saddle 会在调试控制台中打印 Saddle: debugger attached to world '…',并在游戏聊天中打印 [Saddle] Debugger attached。如果聊天消息没有出现在你的世界中,说明另一个拥有 Saddle 的 Minecraft 实例占用了端口(其日志会显示 Failed to bind DAP server),并且你的编辑落在了那个实例中——关闭它或使用 -Dsaddle.port 来区分它们。另请注意,如果一个 tick 函数在每个 tick 都重写记分板/存储,那么在你恢复后,手动编辑会立即被覆盖。
  • Watched/Storage 作用域仅编辑实时数据。在时间回溯时,同名的作用域显示记录的值且为只读——执行器作用域中的 (time travel) 行会告诉你正在查看哪种模式。