cortico 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +4 -11
  2. package/package.json +1 -3
  3. package/src/bot.ts +1 -1
  4. package/src/core/README.md +0 -4
  5. package/src/core/types.ts +1 -1
  6. package/src/core/util.ts +2 -1
  7. package/src/deploy.ts +8 -6
  8. package/src/providers/README.md +4 -5
  9. package/src/providers/base.ts +1 -1
  10. package/src/providers/configuration.ts +2 -1
  11. package/src/providers/console/settings.ts +8 -10
  12. package/src/providers/llamacpp/console/runtime-panel.ts +73 -32
  13. package/src/providers/llamacpp/console/server.ts +1 -20
  14. package/src/providers/llamacpp/index.ts +0 -2
  15. package/src/providers/llamacpp/options.ts +0 -2
  16. package/src/providers/llamacpp/strings.ts +6 -6
  17. package/src/providers/openai-responses-compat/index.ts +0 -3
  18. package/src/web/README.md +1 -5
  19. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +35 -41
  20. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +4 -4
  21. package/src/web/client/console-pages/host.ts +2 -2
  22. package/src/web/client/features/core/strings.ts +2 -2
  23. package/src/web/client/features/feature.ts +1 -1
  24. package/src/web/client/features/live/index.ts +8 -1
  25. package/src/web/client/features/live/onboarding.ts +4 -1
  26. package/src/web/client/features/live/strings.ts +11 -11
  27. package/src/web/client/features/live/timeline.ts +2 -1
  28. package/src/web/client/features/settings/general.ts +1 -0
  29. package/src/web/client/features/settings/index.ts +1 -0
  30. package/src/web/client/features/worlds/index.ts +1 -0
  31. package/src/web/client/shell/index.ts +27 -47
  32. package/src/web/client/shell/strings.ts +0 -22
  33. package/src/web/client/ui/icons.ts +5 -0
  34. package/src/web/console-pages.ts +1 -1
  35. package/src/web/public/styles.css +21 -8
  36. package/src/web/server.ts +11 -3
  37. package/src/web/shared/client-panel.ts +1 -1
  38. package/src/web/shared/console-protocol.ts +2 -1
  39. package/src/worlds/minecraft/README.md +61 -11
  40. package/src/worlds/minecraft/cell-facts.ts +226 -0
  41. package/src/worlds/minecraft/chests.ts +5 -0
  42. package/src/worlds/minecraft/containers.ts +325 -0
  43. package/src/worlds/minecraft/entity-facts.ts +31 -5
  44. package/src/worlds/minecraft/executor.ts +283 -10348
  45. package/src/worlds/minecraft/inventory.ts +268 -0
  46. package/src/worlds/minecraft/melee.ts +419 -0
  47. package/src/worlds/minecraft/placed-ledger.ts +152 -0
  48. package/src/worlds/minecraft/placement.ts +1038 -0
  49. package/src/worlds/minecraft/receipt.ts +340 -0
  50. package/src/worlds/minecraft/skill-context.ts +358 -0
  51. package/src/worlds/minecraft/skills-build.ts +1169 -0
  52. package/src/worlds/minecraft/skills-container.ts +1343 -0
  53. package/src/worlds/minecraft/skills-craft.ts +333 -0
  54. package/src/worlds/minecraft/skills-dig.ts +624 -0
  55. package/src/worlds/minecraft/skills-gather.ts +1230 -0
  56. package/src/worlds/minecraft/skills-interact.ts +1559 -0
  57. package/src/worlds/minecraft/tools.ts +331 -0
  58. package/src/worlds/minecraft/travel.ts +763 -0
  59. package/src/worlds/minecraft/until.ts +75 -0
  60. package/src/worlds/terminal/world.ts +8 -5
  61. package/src/providers/console/config.ts +0 -21
  62. package/src/providers/llamacpp/config.ts +0 -36
  63. package/src/providers/openai-responses-compat/config.ts +0 -20
@@ -135,9 +135,21 @@
135
135
  .navdot.warn { background: var(--warn); }
136
136
  .navdot.bad { background: var(--danger); }
137
137
 
138
- .railfoot { padding: 10px 16px; border-top: 1px solid var(--line); font-size: 11.5px; color: var(--ink-dim); display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
139
- .rail-who { display: flex; align-items: center; gap: 9px; min-width: 0; }
140
- .rail-name { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; color: var(--ink); }
138
+ /* 头像、名字、那几个键在同一行:名字吃掉中间的余量,放不下就省略号。 */
139
+ .railfoot { padding: 10px 12px; border-top: 1px solid var(--line); font-size: 11.5px; color: var(--ink-dim); display: flex; align-items: center; gap: 8px; }
140
+ .rail-who { display: flex; align-items: center; gap: 9px; min-width: 0; flex: 1; }
141
+ .rail-name {
142
+ display: flex; align-items: center; gap: 5px; min-width: 0;
143
+ padding: 3px 6px; border: 0; border-radius: 7px; background: transparent;
144
+ font: 600 13px/1.35 var(--sans); color: var(--ink); cursor: pointer; text-align: left;
145
+ }
146
+ .rail-name-text { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
147
+ /* 铅笔平时不占视线,悬停/聚焦才露出来——名字可点这件事只在那时才需要说。 */
148
+ .rail-name-edit { display: grid; place-items: center; flex: none; opacity: 0; transition: opacity .15s ease; }
149
+ .rail-name-edit .icon { width: 13px; height: 13px; }
150
+ .rail-name:hover { background: var(--sheet-2); }
151
+ .rail-name:hover .rail-name-edit, .rail-name:focus-visible .rail-name-edit { opacity: .75; }
152
+ .rail-rename { flex: 1; min-width: 0; }
141
153
 
142
154
 
143
155
  .pagehead {
@@ -887,7 +899,7 @@
887
899
  background: color-mix(in srgb, var(--border) 84%, transparent);
888
900
  }
889
901
  .navdot { width: 6px; height: 6px; }
890
- .railfoot { min-height: 62px; padding: 10px 16px 14px; border: 0; gap: 9px; }
902
+ .railfoot { min-height: 62px; padding: 10px 12px 14px; border: 0; gap: 8px; }
891
903
  .rail-avatar-wrap {
892
904
  display: grid;
893
905
  flex: none;
@@ -924,11 +936,12 @@
924
936
  }
925
937
  .rail-avatar-edit .icon { width: 15px; height: 15px; }
926
938
  .rail-avatar:hover .rail-avatar-edit { opacity: 1; }
927
- .rail-actions { display: flex; gap: 3px; }
928
- .rail-name { color: var(--foreground); }
939
+ .rail-actions { display: flex; flex: none; gap: 2px; }
940
+ .rail-name { color: var(--foreground); font: 600 13.5px/1.35 var(--sans); }
941
+ .rail-name:hover { background: var(--hover); }
929
942
  .rail-action {
930
- width: 34px;
931
- height: 34px;
943
+ width: 30px;
944
+ height: 30px;
932
945
  display: grid;
933
946
  place-items: center;
934
947
  padding: 0;
package/src/web/server.ts CHANGED
@@ -441,13 +441,16 @@ export interface ConsoleSurface {
441
441
  toolSchemas?: WebAppToolSchemasDeps;
442
442
  /** 当前 session 的非破坏性前缀重载。 */
443
443
  sessionControl?: WebAppSessionControlDeps;
444
+ /**
445
+ * 可选的运行控制,未挂载时 /api/run/* 返回 503;暂停期间事件仍落库排队。
446
+ * shutdown 按装配层顺序停止投递、IO、托管 LLM server 并落盘,各步有时间预算并返回结果;未提供时不显示关机键,进程退出由装配层决定。
447
+ */
444
448
  /** 开场引导的一次性标记;缺席时控制台不给引导。 */
445
449
  onboarding?: {
446
450
  /** 删除标记;已经删过时不报错。 */
447
451
  dismiss(): void;
448
452
  };
449
453
 
450
- /** 运行控制缺席时 /api/run/* 返回 503;暂停期间事件仍落库排队。关机顺序与进程退出由装配层决定。 */
451
454
  run?: {
452
455
  pause(): void;
453
456
  resume(): void;
@@ -469,7 +472,9 @@ export interface ConsoleSurface {
469
472
  * 不挂载时 /api/console/manifest 返回一份只有 framework 能力的空 manifest。
470
473
  */
471
474
  consolePageSources?: () => ConsolePageSource[];
472
- /** 所有 provider 模块的本地可用性汇总;缺席时省略该状态。 */
475
+ /**
476
+ * 有没有一个可用的端点。缺省不挂:「语言模型」那一行不点灯。
477
+ */
473
478
  providersLamp?: (language: Language) => ConsoleLamp;
474
479
  /**
475
480
  * 浏览器端构建产物目录(含 asset-manifest.json)。缺省取仓库的 `dist/web`。
@@ -559,7 +564,9 @@ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
559
564
  const WILDCARD_HOSTS = new Set(['0.0.0.0', '::', '[::]']);
560
565
 
561
566
  /**
562
- * Host 头必须是回环名或显式绑定的地址;绑定通配地址时不校验 Host。
567
+ * Host 头必须是回环名或显式绑定的地址。DNS rebinding 让外站页面的 Origin 与 Host 同名,
568
+ * 同源闸门因此失效;这一关把请求钉在浏览器真正访问的名字上。绑到通配地址即操作员
569
+ * 已选择对外露面,不校验。
563
570
  */
564
571
  function isAllowedHost(hostHeader: unknown, listenHost: string): boolean {
565
572
  if (WILDCARD_HOSTS.has(listenHost)) return true;
@@ -1387,6 +1394,7 @@ export class WebApp {
1387
1394
  })();
1388
1395
  });
1389
1396
 
1397
+ // 开场引导只出现一次:控制台在操作员开口或按下那颗按钮时销掉标记。
1390
1398
  app.post('/api/onboarding/dismiss', wrap((_req, res) => {
1391
1399
  const src = this.deps.onboarding;
1392
1400
  if (!src) { res.status(503).json({ error: '开场引导标记不可用' }); return; }
@@ -440,7 +440,7 @@ export interface ConsolePromptInput {
440
440
  el: HTMLDivElement;
441
441
  focus(): void;
442
442
  setDisabled(disabled: boolean): void;
443
- /** 设置输入框占位文本;`null` 恢复初值。 */
443
+ /** 盖住输入框里的灰字。`null` 回到构造时给的那一句。 */
444
444
  setPlaceholder(text: string | null): void;
445
445
  }
446
446
 
@@ -401,7 +401,8 @@ export interface ConsoleLampsResponse {
401
401
  }
402
402
 
403
403
  /**
404
- * 框架的 provider 状态灯键,与贡献页 id 使用不同的 kind。
404
+ * 「语言模型」那一行的灯在灯表里的键。贡献方的 page id 是 `<kind>:<id>`,
405
+ * kind 取 world / llm / persona,因此这个键与谁都不撞。
405
406
  */
406
407
  export const PROVIDERS_LAMP_ID = 'framework:providers';
407
408
 
@@ -8,20 +8,70 @@
8
8
  ## 结构
9
9
 
10
10
  ```
11
- world.ts World:事件面、工具面、console 自报、世界变化节流、摄像机编排
11
+ definition.ts / config.ts / world.ts World 定义、配置段与 World 本体(事件面、工具面、
12
+ console 自报、世界变化节流、摄像机编排、PWSR 三张表)
13
+ proxy.ts / engine-child.ts / engine-ipc.ts 主进程代理、引擎子进程与两者之间的协议
14
+
15
+ 连上去
12
16
  bridge.ts mineflayer 连接/断线重连/prismarine-viewer 拉起
13
17
  mineflayer-fixes.ts 把 mineflayer 里靠客户端预测的合成与放置换成等服务端回话的版本
14
- world.ts 纯逻辑:世界快照 → 文本渲染、变化检测(可直接单测)
15
- server-config.ts server.properties 与 ops.json 读写 + 存档目录枚举(存档与玩法/权限与作弊面板)
16
- executor.ts 异步执行器:任务队列 + 技能实现 + 自保反射(不经 LLM)
18
+ pathfinder-perf.ts 寻路器的代价与门开关补丁(另见 patches/ 里的 upstream 补丁)
19
+ pathfinder-lib.d.ts 寻路器没带的类型声明
20
+ server.ts / server-config.ts 本地服务端一键启停;server.properties 与 ops.json 读写、
21
+ 存档目录枚举(存档与玩法/权限与作弊面板)
22
+ client.ts / window.ts / client-launch.ts / client-options.ts / client-skins.ts
23
+ 观察者客户端:拉起、等窗口、改标题、.minecraft → java 命令行、皮肤
24
+ level-dat.ts 读存档自述(世界名、种子、生成参数)
25
+
26
+ 读世界
27
+ terrain.ts 世界快照 → 文本渲染与变化检测(纯逻辑,可直接单测)
28
+ cell-facts.ts 格的读法:锚点落到哪一格、脚下与参照面、区域扫描、方块名指纹
29
+ geometry.ts 形状与锚点的纯几何:解析、栅格化
30
+ item-facts.ts / entity-facts.ts / item-pick.ts / item-break.ts / piglin.ts
31
+ 物品与实体的事实读法
32
+ names.ts 方块/实体/生物群系/附魔/效果的中文名表
33
+
34
+ 技能
17
35
  skills.ts 技能注册表:SkillCall/parseSteps/SKILL_DOC/schema 全部由一份声明生成
36
+ executor.ts 任务队列、逐步派发(runSkill)与自保反射(不经 LLM)
37
+ skill-context.ts 技能执行的共享契约:上下文、两种终态异常、World 交给技能的只读取用口
38
+ skills-gather.ts collect / find / fish / trade / probe
39
+ skills-build.ts build 的两种形态与蓝图施工,含上岸
40
+ skills-dig.ts excavate / tunnel
41
+ skills-craft.ts craft / eat / equip
42
+ skills-interact.ts use 的各种形态 + ride / lead / anvil / grindstone
43
+ skills-container.ts pickup / toss / stow / take / smelt / enchant / brew / transit
44
+ travel.ts 寻路一次的时限与卡死判据、寻路目标归属、走不通时的试算现场
45
+ placement.ts 贴哪一面、站哪儿放得着、放完回读确认、补光与耗材许可、工作站
46
+ containers.ts 容器与工作站窗口:找、开得稳、槽位排序、账本登记
47
+ inventory.ts 背包读法与放置/合成/拾取后的确认等待
48
+ tools.ts 选具:拿哪把、够不够级、快断了没有、reserve 被迫动用的记号
49
+ melee.ts 挑兵器、挥击与冷却、远程接管判据、附近敌对生物读数,以及 attack
50
+ until.ts find 与 tunnel 共用的 `until` 早停名单
51
+ receipt.ts 回执措辞:这一步是什么、受阻现场、核验结论怎么念
52
+ precheck.ts 前置试算:受理与出队两刻的纯读判据
53
+
54
+ 战斗与自保
55
+ combat.ts / combat-context.ts / ranged.ts 被动交战、方位场、弓与投掷物
56
+ escape.ts 脱困与回生:重生点解析、tp 回执
57
+ body-lease.ts 身体租约仲裁:谁在什么时候占着身体
58
+
59
+ 蓝图
60
+ blueprint.ts 方块状态规范化与校验
61
+ blueprint-plan.ts 编译成施工步序、账单、回读对账
62
+ blueprint-registry.ts 原版方块状态与放置方式的对照
63
+ blueprint-repair.ts / blueprint-resource.ts 提交修复与版本/作业登记
64
+
65
+ 账本与规矩
66
+ policy.ts mc_policy 六格常驻规矩,五格落盘
67
+ goal-plan.ts PWSR 目标计划的协调与判定
18
68
  check.ts mc_check 的断言器:受理 → 对世界求值 → 只报差异的回执(纯函数)
19
- client.ts 观察者客户端进程:拉起、等窗口、收干净
20
- window.ts 客户端那扇窗:按进程号探测有没有出来、改标题(+ client-window.ps1)
21
- client-launch.ts 纯计算:.minecraft 安装 → java 命令行(认 inheritsFrom)
22
- client-skins.ts 皮肤:选中的那张留底,启动前按账号名铺进游戏目录
23
- server.ts 控制台一键启停:本地 MC 服务器(java -jar server.jar)
24
- log.ts World 日志转发与面板环形缓冲
69
+ chests.ts / works.ts / explored.ts / deaths.ts 容器、成果、探索覆盖、死亡的持久账
70
+ placed-ledger.ts 挂在 bot 实例上的小账:这一场放过、锄过、没放上的那些格
71
+ search-observation.ts / round.ts / show.ts / readouts.ts / log.ts
72
+ find 的短期观察、同轮重复查询、容器演出节拍、三份读数、日志转发
73
+
74
+ console/ 控制台面板两侧:服务器、存档、权限、客户端、皮肤、日志
25
75
  (Minecraft 客户端、服务端与启动器放在仓库外,不进 Git)
26
76
  ```
27
77
 
@@ -375,7 +425,7 @@ collect **只挖看得见的**:`collectVisible` 经 `canSeeBlockAt` 检查视
375
425
 
376
426
  `use.face` 接受绝对面名,省略时使用顶面。显式面同时传给 `activateBlock` 和对应的结果核验;读数中的坐标标识实际检查的格子。
377
427
 
378
- ### 锚点几何技能族(geometry.ts + executor.ts)
428
+ ### 锚点几何技能族(geometry.ts + skills-build.ts + skills-dig.ts)
379
429
 
380
430
  **锚点几何语言**由 build/excavate/tunnel 的操作、probe 的测量及 dryRun 的试算共用:
381
431
 
@@ -0,0 +1,226 @@
1
+ /**
2
+ * 格的读法:锚点落到哪一格、脚下与参照面、区域扫描、方块名指纹。
3
+ *
4
+ * 纯读,不改世界也不发包;`geometry.ts` 算形状,这里把形状对到活着的 bot 上。
5
+ */
6
+ import type { Bot } from 'mineflayer';
7
+ import { Vec3 } from 'vec3';
8
+ import {
9
+ BLOCK_FACES, rasterize, resolveAnchors,
10
+ type Anchor, type BlockFace, type BoxFill, type Cell, type ShapeName,
11
+ } from './geometry.ts';
12
+ import { SkillBlocked } from './skill-context.ts';
13
+ import { cropAgeAt } from './terrain.ts';
14
+
15
+ /** 竖井遇到这些方块时停止下挖。 */
16
+ export const LIQUIDS = new Set(['water', 'lava', 'bubble_column']);
17
+
18
+ export const BUILD_CELL_CAP = 256;
19
+ export const EXCAVATE_CELL_CAP = 512;
20
+ export const PROBE_CELL_CAP = 2048;
21
+ /** probe.where 的格数上限;只逐格读取,不生成材质构成或完整逐格报告。 */
22
+ export const PROBE_WHERE_CELL_CAP = 8192;
23
+
24
+ /** 放置的稳妥手长:服务端上限 5.1,留出身位余量 */
25
+ export const PLACE_REACH = 4;
26
+
27
+ /** 一格的六个邻格偏移;与六个面同一批向量,只是这里问的是"周围有没有",不问哪一面 */
28
+ export const NEIGHBORS6 = Object.values(BLOCK_FACES);
29
+
30
+ /** 检查目标格的六个正交相邻格是否有岩浆;不包含斜对角或隔一格的位置。 */
31
+ export function nearLavaAt(bot: Bot, c: Cell): boolean {
32
+ return NEIGHBORS6.some(([dx, dy, dz]) => blockAtCell(bot, { x: c.x + dx, y: c.y + dy, z: c.z + dz })?.name === 'lava');
33
+ }
34
+
35
+ export const FACE_ZH: Record<BlockFace, string> = {
36
+ up: '上', down: '下', north: '北', south: '南', west: '西', east: '东',
37
+ };
38
+
39
+ /**
40
+ * 没指定面时挨个试的次序:先脚下那一块的上面(地上的火把、路面、垫脚全走这一条),
41
+ * 再头顶那一块的下面,最后四个侧面按原版 Direction 的序。
42
+ */
43
+ export const FACE_TRY_ORDER: readonly BlockFace[] = ['up', 'down', 'north', 'south', 'west', 'east'];
44
+
45
+ /** 「贴着 (x,y,z) 的北面」:回执里点名这一次贴的是谁的哪一面 */
46
+ export function faceText(ref: Cell, face: BlockFace): string {
47
+ return `贴着 ${cellText(ref)} 的${FACE_ZH[face]}面`;
48
+ }
49
+
50
+ /** 贴某一面放时的参照方块:新方块那一格沿面的反方向退一格 */
51
+ export function refCellOf(cell: Cell, face: BlockFace): Cell {
52
+ const [dx, dy, dz] = BLOCK_FACES[face];
53
+ return { x: cell.x - dx, y: cell.y - dy, z: cell.z - dz };
54
+ }
55
+
56
+ export function feetOf(bot: Bot): Cell {
57
+ const p = bot.entity.position.floored();
58
+ return { x: p.x, y: p.y, z: p.z };
59
+ }
60
+
61
+ /** 单个锚点 → 绝对格坐标(相对写法以执行这一刻我脚下那一格为原点) */
62
+ export function resolveAt(bot: Bot, at: Anchor): Cell {
63
+ const resolved = resolveAnchors([at], feetOf(bot));
64
+ if (!Array.isArray(resolved)) throw new SkillBlocked(resolved.error);
65
+ return resolved[0];
66
+ }
67
+
68
+ export function blockAtCell(bot: Bot, c: Cell): ReturnType<Bot['blockAt']> {
69
+ return bot.blockAt(new Vec3(c.x, c.y, c.z));
70
+ }
71
+
72
+ export const cellKeyOf = (c: Cell): string => `${c.x},${c.y},${c.z}`;
73
+
74
+ /** goto [x,z] 的落脚格:从世界顶往下第一块实心的上一格。区块未加载照实受阻,不猜 */
75
+ export function surfaceFeetAt(bot: Bot, at: Anchor): Cell {
76
+ const c = resolveAt(bot, at);
77
+ const game = bot.game as { minY?: number; height?: number } | undefined;
78
+ const minY = game?.minY ?? -64;
79
+ const top = minY + (game?.height ?? 384);
80
+ for (let y = top - 1; y >= minY; y--) {
81
+ const b = bot.blockAt(new Vec3(c.x, y, c.z));
82
+ if (!b) {
83
+ throw new SkillBlocked(
84
+ `(${c.x}, ${c.z}) 那里还没加载,先走近些再用 [x,z];或者直接给 y`,
85
+ );
86
+ }
87
+ if (b.boundingBox === 'block') return { x: c.x, y: y + 1, z: c.z };
88
+ }
89
+ throw new SkillBlocked(`(${c.x}, ${c.z}) 整柱都没有实心方块,落不了脚`);
90
+ }
91
+
92
+ export function solidAt(bot: Bot, c: Cell): boolean {
93
+ const b = blockAtCell(bot, c);
94
+ return b != null && b.boundingBox === 'block';
95
+ }
96
+
97
+ /** 这一格贴不贴得住:实心之外还要排掉服务端必拒的参照面(见 NO_PLACE_REFERENCE) */
98
+ export function refAt(bot: Bot, c: Cell): boolean {
99
+ const b = blockAtCell(bot, c);
100
+ return b != null && b.boundingBox === 'block' && !NO_PLACE_REFERENCE.has(b.name);
101
+ }
102
+
103
+ /** 锚点解析 + 栅格化 + 规模上限,一步到位;错误一律按受阻交回 agent */
104
+ export function shapeCells(
105
+ bot: Bot,
106
+ shape: ShapeName | undefined,
107
+ anchors: readonly Anchor[],
108
+ fill: BoxFill | undefined,
109
+ cap: number,
110
+ ): Cell[] {
111
+ const resolved = resolveAnchors(anchors, feetOf(bot));
112
+ if (!Array.isArray(resolved)) throw new SkillBlocked(resolved.error);
113
+ // 不写形状 = 就这些格,各是各的,不连成片(build 一步放 N 处走的就是这条)
114
+ if (!shape) return resolved;
115
+ const cells = rasterize(shape, resolved, fill ?? 'solid');
116
+ if (!Array.isArray(cells)) throw new SkillBlocked(cells.error);
117
+ if (cells.length > cap) {
118
+ throw new SkillBlocked(`这个${SHAPE_ZH[shape]}有 ${cells.length} 格,一单上限 ${cap}`);
119
+ }
120
+ return cells;
121
+ }
122
+
123
+ export interface RegionReading {
124
+ /** 按方块名计数;sample 是该材质任一 Block(用于工具适配判断),nearest 是离我最近的一格 */
125
+ counts: Map<string, { n: number; nearest: Cell; nearestD: number; sample: NonNullable<ReturnType<Bot['blockAt']>> }>;
126
+ /** 真空气(air/cave_air/void_air)。作物/火把等无碰撞箱方块按名字进 counts */
127
+ air: Cell[];
128
+ unloaded: number;
129
+ }
130
+
131
+ export function readRegion(bot: Bot, cells: Cell[]): RegionReading {
132
+ const me = bot.entity.position;
133
+ const counts: RegionReading['counts'] = new Map();
134
+ const air: Cell[] = [];
135
+ let unloaded = 0;
136
+ for (const c of cells) {
137
+ const b = blockAtCell(bot, c);
138
+ if (!b) { unloaded++; continue; }
139
+ if (AIR_NAMES.has(b.name)) { air.push(c); continue; }
140
+ const d = Math.hypot(c.x - me.x, c.y - me.y, c.z - me.z);
141
+ const e = counts.get(b.name);
142
+ if (!e) counts.set(b.name, { n: 1, nearest: c, nearestD: d, sample: b });
143
+ else {
144
+ e.n++;
145
+ if (d < e.nearestD) { e.nearest = c; e.nearestD = d; }
146
+ }
147
+ }
148
+ return { counts, air, unloaded };
149
+ }
150
+
151
+ /** 这个体积以内逐格报「(x,y,z):方块」,再大只报聚合构成 */
152
+ export const PROBE_CELLWISE_MAX = 27;
153
+ export const AIR_NAMES = new Set(['air', 'cave_air', 'void_air']);
154
+
155
+ /** FNV-1a;probe 差分只比对指纹,不留整片读数 */
156
+ export function fnv32(s: string): number {
157
+ let h = 0x811c9dc5;
158
+ for (let i = 0; i < s.length; i++) {
159
+ h ^= s.charCodeAt(i);
160
+ h = Math.imul(h, 0x01000193);
161
+ }
162
+ return h >>> 0;
163
+ }
164
+
165
+ /** matchBlockIds 的 id 集换成名字集,供按名读格 */
166
+ export function blockNamesOf(bot: Bot, ids: number[]): Set<string> {
167
+ const wanted = new Set(ids);
168
+ const byName = bot.registry.blocksByName as Record<string, { id: number; name: string }>;
169
+ const names = new Set<string>();
170
+ for (const b of Object.values(byName)) if (wanted.has(b.id)) names.add(b.name);
171
+ return names;
172
+ }
173
+
174
+ /** (x,z) 柱在 yFrom 以上有没有实心遮盖;未加载的一律按露天算(宁可少说不说错) */
175
+ export function skyBlocked(bot: Bot, x: number, yFrom: number, z: number): boolean {
176
+ const game = bot.game as { minY?: number; height?: number } | undefined;
177
+ const top = (game?.minY ?? -64) + (game?.height ?? 384);
178
+ for (let y = yFrom; y < top; y++) {
179
+ const b = bot.blockAt(new Vec3(x, y, z));
180
+ if (!b) return false;
181
+ if (b.boundingBox === 'block') return true;
182
+ }
183
+ return false;
184
+ }
185
+
186
+ /** 读取作物 age 前构造 Vec3;真实 Mineflayer 的 blockAt 需要坐标的 floored()。 */
187
+ export function cropAgeOfCell(bot: Bot, c: Cell): { value: number; max: number } | null {
188
+ return cropAgeAt(bot, new Vec3(c.x, c.y, c.z));
189
+ }
190
+
191
+ export function chebyshev(a: Cell, b: Cell): number {
192
+ return Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y), Math.abs(a.z - b.z));
193
+ }
194
+
195
+ export const SHAPE_ZH: Record<ShapeName, string> = {
196
+ line: '直线', rect: '平面', triangle: '三角面', arc: '弧线', box: '长方体',
197
+ };
198
+
199
+ /** 格坐标进回执的样子 */
200
+ export function cellText(c: Cell): string {
201
+ return `(${c.x}, ${c.y}, ${c.z})`;
202
+ }
203
+
204
+ /**
205
+ * 拿它当放置参照面服务端必拒的那些方块。耕地的顶面不是完整实心面,作物根本没有
206
+ * 碰撞箱;两者都点不成一次 use_item_on。这份集合只收原版确定性拒绝的,
207
+ * 悬空/树上那类「有时能成」的不进。
208
+ */
209
+ export const NO_PLACE_REFERENCE = new Set([
210
+ 'farmland',
211
+ 'wheat', 'carrots', 'potatoes', 'beetroots', 'nether_wart',
212
+ 'melon_stem', 'pumpkin_stem', 'attached_melon_stem', 'attached_pumpkin_stem',
213
+ 'torchflower_crop', 'pitcher_crop',
214
+ ]);
215
+
216
+ export function dimensionOf(bot: Bot): string {
217
+ return String(bot.game?.dimension ?? 'overworld');
218
+ }
219
+
220
+ /** 方块状态属性的原值;prismarine 不给 `getProperties` 时返回 null */
221
+ export function blockProp(b: ReturnType<Bot['blockAt']>, key: string): string | null {
222
+ if (!b || typeof b.getProperties !== 'function') return null;
223
+ const v = b.getProperties()[key];
224
+ return v === undefined ? null : String(v);
225
+ }
226
+
@@ -290,3 +290,8 @@ export class ChestBook {
290
290
  writeFileSync(this.file, `${JSON.stringify(obj)}\n`, 'utf8');
291
291
  }
292
292
  }
293
+ /** 箱子族在世界里的扫描名单;与账本认箱子的口径同一份(见 chests.ts CHEST_BLOCKS) */
294
+ export const CONTAINER_FIND: readonly string[] = CHEST_BLOCKS;
295
+
296
+ export const FURNACE_KINDS = new Set<string>(FURNACE_BLOCKS);
297
+