mioku-plugin-mc 2.0.0 → 3.0.0
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/config.md +157 -3
- package/index.ts +38 -2
- package/package.json +26 -3
- package/play/actions/registry.ts +196 -0
- package/play/ai/context-builder.ts +29 -0
- package/play/ai/debug-log.ts +135 -0
- package/play/ai/main-loop.ts +306 -0
- package/play/ai/main-tools.ts +230 -0
- package/play/ai/prompt.ts +207 -0
- package/play/ai/work-subroutine.ts +487 -0
- package/play/behavior/base-behavior.ts +82 -0
- package/play/behavior/catalog/approach-player.ts +65 -0
- package/play/behavior/catalog/defend.ts +68 -0
- package/play/behavior/catalog/explore.ts +72 -0
- package/play/behavior/catalog/factory.ts +26 -0
- package/play/behavior/catalog/farm-mobs.ts +43 -0
- package/play/behavior/catalog/follow.ts +80 -0
- package/play/behavior/catalog/gather.ts +135 -0
- package/play/behavior/catalog/idle.ts +130 -0
- package/play/behavior/catalog/seek-shelter.ts +85 -0
- package/play/behavior/engine.ts +243 -0
- package/play/behavior/survival/auto-eat.ts +46 -0
- package/play/behavior/survival/escape-lava.ts +44 -0
- package/play/behavior/survival/escape-water.ts +35 -0
- package/play/behavior/survival/flee-creeper.ts +76 -0
- package/play/behavior/survival/mlg-fall.ts +56 -0
- package/play/bot/bot-controller.ts +276 -0
- package/play/bot/play-bus.ts +48 -0
- package/play/combat/combat.ts +225 -0
- package/play/combat/index.ts +1 -0
- package/play/config.ts +39 -0
- package/play/context.ts +26 -0
- package/play/debug/README.md +74 -0
- package/play/debug/commands.ts +289 -0
- package/play/index.ts +247 -0
- package/play/mineflayer-shims.d.ts +22 -0
- package/play/missions/bundles/approach-player.ts +26 -0
- package/play/missions/bundles/explore.ts +24 -0
- package/play/missions/bundles/farm-mobs.ts +24 -0
- package/play/missions/bundles/follow-player.ts +40 -0
- package/play/missions/bundles/gather-resource.ts +37 -0
- package/play/missions/bundles/idle-wander.ts +24 -0
- package/play/missions/bundles/seek-shelter.ts +18 -0
- package/play/missions/mission-controller.ts +296 -0
- package/play/missions/registry.ts +107 -0
- package/play/path-engine/astar.ts +514 -0
- package/play/path-engine/goals.ts +84 -0
- package/play/path-engine/index.ts +4 -0
- package/play/path-engine/movements.ts +67 -0
- package/play/path-engine/path-engine.ts +540 -0
- package/play/runtime.ts +14 -0
- package/play/session.ts +486 -0
- package/play/state/cooldowns.ts +40 -0
- package/play/state/event-journal.ts +72 -0
- package/play/state/memory-bus.ts +115 -0
- package/play/state/mode.ts +57 -0
- package/play/state/sensors/entity-scanner.ts +275 -0
- package/play/state/snapshot.ts +360 -0
- package/play/types.ts +284 -0
- package/play/util/async.ts +11 -0
- package/play/util/endpoint.ts +38 -0
- package/play/util/entities.ts +135 -0
- package/play/util/inventory.ts +75 -0
- package/skills.ts +63 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { PlayManager } from "../index";
|
|
2
|
+
|
|
3
|
+
export interface DebugCommandContext {
|
|
4
|
+
text: string;
|
|
5
|
+
isOwner: boolean;
|
|
6
|
+
debugEnabled: boolean;
|
|
7
|
+
playManager: PlayManager;
|
|
8
|
+
groupId: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const OVERLAY_NAMES = ["defend", "auto_eat"];
|
|
12
|
+
const KNOWN_COMMANDS = new Set([
|
|
13
|
+
"/join",
|
|
14
|
+
"/play",
|
|
15
|
+
"/exit",
|
|
16
|
+
"/say",
|
|
17
|
+
"/motion",
|
|
18
|
+
"/stop",
|
|
19
|
+
"/clear",
|
|
20
|
+
"/status",
|
|
21
|
+
"/behaviors",
|
|
22
|
+
"/off",
|
|
23
|
+
"/missions",
|
|
24
|
+
"/actions",
|
|
25
|
+
"/stopmission",
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
export function isDebugCommand(text: string): boolean {
|
|
29
|
+
const head = text.trim().split(/\s+/)[0]?.toLowerCase();
|
|
30
|
+
return !!head && KNOWN_COMMANDS.has(head);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const OVERLAY_HELP: Record<string, string> = {
|
|
34
|
+
defend: "自动战斗。半径内有敌对生物时抢占移动去攻击。参数: [radius=<格, 默认8>]",
|
|
35
|
+
auto_eat: "自动进食。饥饿且有食物且不在战斗时进食。",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface ParsedBundleArgs {
|
|
39
|
+
bundle: string;
|
|
40
|
+
params: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseBundleArgs(arg: string): ParsedBundleArgs | null {
|
|
44
|
+
const trimmed = arg.trim();
|
|
45
|
+
if (!trimmed) return null;
|
|
46
|
+
if (trimmed.startsWith("{")) {
|
|
47
|
+
try {
|
|
48
|
+
const obj = JSON.parse(trimmed);
|
|
49
|
+
if (obj && typeof obj === "object" && typeof obj.bundle === "string") {
|
|
50
|
+
return {
|
|
51
|
+
bundle: obj.bundle,
|
|
52
|
+
params:
|
|
53
|
+
obj.params && typeof obj.params === "object"
|
|
54
|
+
? (obj.params as Record<string, unknown>)
|
|
55
|
+
: {},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const parts = trimmed.split(/\s+/);
|
|
63
|
+
const bundle = parts[0];
|
|
64
|
+
if (!bundle.startsWith("task.")) return null;
|
|
65
|
+
const params: Record<string, unknown> = {};
|
|
66
|
+
for (let i = 1; i < parts.length; i++) {
|
|
67
|
+
const eq = parts[i].indexOf("=");
|
|
68
|
+
if (eq > 0) {
|
|
69
|
+
const k = parts[i].slice(0, eq);
|
|
70
|
+
const raw = parts[i].slice(eq + 1);
|
|
71
|
+
const asNum = Number(raw);
|
|
72
|
+
params[k] = Number.isFinite(asNum) && raw !== "" ? asNum : raw;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { bundle, params };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function handleDebugCommand(
|
|
79
|
+
ctx: DebugCommandContext,
|
|
80
|
+
): Promise<string | null> {
|
|
81
|
+
if (!ctx.debugEnabled || !ctx.isOwner) return null;
|
|
82
|
+
const text = ctx.text.trim();
|
|
83
|
+
if (!text.startsWith("/")) return null;
|
|
84
|
+
const head = text.split(/\s+/)[0]?.toLowerCase();
|
|
85
|
+
if (!head || !KNOWN_COMMANDS.has(head)) return null;
|
|
86
|
+
|
|
87
|
+
const arg = text.slice(head.length).trim();
|
|
88
|
+
const pm = ctx.playManager;
|
|
89
|
+
const groupId = ctx.groupId;
|
|
90
|
+
|
|
91
|
+
switch (head) {
|
|
92
|
+
case "/join": {
|
|
93
|
+
if (!arg) return "用法: /join <服务器ID>";
|
|
94
|
+
const r = await pm.enter(groupId, arg, { debug: true });
|
|
95
|
+
return r.message;
|
|
96
|
+
}
|
|
97
|
+
case "/play": {
|
|
98
|
+
if (!arg) return "用法: /play <服务器ID>";
|
|
99
|
+
const r = await pm.enter(groupId, arg);
|
|
100
|
+
return r.message;
|
|
101
|
+
}
|
|
102
|
+
case "/exit": {
|
|
103
|
+
const r = await pm.exit(groupId);
|
|
104
|
+
return r.message;
|
|
105
|
+
}
|
|
106
|
+
case "/say": {
|
|
107
|
+
if (!arg) return "用法: /say <要说的话>";
|
|
108
|
+
const s = pm.getActiveSession(groupId);
|
|
109
|
+
if (!s) return "当前没有进行中的 mc 会话";
|
|
110
|
+
if (!s.controller.isOnline()) return "bot 尚未连接到服务器";
|
|
111
|
+
s.say(arg);
|
|
112
|
+
return `已发送: ${arg}`;
|
|
113
|
+
}
|
|
114
|
+
case "/motion": {
|
|
115
|
+
const s = pm.getActiveSession(groupId);
|
|
116
|
+
if (!s) return "当前没有进行中的 mc 会话";
|
|
117
|
+
if (!s.controller.isOnline()) return "bot 尚未连接到服务器";
|
|
118
|
+
if (!arg) return motionUsage();
|
|
119
|
+
if (OVERLAY_NAMES.includes(arg.toLowerCase().split(/\s+/)[0])) {
|
|
120
|
+
const head2 = arg.toLowerCase().split(/\s+/)[0];
|
|
121
|
+
const params: Record<string, string> = {};
|
|
122
|
+
for (const p of arg.split(/\s+/).slice(1)) {
|
|
123
|
+
const eq = p.indexOf("=");
|
|
124
|
+
if (eq > 0) params[p.slice(0, eq)] = p.slice(eq + 1);
|
|
125
|
+
}
|
|
126
|
+
const enable = !s.isOverlayEnabled(head2);
|
|
127
|
+
s.toggleOverlay(head2, enable, params);
|
|
128
|
+
return `${head2} ${enable ? "已开启" : "已关闭"}${
|
|
129
|
+
enable && Object.keys(params).length ? " " + formatParams(params) : ""
|
|
130
|
+
}`.trim();
|
|
131
|
+
}
|
|
132
|
+
const parsed = parseBundleArgs(arg);
|
|
133
|
+
if (!parsed) {
|
|
134
|
+
return `无法解析参数: ${arg}\n${motionUsage()}`;
|
|
135
|
+
}
|
|
136
|
+
const result = s.startMission(parsed);
|
|
137
|
+
if (result.kind === "applied") {
|
|
138
|
+
return `任务已启动: ${result.bundleId} (mission=${result.missionId.slice(0, 8)})`;
|
|
139
|
+
}
|
|
140
|
+
return `任务被拒绝 (${result.reason}): ${result.detail}`;
|
|
141
|
+
}
|
|
142
|
+
case "/missions": {
|
|
143
|
+
const s = pm.getActiveSession(groupId);
|
|
144
|
+
if (!s) return "当前没有进行中的 mc 会话";
|
|
145
|
+
const bundles = s.listBundles();
|
|
146
|
+
const lines = [
|
|
147
|
+
"== Task Bundles (startMission API) ==",
|
|
148
|
+
...bundles.map((b) => `- ${b.id} [${b.mode ?? "null"}] ${b.description}`),
|
|
149
|
+
];
|
|
150
|
+
const current = s.getCurrentMission();
|
|
151
|
+
if (current) {
|
|
152
|
+
lines.push(
|
|
153
|
+
"",
|
|
154
|
+
`当前任务: ${current.bundleId} (mission=${current.missionId.slice(0, 8)}, 启动于 ${new Date(current.startedAt).toLocaleTimeString()})`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return lines.join("\n");
|
|
158
|
+
}
|
|
159
|
+
case "/actions": {
|
|
160
|
+
const s = pm.getActiveSession(groupId);
|
|
161
|
+
if (!s) return "当前没有进行中的 mc 会话";
|
|
162
|
+
return [
|
|
163
|
+
"== Atomic Actions ==",
|
|
164
|
+
...s.listActions().map((action) => `- ${action.name} ${action.description}`),
|
|
165
|
+
].join("\n");
|
|
166
|
+
}
|
|
167
|
+
case "/stopmission": {
|
|
168
|
+
const s = pm.getActiveSession(groupId);
|
|
169
|
+
if (!s) return "当前没有进行中的 mc 会话";
|
|
170
|
+
const result = s.stopMission("debug_stop");
|
|
171
|
+
if (result.kind === "applied") {
|
|
172
|
+
return `任务已停止: ${result.bundleId}`;
|
|
173
|
+
}
|
|
174
|
+
return `停止失败 (${result.reason}): ${result.detail}`;
|
|
175
|
+
}
|
|
176
|
+
case "/stop": {
|
|
177
|
+
const s = pm.getActiveSession(groupId);
|
|
178
|
+
if (!s) return "当前没有进行中的 mc 会话";
|
|
179
|
+
s.stopMovement();
|
|
180
|
+
return "移动已停止,回到 idle(叠加状态保留)";
|
|
181
|
+
}
|
|
182
|
+
case "/clear": {
|
|
183
|
+
const s = pm.getActiveSession(groupId);
|
|
184
|
+
if (!s) return "当前没有进行中的 mc 会话";
|
|
185
|
+
s.clearBehaviors();
|
|
186
|
+
return "已清空所有状态,回到 idle";
|
|
187
|
+
}
|
|
188
|
+
case "/off": {
|
|
189
|
+
const s = pm.getActiveSession(groupId);
|
|
190
|
+
if (!s) return "当前没有进行中的 mc 会话";
|
|
191
|
+
if (!arg) return "用法: /off <defend|auto_eat>";
|
|
192
|
+
if (!OVERLAY_NAMES.includes(arg.toLowerCase())) return `未知叠加状态: ${arg}`;
|
|
193
|
+
s.toggleOverlay(arg.toLowerCase(), false);
|
|
194
|
+
return `${arg} 已关闭`;
|
|
195
|
+
}
|
|
196
|
+
case "/status": {
|
|
197
|
+
const s = pm.getActiveSession(groupId);
|
|
198
|
+
if (!s) return "当前没有进行中的 mc 会话";
|
|
199
|
+
const st = s.getStatus();
|
|
200
|
+
const elapsed = Math.floor((Date.now() - st.startedAt) / 1000);
|
|
201
|
+
const states = s.getBehaviorStates();
|
|
202
|
+
const on = states.filter((x) => x.enabled);
|
|
203
|
+
const activeNow = states.find((x) => x.active);
|
|
204
|
+
const mem = s.getMemorySnapshot();
|
|
205
|
+
const snap = s.getBehaviorSnapshot();
|
|
206
|
+
const lastMissionOutcome = s.getLastMissionOutcome();
|
|
207
|
+
const memLines = Object.entries(mem)
|
|
208
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
209
|
+
.map(([k, v]) => {
|
|
210
|
+
const valueStr = formatMemoryValue(v.value);
|
|
211
|
+
return ` ${k}=${valueStr} (${v.ageMs}ms ago)`;
|
|
212
|
+
});
|
|
213
|
+
const snapLines = snap
|
|
214
|
+
? [
|
|
215
|
+
`Mode: ${snap.mode.current} | Cooldowns: ${Object.keys(snap.cooldowns).length} pending`,
|
|
216
|
+
`Active (internal state):`,
|
|
217
|
+
...snap.activeBehaviors
|
|
218
|
+
.filter((x) => x.active)
|
|
219
|
+
.map((x) => ` ▶ ${x.name}: ${formatMemoryValue(x.internalState)}`),
|
|
220
|
+
]
|
|
221
|
+
: [`Snapshot: (engine not ready)`];
|
|
222
|
+
const ws = st.workStatus;
|
|
223
|
+
const workLine = ws
|
|
224
|
+
? `Work 状态: ${ws.running ? "运行中" : "空闲"} - ${ws.summary}${ws.progress ? ` (${ws.progress.current}/${ws.progress.target} ${ws.progress.unit})` : ""}`
|
|
225
|
+
: "Work 状态: 未知";
|
|
226
|
+
return [
|
|
227
|
+
`服务器: ${st.serverName} | 状态: ${st.connected ? "已连接" : "未连接"}${s.debug ? " [debug]" : ""}`,
|
|
228
|
+
`已游玩: ${Math.floor(elapsed / 60)}m${elapsed % 60}s | 当前执行: ${activeNow?.name ?? "none"}`,
|
|
229
|
+
workLine,
|
|
230
|
+
`最近任务结果: ${lastMissionOutcome ? `${lastMissionOutcome.status} ${lastMissionOutcome.bundleId}${lastMissionOutcome.code ? ` (${lastMissionOutcome.code})` : ""}` : "none"}`,
|
|
231
|
+
`已启用状态:`,
|
|
232
|
+
...on.map((x) => ` ${x.active ? "▶" : "○"} ${x.name} (${x.category}, P${x.priority})`),
|
|
233
|
+
...snapLines,
|
|
234
|
+
`Snapshot revisions: ${snap ? formatMemoryValue(snap.revisions) : "none"}`,
|
|
235
|
+
`MemoryBus (${memLines.length} keys):`,
|
|
236
|
+
...memLines,
|
|
237
|
+
].join("\n");
|
|
238
|
+
}
|
|
239
|
+
case "/behaviors": {
|
|
240
|
+
const s = pm.getActiveSession(groupId);
|
|
241
|
+
if (!s) return "当前没有进行中的 mc 会话";
|
|
242
|
+
const bundles = s.listBundles();
|
|
243
|
+
return [
|
|
244
|
+
"== 任务 Bundle (startMission API) ==",
|
|
245
|
+
...bundles.map((b) => `- ${b.id} [${b.mode ?? "null"}] ${b.description}`),
|
|
246
|
+
"",
|
|
247
|
+
"== 叠加状态 (toggleOverlay) ==",
|
|
248
|
+
...OVERLAY_NAMES.map((n) => `- /motion ${n} ${OVERLAY_HELP[n]}`),
|
|
249
|
+
"",
|
|
250
|
+
"生存层(常驻): escape_lava / mlg_fall / flee_creeper / escape_water",
|
|
251
|
+
].join("\n");
|
|
252
|
+
}
|
|
253
|
+
default:
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatParams(params: Record<string, string>): string {
|
|
259
|
+
return Object.entries(params).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatMemoryValue(v: unknown): string {
|
|
263
|
+
if (v === null) return "null";
|
|
264
|
+
if (v === undefined) return "undefined";
|
|
265
|
+
if (typeof v === "string") return v;
|
|
266
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
267
|
+
try {
|
|
268
|
+
return JSON.stringify(v);
|
|
269
|
+
} catch {
|
|
270
|
+
return String(v);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function motionUsage(): string {
|
|
275
|
+
return [
|
|
276
|
+
"用法:",
|
|
277
|
+
' /motion {"bundle":"task.follow_player","params":{"target":"Steve","distance":3}}',
|
|
278
|
+
' /motion task.follow_player target=Steve distance=3',
|
|
279
|
+
" /motion <defend|auto_eat> [key=value ...] 切换叠加状态",
|
|
280
|
+
" /stop 停止移动(回 idle,叠加状态保留)",
|
|
281
|
+
" /stopmission 停止当前任务",
|
|
282
|
+
" /clear 清空所有状态",
|
|
283
|
+
" /off <名称> 关闭指定叠加状态",
|
|
284
|
+
" /status 查看当前状态 + MemoryBus + Snapshot",
|
|
285
|
+
" /behaviors 列出所有任务 bundle",
|
|
286
|
+
" /missions 列出所有任务 bundle(含当前任务)",
|
|
287
|
+
" /actions 列出 Working AI 可用的一次性动作",
|
|
288
|
+
].join("\n");
|
|
289
|
+
}
|
package/play/index.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { MiokiContext } from "mioki";
|
|
2
|
+
import type { AIService, AIInstance, ConfigService } from "mioku";
|
|
3
|
+
import type { ConfigHandler } from "../utils/config-handler";
|
|
4
|
+
import type { PlayPluginContext } from "./context";
|
|
5
|
+
import type { PlayConfigHandler } from "./config";
|
|
6
|
+
import { PlaySession } from "./session";
|
|
7
|
+
import { MainLoop } from "./ai/main-loop";
|
|
8
|
+
import { WorkSubroutine } from "./ai/work-subroutine";
|
|
9
|
+
import { BehaviorEngine } from "./behavior/engine";
|
|
10
|
+
import type { Behavior } from "./behavior/base-behavior";
|
|
11
|
+
import { EscapeLavaBehavior } from "./behavior/survival/escape-lava";
|
|
12
|
+
import { EscapeWaterBehavior } from "./behavior/survival/escape-water";
|
|
13
|
+
import { MlgFallBehavior } from "./behavior/survival/mlg-fall";
|
|
14
|
+
import { FleeCreeperBehavior } from "./behavior/survival/flee-creeper";
|
|
15
|
+
import { AutoEatBehavior } from "./behavior/survival/auto-eat";
|
|
16
|
+
import { SelfDefenseBehavior } from "./behavior/catalog/defend";
|
|
17
|
+
import type { GroupBinding, PlayServerConfig } from "./types";
|
|
18
|
+
|
|
19
|
+
export interface PlayManagerOptions {
|
|
20
|
+
ctx: MiokiContext;
|
|
21
|
+
aiService: AIService | undefined;
|
|
22
|
+
configService: ConfigService | undefined;
|
|
23
|
+
playConfigHandler: PlayConfigHandler;
|
|
24
|
+
syncConfigHandler: ConfigHandler;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PlayEnterResult {
|
|
28
|
+
success: boolean;
|
|
29
|
+
serverId?: string;
|
|
30
|
+
message: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PlayExitResult {
|
|
34
|
+
success: boolean;
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class PlayManager {
|
|
39
|
+
private readonly ctx: MiokiContext;
|
|
40
|
+
private readonly aiService: AIService | undefined;
|
|
41
|
+
private readonly configService: ConfigService | undefined;
|
|
42
|
+
private readonly playConfigHandler: PlayConfigHandler;
|
|
43
|
+
private readonly syncConfigHandler: ConfigHandler;
|
|
44
|
+
private readonly sessions = new Map<number, PlaySession>();
|
|
45
|
+
private mainInstance?: AIInstance;
|
|
46
|
+
private workInstance?: AIInstance;
|
|
47
|
+
|
|
48
|
+
constructor(opts: PlayManagerOptions) {
|
|
49
|
+
this.ctx = opts.ctx;
|
|
50
|
+
this.aiService = opts.aiService;
|
|
51
|
+
this.configService = opts.configService;
|
|
52
|
+
this.playConfigHandler = opts.playConfigHandler;
|
|
53
|
+
this.syncConfigHandler = opts.syncConfigHandler;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private buildPluginCtx(config: ReturnType<PlayConfigHandler["getConfig"]>): PlayPluginContext {
|
|
57
|
+
return {
|
|
58
|
+
ctx: this.ctx,
|
|
59
|
+
config,
|
|
60
|
+
aiService: this.aiService,
|
|
61
|
+
configService: this.configService,
|
|
62
|
+
syncConfigHandler: this.syncConfigHandler,
|
|
63
|
+
mainInstance: this.mainInstance,
|
|
64
|
+
workInstance: this.workInstance,
|
|
65
|
+
getPlayConfig: () => this.playConfigHandler.getConfig(),
|
|
66
|
+
refreshInstances: () => {
|
|
67
|
+
this.mainInstance = this.aiService?.get("main");
|
|
68
|
+
this.workInstance = this.aiService?.get("work");
|
|
69
|
+
},
|
|
70
|
+
createWorkSubroutine: (opts) => new WorkSubroutine(opts),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async ensureInstances(): Promise<void> {
|
|
75
|
+
if (!this.aiService) return;
|
|
76
|
+
this.mainInstance = this.aiService.get("main");
|
|
77
|
+
this.workInstance = this.aiService.get("work");
|
|
78
|
+
if (!this.mainInstance || !this.workInstance) {
|
|
79
|
+
this.ctx.logger.warn(
|
|
80
|
+
"[MC/play] chat 插件未注册 main/work AI 实例,AI 循环不可用(debug 命令仍可用)",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private buildSurvivalBehaviors(): Behavior[] {
|
|
86
|
+
return [
|
|
87
|
+
new EscapeLavaBehavior(),
|
|
88
|
+
new MlgFallBehavior(),
|
|
89
|
+
new FleeCreeperBehavior(),
|
|
90
|
+
new EscapeWaterBehavior(),
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private buildOverlays(): Behavior[] {
|
|
95
|
+
const defend = new SelfDefenseBehavior();
|
|
96
|
+
defend.enabled = true;
|
|
97
|
+
const autoEat = new AutoEatBehavior();
|
|
98
|
+
autoEat.enabled = true;
|
|
99
|
+
return [defend, autoEat];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async enter(
|
|
103
|
+
groupId: number,
|
|
104
|
+
serverId: string,
|
|
105
|
+
opts: { debug?: boolean } = {},
|
|
106
|
+
): Promise<PlayEnterResult> {
|
|
107
|
+
await this.ensureInstances();
|
|
108
|
+
|
|
109
|
+
const debug = opts.debug ?? false;
|
|
110
|
+
const config = this.playConfigHandler.getConfig();
|
|
111
|
+
const binding = this.playConfigHandler.findBinding(groupId);
|
|
112
|
+
if (!binding) {
|
|
113
|
+
return { success: false, message: `未配置群 ${groupId} 的 mc 游玩绑定` };
|
|
114
|
+
}
|
|
115
|
+
const id = String(serverId ?? "").trim();
|
|
116
|
+
if (!binding.allowedServerIds.includes(id)) {
|
|
117
|
+
return {
|
|
118
|
+
success: false,
|
|
119
|
+
message: `服务器 ${id} 未对该群开放(允许: ${binding.allowedServerIds.join(", ") || "无"})`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const server = this.playConfigHandler.findServer(id);
|
|
123
|
+
if (!server) {
|
|
124
|
+
return { success: false, message: `未找到服务器配置 ${id}` };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const existing = this.sessions.get(groupId);
|
|
128
|
+
if (existing && !existing.isStopped) {
|
|
129
|
+
await existing.stop("reenter");
|
|
130
|
+
this.sessions.delete(groupId);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pluginCtx = this.buildPluginCtx(config);
|
|
134
|
+
const session = new PlaySession({
|
|
135
|
+
pluginCtx,
|
|
136
|
+
server,
|
|
137
|
+
binding,
|
|
138
|
+
});
|
|
139
|
+
session.debug = debug;
|
|
140
|
+
|
|
141
|
+
const behaviorCtxBuilder = () => {
|
|
142
|
+
const bot = session.controller.bot;
|
|
143
|
+
const movements = session.controller.getMovements();
|
|
144
|
+
if (!bot || !movements) return null;
|
|
145
|
+
return {
|
|
146
|
+
bot,
|
|
147
|
+
movements,
|
|
148
|
+
log: (m: string) => this.ctx.logger.info(`[MC/play] ${m}`),
|
|
149
|
+
};
|
|
150
|
+
};
|
|
151
|
+
const engine = new BehaviorEngine({
|
|
152
|
+
ctxBuilder: behaviorCtxBuilder,
|
|
153
|
+
tickInterval: config.behaviorTickIntervalMs,
|
|
154
|
+
survival: this.buildSurvivalBehaviors(),
|
|
155
|
+
overlays: this.buildOverlays(),
|
|
156
|
+
initialMovement: { name: "idle", params: {} },
|
|
157
|
+
});
|
|
158
|
+
session.engine = engine;
|
|
159
|
+
session.addCompanion(engine);
|
|
160
|
+
|
|
161
|
+
let mainLoop: MainLoop | undefined;
|
|
162
|
+
if (!debug) {
|
|
163
|
+
pluginCtx.notifyChatScan = () => mainLoop?.markChatScanDue();
|
|
164
|
+
mainLoop = new MainLoop({ session, pluginCtx });
|
|
165
|
+
session.addCompanion(mainLoop);
|
|
166
|
+
} else {
|
|
167
|
+
pluginCtx.notifyChatScan = undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
this.sessions.set(groupId, session);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
await session.start();
|
|
174
|
+
} catch (err) {
|
|
175
|
+
this.sessions.delete(groupId);
|
|
176
|
+
await session.stop("enter_failed");
|
|
177
|
+
return { success: false, message: `进入服务器失败: ${err}` };
|
|
178
|
+
}
|
|
179
|
+
const mode = debug ? " (debug)" : "";
|
|
180
|
+
return { success: true, serverId: server.id, message: `已进入 ${server.name}${mode}` };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async exit(groupId: number): Promise<PlayExitResult> {
|
|
184
|
+
const session = this.sessions.get(groupId);
|
|
185
|
+
if (!session || session.isStopped) {
|
|
186
|
+
return { success: false, message: "当前没有进行中的 mc 会话" };
|
|
187
|
+
}
|
|
188
|
+
await session.stop("tool_exit");
|
|
189
|
+
this.sessions.delete(groupId);
|
|
190
|
+
return { success: true, message: "已离开服务器" };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
onQqMessage(event: any): void {
|
|
194
|
+
const groupId = Number(event?.group_id);
|
|
195
|
+
if (!Number.isFinite(groupId) || groupId <= 0) return;
|
|
196
|
+
const session = this.sessions.get(groupId);
|
|
197
|
+
if (!session || session.isStopped) return;
|
|
198
|
+
const text = String(event?.raw_message ?? event?.message ?? "").trim();
|
|
199
|
+
if (!text) return;
|
|
200
|
+
const sender = event?.sender?.card || event?.sender?.nickname || event?.user_id || "群友";
|
|
201
|
+
const atBot = detectQqAtBot(event, session.server.username, session.binding.botSelfId);
|
|
202
|
+
session.onQqMessage(`[${sender}] ${text}`, { sender: String(sender), atBot });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
getStatusList() {
|
|
206
|
+
return [...this.sessions.values()].map((s) => s.getStatus());
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
getActiveSession(groupId: number): PlaySession | undefined {
|
|
210
|
+
const s = this.sessions.get(groupId);
|
|
211
|
+
return s && !s.isStopped ? s : undefined;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async dispose(): Promise<void> {
|
|
215
|
+
await Promise.all(
|
|
216
|
+
[...this.sessions.values()].map((s) =>
|
|
217
|
+
s.stop("shutdown").catch(() => undefined),
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
this.sessions.clear();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface PlayServer extends PlayServerConfig {}
|
|
225
|
+
export interface PlayGroupBinding extends GroupBinding {}
|
|
226
|
+
|
|
227
|
+
export function createPlayManager(opts: PlayManagerOptions): PlayManager {
|
|
228
|
+
return new PlayManager(opts);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function detectQqAtBot(
|
|
232
|
+
event: any,
|
|
233
|
+
botName: string,
|
|
234
|
+
botSelfId: number | undefined,
|
|
235
|
+
): boolean {
|
|
236
|
+
const lower = String(event?.raw_message ?? event?.message ?? "").toLowerCase();
|
|
237
|
+
if (botName && lower.includes(botName.toLowerCase())) return true;
|
|
238
|
+
if (Array.isArray(event?.message)) {
|
|
239
|
+
for (const segment of event.message) {
|
|
240
|
+
if (!segment || typeof segment !== "object") continue;
|
|
241
|
+
if (segment.type !== "at") continue;
|
|
242
|
+
const qq = Number(segment.data?.qq ?? 0);
|
|
243
|
+
if (qq > 0 && qq === botSelfId) return true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {};
|
|
2
|
+
|
|
3
|
+
declare module "minecraft-data" {
|
|
4
|
+
export interface IndexedData {
|
|
5
|
+
blocksByName: Record<string, any>;
|
|
6
|
+
itemsByName: Record<string, any>;
|
|
7
|
+
entitiesByName: Record<string, any>;
|
|
8
|
+
foodsByName?: Record<string, any>;
|
|
9
|
+
items?: any[];
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
}
|
|
12
|
+
function minecraftData(version: string): IndexedData;
|
|
13
|
+
export default minecraftData;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare module "mineflayer" {
|
|
17
|
+
interface Bot {
|
|
18
|
+
pathEngine?: any;
|
|
19
|
+
combat?: any;
|
|
20
|
+
pvp?: any;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { CATEGORY_PRIORITY, type Behavior } from "../../behavior/base-behavior";
|
|
3
|
+
import { ApproachPlayerBehavior } from "../../behavior/catalog/approach-player";
|
|
4
|
+
import type { BehaviorBundle, BehaviorEntry } from "../registry";
|
|
5
|
+
|
|
6
|
+
const paramsSchema = z.object({
|
|
7
|
+
target: z.string().min(1).describe("玩家名"),
|
|
8
|
+
distance: z.number().int().min(1).max(10).default(3).describe("接近到多少格"),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const approachPlayerBundle: BehaviorBundle<
|
|
12
|
+
z.infer<typeof paramsSchema>
|
|
13
|
+
> = {
|
|
14
|
+
id: "task.approach_player",
|
|
15
|
+
description: "接近指定玩家,到达目标距离后完成。",
|
|
16
|
+
mode: "MISSION",
|
|
17
|
+
paramsSchema,
|
|
18
|
+
build(params): BehaviorEntry[] {
|
|
19
|
+
const behavior: Behavior = new ApproachPlayerBehavior();
|
|
20
|
+
behavior.configure({
|
|
21
|
+
target: params.target,
|
|
22
|
+
distance: String(params.distance),
|
|
23
|
+
});
|
|
24
|
+
return [{ priority: CATEGORY_PRIORITY.movement, behavior }];
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CATEGORY_PRIORITY,
|
|
3
|
+
type Behavior,
|
|
4
|
+
} from "../../behavior/base-behavior";
|
|
5
|
+
import { ExploreBehavior } from "../../behavior/catalog/explore";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import type {
|
|
8
|
+
BehaviorBundle,
|
|
9
|
+
BehaviorEntry,
|
|
10
|
+
} from "../registry";
|
|
11
|
+
|
|
12
|
+
const paramsSchema = z.object({}).strict();
|
|
13
|
+
|
|
14
|
+
export const exploreBundle: BehaviorBundle<Record<string, never>> = {
|
|
15
|
+
id: "task.explore",
|
|
16
|
+
description: "随机探索 12 格范围以加载新区块。",
|
|
17
|
+
mode: "MISSION",
|
|
18
|
+
paramsSchema,
|
|
19
|
+
build(): BehaviorEntry[] {
|
|
20
|
+
const b: Behavior = new ExploreBehavior();
|
|
21
|
+
b.configure({});
|
|
22
|
+
return [{ priority: CATEGORY_PRIORITY.movement, behavior: b }];
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CATEGORY_PRIORITY,
|
|
3
|
+
type Behavior,
|
|
4
|
+
} from "../../behavior/base-behavior";
|
|
5
|
+
import { FarmMobsBehavior } from "../../behavior/catalog/farm-mobs";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import type {
|
|
8
|
+
BehaviorBundle,
|
|
9
|
+
BehaviorEntry,
|
|
10
|
+
} from "../registry";
|
|
11
|
+
|
|
12
|
+
const paramsSchema = z.object({}).strict();
|
|
13
|
+
|
|
14
|
+
export const farmMobsBundle: BehaviorBundle<Record<string, never>> = {
|
|
15
|
+
id: "task.farm_mobs",
|
|
16
|
+
description: "猎杀附近被动生物获取掉落物(牛/羊/鸡等)。",
|
|
17
|
+
mode: "MISSION",
|
|
18
|
+
paramsSchema,
|
|
19
|
+
build(): BehaviorEntry[] {
|
|
20
|
+
const b: Behavior = new FarmMobsBehavior();
|
|
21
|
+
b.configure({});
|
|
22
|
+
return [{ priority: CATEGORY_PRIORITY.movement, behavior: b }];
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
CATEGORY_PRIORITY,
|
|
4
|
+
type Behavior,
|
|
5
|
+
} from "../../behavior/base-behavior";
|
|
6
|
+
import { FollowPlayerBehavior } from "../../behavior/catalog/follow";
|
|
7
|
+
import type { BehaviorBundle, BehaviorEntry, BundleBuildContext } from "../registry";
|
|
8
|
+
|
|
9
|
+
const paramsSchema = z.object({
|
|
10
|
+
target: z.string().min(1).describe("玩家名"),
|
|
11
|
+
distance: z
|
|
12
|
+
.number()
|
|
13
|
+
.int()
|
|
14
|
+
.min(1)
|
|
15
|
+
.max(20)
|
|
16
|
+
.optional()
|
|
17
|
+
.default(3)
|
|
18
|
+
.describe("保持距离 (1-20)"),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type FollowPlayerParams = z.infer<typeof paramsSchema>;
|
|
22
|
+
|
|
23
|
+
export const followPlayerBundle: BehaviorBundle<FollowPlayerParams> = {
|
|
24
|
+
id: "task.follow_player",
|
|
25
|
+
description: "跟随指定玩家。玩家离线或跨维度时自动停止。",
|
|
26
|
+
mode: "MISSION",
|
|
27
|
+
paramsSchema,
|
|
28
|
+
build(params: FollowPlayerParams, ctx: BundleBuildContext): BehaviorEntry[] {
|
|
29
|
+
const b: Behavior = new FollowPlayerBehavior();
|
|
30
|
+
b.configure({
|
|
31
|
+
target: params.target,
|
|
32
|
+
distance: String(params.distance),
|
|
33
|
+
});
|
|
34
|
+
return [{ priority: CATEGORY_PRIORITY.movement, behavior: b }];
|
|
35
|
+
},
|
|
36
|
+
snapshot(internal: unknown): unknown {
|
|
37
|
+
const i = internal as { target: string; distance: number } | null;
|
|
38
|
+
return i ? { target: i.target, distance: i.distance } : null;
|
|
39
|
+
},
|
|
40
|
+
};
|