cortico 0.1.0 → 0.1.2
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.
- package/README.md +7 -14
- package/package.json +1 -3
- package/src/boot.ts +18 -0
- package/src/bot.ts +41 -6
- package/src/core/README.md +4 -6
- package/src/core/blobs.ts +23 -2
- package/src/core/config-schema.ts +1 -1
- package/src/core/config.ts +2 -16
- package/src/core/core.ts +15 -5
- package/src/core/loop.ts +55 -27
- package/src/core/session.ts +3 -2
- package/src/core/timers.ts +8 -6
- package/src/core/types.ts +19 -3
- package/src/core/util.ts +17 -1
- package/src/deploy.ts +4 -1
- package/src/extensions/README.md +6 -4
- package/src/extensions/dry-mount.ts +23 -2
- package/src/extensions/manifest.ts +21 -11
- package/src/extensions.ts +147 -23
- package/src/launcher.ts +23 -9
- package/src/protocol/open-responses/context-log.ts +37 -9
- package/src/providers/README.md +32 -8
- package/src/providers/base.ts +3 -1
- package/src/providers/configuration.ts +2 -1
- package/src/providers/console/hub.ts +270 -0
- package/src/providers/console/settings.ts +8 -18
- package/src/providers/console/types.ts +5 -0
- package/src/providers/hub-api.ts +3 -0
- package/src/providers/llamacpp/config.ts +8 -6
- package/src/providers/llamacpp/console/runtime-panel.ts +5 -1
- package/src/providers/llamacpp/console/server.ts +3 -1
- package/src/providers/llamacpp/index.ts +3 -1
- package/src/providers/llamacpp/native.ts +10 -5
- package/src/providers/llamacpp/strings.ts +6 -6
- package/src/providers/name.ts +8 -0
- package/src/providers/openai-responses-compat/config.ts +13 -0
- package/src/providers/openai-responses-compat/console/client.ts +5 -0
- package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
- package/src/providers/openai-responses-compat/console/server.ts +127 -0
- package/src/providers/openai-responses-compat/index.ts +38 -13
- package/src/providers/openai-responses-compat/native.ts +9 -4
- package/src/providers/openai-responses-compat/strings.ts +62 -1
- package/src/providers/registry.ts +5 -0
- package/src/providers/transport/responses-input.ts +59 -10
- package/src/web/README.md +11 -6
- package/src/web/auth.ts +80 -0
- package/src/web/client/console-pages/builtins/llm-settings/panel.ts +5 -4
- package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
- package/src/web/client/console-pages/builtins/llm-settings/strings.ts +6 -8
- package/src/web/client/console-pages/host.ts +24 -7
- package/src/web/client/core/api.ts +5 -1
- package/src/web/client/core/router.ts +15 -0
- package/src/web/client/features/core/strings.ts +2 -2
- package/src/web/client/features/extensions/index.ts +276 -41
- package/src/web/client/features/extensions/strings.ts +84 -10
- package/src/web/client/features/feature.ts +2 -2
- package/src/web/client/features/live/diagnostics.ts +32 -0
- package/src/web/client/features/live/index.ts +34 -11
- package/src/web/client/features/live/onboarding.ts +4 -1
- package/src/web/client/features/live/protocol.ts +1 -0
- package/src/web/client/features/live/strings.ts +20 -14
- package/src/web/client/features/live/timeline.ts +2 -1
- package/src/web/client/features/providers/detail.ts +262 -0
- package/src/web/client/features/providers/drafts.ts +23 -0
- package/src/web/client/features/providers/index.ts +173 -87
- package/src/web/client/features/providers/strings.ts +44 -17
- package/src/web/client/features/providers/types.ts +15 -0
- package/src/web/client/features/settings/general.ts +13 -0
- package/src/web/client/features/settings/index.ts +1 -0
- package/src/web/client/features/settings/strings.ts +6 -0
- package/src/web/client/features/worlds/index.ts +1 -0
- package/src/web/client/main.ts +4 -0
- package/src/web/client/shell/index.ts +28 -48
- package/src/web/client/shell/strings.ts +0 -22
- package/src/web/client/ui/icons.ts +14 -1
- package/src/web/client/ui/prompt-input.tsx +17 -5
- package/src/web/client/ui/strings.ts +0 -2
- package/src/web/console-pages.ts +1 -1
- package/src/web/diagnostics.ts +133 -0
- package/src/web/public/login.html +67 -0
- package/src/web/public/styles.css +143 -26
- package/src/web/server.ts +233 -19
- package/src/web/shared/client-panel.ts +4 -1
- package/src/web/shared/console-protocol.ts +4 -1
- package/src/worlds/bilibili/README.md +1 -1
- package/src/worlds/bilibili/overlay/server.ts +4 -2
- package/src/worlds/minecraft/ADAPT.md +66 -0
- package/src/worlds/minecraft/README.md +69 -11
- package/src/worlds/minecraft/cell-facts.ts +226 -0
- package/src/worlds/minecraft/chests.ts +5 -0
- package/src/worlds/minecraft/containers.ts +325 -0
- package/src/worlds/minecraft/entity-facts.ts +31 -5
- package/src/worlds/minecraft/executor.ts +283 -10348
- package/src/worlds/minecraft/inventory.ts +268 -0
- package/src/worlds/minecraft/melee.ts +419 -0
- package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
- package/src/worlds/minecraft/placed-ledger.ts +152 -0
- package/src/worlds/minecraft/placement.ts +1038 -0
- package/src/worlds/minecraft/receipt.ts +340 -0
- package/src/worlds/minecraft/skill-context.ts +358 -0
- package/src/worlds/minecraft/skills-build.ts +1169 -0
- package/src/worlds/minecraft/skills-container.ts +1343 -0
- package/src/worlds/minecraft/skills-craft.ts +333 -0
- package/src/worlds/minecraft/skills-dig.ts +624 -0
- package/src/worlds/minecraft/skills-gather.ts +1230 -0
- package/src/worlds/minecraft/skills-interact.ts +1559 -0
- package/src/worlds/minecraft/tools.ts +331 -0
- package/src/worlds/minecraft/travel.ts +763 -0
- package/src/worlds/minecraft/until.ts +75 -0
- package/src/worlds/qq/normalize.ts +14 -0
- package/src/worlds/qq/world.ts +53 -7
- package/src/worlds/terminal/world.ts +5 -3
|
@@ -5,23 +5,80 @@
|
|
|
5
5
|
实时游戏 World:以 mineflayer 玩家客户端连原版服务器,把"游戏状态 → 文字观察"
|
|
6
6
|
与"高层意图 → 游戏操作"接通。
|
|
7
7
|
|
|
8
|
+
**症状跟服务端版本有关就先读 [ADAPT.md](ADAPT.md)。** 那里按版本记着已知的服务端判据
|
|
9
|
+
差异、在日志里认出它的特征、以及客户端这边的改法。
|
|
10
|
+
|
|
11
|
+
新量到一条就补进 ADAPT.md,并在情况允许时向操作员申请提一个 PR。通用的适配问题连代码
|
|
12
|
+
一起提;改动牵涉上游(mineflayer、prismarine-*、服务端行为)时,代码不一定要提,那条经验
|
|
13
|
+
仍然值得提。
|
|
14
|
+
|
|
8
15
|
## 结构
|
|
9
16
|
|
|
10
17
|
```
|
|
11
|
-
world.ts
|
|
18
|
+
definition.ts / config.ts / world.ts World 定义、配置段与 World 本体(事件面、工具面、
|
|
19
|
+
console 自报、世界变化节流、摄像机编排、PWSR 三张表)
|
|
20
|
+
proxy.ts / engine-child.ts / engine-ipc.ts 主进程代理、引擎子进程与两者之间的协议
|
|
21
|
+
|
|
22
|
+
连上去
|
|
12
23
|
bridge.ts mineflayer 连接/断线重连/prismarine-viewer 拉起
|
|
13
24
|
mineflayer-fixes.ts 把 mineflayer 里靠客户端预测的合成与放置换成等服务端回话的版本
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
pathfinder-perf.ts 寻路器的代价与门开关补丁(另见 patches/ 里的 upstream 补丁)
|
|
26
|
+
pathfinder-lib.d.ts 寻路器没带的类型声明
|
|
27
|
+
server.ts / server-config.ts 本地服务端一键启停;server.properties 与 ops.json 读写、
|
|
28
|
+
存档目录枚举(存档与玩法/权限与作弊面板)
|
|
29
|
+
client.ts / window.ts / client-launch.ts / client-options.ts / client-skins.ts
|
|
30
|
+
观察者客户端:拉起、等窗口、改标题、.minecraft → java 命令行、皮肤
|
|
31
|
+
level-dat.ts 读存档自述(世界名、种子、生成参数)
|
|
32
|
+
|
|
33
|
+
读世界
|
|
34
|
+
terrain.ts 世界快照 → 文本渲染与变化检测(纯逻辑,可直接单测)
|
|
35
|
+
cell-facts.ts 格的读法:锚点落到哪一格、脚下与参照面、区域扫描、方块名指纹
|
|
36
|
+
geometry.ts 形状与锚点的纯几何:解析、栅格化
|
|
37
|
+
item-facts.ts / entity-facts.ts / item-pick.ts / item-break.ts / piglin.ts
|
|
38
|
+
物品与实体的事实读法
|
|
39
|
+
names.ts 方块/实体/生物群系/附魔/效果的中文名表
|
|
40
|
+
|
|
41
|
+
技能
|
|
17
42
|
skills.ts 技能注册表:SkillCall/parseSteps/SKILL_DOC/schema 全部由一份声明生成
|
|
43
|
+
executor.ts 任务队列、逐步派发(runSkill)与自保反射(不经 LLM)
|
|
44
|
+
skill-context.ts 技能执行的共享契约:上下文、两种终态异常、World 交给技能的只读取用口
|
|
45
|
+
skills-gather.ts collect / find / fish / trade / probe
|
|
46
|
+
skills-build.ts build 的两种形态与蓝图施工,含上岸
|
|
47
|
+
skills-dig.ts excavate / tunnel
|
|
48
|
+
skills-craft.ts craft / eat / equip
|
|
49
|
+
skills-interact.ts use 的各种形态 + ride / lead / anvil / grindstone
|
|
50
|
+
skills-container.ts pickup / toss / stow / take / smelt / enchant / brew / transit
|
|
51
|
+
travel.ts 寻路一次的时限与卡死判据、寻路目标归属、走不通时的试算现场
|
|
52
|
+
placement.ts 贴哪一面、站哪儿放得着、放完回读确认、补光与耗材许可、工作站
|
|
53
|
+
containers.ts 容器与工作站窗口:找、开得稳、槽位排序、账本登记
|
|
54
|
+
inventory.ts 背包读法与放置/合成/拾取后的确认等待
|
|
55
|
+
tools.ts 选具:拿哪把、够不够级、快断了没有、reserve 被迫动用的记号
|
|
56
|
+
melee.ts 挑兵器、挥击与冷却、远程接管判据、附近敌对生物读数,以及 attack
|
|
57
|
+
until.ts find 与 tunnel 共用的 `until` 早停名单
|
|
58
|
+
receipt.ts 回执措辞:这一步是什么、受阻现场、核验结论怎么念
|
|
59
|
+
precheck.ts 前置试算:受理与出队两刻的纯读判据
|
|
60
|
+
|
|
61
|
+
战斗与自保
|
|
62
|
+
combat.ts / combat-context.ts / ranged.ts 被动交战、方位场、弓与投掷物
|
|
63
|
+
escape.ts 脱困与回生:重生点解析、tp 回执
|
|
64
|
+
body-lease.ts 身体租约仲裁:谁在什么时候占着身体
|
|
65
|
+
|
|
66
|
+
蓝图
|
|
67
|
+
blueprint.ts 方块状态规范化与校验
|
|
68
|
+
blueprint-plan.ts 编译成施工步序、账单、回读对账
|
|
69
|
+
blueprint-registry.ts 原版方块状态与放置方式的对照
|
|
70
|
+
blueprint-repair.ts / blueprint-resource.ts 提交修复与版本/作业登记
|
|
71
|
+
|
|
72
|
+
账本与规矩
|
|
73
|
+
policy.ts mc_policy 六格常驻规矩,五格落盘
|
|
74
|
+
goal-plan.ts PWSR 目标计划的协调与判定
|
|
18
75
|
check.ts mc_check 的断言器:受理 → 对世界求值 → 只报差异的回执(纯函数)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
76
|
+
chests.ts / works.ts / explored.ts / deaths.ts 容器、成果、探索覆盖、死亡的持久账
|
|
77
|
+
placed-ledger.ts 挂在 bot 实例上的小账:这一场放过、锄过、没放上的那些格
|
|
78
|
+
search-observation.ts / round.ts / show.ts / readouts.ts / log.ts
|
|
79
|
+
find 的短期观察、同轮重复查询、容器演出节拍、三份读数、日志转发
|
|
80
|
+
|
|
81
|
+
console/ 控制台面板两侧:服务器、存档、权限、客户端、皮肤、日志
|
|
25
82
|
(Minecraft 客户端、服务端与启动器放在仓库外,不进 Git)
|
|
26
83
|
```
|
|
27
84
|
|
|
@@ -375,7 +432,7 @@ collect **只挖看得见的**:`collectVisible` 经 `canSeeBlockAt` 检查视
|
|
|
375
432
|
|
|
376
433
|
`use.face` 接受绝对面名,省略时使用顶面。显式面同时传给 `activateBlock` 和对应的结果核验;读数中的坐标标识实际检查的格子。
|
|
377
434
|
|
|
378
|
-
### 锚点几何技能族(geometry.ts +
|
|
435
|
+
### 锚点几何技能族(geometry.ts + skills-build.ts + skills-dig.ts)
|
|
379
436
|
|
|
380
437
|
**锚点几何语言**由 build/excavate/tunnel 的操作、probe 的测量及 dryRun 的试算共用:
|
|
381
438
|
|
|
@@ -473,6 +530,7 @@ surface 是尽力而为:先浮上水面换气(这步永远做得到),再
|
|
|
473
530
|
2. **合成前清空合成格。** 光标物品无法放回时返回错误;徒手 2×2 合成使用不会关闭的窗口 0,失败后由补丁取回格内材料。
|
|
474
531
|
3. **放置后回读目标格。** 每次等待 400ms,最多尝试三次;最终错误保留 Mineflayer 的文本契约,供寻路器处理。
|
|
475
532
|
4. **丢弃 `windowId=-2` 的 `set_slot`。** 其他窗口的 `set_slot` 和 `window_items` 继续传递;已收到当前窗口的 `stateId` 时,用该值替换其他窗口包中的 `stateId`。
|
|
533
|
+
5. **水平碰撞停在离方块面 `WALL_GAP` 处。** 服务端拒绝包围盒恰好贴着方块侧面的位置(静默传送回上一个好位置,不打 `moved wrongly`),prismarine-physics 原本停在贴面处;竖直方向贴面服务端照收,不改。改的是 `prismarine-physics` 的 AABB 原型,bot 物理与寻路器模拟同时生效。
|
|
476
534
|
|
|
477
535
|
合成与放置期间的包流记为 World 日志 `craft` 类别(`click-out`、`slot-in`、
|
|
478
536
|
`items-in`、`stateid-guard`、`result-slot`、`grid-clear`);窗口外的高频 `set_slot`
|
|
@@ -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
|
+
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 容器与工作站的窗口:找到哪一个、开得稳、槽位怎么排、这一次看见了什么记进账本。
|
|
3
|
+
*
|
|
4
|
+
* 只管开窗与读写槽位,存什么取什么由技能族决定。
|
|
5
|
+
*/
|
|
6
|
+
import type { Bot } from 'mineflayer';
|
|
7
|
+
import pathfinderPkg from 'mineflayer-pathfinder';
|
|
8
|
+
import { Vec3 } from 'vec3';
|
|
9
|
+
import { type Cell } from './geometry.ts';
|
|
10
|
+
import { SkillBlocked, checkAbort, sleep, type SkillContext } from './skill-context.ts';
|
|
11
|
+
import { gotoGoal, reachCell } from './travel.ts';
|
|
12
|
+
import { blockAtCell, cellText, dimensionOf } from './cell-facts.ts';
|
|
13
|
+
import { zhName } from './names.ts';
|
|
14
|
+
import { moveExactSlot, playerInvIn, type InvPred } from './inventory.ts';
|
|
15
|
+
import { readDurability, readEnchants, type EnchantRegistry } from './item-facts.ts';
|
|
16
|
+
import {
|
|
17
|
+
CONTAINER_FIND, ChestBook, FURNACE_KINDS, chestBlockName, hasItem, hasRoom, type ChestRecord,
|
|
18
|
+
} from './chests.ts';
|
|
19
|
+
import { type ItemStack } from './terrain.ts';
|
|
20
|
+
import { contentsText } from './receipt.ts';
|
|
21
|
+
|
|
22
|
+
const { goals } = pathfinderPkg;
|
|
23
|
+
|
|
24
|
+
/** 三种磨损态的铁砧都认 */
|
|
25
|
+
export const ANVIL_BLOCKS = ['anvil', 'chipped_anvil', 'damaged_anvil'] as const;
|
|
26
|
+
/** 找工作方块的半径,与 smelt 找炉子一个量级 */
|
|
27
|
+
export const STATION_FIND_R = 16;
|
|
28
|
+
/** 放料/取货后等服务端回灌窗口槽位 */
|
|
29
|
+
export const WINDOW_SETTLE_MS = 600;
|
|
30
|
+
|
|
31
|
+
export interface StationWindow {
|
|
32
|
+
id: number;
|
|
33
|
+
type: string;
|
|
34
|
+
slots: Array<{ name: string; type: number; count: number } | null>;
|
|
35
|
+
inventoryStart: number;
|
|
36
|
+
inventoryEnd: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function findStationCell(bot: Bot, names: readonly string[]): Cell | null {
|
|
40
|
+
const reg = bot.registry.blocksByName as unknown as Record<string, { id: number } | undefined>;
|
|
41
|
+
const ids = names.map((n) => reg[n]?.id).filter((n): n is number => typeof n === 'number');
|
|
42
|
+
if (ids.length === 0) return null;
|
|
43
|
+
const found = bot.findBlocks({ matching: ids, maxDistance: STATION_FIND_R, count: 1 });
|
|
44
|
+
return found.length > 0 ? { x: found[0].x, y: found[0].y, z: found[0].z } : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 走到 cell、开它的窗;那一格不是这种工作方块当场说清 */
|
|
48
|
+
export async function openStationWindow(
|
|
49
|
+
bot: Bot, ctx: SkillContext, cell: Cell, blockNames: readonly string[], zhStation: string,
|
|
50
|
+
): Promise<{ win: StationWindow; blockName: string }> {
|
|
51
|
+
await reachCell(bot, cell, ctx);
|
|
52
|
+
checkAbort(ctx);
|
|
53
|
+
const block = blockAtCell(bot, cell);
|
|
54
|
+
if (!block) throw new SkillBlocked(`${cellText(cell)} 所在区块没加载`);
|
|
55
|
+
if (!blockNames.includes(block.name)) {
|
|
56
|
+
throw new SkillBlocked(`${cellText(cell)} 那一格是${zhName(block.name)},不是${zhStation}`);
|
|
57
|
+
}
|
|
58
|
+
const win = await (bot as unknown as { openBlock(b: unknown): Promise<StationWindow> }).openBlock(block);
|
|
59
|
+
return { win, blockName: block.name };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 把包里符合判据的一件挪进窗口的某个格。
|
|
64
|
+
*
|
|
65
|
+
* 找料照**开着的那扇窗**自己的账读(见 playerInvIn),槽位号也只在那扇窗里成立。
|
|
66
|
+
* 一格一件的按槽位挪(见 moveExactSlot):`transfer` 按类型找槽,同 id 的几件在它
|
|
67
|
+
* 眼里没有分别,点名挑的那一件就白挑了。
|
|
68
|
+
*/
|
|
69
|
+
export async function putIntoStation(
|
|
70
|
+
bot: Bot, win: StationWindow, pred: InvPred, destSlot: number, asked: string,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const item = playerInvIn(bot, win).items().find((i) => pred(i.name, i));
|
|
73
|
+
if (!item) throw new SkillBlocked(`包里没有${asked}`);
|
|
74
|
+
if (item.count === 1) {
|
|
75
|
+
await moveExactSlot(bot, item.slot, destSlot);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
await (bot as unknown as { transfer(o: Record<string, unknown>): Promise<void> }).transfer({
|
|
79
|
+
window: win, itemType: item.type, metadata: null, count: 1,
|
|
80
|
+
sourceStart: win.inventoryStart, sourceEnd: win.inventoryEnd,
|
|
81
|
+
destStart: destSlot, destEnd: destSlot + 1,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 一件东西的读数:耐久 + 附魔,回执格式统一 */
|
|
86
|
+
export function stationItemFacts(bot: Bot, item: { name: string } | null): string {
|
|
87
|
+
if (!item) return '';
|
|
88
|
+
const parts: string[] = [];
|
|
89
|
+
const dur = readDurability(item as never);
|
|
90
|
+
if (dur) parts.push(`耐久 ${dur.left}/${dur.max}`);
|
|
91
|
+
const ench = readEnchants(item as never, bot.registry as never);
|
|
92
|
+
parts.push(ench.length > 0 ? `附魔 ${ench.map((e) => `${e.name}${e.level}`).join('、')}` : '没有附魔');
|
|
93
|
+
return parts.join(',');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function findContainers(bot: Bot, range: number): Array<{ x: number; y: number; z: number; name: string; d: number }> {
|
|
97
|
+
const ids = CONTAINER_FIND
|
|
98
|
+
.map((n) => (bot.registry.blocksByName as Record<string, { id: number } | undefined>)[n]?.id)
|
|
99
|
+
.filter((id): id is number => id !== undefined);
|
|
100
|
+
if (ids.length === 0) return [];
|
|
101
|
+
const me = bot.entity.position;
|
|
102
|
+
const found = bot.findBlocks({ matching: ids, maxDistance: range, count: 16 });
|
|
103
|
+
const out: Array<{ x: number; y: number; z: number; name: string; d: number }> = [];
|
|
104
|
+
for (const p of found) {
|
|
105
|
+
const b = bot.blockAt(p);
|
|
106
|
+
if (!b) continue;
|
|
107
|
+
out.push({ x: p.x, y: p.y, z: p.z, name: b.name, d: p.distanceTo(me) });
|
|
108
|
+
}
|
|
109
|
+
out.sort((a, b) => a.d - b.d);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 报告本维度账本中最近箱子的上次观测位置与取整直线距离;没有则返回 null。
|
|
115
|
+
* 不跨维度比较坐标,也不决定走过去或放新箱子。
|
|
116
|
+
*/
|
|
117
|
+
export function knownChestNote(bot: Bot, chests: ChestBook | undefined): string | null {
|
|
118
|
+
const me = bot.entity.position;
|
|
119
|
+
let best: { rec: ChestRecord; d: number } | null = null;
|
|
120
|
+
for (const rec of chests?.chestsIn(dimensionOf(bot)) ?? []) {
|
|
121
|
+
const d = Math.hypot(rec.x - me.x, rec.y - me.y, rec.z - me.z);
|
|
122
|
+
if (!best || d < best.d) best = { rec, d };
|
|
123
|
+
}
|
|
124
|
+
if (!best) return null;
|
|
125
|
+
const r = best.rec;
|
|
126
|
+
const origin = r.placedAt !== undefined ? '你放的' : '你开过的';
|
|
127
|
+
return `;账上本维度最近的是${origin}${zhName(chestBlockName(r))}`
|
|
128
|
+
+ ` (${r.x}, ${r.y}, ${r.z}),直线 ${Math.round(best.d)} 格(上次看见)`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 附近一个容器都扫不到:两处(stow / take)共用同一句措辞与同一段账本补注 */
|
|
132
|
+
export function noContainerNearby(bot: Bot, ctx: SkillContext): SkillBlocked {
|
|
133
|
+
return new SkillBlocked(`32 格内没有箱子${knownChestNote(bot, ctx.chests) ?? ''}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* 箱内容按「名字 + 附魔」并栈,与背包同一条线(原版里带不同附魔的同名物品不能堆叠)。
|
|
138
|
+
* 只按名字并,五本各不相同的附魔书会念成「附魔书×5」:数量读得到,哪一本在里面读不到,
|
|
139
|
+
* 于是也点不了名(见 item-pick)。
|
|
140
|
+
*/
|
|
141
|
+
export function containerStacks(win: {
|
|
142
|
+
containerItems?: () => Array<{ name: string; count: number }>;
|
|
143
|
+
slots?: Array<{ name: string; count: number } | null>;
|
|
144
|
+
inventoryStart?: number;
|
|
145
|
+
}, registry?: EnchantRegistry | null): { items: ItemStack[]; usedSlots: number; slots: number } {
|
|
146
|
+
const raw = typeof win.containerItems === 'function'
|
|
147
|
+
? win.containerItems()
|
|
148
|
+
: (win.slots ?? []).slice(0, win.inventoryStart ?? 27).filter((s): s is { name: string; count: number } => s != null);
|
|
149
|
+
const merged = new Map<string, ItemStack>();
|
|
150
|
+
for (const it of raw) {
|
|
151
|
+
const ench = readEnchants(it as never, registry as never);
|
|
152
|
+
const key = ench.length > 0 ? `${it.name}|${ench.map((e) => `${e.name}${e.level}`).join(',')}` : it.name;
|
|
153
|
+
const cur = merged.get(key);
|
|
154
|
+
if (cur) cur.count += it.count;
|
|
155
|
+
else merged.set(key, { name: it.name, count: it.count, ...(ench.length > 0 ? { enchantments: ench } : {}) });
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
items: [...merged.values()],
|
|
159
|
+
usedSlots: raw.length,
|
|
160
|
+
slots: win.inventoryStart ?? 27,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function rememberChest(
|
|
165
|
+
ctx: SkillContext,
|
|
166
|
+
bot: Bot,
|
|
167
|
+
pos: { x: number; y: number; z: number },
|
|
168
|
+
win: Parameters<typeof containerStacks>[0],
|
|
169
|
+
): { items: ItemStack[]; usedSlots: number; slots: number } {
|
|
170
|
+
const snap = containerStacks(win, bot.registry as never);
|
|
171
|
+
ctx.chests?.remember(dimensionOf(bot), pos, snap.items, snap.usedSlots, snap.slots);
|
|
172
|
+
return snap;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 原版烧一件的耗时:熔炉 200 刻(10 秒);高炉与烟熏炉减半(100 刻)。
|
|
177
|
+
* 这是确定性数值,所以熔炉的到期**可以**估;作物不行(随机刻),别把这条挪去作物。
|
|
178
|
+
*/
|
|
179
|
+
export function smeltPerItemMs(blockName: string): number {
|
|
180
|
+
return blockName === 'furnace' ? 10_000 : 5_000;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 槽位有料不能证明正在烧炼;估时需要炉火和进度读数同时确认。 */
|
|
184
|
+
export function furnaceDoneAt(win: unknown, input: ItemStack | null, blockName: string, now: number): number | null {
|
|
185
|
+
const { fuel, progress } = win as { fuel?: number | null; progress?: number | null };
|
|
186
|
+
return input && typeof fuel === 'number' && fuel > 0 && typeof progress === 'number' && progress > 0
|
|
187
|
+
? now + (input.count - progress) * smeltPerItemMs(blockName)
|
|
188
|
+
: null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** 窗口 slots 里的一格转成账本的 ItemStack;空格与读不出都算 null */
|
|
192
|
+
export function slotStack(win: unknown, i: number): ItemStack | null {
|
|
193
|
+
const slots = (win as { slots?: Array<{ name?: string; count?: number } | null> }).slots;
|
|
194
|
+
const s = slots?.[i];
|
|
195
|
+
return s && typeof s.name === 'string' && typeof s.count === 'number'
|
|
196
|
+
? { name: s.name, count: s.count }
|
|
197
|
+
: null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 右键开出来的窗口在关窗前记进容器账本,回执带上看见了什么。
|
|
202
|
+
* 箱子族整窗记;炉子族记三槽位,有炉火和进度读数才估到期。不是容器就什么都不做。
|
|
203
|
+
*/
|
|
204
|
+
export function rememberWindow(
|
|
205
|
+
bot: Bot,
|
|
206
|
+
ctx: SkillContext,
|
|
207
|
+
cell: Cell,
|
|
208
|
+
blockName: string,
|
|
209
|
+
win: NonNullable<Bot['currentWindow']>,
|
|
210
|
+
): string {
|
|
211
|
+
if (CONTAINER_FIND.includes(blockName)) {
|
|
212
|
+
const snap = rememberChest(ctx, bot, cell, win as Parameters<typeof containerStacks>[0]);
|
|
213
|
+
return `。箱里:${contentsText(snap.items)}`;
|
|
214
|
+
}
|
|
215
|
+
if (FURNACE_KINDS.has(blockName)) {
|
|
216
|
+
const state = { input: slotStack(win, 0), fuel: slotStack(win, 1), output: slotStack(win, 2) };
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
const expected = furnaceDoneAt(win, state.input, blockName, now);
|
|
219
|
+
ctx.chests?.rememberFurnace(dimensionOf(bot), cell, blockName, state, now, expected);
|
|
220
|
+
const slot = (s: ItemStack | null): string => (s ? `${zhName(s.name)}×${s.count}` : '空');
|
|
221
|
+
return `。炉里:输入${slot(state.input)},燃料${slot(state.fuel)},输出${slot(state.output)}`;
|
|
222
|
+
}
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function orderForStow(
|
|
227
|
+
found: Array<{ x: number; y: number; z: number; name: string; d: number }>,
|
|
228
|
+
ctx: SkillContext,
|
|
229
|
+
bot: Bot,
|
|
230
|
+
item: string,
|
|
231
|
+
): typeof found {
|
|
232
|
+
const dim = dimensionOf(bot);
|
|
233
|
+
const withRoom: typeof found = [];
|
|
234
|
+
const unseen: typeof found = [];
|
|
235
|
+
const rest: typeof found = [];
|
|
236
|
+
// 堆叠上限是物品自带的属性(原版:大多 64,鸡蛋/雪球 16,工具/桶 1),registry 里就有
|
|
237
|
+
const stackMax = (bot.registry.itemsByName as Record<string, { stackSize?: number } | undefined>)
|
|
238
|
+
?.[item]?.stackSize ?? 64;
|
|
239
|
+
for (const s of found) {
|
|
240
|
+
const rec = ctx.chests?.get(dim, s);
|
|
241
|
+
if (rec && hasRoom(rec, item, stackMax)) withRoom.push(s);
|
|
242
|
+
else if (!rec) unseen.push(s);
|
|
243
|
+
else rest.push(s);
|
|
244
|
+
}
|
|
245
|
+
return [...withRoom, ...unseen, ...rest];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function orderForTake(
|
|
249
|
+
found: Array<{ x: number; y: number; z: number; name: string; d: number }>,
|
|
250
|
+
ctx: SkillContext,
|
|
251
|
+
bot: Bot,
|
|
252
|
+
item: string,
|
|
253
|
+
): typeof found {
|
|
254
|
+
const dim = dimensionOf(bot);
|
|
255
|
+
const withItem: typeof found = [];
|
|
256
|
+
const rest: typeof found = [];
|
|
257
|
+
for (const s of found) {
|
|
258
|
+
const rec = ctx.chests?.get(dim, s);
|
|
259
|
+
if (rec && hasItem(rec, item)) withItem.push(s);
|
|
260
|
+
else rest.push(s);
|
|
261
|
+
}
|
|
262
|
+
return [...withItem, ...rest];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** mineflayer 拿错窗口身份时抛的话:「Non-container window used as a container」一族 */
|
|
266
|
+
export function isWindowIdentityError(err: unknown): boolean {
|
|
267
|
+
return err instanceof Error && / window used as a /i.test(err.message);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 开容器前退役遗留窗口;返回窗口身份不符时关闭并重开一次,连续失败才受阻。
|
|
272
|
+
* 此处校验窗口身份,窗口状态版本由 mineflayer-fixes 的 stateId 守卫处理。
|
|
273
|
+
*/
|
|
274
|
+
export async function openWindowGuarded<T>(bot: Bot, ctx: SkillContext, open: () => Promise<T>): Promise<T> {
|
|
275
|
+
const cur = bot.currentWindow;
|
|
276
|
+
if (cur) {
|
|
277
|
+
ctx.diag?.write({
|
|
278
|
+
lane: 'skill', event: 'stale-window-retire',
|
|
279
|
+
msg: `开容器前发现还挂着窗口${cur.id}(${cur.type}),先关掉`,
|
|
280
|
+
data: { windowId: cur.id, type: String(cur.type) },
|
|
281
|
+
});
|
|
282
|
+
bot.closeWindow(cur);
|
|
283
|
+
await sleep(150);
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
return await open();
|
|
287
|
+
} catch (err) {
|
|
288
|
+
if (!isWindowIdentityError(err)) throw err;
|
|
289
|
+
const w = bot.currentWindow;
|
|
290
|
+
ctx.diag?.write({
|
|
291
|
+
lane: 'skill', event: 'window-identity-retry',
|
|
292
|
+
msg: `开出来的窗口身份不对(${w ? `窗口${w.id} ${w.type}` : '窗口已不在'}),强制关窗重开一次`,
|
|
293
|
+
data: { windowId: w?.id, type: w ? String(w.type) : null },
|
|
294
|
+
});
|
|
295
|
+
if (w) bot.closeWindow(w);
|
|
296
|
+
await sleep(300);
|
|
297
|
+
try {
|
|
298
|
+
return await open();
|
|
299
|
+
} catch (err2) {
|
|
300
|
+
if (!isWindowIdentityError(err2)) throw err2;
|
|
301
|
+
throw new SkillBlocked(
|
|
302
|
+
'窗口串号了:上一个界面窗口没退干净,这次开容器拿到的是错的窗口身份;' +
|
|
303
|
+
'已强制关窗重试一次仍没成。东西都还在,没有丢——过几秒再开一次多半就好',
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function openNearbyContainer(
|
|
310
|
+
bot: Bot,
|
|
311
|
+
spot: { x: number; y: number; z: number; name: string },
|
|
312
|
+
ctx: SkillContext,
|
|
313
|
+
): Promise<Awaited<ReturnType<Bot['openContainer']>>> {
|
|
314
|
+
await gotoGoal(bot, new goals.GoalNear(spot.x, spot.y, spot.z, 2), ctx);
|
|
315
|
+
const block = bot.blockAt(new Vec3(spot.x, spot.y, spot.z));
|
|
316
|
+
if (!block) throw new SkillBlocked(`${zhName(spot.name)}不见了`);
|
|
317
|
+
return openWindowGuarded(bot, ctx, () => bot.openContainer(block));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export interface GenericWindow {
|
|
321
|
+
slots: Array<{ name: string; count: number; slot: number } | null>;
|
|
322
|
+
items(): Array<{ name: string; count: number; slot: number }>;
|
|
323
|
+
close(): void;
|
|
324
|
+
}
|
|
325
|
+
|