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
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 走到那里:寻路一次的时限与卡死判据、寻路目标归谁、走不通时的试算现场。
|
|
3
|
+
*
|
|
4
|
+
* 技能族要挪动身体一律经这里;谁占着寻路目标由 goalOwner 一处记账,抢占按归属判。
|
|
5
|
+
*/
|
|
6
|
+
import type { Bot } from 'mineflayer';
|
|
7
|
+
import pathfinderPkg from 'mineflayer-pathfinder';
|
|
8
|
+
import { Vec3 } from 'vec3';
|
|
9
|
+
import { nowIso } from '../../core/util.ts';
|
|
10
|
+
import {
|
|
11
|
+
Aborted, SkillBlocked, checkAbort, sleep, type RouteProbe, type SkillContext, type TargetDiag,
|
|
12
|
+
} from './skill-context.ts';
|
|
13
|
+
import { DIRECTION_ZH, bearing } from './terrain.ts';
|
|
14
|
+
import { cellText, dimensionOf, feetOf } from './cell-facts.ts';
|
|
15
|
+
import { MinecraftLog } from './log.ts';
|
|
16
|
+
import { zhName } from './names.ts';
|
|
17
|
+
import { fmtDur, zhErrorText } from './receipt.ts';
|
|
18
|
+
import { type Cell } from './geometry.ts';
|
|
19
|
+
import { probabilisticDropsOf } from './inventory.ts';
|
|
20
|
+
import { PLACE_REACH } from './cell-facts.ts';
|
|
21
|
+
|
|
22
|
+
export const { goals } = pathfinderPkg;
|
|
23
|
+
|
|
24
|
+
/** 单次寻路上限;超时按不可达处理。 */
|
|
25
|
+
export const GOTO_DEADLINE_MS = 120_000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 未刷新离目标最近距离的无进展时限;寻路器 isMining/isBuilding 期间暂停此计时。
|
|
29
|
+
* 到期结束当前步,并释放该步持有的 escape.active。
|
|
30
|
+
*/
|
|
31
|
+
export const GOTO_STALL_MS = 10_000;
|
|
32
|
+
/** 挖掘或搭建期间的无进展时限;距离刷新低值时归零,防止工作状态无限延长等待。 */
|
|
33
|
+
export const GOTO_WORK_STALL_MS = 25_000;
|
|
34
|
+
/** 距离要缩到比历史最近还少这么多才算真前进了;更小的变化是寻路器的抖动 */
|
|
35
|
+
export const GOTO_STALL_EPS = 1;
|
|
36
|
+
/** 净位移超过此格数也视为进展,允许绕路;按净位移而非累计路程判断。 */
|
|
37
|
+
export const GOTO_STALL_MOVE = 8;
|
|
38
|
+
/**
|
|
39
|
+
* 这一段里净位移没超过这么多 = 人根本没挪过窝,不是"绕远路没更近"。
|
|
40
|
+
*
|
|
41
|
+
* 两者的招不一样:钉在原地要先解卡(见 `unwedgeBody`),绕远路要换目标或拆航点。
|
|
42
|
+
* 1.5 格是一格多一点,容得下站位抖动与被水流顶开的幅度。
|
|
43
|
+
*/
|
|
44
|
+
export const GOTO_WEDGE_MOVE = 1.5;
|
|
45
|
+
|
|
46
|
+
/** flee 的独立时限;到期按受阻终态报告耗时、已拉开距离及剩余威胁。 */
|
|
47
|
+
export const FLEE_DEADLINE_MS = 30_000;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 水平行军只判 XZ,半径 2 格容纳台阶和半砖落脚。
|
|
51
|
+
* 下坠上限由 Movements.maxDropDown 控制,不由目标高度限制。
|
|
52
|
+
*/
|
|
53
|
+
export function levelTravelGoal(x: number, z: number): InstanceType<typeof goals.GoalNearXZ> {
|
|
54
|
+
return new goals.GoalNearXZ(x, z, 2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 超过这些数就把试算附进回执(只管啰不啰嗦,不管走不走) */
|
|
58
|
+
export const ROUTE_PLACE_LIMIT = 12;
|
|
59
|
+
export const ROUTE_BREAK_LIMIT = 20;
|
|
60
|
+
export const ROUTE_PROGRESS_MIN = 16;
|
|
61
|
+
|
|
62
|
+
export const PROBE_LABEL: Record<RouteProbe['profile'], string> = {
|
|
63
|
+
style: '按当前风格', dig: '只挖不垫', walk: '只靠走',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 闸门拒绝理由里的回读时刻(HH:MM:SS)。技能层拿不到执行器的时区配置,用默认那档;
|
|
68
|
+
* 它与执行器的 `clock()` 同一口径。
|
|
69
|
+
*/
|
|
70
|
+
export function readStamp(): string {
|
|
71
|
+
return nowIso('Asia/Shanghai').slice(11, 19);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 与判决同口径的距离文案:2.4 显示 2.4,别四舍五入成"2 格却不放行"的自相矛盾 */
|
|
75
|
+
export function fmtDist(d: number): string {
|
|
76
|
+
return Number.isInteger(d) ? String(d) : d.toFixed(1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 目标格分诊的一句话;`open` 与缺席都返回 null。
|
|
81
|
+
*
|
|
82
|
+
* 这是当场读得出的世界读数(落脚预检 O(27) + 死角灌水),比寻路器的
|
|
83
|
+
* "限时内没算完"硬——超时只说明 A* 没搜完,分诊说明搜什么都没用。
|
|
84
|
+
*/
|
|
85
|
+
export function diagText(diag: TargetDiag | null | undefined): string | null {
|
|
86
|
+
if (diag?.kind === 'noStand') return '目标那一格站不进人:它和四周都被方块占着';
|
|
87
|
+
if (diag?.kind === 'sealed') return `目标封在一个约 ${diag.size} 格的死角里`;
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 试算菜单渲染:goto 的 dryRun、走不到的受阻现场、代价偏高时的完成回执共用同一份文本 */
|
|
92
|
+
export function renderRouteMenu(
|
|
93
|
+
probes: RouteProbe[],
|
|
94
|
+
target: { x: number; y: number; z: number },
|
|
95
|
+
opts?: { startDist?: number; diag?: TargetDiag | null; head?: string },
|
|
96
|
+
): string {
|
|
97
|
+
const head = [opts?.head ?? `探路到 (${target.x}, ${target.y}, ${target.z}):`];
|
|
98
|
+
const diagLine = diagText(opts?.diag);
|
|
99
|
+
if (diagLine) head.push(diagLine);
|
|
100
|
+
// 已有目标格分诊结论时省略出发距离;超时不解释为距离过远或绕路。
|
|
101
|
+
const near = diagLine || opts?.startDist === undefined ? null
|
|
102
|
+
: `出发点离目标 ${Math.round(opts.startDist)} 格`;
|
|
103
|
+
const lines = probes.map((p) => {
|
|
104
|
+
const label = `${PROBE_LABEL[p.profile]}:`;
|
|
105
|
+
switch (p.status) {
|
|
106
|
+
case 'complete': {
|
|
107
|
+
const bits = [`走 ${p.steps} 步`];
|
|
108
|
+
if (p.place > 0) bits.push(`垫 ${p.place} 块`);
|
|
109
|
+
if (p.breaks > 0) bits.push(`挖 ${p.breaks} 块`);
|
|
110
|
+
return label + bits.join(',');
|
|
111
|
+
}
|
|
112
|
+
case 'partial': return label + `只有部分路,能推进到离目标 ${fmtDist(p.endDist)} 格`;
|
|
113
|
+
case 'timeout': return label + (near ? `限时内没算完(${near})` : '限时内没算完');
|
|
114
|
+
// 三份试算同因全灭时,光说"算不出路"三遍会把她引向目标;起点锁死是另一回事
|
|
115
|
+
case 'noPath': return label + (
|
|
116
|
+
p.visited === undefined ? '算不出路'
|
|
117
|
+
: p.visited <= 1 ? '算不出路:从我站的这一格一步都迈不出去'
|
|
118
|
+
: `算不出路(从出发点铺开试了 ${p.visited} 个落脚点)`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return `${head.join('\n')}\n- ${lines.join('\n- ')}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 方位短语:目标相对我在哪个方向多远、高差几格 */
|
|
126
|
+
export function whereFromMe(bot: Bot, target: { x: number; y: number; z: number }): string {
|
|
127
|
+
const me = bot.entity.position;
|
|
128
|
+
const dir = bearing(target.x - me.x, target.z - me.z);
|
|
129
|
+
const away = Math.round(Math.hypot(target.x - me.x, target.y - me.y, target.z - me.z));
|
|
130
|
+
const dy = Math.round(target.y - me.y);
|
|
131
|
+
const height = dy >= 2 ? `,比我高 ${dy} 格` : dy <= -2 ? `,比我低 ${-dy} 格` : '';
|
|
132
|
+
return `${dir ? DIRECTION_ZH[dir] + '边' : '就在脚边'}约 ${away} 格${height}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 受阻回执附位置、目标高差和三种路线试算;行动选择留给 agent。
|
|
137
|
+
* 目标格分诊结论优先呈现,A* 超时信息随后。
|
|
138
|
+
*/
|
|
139
|
+
export function withRouteScene(
|
|
140
|
+
bot: Bot, ctx: SkillContext, err: unknown, target: { x: number; y: number; z: number },
|
|
141
|
+
extra: string[] = [],
|
|
142
|
+
/** 这一趟实际下达的目标;水平行军必须递进来,否则试算判的是另一个目标 */
|
|
143
|
+
goal?: InstanceType<typeof goals.Goal>,
|
|
144
|
+
): unknown {
|
|
145
|
+
if (!(err instanceof SkillBlocked)) return err;
|
|
146
|
+
const me = bot.entity.position;
|
|
147
|
+
// 目标格分诊问的是"那一格站不站得进人",只对精确坐标目标成立。水平行军的 target.y
|
|
148
|
+
// 是脚下高度凑出来的、根本不是目标的一部分,拿它去分诊会凭空造出「站不进人」。
|
|
149
|
+
const diag = goal ? null : ctx.probeTarget?.(target) ?? null;
|
|
150
|
+
const scene = [
|
|
151
|
+
...extra,
|
|
152
|
+
`我在 (${Math.round(me.x)}, ${Math.round(me.y)}, ${Math.round(me.z)}),目标在${whereFromMe(bot, target)}`,
|
|
153
|
+
];
|
|
154
|
+
const probes = ctx.probeRoutes?.(target, goal);
|
|
155
|
+
if (probes && probes.length > 0) {
|
|
156
|
+
const startDist = Math.hypot(me.x - target.x, me.y - target.y, me.z - target.z);
|
|
157
|
+
scene.push(renderRouteMenu(probes, target, { startDist, diag }));
|
|
158
|
+
}
|
|
159
|
+
const lead = diagText(diag);
|
|
160
|
+
return new SkillBlocked(lead ? `${lead};${err.message}` : err.message, [...err.scene, ...scene]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 这一趟里挖不动、被寻路器绕开的格子。退避本身是机械兜底(不重规划就打满物理刻),
|
|
165
|
+
* 但「路上有一格挖不动」是关于世界的事实,受阻回执里必须看得见。
|
|
166
|
+
*/
|
|
167
|
+
export function digBackoffScene(ctx: SkillContext, since: number): string[] {
|
|
168
|
+
const cells = ctx.digBackoffSince?.(since) ?? [];
|
|
169
|
+
if (cells.length === 0) return [];
|
|
170
|
+
return [`${cells.map(cellText).join('、')} 连续挖不动,绕开了`];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 这一条试算贵不贵——只决定回执里啰不啰嗦,不决定走不走 */
|
|
174
|
+
export function routeCostly(p: RouteProbe, startDist: number): boolean {
|
|
175
|
+
if (p.status === 'complete') {
|
|
176
|
+
return p.place > ROUTE_PLACE_LIMIT || p.breaks > ROUTE_BREAK_LIMIT;
|
|
177
|
+
}
|
|
178
|
+
// timeout 与 partial 一样带着"算到哪了":A* 到点返回的是目前最好的部分路。
|
|
179
|
+
// 尽头已贴着目标 = 实质可达;推进足够 = 远目标只算得出前半段的长途常态
|
|
180
|
+
if (p.endDist <= 2.5) return false;
|
|
181
|
+
return startDist - p.endDist < ROUTE_PROGRESS_MIN;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 出发前试算一次。
|
|
186
|
+
*
|
|
187
|
+
* **目标本身进不去**(站不进人、封在死角)是事实,拦下——确认一万次也走不到。
|
|
188
|
+
* **路贵不贵**是权衡,不拦:三种走法的数字附进回执,要不要换归 agent(她有 mc_stop)。
|
|
189
|
+
* 旧版在这里设阈值代她判"这条路太贵不许走",每触发一次烧一整轮,还误杀过有完整路的目标。
|
|
190
|
+
*
|
|
191
|
+
* 返回值是要附进回执的试算文本;代价平常时为 null(不啰嗦)。
|
|
192
|
+
*/
|
|
193
|
+
export function routeNote(bot: Bot, ctx: SkillContext, target: { x: number; y: number; z: number }): string | null {
|
|
194
|
+
const probes = ctx.probeRoutes?.(target);
|
|
195
|
+
if (!probes || probes.length === 0) return null;
|
|
196
|
+
const style = probes.find((p) => p.profile === 'style') ?? probes[0];
|
|
197
|
+
const me = bot.entity.position;
|
|
198
|
+
const startDist = Math.hypot(me.x - target.x, me.y - target.y, me.z - target.z);
|
|
199
|
+
const diag = style.status !== 'complete' ? ctx.probeTarget?.(target) ?? null : null;
|
|
200
|
+
if (diag && diag.kind !== 'open') {
|
|
201
|
+
throw new SkillBlocked(renderRouteMenu(probes, target, { startDist, diag }));
|
|
202
|
+
}
|
|
203
|
+
if (!routeCostly(style, startDist)) return null;
|
|
204
|
+
return renderRouteMenu(probes, target, { startDist, diag, head: '出发前按三种走法的试算:' });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface PathSupportFailure {
|
|
208
|
+
seq: number;
|
|
209
|
+
generation: number;
|
|
210
|
+
was: string;
|
|
211
|
+
x: number;
|
|
212
|
+
y: number;
|
|
213
|
+
z: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function pathSupportFailureOf(bot: Bot): PathSupportFailure | null {
|
|
217
|
+
return (bot as unknown as { pathSupportFailure?: PathSupportFailure }).pathSupportFailure ?? null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 谁下的这个寻路目标。
|
|
222
|
+
*
|
|
223
|
+
* `escape` = 反射的救命目标(登岸/逃岩浆/低血脱离),`task` = 执行器的技能,
|
|
224
|
+
* `combat` = 战斗会话,`fall` = 深坠反射,`path-support` = 寻路器搭路支撑没被
|
|
225
|
+
* 服务端确认时的自撤,`link` = 断线/停机这类连接层收尾。
|
|
226
|
+
*/
|
|
227
|
+
export type GoalOwnerKind = 'task' | 'combat' | 'escape' | 'fall' | 'path-support' | 'link';
|
|
228
|
+
|
|
229
|
+
/** 一次登记:这张目标是谁下的、为了什么、什么时候 */
|
|
230
|
+
export interface GoalOwnerRecord {
|
|
231
|
+
goal: InstanceType<typeof goals.Goal>;
|
|
232
|
+
kind: GoalOwnerKind;
|
|
233
|
+
intent: string;
|
|
234
|
+
at: number;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* 寻路目标按 bot 实例登记所有权;允许覆盖,但必须给出理由并记录 diag。
|
|
239
|
+
* 重连更换实例后,旧实例的登记失效。
|
|
240
|
+
*/
|
|
241
|
+
export const goalOwners = new WeakMap<object, GoalOwnerRecord>();
|
|
242
|
+
|
|
243
|
+
/** 此刻真正挂在寻路器上的那张目标的登记;已经被换掉/撤掉的登记不算数 */
|
|
244
|
+
export function goalOwnerOf(bot: Bot | null | undefined): GoalOwnerRecord | null {
|
|
245
|
+
if (!bot) return null;
|
|
246
|
+
const rec = goalOwners.get(bot);
|
|
247
|
+
if (!rec) return null;
|
|
248
|
+
const live = bot.pathfinder?.goal ?? null;
|
|
249
|
+
return live !== null && live === rec.goal ? rec : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 当前寻路目标归谁;没有目标(或没登记过)为 null。给探针与回执读 */
|
|
253
|
+
export function goalOwnerKind(bot: Bot | null | undefined): GoalOwnerKind | null {
|
|
254
|
+
return goalOwnerOf(bot)?.kind ?? null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** 覆盖/撤销别人的目标时落一条事实。不拦,只记 */
|
|
258
|
+
export function noteGoalOverride(
|
|
259
|
+
prev: GoalOwnerRecord, by: GoalOwnerKind, why: string, diag: MinecraftLog | undefined,
|
|
260
|
+
): void {
|
|
261
|
+
diag?.write({
|
|
262
|
+
lane: 'body', event: 'goal-owner-override',
|
|
263
|
+
msg: `${by} 覆盖了 ${prev.kind} 的寻路目标(${prev.intent}):${why}`,
|
|
264
|
+
data: {
|
|
265
|
+
from: prev.kind, fromIntent: prev.intent, heldMs: Date.now() - prev.at,
|
|
266
|
+
by, why,
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 下寻路目标的统一入口:登记 owner,顺带把"这一下抹掉了谁的目标"记成事实。
|
|
273
|
+
*
|
|
274
|
+
* `dynamic` 透传给 `pathfinder.setGoal(goal, dynamic)`(跟随类目标要它)。
|
|
275
|
+
*/
|
|
276
|
+
export function setOwnedGoal(
|
|
277
|
+
bot: Bot | null | undefined,
|
|
278
|
+
goal: InstanceType<typeof goals.Goal>,
|
|
279
|
+
kind: GoalOwnerKind,
|
|
280
|
+
intent: string,
|
|
281
|
+
opts?: { dynamic?: boolean; diag?: MinecraftLog },
|
|
282
|
+
): void {
|
|
283
|
+
if (!bot?.pathfinder) return;
|
|
284
|
+
noteGoalOwner(bot, goal, kind, intent, opts?.diag);
|
|
285
|
+
bot.pathfinder.setGoal(goal, opts?.dynamic);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* 只记账不下达。`pathfinder.goto()` 自己会 `setGoal`,那条路进不了 `setOwnedGoal`,
|
|
290
|
+
* 而它恰恰是执行器技能的主路 —— 不在这里记一笔,"战斗/反射把技能的目标换走了"
|
|
291
|
+
* 就仍然是账外的事。目标对象同一个,身份对得上。
|
|
292
|
+
*/
|
|
293
|
+
export function noteGoalOwner(
|
|
294
|
+
bot: Bot,
|
|
295
|
+
goal: InstanceType<typeof goals.Goal>,
|
|
296
|
+
kind: GoalOwnerKind,
|
|
297
|
+
intent: string,
|
|
298
|
+
diag?: MinecraftLog,
|
|
299
|
+
): void {
|
|
300
|
+
const prev = goalOwnerOf(bot);
|
|
301
|
+
if (prev && prev.kind !== kind && prev.goal !== goal) {
|
|
302
|
+
noteGoalOverride(prev, kind, `改下 ${intent}`, diag);
|
|
303
|
+
}
|
|
304
|
+
goalOwners.set(bot, { goal, kind, intent, at: Date.now() });
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* 通过 `setGoal(null)` 同步撤销目标并清理控制键。
|
|
309
|
+
* `pathfinder.stop()` 仅设置延迟处理的 `stopPathing`,会影响紧随其后的新目标。
|
|
310
|
+
*
|
|
311
|
+
* `by` 是撤销方,`why` 是理由 —— 撤的是别人登记的目标时两者一起落 diag(不禁止)。
|
|
312
|
+
*/
|
|
313
|
+
export function dropGoal(
|
|
314
|
+
bot: Bot | null | undefined,
|
|
315
|
+
by: GoalOwnerKind = 'task',
|
|
316
|
+
why = '未说明',
|
|
317
|
+
diag?: MinecraftLog,
|
|
318
|
+
): void {
|
|
319
|
+
const prev = goalOwnerOf(bot);
|
|
320
|
+
if (prev && prev.kind !== by) noteGoalOverride(prev, by, `撤销:${why}`, diag);
|
|
321
|
+
// 连接到 spawn 之间 pathfinder 尚未注入。
|
|
322
|
+
bot?.pathfinder?.setGoal(null);
|
|
323
|
+
if (bot) goalOwners.delete(bot);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** 给别的 World(战斗会话、寻路器搭路自撤)用的撤销口,与执行器内部同一本账 */
|
|
327
|
+
export function dropOwnedGoal(
|
|
328
|
+
bot: Bot | null | undefined, by: GoalOwnerKind, why: string, diag?: MinecraftLog,
|
|
329
|
+
): void {
|
|
330
|
+
dropGoal(bot, by, why, diag);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 反射停机/死亡时把逃生目标那一笔从账上撤下来(下达一路走 `setOwnedGoal`)。
|
|
335
|
+
* 只清账不动寻路器 —— 那时目标要么已经另有归属,要么各自的收尾已经撤过。
|
|
336
|
+
* 别人名下的那一笔不许顺手抹掉:反射停机不代表战斗的目标也作废了。
|
|
337
|
+
*/
|
|
338
|
+
export function clearEscapeGoalOwner(bot: Bot): void {
|
|
339
|
+
if (goalOwners.get(bot)?.kind === 'escape') goalOwners.delete(bot);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** 三条反射自救各自的说法;只出现在所有权账与回报里 */
|
|
343
|
+
export function escapeIntent(kind: 'drown' | 'lava' | 'flee'): string {
|
|
344
|
+
return kind === 'drown' ? '登岸' : kind === 'lava' ? '逃离岩浆' : '低血脱离';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** 松开右键等于发射的那几样:取消归它们自己的持有者(切槽,不放箭) */
|
|
348
|
+
export const RELEASE_FIRES = new Set(['bow', 'crossbow', 'trident']);
|
|
349
|
+
|
|
350
|
+
/** 此刻挂着的寻路目标是不是登记在案的那张逃生目标。 */
|
|
351
|
+
export function onEscapeGoal(bot: Bot): boolean {
|
|
352
|
+
return goalOwnerOf(bot)?.kind === 'escape';
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 交还身体:撤目标、松全部控制键、停挖并结束不会在松手时发射的右键使用。
|
|
357
|
+
* 当前目标由反射登记为逃生目标时,保留目标和方向键。
|
|
358
|
+
*/
|
|
359
|
+
export function releaseBody(
|
|
360
|
+
bot: Bot | null | undefined,
|
|
361
|
+
reason: string,
|
|
362
|
+
diag?: MinecraftLog,
|
|
363
|
+
/** 谁在交还身体。撤的是别人登记的目标时,这个名字和理由一起进 diag */
|
|
364
|
+
by: GoalOwnerKind = 'task',
|
|
365
|
+
): void {
|
|
366
|
+
if (!bot?.entity) return;
|
|
367
|
+
const escaping = onEscapeGoal(bot);
|
|
368
|
+
if (!escaping) {
|
|
369
|
+
dropGoal(bot, by, `交还身体(${reason})`, diag);
|
|
370
|
+
// pathfinder 的 resetPath 已经清过一遍;这一下管的是它还没注入、或目标本来就是空的时候
|
|
371
|
+
try { bot.clearControlStates(); } catch { /* 台架假 bot 没有这一面 */ }
|
|
372
|
+
}
|
|
373
|
+
try { bot.stopDigging(); } catch { /* 没在挖 */ }
|
|
374
|
+
// 松右键只对"按着不放"的那几样成立(盾、吃东西、望远镜)。弓/弩/三叉戟的松手
|
|
375
|
+
// 就是**发射** —— 撤单时误放一箭正是 ranged.ts 绕开 deactivateItem、改用切槽
|
|
376
|
+
// 取消的原因,身体交还这一处不许把那件事又做回去。
|
|
377
|
+
if ((bot as unknown as { usingHeldItem?: boolean }).usingHeldItem === true
|
|
378
|
+
&& !RELEASE_FIRES.has(bot.heldItem?.name ?? '')) {
|
|
379
|
+
try { bot.deactivateItem(); } catch { /* 手上没有正在用的东西 */ }
|
|
380
|
+
}
|
|
381
|
+
if (escaping) {
|
|
382
|
+
diag?.write({
|
|
383
|
+
lane: 'body', event: 'release-body-kept-escape',
|
|
384
|
+
msg: `交还身体(${reason}):当前挂的是反射的逃生目标,只收了挖掘与按着的右键,目标与方向键留给它`,
|
|
385
|
+
data: { reason },
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* 寻路完成必须同时满足 deadline、中止状态与目标位置校验。
|
|
392
|
+
* `pathfinder.goto()` 在空路径时可能 resolve,因此返回后仍需用 `goal.isEnd` 验证。
|
|
393
|
+
* watchdog 仅在本次 goto 存续期间有效,不能撤销后续任务的目标。
|
|
394
|
+
*/
|
|
395
|
+
/**
|
|
396
|
+
* 钉在原地跳闸时抛的内部标记:外层据它决定"解卡再来一遍"还是照实受阻。
|
|
397
|
+
* 不出 World —— 两次都钉住时它会被换成 `stallBlocked` 的受阻句。
|
|
398
|
+
*/
|
|
399
|
+
export class Wedged extends Error {
|
|
400
|
+
constructor(readonly stall: Stall) { super('钉在原地'); }
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* 寻路卡住时释放控制键,再跳跃、后退和侧移解卡。
|
|
405
|
+
* 必须先撤寻路目标,避免寻路器逐 tick 覆盖控制键。
|
|
406
|
+
*/
|
|
407
|
+
export async function unwedgeBody(bot: Bot, ctx: SkillContext): Promise<number> {
|
|
408
|
+
const hold = async (keys: readonly string[], ms: number): Promise<void> => {
|
|
409
|
+
for (const k of keys) bot.setControlState(k as never, true);
|
|
410
|
+
await sleep(ms);
|
|
411
|
+
for (const k of keys) bot.setControlState(k as never, false);
|
|
412
|
+
};
|
|
413
|
+
bot.clearControlStates();
|
|
414
|
+
const before = bot.entity.position.clone();
|
|
415
|
+
await hold(['jump', 'back'], 400);
|
|
416
|
+
checkAbort(ctx);
|
|
417
|
+
await hold(['jump', 'left'], 300);
|
|
418
|
+
bot.clearControlStates();
|
|
419
|
+
await sleep(200);
|
|
420
|
+
const after = bot.entity.position;
|
|
421
|
+
const moved = Math.hypot(after.x - before.x, after.y - before.y, after.z - before.z);
|
|
422
|
+
ctx.diag?.write({
|
|
423
|
+
lane: 'path', event: 'goto-unwedge', taskId: ctx.taskId,
|
|
424
|
+
msg: `钉在原地,跳/退解卡:挪了 ${moved.toFixed(2)} 格`,
|
|
425
|
+
data: {
|
|
426
|
+
moved: Number(moved.toFixed(3)),
|
|
427
|
+
from: { x: Number(before.x.toFixed(3)), y: Number(before.y.toFixed(3)), z: Number(before.z.toFixed(3)) },
|
|
428
|
+
to: { x: Number(after.x.toFixed(3)), y: Number(after.y.toFixed(3)), z: Number(after.z.toFixed(3)) },
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
return moved;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** 解卡挪不到这么多 = 身体是冻着的,同一个目标再等一轮静止档是白等 */
|
|
435
|
+
export const UNWEDGE_MOVED = 0.2;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 寻路一段。钉在原地跳闸时先解卡:解卡挪动了就再来一遍,一动没动就直接受阻。
|
|
439
|
+
*
|
|
440
|
+
* 只重试这一种:绕远路、走满 deadline、支撑没确认都各有各的招,重发同一个目标只是白等。
|
|
441
|
+
* 逃生路上尤其不许白等 —— 那 10 秒静止档是 flee 三十秒时限里的三分之一。
|
|
442
|
+
*/
|
|
443
|
+
export async function gotoGoal(bot: Bot, goal: InstanceType<typeof goals.Goal>, ctx: SkillContext): Promise<void> {
|
|
444
|
+
let unwedged: number | null = null;
|
|
445
|
+
for (let attempt = 0; ; attempt++) {
|
|
446
|
+
try {
|
|
447
|
+
await gotoGoalOnce(bot, goal, ctx);
|
|
448
|
+
return;
|
|
449
|
+
} catch (err) {
|
|
450
|
+
if (!(err instanceof Wedged)) throw err;
|
|
451
|
+
if (attempt > 0) throw stallBlocked(bot, err.stall, unwedged);
|
|
452
|
+
unwedged = await unwedgeBody(bot, ctx);
|
|
453
|
+
if (unwedged < UNWEDGE_MOVED) throw stallBlocked(bot, err.stall, unwedged);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export async function gotoGoalOnce(bot: Bot, goal: InstanceType<typeof goals.Goal>, ctx: SkillContext): Promise<void> {
|
|
459
|
+
let done = false;
|
|
460
|
+
let timedOut = false;
|
|
461
|
+
let stalled: Stall | null = null;
|
|
462
|
+
/** 钉住的那一档抛 `Wedged` 交给外层解卡;绕远路那一档照实受阻 */
|
|
463
|
+
const stallError = (s: Stall): Error => (s.wedged ? new Wedged(s) : stallBlocked(bot, s));
|
|
464
|
+
const deadline = Date.now() + GOTO_DEADLINE_MS;
|
|
465
|
+
const supportSeq = pathSupportFailureOf(bot)?.seq ?? 0;
|
|
466
|
+
const supportBlocked = (): PathSupportFailure | null => {
|
|
467
|
+
const failure = pathSupportFailureOf(bot);
|
|
468
|
+
return failure && failure.seq > supportSeq ? failure : null;
|
|
469
|
+
};
|
|
470
|
+
// pathfinder.goto() 内部自行 setGoal,须先登记目标所有权。
|
|
471
|
+
noteGoalOwner(bot, goal, 'task', `技能寻路(${goal.constructor?.name ?? '目标'})`, ctx.diag);
|
|
472
|
+
void (async () => {
|
|
473
|
+
// 离目标的最近距离、它刷出新低的时刻与当时人在哪;heuristic 声明收 Move,实现只读 x/y/z
|
|
474
|
+
let best = Infinity;
|
|
475
|
+
let bestAt = Date.now();
|
|
476
|
+
let bestPos = { x: 0, y: 0, z: 0 };
|
|
477
|
+
// 这一段里离 bestPos 最远到过多少:区分"钉在原地"与"绕远路没更近"
|
|
478
|
+
let wander = 0;
|
|
479
|
+
// 上一次看到"正在挖/搭"的时刻:两段挖之间 isMining 会闪断一拍,
|
|
480
|
+
// 不留观察期的话攒了一阵的账龄会把 10s 闸当场引爆
|
|
481
|
+
let lastWorkAt = 0;
|
|
482
|
+
while (!done) {
|
|
483
|
+
// 中止由发起方撤销目标;watchdog 仅处理 deadline 与原地打转。
|
|
484
|
+
if (ctx.aborted()) return;
|
|
485
|
+
const now = Date.now();
|
|
486
|
+
if (now > deadline) {
|
|
487
|
+
timedOut = true;
|
|
488
|
+
writeStallProbe(bot, ctx, goal, 'timeout');
|
|
489
|
+
dropGoal(bot, 'task', '寻路到点未达', ctx.diag);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const here = bot.entity.position;
|
|
493
|
+
const dist = goal.heuristic({ x: here.x, y: here.y, z: here.z } as never);
|
|
494
|
+
// 挖穿一格、搭一段路的时候人本来就该站着不动:钟走慢档,不按 10 秒计——
|
|
495
|
+
// 但不能停走(见 GOTO_WORK_STALL_MS:挖-放振荡全程都算"在干活")
|
|
496
|
+
const working = Boolean(bot.pathfinder?.isMining?.() || bot.pathfinder?.isBuilding?.());
|
|
497
|
+
// 还在挪窝就不算卡住:绕远路时直线距离可以一直不降,人却一路在走
|
|
498
|
+
const moved = Math.hypot(here.x - bestPos.x, here.y - bestPos.y, here.z - bestPos.z);
|
|
499
|
+
wander = Math.max(wander, moved);
|
|
500
|
+
if (working) lastWorkAt = now;
|
|
501
|
+
if (best === Infinity || dist <= best - GOTO_STALL_EPS || moved > GOTO_STALL_MOVE) {
|
|
502
|
+
best = Math.min(best, dist);
|
|
503
|
+
bestAt = now;
|
|
504
|
+
bestPos = { x: here.x, y: here.y, z: here.z };
|
|
505
|
+
wander = 0;
|
|
506
|
+
} else if (working
|
|
507
|
+
? now - bestAt > GOTO_WORK_STALL_MS
|
|
508
|
+
: now - bestAt > GOTO_STALL_MS && now - lastWorkAt > 4_000) {
|
|
509
|
+
stalled = {
|
|
510
|
+
best, now: dist, at: feetOf(bot), metric: goalDistanceMetric(goal),
|
|
511
|
+
ms: now - bestAt, gate: working ? 'work' : 'idle',
|
|
512
|
+
// 挖/搭那一档不算钉住:人本来就该站着不动
|
|
513
|
+
wedged: !working && wander < GOTO_WEDGE_MOVE,
|
|
514
|
+
leg: goalCell(goal),
|
|
515
|
+
};
|
|
516
|
+
// 撤目标之前采样:dropGoal 会把 digging/placing/控制键一起清掉,
|
|
517
|
+
// 清完再看等于把要找的证据先擦了
|
|
518
|
+
writeStallProbe(bot, ctx, goal, 'stall');
|
|
519
|
+
dropGoal(bot, 'task', '寻路零推进', ctx.diag);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
await sleep(200);
|
|
523
|
+
}
|
|
524
|
+
})();
|
|
525
|
+
try {
|
|
526
|
+
await bot.pathfinder.goto(goal);
|
|
527
|
+
} catch (err) {
|
|
528
|
+
// 中止时目标可能已归抢占方;此处不撤目标,撤销由 abortTask/pump 负责。
|
|
529
|
+
if (ctx.aborted()) throw new Aborted(ctx.abortedBy?.() ?? null);
|
|
530
|
+
dropGoal(bot, 'task', '寻路报错,技能退出', ctx.diag);
|
|
531
|
+
const support = supportBlocked();
|
|
532
|
+
if (support) {
|
|
533
|
+
throw new SkillBlocked(
|
|
534
|
+
`走不过去:搭路支撑 (${support.x}, ${support.y}, ${support.z}) 放了三次仍是${zhName(support.was)},`
|
|
535
|
+
+ '服务端未确认;已取消这段路径',
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
if (stalled) throw stallError(stalled);
|
|
539
|
+
if (timedOut) throw new SkillBlocked(`走不过去: 走了 ${Math.round(GOTO_DEADLINE_MS / 1000)} 秒还没到`);
|
|
540
|
+
throw new SkillBlocked(`走不过去: ${zhErrorText((err as Error).message)}`);
|
|
541
|
+
} finally {
|
|
542
|
+
done = true;
|
|
543
|
+
}
|
|
544
|
+
checkAbort(ctx);
|
|
545
|
+
const support = supportBlocked();
|
|
546
|
+
if (support) {
|
|
547
|
+
dropGoal(bot, 'task', '搭路支撑未确认', ctx.diag);
|
|
548
|
+
throw new SkillBlocked(
|
|
549
|
+
`走不过去:搭路支撑 (${support.x}, ${support.y}, ${support.z}) 放了三次仍是${zhName(support.was)},`
|
|
550
|
+
+ '服务端未确认;已取消这段路径',
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
// 撤目标之后 goto 也可能是 resolve 而不是 reject,两条路都要认这份卡住
|
|
554
|
+
if (stalled) throw stallError(stalled);
|
|
555
|
+
// 半砖等位置需同时检查 floored 坐标及其上方一格,与寻路库判据一致。
|
|
556
|
+
// isEnd 类型声明为 Move,但实现只读取 x/y/z。
|
|
557
|
+
const p = bot.entity.position.floored();
|
|
558
|
+
const at = (v: typeof p): boolean => goal.isEnd(v as never);
|
|
559
|
+
if (!at(p) && !at(p.offset(0, 1, 0))) {
|
|
560
|
+
dropGoal(bot, 'task', '收尾校验没到目标格', ctx.diag);
|
|
561
|
+
throw new SkillBlocked('走不过去: 找不到可行路线(目标被封住,或者中间没有能走的路)');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** 看门狗跳闸时记录控制、寻路与持有权的结构化快照;只进诊断日志,不进回执。 */
|
|
566
|
+
export function writeStallProbe(
|
|
567
|
+
bot: Bot, ctx: SkillContext, goal: InstanceType<typeof goals.Goal>, trip: 'stall' | 'timeout',
|
|
568
|
+
): void {
|
|
569
|
+
const diag = ctx.diag;
|
|
570
|
+
if (!diag) return;
|
|
571
|
+
const pf = bot.pathfinder as unknown as {
|
|
572
|
+
isMining?: () => boolean; isBuilding?: () => boolean; isMoving?: () => boolean;
|
|
573
|
+
goal?: unknown; path?: unknown[];
|
|
574
|
+
} | undefined;
|
|
575
|
+
const e = bot.entity as unknown as {
|
|
576
|
+
position?: { x: number; y: number; z: number };
|
|
577
|
+
velocity?: { x: number; y: number; z: number };
|
|
578
|
+
onGround?: boolean; isInWater?: boolean;
|
|
579
|
+
} | undefined;
|
|
580
|
+
const round3 = (v: number | undefined): number | null =>
|
|
581
|
+
typeof v === 'number' ? Number(v.toFixed(3)) : null;
|
|
582
|
+
const body = ctx.bodyState?.() ?? null;
|
|
583
|
+
const patched = bot as unknown as {
|
|
584
|
+
pathPlacementActive?: number;
|
|
585
|
+
pathSupportFailure?: { seq: number };
|
|
586
|
+
};
|
|
587
|
+
diag.write({
|
|
588
|
+
lane: 'path', event: 'goto-stall-probe', taskId: ctx.taskId,
|
|
589
|
+
msg: `零位移探针(${trip === 'stall' ? '原地打转跳闸' : '走满 deadline'}):` +
|
|
590
|
+
`挖=${pf?.isMining?.() ?? '?'} 搭=${pf?.isBuilding?.() ?? '?'} 走=${pf?.isMoving?.() ?? '?'}`,
|
|
591
|
+
data: {
|
|
592
|
+
trip,
|
|
593
|
+
goalKind: goal.constructor?.name ?? null,
|
|
594
|
+
isMining: pf?.isMining?.() ?? null,
|
|
595
|
+
isBuilding: pf?.isBuilding?.() ?? null,
|
|
596
|
+
isMoving: pf?.isMoving?.() ?? null,
|
|
597
|
+
// 上游没把 path 挂出来,拿得到就记,拿不到记 null(isMoving 已经说了空不空)
|
|
598
|
+
pathLen: Array.isArray(pf?.path) ? pf.path.length : null,
|
|
599
|
+
goalStillMine: pf?.goal === goal,
|
|
600
|
+
// 此刻这张目标记在谁名下(见 goalOwners):"目标被别人换走了"从此在案卷里认得出来
|
|
601
|
+
goalOwner: goalOwnerKind(bot),
|
|
602
|
+
controlState: (bot as unknown as { controlState?: Record<string, boolean> }).controlState ?? null,
|
|
603
|
+
onGround: e?.onGround ?? null,
|
|
604
|
+
isInWater: e?.isInWater ?? null,
|
|
605
|
+
velocity: e?.velocity
|
|
606
|
+
? { x: round3(e.velocity.x), y: round3(e.velocity.y), z: round3(e.velocity.z) }
|
|
607
|
+
: null,
|
|
608
|
+
position: e?.position
|
|
609
|
+
? { x: round3(e.position.x), y: round3(e.position.y), z: round3(e.position.z) }
|
|
610
|
+
: null,
|
|
611
|
+
combatActive: body?.combatActive ?? null,
|
|
612
|
+
environmentOwnerKind: body?.environmentOwnerKind ?? null,
|
|
613
|
+
queueHold: body?.queueHold ?? null,
|
|
614
|
+
frozenTaskId: body?.frozenTaskId ?? null,
|
|
615
|
+
pathPlacementActive: patched.pathPlacementActive ?? 0,
|
|
616
|
+
pathSupportSeq: patched.pathSupportFailure?.seq ?? 0,
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** 原地打转跳闸时记下的读数:最近到过多远、此刻多远、人在哪、这个"多远"是哪种口径、憋了多久 */
|
|
622
|
+
export interface Stall {
|
|
623
|
+
best: number;
|
|
624
|
+
now: number;
|
|
625
|
+
at: Cell;
|
|
626
|
+
/** 距离口径。GoalNearXZ 的 heuristic 只算水平,其余目标算三维直线 */
|
|
627
|
+
metric: DistanceMetric;
|
|
628
|
+
/** 从最近一次进展到跳闸的实际耗时,单位为毫秒。 */
|
|
629
|
+
ms: number;
|
|
630
|
+
/** 跳闸的是哪一档:`work` = 挖/搭那 25 秒档,`idle` = 静止那 10 秒档 */
|
|
631
|
+
gate: 'idle' | 'work';
|
|
632
|
+
/** 这一段里人一格都没挪过(净位移不到 `GOTO_WEDGE_MOVE`) */
|
|
633
|
+
wedged: boolean;
|
|
634
|
+
/** 这一段寻路目标的那一格;拿不到全部三轴时 null(GoalNearXZ 没有 y) */
|
|
635
|
+
leg: Cell | null;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
export type DistanceMetric = '水平' | '直线';
|
|
639
|
+
|
|
640
|
+
/** 当前寻路段的目标格;受阻句须区别此格与整个步骤的终点。 */
|
|
641
|
+
export function goalCell(goal: InstanceType<typeof goals.Goal>): Cell | null {
|
|
642
|
+
const g = goal as unknown as { x?: unknown; y?: unknown; z?: unknown };
|
|
643
|
+
return typeof g.x === 'number' && typeof g.y === 'number' && typeof g.z === 'number'
|
|
644
|
+
? { x: g.x, y: g.y, z: g.z }
|
|
645
|
+
: null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** 寻路目标的距离口径;回执须注明水平或三维直线,不换算。 */
|
|
649
|
+
export function goalDistanceMetric(goal: InstanceType<typeof goals.Goal>): DistanceMetric {
|
|
650
|
+
return goal instanceof goals.GoalNearXZ ? '水平' : '直线';
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** 到这个距离以内就不能再说「走不过去」:人已经在目标旁边了 */
|
|
654
|
+
export const REACHED_NEARBY_BLOCKS = 3;
|
|
655
|
+
|
|
656
|
+
/** 卡住时报告最近距离、当前距离及是否解卡成功;区分无法走近、已近但够不到和原地未动。 */
|
|
657
|
+
export function stallBlocked(bot: Bot, s: Stall, unwedged: number | null = null): SkillBlocked {
|
|
658
|
+
const under = bot.blockAt(new Vec3(s.at.x, s.at.y - 1, s.at.z));
|
|
659
|
+
// 报的是这一次实际憋了多久,不是那个 10 秒常量:钟有静止 10 秒、"正在挖/搭" 25 秒
|
|
660
|
+
// 两档,`GOTO_STALL_MOVE` 每把净位移刷过 8 格就再归零一次,一路能续到两分钟的 deadline。
|
|
661
|
+
// 跳闸档位一并说出来 —— "一直在挖却没更近" 与 "站着没动" 她要换的招不一样。
|
|
662
|
+
const held = fmtDur(s.ms);
|
|
663
|
+
const gate = s.gate === 'work' ? '(这一段一直在挖或搭)' : '';
|
|
664
|
+
// 段的目标不是步的终点:点名这一格,免得同一条回执里两个"目标"指着两处
|
|
665
|
+
const leg = s.leg ? `这一段的落点 ${cellText(s.leg)}` : '这一段的落点';
|
|
666
|
+
const near = s.best <= REACHED_NEARBY_BLOCKS;
|
|
667
|
+
const head = s.wedged
|
|
668
|
+
? `钉在原地: ${held} 一格都没挪动过,${unwedged !== null && unwedged >= UNWEDGE_MOVED
|
|
669
|
+
? `跳/退解卡挪开了 ${fmtDist(unwedged)} 格,接着走还是没动`
|
|
670
|
+
: '跳/退解卡也没能动一下'};这不是路难走,是人动不了`
|
|
671
|
+
: near
|
|
672
|
+
? `够不到${leg}: 已经到它旁边(${s.metric} ${fmtDist(s.best)} 格),${held} 没能再靠近一步${gate}`
|
|
673
|
+
: `走不过去: ${held} 没能离${leg}更近一步${gate}`;
|
|
674
|
+
return new SkillBlocked(
|
|
675
|
+
`${head}(最近到过 ${s.metric} ${fmtDist(s.best)} 格,现在 ${s.metric} ${fmtDist(s.now)} 格),` +
|
|
676
|
+
`人在 ${cellText(s.at)},脚下是${under ? zhName(under.name) : '空的'}`,
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** 挖掘在不可达时立即失败,并以 deadline 限制服务端无响应。 */
|
|
681
|
+
export async function digBlock(bot: Bot, target: NonNullable<ReturnType<Bot['blockAt']>>, ctx: SkillContext): Promise<void> {
|
|
682
|
+
// canDigBlock 与服务端使用相同的可挖掘性和 5.1 格距离约束。
|
|
683
|
+
if (!bot.canDigBlock(target)) {
|
|
684
|
+
throw new SkillBlocked(`够不到${zhName(target.name)}(离得太远或者隔着方块),没挖成`);
|
|
685
|
+
}
|
|
686
|
+
const budget = Math.max(15_000, bot.digTime(target) * 3);
|
|
687
|
+
let finished = false;
|
|
688
|
+
const guard = setTimeout(() => {
|
|
689
|
+
if (!finished) bot.stopDigging();
|
|
690
|
+
}, budget);
|
|
691
|
+
try {
|
|
692
|
+
await bot.dig(target);
|
|
693
|
+
} catch (err) {
|
|
694
|
+
if (ctx.aborted()) throw new Aborted(ctx.abortedBy?.() ?? null);
|
|
695
|
+
const msg = (err as Error).message;
|
|
696
|
+
if (/digging aborted/i.test(msg)) {
|
|
697
|
+
throw new SkillBlocked(`挖${zhName(target.name)}挖到一半被打断了,这一块没挖完`);
|
|
698
|
+
}
|
|
699
|
+
throw new SkillBlocked(`挖不动${zhName(target.name)}: ${zhErrorText(msg)}`);
|
|
700
|
+
} finally {
|
|
701
|
+
finished = true;
|
|
702
|
+
clearTimeout(guard);
|
|
703
|
+
}
|
|
704
|
+
// 挖掉的那一格若在容器账本上,账跟着划掉——不然几何族试算还会点名一座已经不在的工作台
|
|
705
|
+
ctx.chests?.forget(dimensionOf(bot), target.position);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
export function findEntity(bot: Bot, nameOrType: string, range = 24) {
|
|
709
|
+
const me = bot.entity.position;
|
|
710
|
+
let best: { e: NonNullable<Bot['entities'][string]>; d: number } | null = null;
|
|
711
|
+
for (const id of Object.keys(bot.entities)) {
|
|
712
|
+
const e = bot.entities[id];
|
|
713
|
+
if (!e || e === bot.entity || !e.position) continue;
|
|
714
|
+
const label = e.type === 'player' ? e.username : (e.name ?? '');
|
|
715
|
+
if (label?.toLowerCase() !== nameOrType.toLowerCase()) continue;
|
|
716
|
+
const d = e.position.distanceTo(me);
|
|
717
|
+
if (d <= range && (!best || d < best.d)) best = { e, d };
|
|
718
|
+
}
|
|
719
|
+
return best?.e ?? null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* 方块匹配顺序:完整名称、类别前后缀、掉落物反查。
|
|
724
|
+
* 掉落物反查处理 cobblestone 等物品名与来源方块名不一致的情况。
|
|
725
|
+
*/
|
|
726
|
+
export function matchBlockIds(bot: Bot, name: string): number[] {
|
|
727
|
+
const byName = bot.registry.blocksByName as Record<string, { id: number; name: string; drops?: unknown[] }>;
|
|
728
|
+
const ids = new Set<number>();
|
|
729
|
+
if (byName[name]) ids.add(byName[name].id);
|
|
730
|
+
const suffix = `_${name}`;
|
|
731
|
+
for (const b of Object.values(byName)) {
|
|
732
|
+
if (b.name.endsWith(suffix) || b.name.startsWith(`${name}_`)) ids.add(b.id);
|
|
733
|
+
}
|
|
734
|
+
const item = (bot.registry.itemsByName as Record<string, { id: number } | undefined>)[name];
|
|
735
|
+
if (item) {
|
|
736
|
+
for (const b of Object.values(byName)) {
|
|
737
|
+
// drops 在不同版本里是 id 数字或 {drop:id} 对象;概率掉落不在简表,查补充表
|
|
738
|
+
const dropsIt = (b.drops ?? []).some(
|
|
739
|
+
(d) => (typeof d === 'number' ? d : (d as { drop?: number } | null)?.drop) === item.id,
|
|
740
|
+
) || probabilisticDropsOf(b.name).includes(name);
|
|
741
|
+
if (dropsIt) ids.add(b.id);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return [...ids];
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** 脚下方块挖开后的下落结束前,不读取下一格位置。 */
|
|
748
|
+
export async function settleOnGround(bot: Bot, ctx: SkillContext, budgetMs = 5_000): Promise<void> {
|
|
749
|
+
const deadline = Date.now() + budgetMs;
|
|
750
|
+
while (!bot.entity.onGround && Date.now() < deadline) {
|
|
751
|
+
checkAbort(ctx);
|
|
752
|
+
await sleep(100);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** 走到够得着那一格的地方 */
|
|
757
|
+
export async function reachCell(bot: Bot, c: Cell, ctx: SkillContext): Promise<void> {
|
|
758
|
+
const me = bot.entity.position;
|
|
759
|
+
const d = Math.hypot(me.x - (c.x + 0.5), me.y - (c.y + 0.5), me.z - (c.z + 0.5));
|
|
760
|
+
if (d <= PLACE_REACH) return;
|
|
761
|
+
await gotoGoal(bot, new goals.GoalNear(c.x, c.y, c.z, 2), ctx);
|
|
762
|
+
}
|
|
763
|
+
|