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.
Files changed (64) hide show
  1. package/config.md +157 -3
  2. package/index.ts +38 -2
  3. package/package.json +26 -3
  4. package/play/actions/registry.ts +196 -0
  5. package/play/ai/context-builder.ts +29 -0
  6. package/play/ai/debug-log.ts +135 -0
  7. package/play/ai/main-loop.ts +306 -0
  8. package/play/ai/main-tools.ts +230 -0
  9. package/play/ai/prompt.ts +207 -0
  10. package/play/ai/work-subroutine.ts +487 -0
  11. package/play/behavior/base-behavior.ts +82 -0
  12. package/play/behavior/catalog/approach-player.ts +65 -0
  13. package/play/behavior/catalog/defend.ts +68 -0
  14. package/play/behavior/catalog/explore.ts +72 -0
  15. package/play/behavior/catalog/factory.ts +26 -0
  16. package/play/behavior/catalog/farm-mobs.ts +43 -0
  17. package/play/behavior/catalog/follow.ts +80 -0
  18. package/play/behavior/catalog/gather.ts +135 -0
  19. package/play/behavior/catalog/idle.ts +130 -0
  20. package/play/behavior/catalog/seek-shelter.ts +85 -0
  21. package/play/behavior/engine.ts +243 -0
  22. package/play/behavior/survival/auto-eat.ts +46 -0
  23. package/play/behavior/survival/escape-lava.ts +44 -0
  24. package/play/behavior/survival/escape-water.ts +35 -0
  25. package/play/behavior/survival/flee-creeper.ts +76 -0
  26. package/play/behavior/survival/mlg-fall.ts +56 -0
  27. package/play/bot/bot-controller.ts +276 -0
  28. package/play/bot/play-bus.ts +48 -0
  29. package/play/combat/combat.ts +225 -0
  30. package/play/combat/index.ts +1 -0
  31. package/play/config.ts +39 -0
  32. package/play/context.ts +26 -0
  33. package/play/debug/README.md +74 -0
  34. package/play/debug/commands.ts +289 -0
  35. package/play/index.ts +247 -0
  36. package/play/mineflayer-shims.d.ts +22 -0
  37. package/play/missions/bundles/approach-player.ts +26 -0
  38. package/play/missions/bundles/explore.ts +24 -0
  39. package/play/missions/bundles/farm-mobs.ts +24 -0
  40. package/play/missions/bundles/follow-player.ts +40 -0
  41. package/play/missions/bundles/gather-resource.ts +37 -0
  42. package/play/missions/bundles/idle-wander.ts +24 -0
  43. package/play/missions/bundles/seek-shelter.ts +18 -0
  44. package/play/missions/mission-controller.ts +296 -0
  45. package/play/missions/registry.ts +107 -0
  46. package/play/path-engine/astar.ts +514 -0
  47. package/play/path-engine/goals.ts +84 -0
  48. package/play/path-engine/index.ts +4 -0
  49. package/play/path-engine/movements.ts +67 -0
  50. package/play/path-engine/path-engine.ts +540 -0
  51. package/play/runtime.ts +14 -0
  52. package/play/session.ts +486 -0
  53. package/play/state/cooldowns.ts +40 -0
  54. package/play/state/event-journal.ts +72 -0
  55. package/play/state/memory-bus.ts +115 -0
  56. package/play/state/mode.ts +57 -0
  57. package/play/state/sensors/entity-scanner.ts +275 -0
  58. package/play/state/snapshot.ts +360 -0
  59. package/play/types.ts +284 -0
  60. package/play/util/async.ts +11 -0
  61. package/play/util/endpoint.ts +38 -0
  62. package/play/util/entities.ts +135 -0
  63. package/play/util/inventory.ts +75 -0
  64. package/skills.ts +63 -0
@@ -0,0 +1,486 @@
1
+ import type { PlayPluginContext } from "./context";
2
+ import { BotController } from "./bot/bot-controller";
3
+ import { PlayBus } from "./bot/play-bus";
4
+ import { buildGoodbyePrompt } from "./ai/prompt";
5
+ import type { BehaviorEngine } from "./behavior/engine";
6
+ import { withTimeoutMs } from "./util/async";
7
+ import { MemoryBus } from "./state/memory-bus";
8
+ import { CooldownRegistry } from "./state/cooldowns";
9
+ import { EntityScanner } from "./state/sensors/entity-scanner";
10
+ import { SnapshotCollector, type BehaviorSnapshot } from "./state/snapshot";
11
+ import { PlayEventJournal } from "./state/event-journal";
12
+ import { ActionRegistry, type ActionOutcome } from "./actions/registry";
13
+ import { TaskRegistry } from "./missions/registry";
14
+ import { MissionController, type SwitchResult, type MissionSpec } from "./missions/mission-controller";
15
+ import { followPlayerBundle } from "./missions/bundles/follow-player";
16
+ import { gatherResourceBundle } from "./missions/bundles/gather-resource";
17
+ import { farmMobsBundle } from "./missions/bundles/farm-mobs";
18
+ import { exploreBundle } from "./missions/bundles/explore";
19
+ import { idleWanderBundle } from "./missions/bundles/idle-wander";
20
+ import { approachPlayerBundle } from "./missions/bundles/approach-player";
21
+ import { seekShelterBundle } from "./missions/bundles/seek-shelter";
22
+ import type { MissionOutcome } from "./state/mode";
23
+ import type {
24
+ GroupBinding,
25
+ PlayServerConfig,
26
+ PlaySessionStatus,
27
+ WorkStatus,
28
+ } from "./types";
29
+
30
+ export interface PlaySessionCompanion {
31
+ start?: () => void;
32
+ stop?: () => void;
33
+ }
34
+
35
+ export interface PlaySessionOptions {
36
+ pluginCtx: PlayPluginContext;
37
+ server: PlayServerConfig;
38
+ binding: GroupBinding;
39
+ }
40
+
41
+ export class PlaySession {
42
+ private readonly pluginCtx: PlayPluginContext;
43
+ readonly server: PlayServerConfig;
44
+ readonly binding: GroupBinding;
45
+ readonly bus = new PlayBus();
46
+ readonly memory = new MemoryBus();
47
+ readonly events = new PlayEventJournal();
48
+ readonly cooldowns = new CooldownRegistry();
49
+ readonly taskRegistry = new TaskRegistry();
50
+ readonly actionRegistry = new ActionRegistry();
51
+ readonly controller: BotController;
52
+ private readonly scanner: EntityScanner;
53
+ startedAt = Date.now();
54
+ connected = false;
55
+ debug = false;
56
+ engine?: BehaviorEngine;
57
+ private missionController?: MissionController;
58
+ private snapshotCollector?: SnapshotCollector;
59
+ private stopped = false;
60
+ private watchdog?: NodeJS.Timeout;
61
+ private companions: PlaySessionCompanion[] = [];
62
+ private qqWindow?: { start: number; count: number };
63
+ private workStatus: WorkStatus = {
64
+ running: false,
65
+ goal: null,
66
+ summary: "空闲",
67
+ updatedAt: Date.now(),
68
+ };
69
+
70
+ constructor(opts: PlaySessionOptions) {
71
+ this.pluginCtx = opts.pluginCtx;
72
+ const ctx = this.pluginCtx;
73
+ this.server = opts.server;
74
+ this.binding = opts.binding;
75
+ this.controller = new BotController({
76
+ server: opts.server,
77
+ bus: this.bus,
78
+ log: (msg) => ctx.ctx.logger.info(`[MC/play] ${msg}`),
79
+ });
80
+ this.scanner = new EntityScanner({
81
+ bus: this.memory,
82
+ bot: () => this.controller.bot,
83
+ onEvent: (type, data) => this.events.append(type, data),
84
+ });
85
+ this.registerDefaultBundles();
86
+ }
87
+
88
+ private registerDefaultBundles(): void {
89
+ this.taskRegistry.register(followPlayerBundle);
90
+ this.taskRegistry.register(gatherResourceBundle);
91
+ this.taskRegistry.register(farmMobsBundle);
92
+ this.taskRegistry.register(exploreBundle);
93
+ this.taskRegistry.register(idleWanderBundle);
94
+ this.taskRegistry.register(approachPlayerBundle);
95
+ this.taskRegistry.register(seekShelterBundle);
96
+ }
97
+
98
+ addCompanion(companion: PlaySessionCompanion): void {
99
+ this.companions.push(companion);
100
+ }
101
+
102
+ async start(): Promise<void> {
103
+ const ctx = this.pluginCtx;
104
+ ctx.ctx.logger.info(`[MC/play] 正在进入服务器 ${this.server.name} (${this.server.host})`);
105
+
106
+ this.bus.on("chat", (line) => {
107
+ this.events.append("game_chat", line);
108
+ });
109
+ this.bus.on("entityHurt", (entity) => {
110
+ const bot = this.controller.bot;
111
+ if (bot?.entity && entity?.id === bot.entity.id) {
112
+ this.events.append("damage", {
113
+ health: bot.health,
114
+ food: bot.food,
115
+ source: entity?.name ?? entity?.username ?? "unknown",
116
+ });
117
+ }
118
+ });
119
+ this.bus.on("death", () => this.events.append("death", { at: Date.now() }));
120
+ this.bus.on("respawn", () => this.events.append("respawn", { at: Date.now() }));
121
+ this.bus.on("inventoryChanged", () => this.scanner.refresh());
122
+ this.bus.on("end", (reason) => this.onUnexpectedEnd(`连接结束: ${reason}`));
123
+ this.bus.on("kicked", (reason) => this.onUnexpectedEnd(`被踢出: ${reason}`));
124
+ this.bus.on("error", () => this.onUnexpectedEnd("bot 错误"));
125
+
126
+ await this.controller.connect();
127
+ await this.controller.waitForChunksLoaded();
128
+ this.connected = true;
129
+ this.scanner.refresh();
130
+ this.scanner.start();
131
+ this.startWatchdog();
132
+
133
+ for (const companion of this.companions) companion.start?.();
134
+ ctx.ctx.logger.info(`[MC/play] 已进入 ${this.server.name}`);
135
+ }
136
+
137
+ onQqMessage(text: string, opts: { sender?: string; atBot?: boolean } = {}): void {
138
+ this.events.append("qq_chat", {
139
+ text,
140
+ sender: opts.sender,
141
+ atBot: Boolean(opts.atBot),
142
+ });
143
+ }
144
+
145
+ private startWatchdog(): void {
146
+ this.watchdog = setInterval(() => this.tick(), 1_000);
147
+ }
148
+
149
+ private stopWatchdog(): void {
150
+ if (this.watchdog) {
151
+ clearInterval(this.watchdog);
152
+ this.watchdog = undefined;
153
+ }
154
+ }
155
+
156
+ private tick(): void {
157
+ if (this.stopped) return;
158
+ const elapsed = Date.now() - this.startedAt;
159
+ this.missionController?.tick();
160
+ this.pluginCtx.notifyChatScan?.();
161
+ if (elapsed >= this.server.maxPlayMs) {
162
+ void this.stop("time_up");
163
+ }
164
+ }
165
+
166
+ async stop(reason: string, opts: { skipGoodbye?: boolean } = {}): Promise<void> {
167
+ if (this.stopped) return;
168
+ this.stopped = true;
169
+ this.stopWatchdog();
170
+ this.scanner.stop();
171
+
172
+ for (const companion of this.companions) companion.stop?.();
173
+
174
+ if (this.connected && this.controller.isOnline()) {
175
+ if (!opts.skipGoodbye) await this.sayGoodbye();
176
+ await this.controller.disconnect("leaving");
177
+ }
178
+ this.connected = false;
179
+ this.bus.removeAll();
180
+ this.memory.clear();
181
+ this.events.clear();
182
+ this.workStatus = {
183
+ running: false,
184
+ goal: null,
185
+ summary: "已停止",
186
+ updatedAt: Date.now(),
187
+ };
188
+
189
+ const ctx = this.pluginCtx;
190
+ ctx.ctx.logger.info(`[MC/play] 已离开 ${this.server.name} (reason: ${reason})`);
191
+ }
192
+
193
+ private async onUnexpectedEnd(reason: string): Promise<void> {
194
+ if (this.stopped) return;
195
+ this.connected = false;
196
+ this.stopWatchdog();
197
+ this.scanner.stop();
198
+ for (const companion of this.companions) companion.stop?.();
199
+ this.stopped = true;
200
+ this.bus.removeAll();
201
+ this.memory.clear();
202
+ this.events.clear();
203
+
204
+ const ctx = this.pluginCtx;
205
+ ctx.ctx.logger.warn(`[MC/play] ${this.server.name} 异常断开: ${reason}`);
206
+ await this.notifyQq(`bot 已从 ${this.server.name} 断开: ${reason}`);
207
+ }
208
+
209
+ private async sayGoodbye(): Promise<void> {
210
+ const ctx = this.pluginCtx;
211
+ const main = ctx.mainInstance;
212
+ if (!main) {
213
+ this.controller.chat("我先走啦~");
214
+ return;
215
+ }
216
+ try {
217
+ const persona = main.getPrompt("persona") ?? "";
218
+ const prompt = buildGoodbyePrompt(persona, this.server);
219
+ const config = ctx.getPlayConfig();
220
+ const startedAt = Date.now();
221
+ const debugConfig = ctx.getPlayConfig();
222
+ void startedAt;
223
+ void debugConfig;
224
+ const text = await withTimeoutMs(
225
+ main.generateText({ prompt, messages: [] }),
226
+ ctx.config.goodbyeTimeoutMs,
227
+ );
228
+ const clean = text
229
+ .split("\n")
230
+ .map((l) => l.trim())
231
+ .filter(Boolean)
232
+ .slice(0, 2)
233
+ .join(" ");
234
+ this.controller.chat(clean || "我先走啦~");
235
+ } catch {
236
+ this.controller.chat("我先走啦~");
237
+ }
238
+ }
239
+
240
+ async notifyQq(message: string): Promise<void> {
241
+ const ctx = this.pluginCtx;
242
+ const bot = ctx.ctx.pickBot(this.binding.botSelfId);
243
+ if (!bot) return;
244
+ try {
245
+ await bot.sendGroupMsg(this.binding.groupId, message);
246
+ } catch (err) {
247
+ ctx.ctx.logger.error(`[MC/play] 发送 QQ 通知失败: ${err}`);
248
+ }
249
+ }
250
+
251
+ async sendQq(message: string): Promise<boolean> {
252
+ const ctx = this.pluginCtx;
253
+ const now = Date.now();
254
+ const windowMs = 60_000;
255
+ if (!this.qqWindow) this.qqWindow = { start: now, count: 0 };
256
+ if (now - this.qqWindow.start > windowMs) {
257
+ this.qqWindow = { start: now, count: 0 };
258
+ }
259
+ if (this.qqWindow.count >= ctx.config.qqSendPerMinute) {
260
+ ctx.ctx.logger.warn(`[MC/play] QQ 发送频率达上限,已丢弃: ${message}`);
261
+ return false;
262
+ }
263
+ this.qqWindow.count++;
264
+ const bot = ctx.ctx.pickBot(this.binding.botSelfId);
265
+ if (!bot) return false;
266
+ try {
267
+ await bot.sendGroupMsg(this.binding.groupId, message);
268
+ return true;
269
+ } catch (err) {
270
+ ctx.ctx.logger.error(`[MC/play] 发送 QQ 消息失败: ${err}`);
271
+ return false;
272
+ }
273
+ }
274
+
275
+ say(text: string): boolean {
276
+ if (!this.controller.isOnline()) return false;
277
+ this.controller.chat(text);
278
+ return true;
279
+ }
280
+
281
+ stopMovement(): void {
282
+ this.engine?.stopMission();
283
+ }
284
+
285
+ toggleOverlay(name: string, enabled: boolean, params?: Record<string, string>): boolean {
286
+ return this.engine?.toggleOverlay(name, enabled, params) ?? false;
287
+ }
288
+
289
+ isOverlayEnabled(name: string): boolean {
290
+ return this.engine?.isOverlayEnabled(name) ?? false;
291
+ }
292
+
293
+ clearBehaviors(): void {
294
+ this.engine?.clear();
295
+ }
296
+
297
+ getBehaviorStates(): import("./behavior/engine").BehaviorStateInfo[] {
298
+ const engine = this.engine;
299
+ const ctx = this.buildBehaviorContext();
300
+ if (!engine || !ctx) return [];
301
+ return engine.getStates(ctx);
302
+ }
303
+
304
+ getMemorySnapshot(): Record<string, { value: unknown; ageMs: number }> {
305
+ return this.memory.snapshot();
306
+ }
307
+
308
+ getBehaviorSnapshot(): BehaviorSnapshot | null {
309
+ if (!this.engine) return null;
310
+ if (!this.snapshotCollector) {
311
+ this.snapshotCollector = new SnapshotCollector({
312
+ bus: this.memory,
313
+ engine: this.engine,
314
+ cooldowns: this.cooldowns,
315
+ getContext: () => this.buildBehaviorContext(),
316
+ getMission: () => this.getCurrentMission(),
317
+ getLastOutcome: () => this.getLastMissionOutcome(),
318
+ });
319
+ }
320
+ return this.snapshotCollector.collect();
321
+ }
322
+
323
+ listBundles(): Array<{ id: string; description: string; mode: string | null }> {
324
+ return this.taskRegistry.list();
325
+ }
326
+
327
+ describeBundles(): ReturnType<TaskRegistry["describe"]> {
328
+ return this.taskRegistry.describe();
329
+ }
330
+
331
+ listActions(): Array<{ name: string; description: string }> {
332
+ return this.actionRegistry.list();
333
+ }
334
+
335
+ startMission(spec: MissionSpec): SwitchResult {
336
+ if (!this.engine) {
337
+ return {
338
+ kind: "rejected",
339
+ reason: "no_bot_session",
340
+ detail: "engine 尚未初始化",
341
+ };
342
+ }
343
+ if (!this.missionController) {
344
+ this.missionController = new MissionController({
345
+ registry: this.taskRegistry,
346
+ engine: this.engine,
347
+ bus: this.memory,
348
+ buildContext: () => this.buildBehaviorContext(),
349
+ log: (msg) => this.pluginCtx.ctx.logger.info(`[MC/play] ${msg}`),
350
+ onOutcome: (outcome) => this.handleMissionOutcome(outcome),
351
+ });
352
+ }
353
+ return this.missionController.startMission(spec);
354
+ }
355
+
356
+ stopMission(reason?: string): SwitchResult {
357
+ if (!this.missionController) {
358
+ return {
359
+ kind: "rejected",
360
+ reason: "rejected_by_engine",
361
+ detail: "没有进行中的任务",
362
+ };
363
+ }
364
+ return this.missionController.stopMission(reason);
365
+ }
366
+
367
+ getCurrentMission(): import("./state/mode").MissionState | null {
368
+ return this.missionController?.getCurrentMission() ?? null;
369
+ }
370
+
371
+ getLastMissionOutcome(): MissionOutcome | null {
372
+ return this.missionController?.getLastOutcome() ?? null;
373
+ }
374
+
375
+ updateWorkStatus(status: Partial<WorkStatus>): void {
376
+ this.workStatus = {
377
+ ...this.workStatus,
378
+ ...status,
379
+ updatedAt: Date.now(),
380
+ };
381
+ }
382
+
383
+ getWorkStatus(): WorkStatus {
384
+ return { ...this.workStatus };
385
+ }
386
+
387
+ getBot(): any {
388
+ return this.controller.bot;
389
+ }
390
+
391
+ getPluginCtx(): PlayPluginContext {
392
+ return this.pluginCtx;
393
+ }
394
+
395
+ requestMainAttention(reason: string): void {
396
+ this.events.append("work_completed", { reason, manual: true });
397
+ }
398
+
399
+ async performAction(
400
+ action: string,
401
+ params: Record<string, unknown>,
402
+ meta: {
403
+ directiveId?: string;
404
+ goalId?: string;
405
+ completesGoalOnSuccess?: boolean;
406
+ } = {},
407
+ ): Promise<ActionOutcome> {
408
+ const bot = this.controller.bot;
409
+ if (!bot) {
410
+ const outcome: ActionOutcome = {
411
+ action,
412
+ status: "failed",
413
+ code: "disconnected",
414
+ detail: "bot 尚未连接",
415
+ at: Date.now(),
416
+ directiveId: meta.directiveId,
417
+ completesDirectiveOnSuccess: meta.completesGoalOnSuccess ?? true,
418
+ };
419
+ this.recordActionOutcome(outcome);
420
+ return outcome;
421
+ }
422
+ const outcome = await this.actionRegistry.execute(
423
+ action,
424
+ params,
425
+ {
426
+ bot,
427
+ server: this.server,
428
+ stopCurrentTask: (reason) => {
429
+ this.stopMission(reason);
430
+ },
431
+ },
432
+ meta,
433
+ );
434
+ this.recordActionOutcome(outcome);
435
+ return outcome;
436
+ }
437
+
438
+ getLastActionOutcome(): ActionOutcome | null {
439
+ return null;
440
+ }
441
+
442
+ private lastSwitchResult: SwitchResult | null = null;
443
+
444
+ recordSwitchResult(result: SwitchResult): void {
445
+ this.lastSwitchResult = result;
446
+ }
447
+
448
+ consumeLastSwitchResult(): SwitchResult | null {
449
+ const r = this.lastSwitchResult;
450
+ this.lastSwitchResult = null;
451
+ return r;
452
+ }
453
+
454
+ private buildBehaviorContext(): import("./behavior/base-behavior").BehaviorContext | null {
455
+ const bot = this.controller.bot;
456
+ const movements = this.controller.getMovements();
457
+ if (!bot || !movements) return null;
458
+ return { bot, movements, log: (m: string) => this.pluginCtx.ctx.logger.info(`[MC/play] ${m}`) };
459
+ }
460
+
461
+ private handleMissionOutcome(outcome: MissionOutcome): void {
462
+ this.events.append("mission_outcome", outcome);
463
+ }
464
+
465
+ private recordActionOutcome(outcome: ActionOutcome): void {
466
+ this.events.append("action_outcome", outcome);
467
+ }
468
+
469
+ getStatus(): PlaySessionStatus {
470
+ return {
471
+ serverId: this.server.id,
472
+ serverName: this.server.name,
473
+ groupId: this.binding.groupId,
474
+ botSelfId: this.binding.botSelfId,
475
+ startedAt: this.startedAt,
476
+ connected: this.connected,
477
+ currentBehavior: this.engine?.currentLabel() ?? null,
478
+ lastAction: this.workStatus.goal ?? null,
479
+ workStatus: this.getWorkStatus(),
480
+ };
481
+ }
482
+
483
+ get isStopped(): boolean {
484
+ return this.stopped;
485
+ }
486
+ }
@@ -0,0 +1,40 @@
1
+ export class CooldownRegistry {
2
+ private until = new Map<string, number>();
3
+
4
+ set(key: string, durationMs: number, now = Date.now()): void {
5
+ this.until.set(key, now + durationMs);
6
+ }
7
+
8
+ setUntil(key: string, untilMs: number): void {
9
+ this.until.set(key, untilMs);
10
+ }
11
+
12
+ clear(key: string): void {
13
+ this.until.delete(key);
14
+ }
15
+
16
+ clearAll(): void {
17
+ this.until.clear();
18
+ }
19
+
20
+ /** 剩余毫秒,0 表示已就绪。 */
21
+ remaining(key: string, now = Date.now()): number {
22
+ const until = this.until.get(key);
23
+ if (until === undefined) return 0;
24
+ return until > now ? until - now : 0;
25
+ }
26
+
27
+ isReady(key: string, now = Date.now()): boolean {
28
+ return this.remaining(key, now) === 0;
29
+ }
30
+
31
+ /** 调试快照:key -> 剩余毫秒(已就绪的不输出)。 */
32
+ snapshot(now = Date.now()): Record<string, number> {
33
+ const out: Record<string, number> = {};
34
+ for (const [key, until] of this.until) {
35
+ const left = until - now;
36
+ if (left > 0) out[key] = left;
37
+ }
38
+ return out;
39
+ }
40
+ }
@@ -0,0 +1,72 @@
1
+ import type { PlayEventType } from "../types";
2
+
3
+ export type { PlayEventType };
4
+
5
+ export interface PlayEvent<T = unknown> {
6
+ seq: number;
7
+ at: number;
8
+ type: PlayEventType;
9
+ data: T;
10
+ }
11
+
12
+ export interface EventBatch {
13
+ events: PlayEvent[];
14
+ cursor: number;
15
+ }
16
+
17
+ export class PlayEventJournal {
18
+ private seq = 0;
19
+ private events: PlayEvent[] = [];
20
+ private listeners = new Set<(event: PlayEvent) => void>();
21
+
22
+ constructor(private readonly maxEntries = 300) {}
23
+
24
+ append<T>(type: PlayEventType, data: T): PlayEvent<T> {
25
+ const event: PlayEvent<T> = {
26
+ seq: ++this.seq,
27
+ at: Date.now(),
28
+ type,
29
+ data,
30
+ };
31
+ this.events.push(event);
32
+ if (this.events.length > this.maxEntries) {
33
+ this.events.splice(0, this.events.length - this.maxEntries);
34
+ }
35
+ for (const listener of this.listeners) {
36
+ try {
37
+ listener(event);
38
+ } catch {
39
+ // Journal consumers must not affect the game loop.
40
+ }
41
+ }
42
+ return event;
43
+ }
44
+
45
+ readAfter(
46
+ cursor: number,
47
+ filter?: (event: PlayEvent) => boolean,
48
+ limit?: number,
49
+ ): EventBatch {
50
+ const matched = this.events.filter(
51
+ (event) => event.seq > cursor && (!filter || filter(event)),
52
+ );
53
+ return {
54
+ events: limit && matched.length > limit ? matched.slice(-limit) : matched,
55
+ cursor: matched.at(-1)?.seq ?? cursor,
56
+ };
57
+ }
58
+
59
+ subscribe(listener: (event: PlayEvent) => void): () => void {
60
+ this.listeners.add(listener);
61
+ return () => this.listeners.delete(listener);
62
+ }
63
+
64
+ latestCursor(): number {
65
+ return this.seq;
66
+ }
67
+
68
+ clear(): void {
69
+ this.events = [];
70
+ this.listeners.clear();
71
+ }
72
+ }
@@ -0,0 +1,115 @@
1
+ export type MemoryKey =
2
+ | "self"
3
+ | "vitals"
4
+ | "position"
5
+ | "dimension"
6
+ | "inventory"
7
+ | "heldItem"
8
+ | "armor"
9
+ | "environment"
10
+ | "equipment"
11
+ | "nearestHostile"
12
+ | "nearestPlayer"
13
+ | "nearestCreeper"
14
+ | "nearestPassiveMob"
15
+ | "nearbyHostileNames"
16
+ | "nearbyPlayerNames"
17
+ | "combat"
18
+ | "movement"
19
+ | "mission";
20
+
21
+ export interface MemoryEntry<T = unknown> {
22
+ value: T;
23
+ updatedAt: number;
24
+ ttlMs: number;
25
+ }
26
+
27
+ export interface MemoryChange<T = unknown> {
28
+ key: MemoryKey;
29
+ prev: T | undefined;
30
+ next: T | undefined;
31
+ at: number;
32
+ }
33
+
34
+ export class MemoryBus {
35
+ private store = new Map<MemoryKey, MemoryEntry>();
36
+ private watchers = new Map<MemoryKey, Set<(c: MemoryChange) => void>>();
37
+
38
+ set<T>(key: MemoryKey, value: T, opts?: { ttlMs?: number }): void {
39
+ const prev = this.store.get(key);
40
+ const ttlMs = opts?.ttlMs ?? 0;
41
+ const at = Date.now();
42
+ this.store.set(key, { value, updatedAt: at, ttlMs });
43
+ this.notify(key, prev?.value as T | undefined, value, at);
44
+ }
45
+
46
+ get<T = unknown>(key: MemoryKey): T | undefined {
47
+ const entry = this.store.get(key);
48
+ if (!entry) return undefined;
49
+ if (entry.ttlMs > 0 && Date.now() - entry.updatedAt > entry.ttlMs) {
50
+ this.store.delete(key);
51
+ return undefined;
52
+ }
53
+ return entry.value as T;
54
+ }
55
+
56
+ has(key: MemoryKey): boolean {
57
+ return this.get(key) !== undefined;
58
+ }
59
+
60
+ watch<T = unknown>(key: MemoryKey, handler: (c: MemoryChange<T>) => void): () => void {
61
+ let set = this.watchers.get(key);
62
+ if (!set) {
63
+ set = new Set();
64
+ this.watchers.set(key, set);
65
+ }
66
+ const wrapped = handler as (c: MemoryChange) => void;
67
+ set.add(wrapped);
68
+ return () => {
69
+ set!.delete(wrapped);
70
+ };
71
+ }
72
+
73
+ update(mut: (bus: MemoryBus) => void): void {
74
+ mut(this);
75
+ }
76
+
77
+ delete(key: MemoryKey): void {
78
+ const prev = this.store.get(key);
79
+ if (!prev) return;
80
+ this.store.delete(key);
81
+ this.notify(key, prev.value, undefined, Date.now());
82
+ }
83
+
84
+ clear(): void {
85
+ const keys = [...this.store.keys()];
86
+ const at = Date.now();
87
+ for (const key of keys) {
88
+ const prev = this.store.get(key);
89
+ this.store.delete(key);
90
+ this.notify(key, prev?.value, undefined, at);
91
+ }
92
+ }
93
+
94
+ snapshot(): Record<string, { value: unknown; ageMs: number }> {
95
+ const now = Date.now();
96
+ const out: Record<string, { value: unknown; ageMs: number }> = {};
97
+ for (const [key, entry] of this.store) {
98
+ if (entry.ttlMs > 0 && now - entry.updatedAt > entry.ttlMs) continue;
99
+ out[key] = { value: entry.value, ageMs: now - entry.updatedAt };
100
+ }
101
+ return out;
102
+ }
103
+
104
+ private notify(key: MemoryKey, prev: unknown, next: unknown, at: number): void {
105
+ const set = this.watchers.get(key);
106
+ if (!set) return;
107
+ for (const handler of set) {
108
+ try {
109
+ handler({ key, prev, next, at });
110
+ } catch {
111
+ // 观察者错误不影响主流程
112
+ }
113
+ }
114
+ }
115
+ }