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,57 @@
|
|
|
1
|
+
export type BehaviorMode = "EMERGENCY" | "MISSION" | "IDLE";
|
|
2
|
+
|
|
3
|
+
export type MissionStatus = "running" | "succeeded" | "failed" | "blocked" | "cancelled";
|
|
4
|
+
|
|
5
|
+
export type MissionErrorCode =
|
|
6
|
+
| "target_not_found"
|
|
7
|
+
| "target_lost"
|
|
8
|
+
| "resource_not_found"
|
|
9
|
+
| "missing_item"
|
|
10
|
+
| "missing_tool"
|
|
11
|
+
| "inventory_full"
|
|
12
|
+
| "path_unreachable"
|
|
13
|
+
| "path_timeout"
|
|
14
|
+
| "permission_denied"
|
|
15
|
+
| "command_rejected"
|
|
16
|
+
| "disconnected"
|
|
17
|
+
| "cancelled"
|
|
18
|
+
| "timeout"
|
|
19
|
+
| "unknown";
|
|
20
|
+
|
|
21
|
+
export interface MissionState {
|
|
22
|
+
missionId: string;
|
|
23
|
+
bundleId: string;
|
|
24
|
+
params: Record<string, unknown>;
|
|
25
|
+
startedAt: number;
|
|
26
|
+
status: "running";
|
|
27
|
+
progress: unknown;
|
|
28
|
+
objective?: string;
|
|
29
|
+
directiveId?: string;
|
|
30
|
+
completesDirectiveOnSuccess: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MissionOutcome {
|
|
34
|
+
missionId: string;
|
|
35
|
+
bundleId: string;
|
|
36
|
+
status: Exclude<MissionStatus, "running">;
|
|
37
|
+
code?: MissionErrorCode;
|
|
38
|
+
detail?: string;
|
|
39
|
+
progress: unknown;
|
|
40
|
+
startedAt: number;
|
|
41
|
+
endedAt: number;
|
|
42
|
+
directiveId?: string;
|
|
43
|
+
completesDirectiveOnSuccess: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ModeSwitch {
|
|
47
|
+
from: BehaviorMode | null;
|
|
48
|
+
to: BehaviorMode;
|
|
49
|
+
reason: string;
|
|
50
|
+
at: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ModeState {
|
|
54
|
+
current: BehaviorMode;
|
|
55
|
+
mission: MissionState | null;
|
|
56
|
+
lastSwitch: ModeSwitch;
|
|
57
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { Bot } from "mineflayer";
|
|
2
|
+
import type { MemoryBus } from "../memory-bus";
|
|
3
|
+
import type { PlayEventType } from "../event-journal";
|
|
4
|
+
import {
|
|
5
|
+
listNearbyHostiles,
|
|
6
|
+
nearestCreeper,
|
|
7
|
+
nearestHostile,
|
|
8
|
+
nearestPassiveMob,
|
|
9
|
+
} from "../../util/entities";
|
|
10
|
+
|
|
11
|
+
export interface EntityRef {
|
|
12
|
+
id: number;
|
|
13
|
+
name: string;
|
|
14
|
+
position: { x: number; y: number; z: number };
|
|
15
|
+
distance: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PlayerRef {
|
|
19
|
+
username: string;
|
|
20
|
+
position: { x: number; y: number; z: number };
|
|
21
|
+
distance: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface EntityScannerOptions {
|
|
25
|
+
bus: MemoryBus;
|
|
26
|
+
bot: () => Bot | null;
|
|
27
|
+
intervalMs?: number;
|
|
28
|
+
hostileRadius?: number;
|
|
29
|
+
playerRadius?: number;
|
|
30
|
+
passiveRadius?: number;
|
|
31
|
+
creeperRadius?: number;
|
|
32
|
+
onEvent?: (type: PlayEventType, data: unknown) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function toEntityRef(entity: any, distance: number): EntityRef {
|
|
36
|
+
const pos = entity.position;
|
|
37
|
+
return {
|
|
38
|
+
id: entity.id,
|
|
39
|
+
name: String(entity.name ?? entity.entityType ?? "unknown")
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/^minecraft:/, ""),
|
|
42
|
+
position: pos
|
|
43
|
+
? {
|
|
44
|
+
x: pos.x,
|
|
45
|
+
y: pos.y,
|
|
46
|
+
z: pos.z,
|
|
47
|
+
}
|
|
48
|
+
: { x: 0, y: 0, z: 0 },
|
|
49
|
+
distance,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toPlayerRef(
|
|
54
|
+
username: string,
|
|
55
|
+
entity: any,
|
|
56
|
+
distance: number,
|
|
57
|
+
): PlayerRef {
|
|
58
|
+
const pos = entity?.position;
|
|
59
|
+
return {
|
|
60
|
+
username,
|
|
61
|
+
position: pos
|
|
62
|
+
? { x: pos.x, y: pos.y, z: pos.z }
|
|
63
|
+
: { x: 0, y: 0, z: 0 },
|
|
64
|
+
distance,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function distanceTo(myPos: any, other: any): number {
|
|
69
|
+
if (!other?.position) return Infinity;
|
|
70
|
+
return Math.hypot(
|
|
71
|
+
other.position.x - myPos.x,
|
|
72
|
+
other.position.y - myPos.y,
|
|
73
|
+
other.position.z - myPos.z,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class EntityScanner {
|
|
78
|
+
private readonly bus: MemoryBus;
|
|
79
|
+
private readonly bot: () => Bot | null;
|
|
80
|
+
private readonly intervalMs: number;
|
|
81
|
+
private readonly hostileRadius: number;
|
|
82
|
+
private readonly playerRadius: number;
|
|
83
|
+
private readonly passiveRadius: number;
|
|
84
|
+
private readonly creeperRadius: number;
|
|
85
|
+
private readonly onEvent?: (type: PlayEventType, data: unknown) => void;
|
|
86
|
+
private timer?: NodeJS.Timeout;
|
|
87
|
+
private previousDay?: boolean;
|
|
88
|
+
private previousVitalsBucket = "";
|
|
89
|
+
private previousInventorySignature = "";
|
|
90
|
+
private previousEquipmentSignature = "";
|
|
91
|
+
private previousEquipment: Record<string, EquipmentSummary | null> | null = null;
|
|
92
|
+
|
|
93
|
+
constructor(opts: EntityScannerOptions) {
|
|
94
|
+
this.bus = opts.bus;
|
|
95
|
+
this.bot = opts.bot;
|
|
96
|
+
this.intervalMs = opts.intervalMs ?? 500;
|
|
97
|
+
this.hostileRadius = opts.hostileRadius ?? 16;
|
|
98
|
+
this.playerRadius = opts.playerRadius ?? 16;
|
|
99
|
+
this.passiveRadius = opts.passiveRadius ?? 20;
|
|
100
|
+
this.creeperRadius = opts.creeperRadius ?? 6;
|
|
101
|
+
this.onEvent = opts.onEvent;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
start(): void {
|
|
105
|
+
if (this.timer) return;
|
|
106
|
+
this.timer = setInterval(() => this.refresh(), this.intervalMs);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
stop(): void {
|
|
110
|
+
if (this.timer) {
|
|
111
|
+
clearInterval(this.timer);
|
|
112
|
+
this.timer = undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
refresh(): void {
|
|
117
|
+
const bot = this.bot();
|
|
118
|
+
if (!bot?.entity?.position) return;
|
|
119
|
+
const myPos = bot.entity.position;
|
|
120
|
+
|
|
121
|
+
const hostile = nearestHostile(bot, this.hostileRadius);
|
|
122
|
+
const passive = nearestPassiveMob(bot, this.passiveRadius);
|
|
123
|
+
const creeper = nearestCreeper(bot, this.creeperRadius);
|
|
124
|
+
const hostileNames = listNearbyHostiles(bot, this.hostileRadius);
|
|
125
|
+
|
|
126
|
+
const hostileRef = hostile ? toEntityRef(hostile, distanceTo(myPos, hostile)) : null;
|
|
127
|
+
const passiveRef = passive ? toEntityRef(passive, distanceTo(myPos, passive)) : null;
|
|
128
|
+
const creeperRef = creeper ? toEntityRef(creeper, distanceTo(myPos, creeper)) : null;
|
|
129
|
+
|
|
130
|
+
const { nearestPlayer, playerNames } = this.scanPlayers(bot, myPos);
|
|
131
|
+
const vitals = {
|
|
132
|
+
health: bot.health ?? 20,
|
|
133
|
+
food: bot.food ?? 20,
|
|
134
|
+
oxygen: bot.oxygenLevel ?? 20,
|
|
135
|
+
};
|
|
136
|
+
const inventory = summarizeInventory(bot);
|
|
137
|
+
const equipment = summarizeEquipment(bot);
|
|
138
|
+
const environment = {
|
|
139
|
+
isDay: Boolean(bot.time?.isDay ?? true),
|
|
140
|
+
timeOfDay: Number(bot.time?.timeOfDay ?? 0),
|
|
141
|
+
weather: (bot as any).thunderState > 0 ? "thunder" : (bot as any).isRaining ? "rain" : "clear",
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
this.bus.update((m) => {
|
|
145
|
+
m.set("nearestHostile", hostileRef, { ttlMs: 600 });
|
|
146
|
+
m.set("nearestPassiveMob", passiveRef, { ttlMs: 1000 });
|
|
147
|
+
m.set("nearestCreeper", creeperRef, { ttlMs: 400 });
|
|
148
|
+
m.set("nearestPlayer", nearestPlayer, { ttlMs: 600 });
|
|
149
|
+
m.set("nearbyHostileNames", hostileNames, { ttlMs: 600 });
|
|
150
|
+
m.set("nearbyPlayerNames", playerNames, { ttlMs: 600 });
|
|
151
|
+
m.set("vitals", vitals, { ttlMs: 1000 });
|
|
152
|
+
m.set("dimension", bot.game?.dimension ?? "overworld", { ttlMs: 5000 });
|
|
153
|
+
m.set("position", { x: myPos.x, y: myPos.y, z: myPos.z }, { ttlMs: 1000 });
|
|
154
|
+
m.set("inventory", inventory, { ttlMs: 2000 });
|
|
155
|
+
m.set("equipment", equipment, { ttlMs: 2000 });
|
|
156
|
+
m.set("environment", environment, { ttlMs: 2000 });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.emitChanges(vitals, inventory, equipment, environment);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private emitChanges(
|
|
163
|
+
vitals: { health: number; food: number; oxygen: number },
|
|
164
|
+
inventory: unknown,
|
|
165
|
+
equipment: Record<string, EquipmentSummary | null>,
|
|
166
|
+
environment: { isDay: boolean; timeOfDay: number; weather: string },
|
|
167
|
+
): void {
|
|
168
|
+
const bucket = `${thresholdBucket(vitals.health)}:${thresholdBucket(vitals.food)}:${thresholdBucket(vitals.oxygen)}`;
|
|
169
|
+
if (this.previousVitalsBucket && bucket !== this.previousVitalsBucket) {
|
|
170
|
+
this.onEvent?.("vitals_threshold", { ...vitals, bucket });
|
|
171
|
+
}
|
|
172
|
+
this.previousVitalsBucket = bucket;
|
|
173
|
+
|
|
174
|
+
if (this.previousDay !== undefined && environment.isDay !== this.previousDay) {
|
|
175
|
+
this.onEvent?.("day_phase", environment);
|
|
176
|
+
}
|
|
177
|
+
this.previousDay = environment.isDay;
|
|
178
|
+
|
|
179
|
+
const inventorySignature = JSON.stringify(inventory);
|
|
180
|
+
if (this.previousInventorySignature && inventorySignature !== this.previousInventorySignature) {
|
|
181
|
+
this.onEvent?.("inventory_change", inventory);
|
|
182
|
+
}
|
|
183
|
+
this.previousInventorySignature = inventorySignature;
|
|
184
|
+
|
|
185
|
+
const equipmentSignature = JSON.stringify(equipment);
|
|
186
|
+
if (this.previousEquipmentSignature && equipmentSignature !== this.previousEquipmentSignature) {
|
|
187
|
+
this.onEvent?.("equipment_change", {
|
|
188
|
+
equipment,
|
|
189
|
+
critical: Object.values(equipment).some(
|
|
190
|
+
(item) => item?.durabilityRatio !== undefined && item.durabilityRatio <= 0.1,
|
|
191
|
+
),
|
|
192
|
+
missing: this.previousEquipment
|
|
193
|
+
? Object.keys(this.previousEquipment).some(
|
|
194
|
+
(key) => this.previousEquipment?.[key] && !equipment[key],
|
|
195
|
+
)
|
|
196
|
+
: false,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
this.previousEquipmentSignature = equipmentSignature;
|
|
200
|
+
this.previousEquipment = equipment;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private scanPlayers(
|
|
204
|
+
bot: any,
|
|
205
|
+
myPos: any,
|
|
206
|
+
): { nearestPlayer: PlayerRef | null; playerNames: string[] } {
|
|
207
|
+
const names: string[] = [];
|
|
208
|
+
let nearest: { ref: PlayerRef; dist: number } | null = null;
|
|
209
|
+
const players: any = bot.players ?? {};
|
|
210
|
+
for (const username in players) {
|
|
211
|
+
if (username === bot.username) continue;
|
|
212
|
+
const entry = players[username];
|
|
213
|
+
const entity = entry?.entity;
|
|
214
|
+
if (!entity?.position) continue;
|
|
215
|
+
const dist = distanceTo(myPos, entity);
|
|
216
|
+
if (dist > this.playerRadius) continue;
|
|
217
|
+
names.push(username);
|
|
218
|
+
if (!nearest || dist < nearest.dist) {
|
|
219
|
+
nearest = { ref: toPlayerRef(username, entity, dist), dist };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
names.sort();
|
|
223
|
+
return { nearestPlayer: nearest?.ref ?? null, playerNames: names };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function thresholdBucket(value: number): string {
|
|
228
|
+
if (value <= 0) return "empty";
|
|
229
|
+
if (value <= 6) return "critical";
|
|
230
|
+
if (value <= 12) return "low";
|
|
231
|
+
return "ok";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function summarizeInventory(bot: any): Array<{ name: string; count: number }> {
|
|
235
|
+
const totals = new Map<string, number>();
|
|
236
|
+
for (const item of bot.inventory?.items?.() ?? []) {
|
|
237
|
+
totals.set(item.name, (totals.get(item.name) ?? 0) + Number(item.count ?? 0));
|
|
238
|
+
}
|
|
239
|
+
return [...totals.entries()]
|
|
240
|
+
.map(([name, count]) => ({ name, count }))
|
|
241
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface EquipmentSummary {
|
|
245
|
+
name: string;
|
|
246
|
+
durabilityRatio?: number;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function summarizeEquipment(bot: any): Record<string, EquipmentSummary | null> {
|
|
250
|
+
const itemSummary = (item: any): EquipmentSummary | null => {
|
|
251
|
+
if (!item) return null;
|
|
252
|
+
const max = Number(bot.registry?.items?.[item.type]?.maxDurability ?? 0);
|
|
253
|
+
const used = Number(item.durabilityUsed ?? 0);
|
|
254
|
+
return {
|
|
255
|
+
name: item.name,
|
|
256
|
+
...(max > 0 ? { durabilityRatio: Math.max(0, max - used) / max } : {}),
|
|
257
|
+
};
|
|
258
|
+
};
|
|
259
|
+
const read = (destination: string): EquipmentSummary | null => {
|
|
260
|
+
try {
|
|
261
|
+
const slot = bot.getEquipmentDestSlot?.(destination);
|
|
262
|
+
return slot == null ? null : itemSummary(bot.inventory?.slots?.[slot]);
|
|
263
|
+
} catch {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
hand: itemSummary(bot.heldItem),
|
|
269
|
+
offHand: read("off-hand"),
|
|
270
|
+
head: read("head"),
|
|
271
|
+
torso: read("torso"),
|
|
272
|
+
legs: read("legs"),
|
|
273
|
+
feet: read("feet"),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import type { CooldownRegistry } from "./cooldowns";
|
|
2
|
+
import type { MemoryBus } from "./memory-bus";
|
|
3
|
+
import type { MissionOutcome, MissionState, ModeState } from "./mode";
|
|
4
|
+
import type { Behavior, BehaviorContext } from "../behavior/base-behavior";
|
|
5
|
+
import type { BehaviorEngine, BehaviorStateInfo } from "../behavior/engine";
|
|
6
|
+
import {
|
|
7
|
+
entityDistance,
|
|
8
|
+
entityName,
|
|
9
|
+
isHostileEntity,
|
|
10
|
+
isPassiveMob,
|
|
11
|
+
} from "../util/entities";
|
|
12
|
+
import { SectionRevisionTracker } from "../ai/context-builder";
|
|
13
|
+
|
|
14
|
+
export interface ItemSnapshot {
|
|
15
|
+
slot: number | null;
|
|
16
|
+
name: string;
|
|
17
|
+
count: number;
|
|
18
|
+
durability?: { used: number; max: number; remaining: number; ratio: number };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface EntitySnapshot {
|
|
22
|
+
id: number;
|
|
23
|
+
name: string;
|
|
24
|
+
kind: "player" | "hostile" | "passive" | "item" | "other";
|
|
25
|
+
username?: string;
|
|
26
|
+
distance: number;
|
|
27
|
+
position: { x: number; y: number; z: number };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BehaviorSnapshot {
|
|
31
|
+
seq: number;
|
|
32
|
+
takenAt: number;
|
|
33
|
+
revisions: Record<string, number>;
|
|
34
|
+
self: {
|
|
35
|
+
username: string;
|
|
36
|
+
health: number;
|
|
37
|
+
food: number;
|
|
38
|
+
saturation: number;
|
|
39
|
+
oxygen: number;
|
|
40
|
+
onGround: boolean;
|
|
41
|
+
velocity: { x: number; y: number; z: number } | null;
|
|
42
|
+
gameMode: string;
|
|
43
|
+
experience: unknown;
|
|
44
|
+
};
|
|
45
|
+
vitals: { health: number; food: number; oxygen: number };
|
|
46
|
+
position: { x: number; y: number; z: number } | null;
|
|
47
|
+
dimension: string;
|
|
48
|
+
heldItem: ItemSnapshot | null;
|
|
49
|
+
inventory: {
|
|
50
|
+
items: ItemSnapshot[];
|
|
51
|
+
emptySlots: number;
|
|
52
|
+
full: boolean;
|
|
53
|
+
};
|
|
54
|
+
equipment: {
|
|
55
|
+
hand: ItemSnapshot | null;
|
|
56
|
+
offHand: ItemSnapshot | null;
|
|
57
|
+
head: ItemSnapshot | null;
|
|
58
|
+
torso: ItemSnapshot | null;
|
|
59
|
+
legs: ItemSnapshot | null;
|
|
60
|
+
feet: ItemSnapshot | null;
|
|
61
|
+
};
|
|
62
|
+
entities: EntitySnapshot[];
|
|
63
|
+
environment: {
|
|
64
|
+
timeOfDay: number;
|
|
65
|
+
isDay: boolean;
|
|
66
|
+
weather: "clear" | "rain" | "thunder";
|
|
67
|
+
biome: string | null;
|
|
68
|
+
terrain: {
|
|
69
|
+
below: string | null;
|
|
70
|
+
feet: string | null;
|
|
71
|
+
head: string | null;
|
|
72
|
+
nearbyInteresting: Array<{
|
|
73
|
+
name: string;
|
|
74
|
+
distance: number;
|
|
75
|
+
position: { x: number; y: number; z: number };
|
|
76
|
+
}>;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
sensor: {
|
|
80
|
+
nearestHostile: unknown;
|
|
81
|
+
nearestPlayer: unknown;
|
|
82
|
+
nearestCreeper: unknown;
|
|
83
|
+
nearestPassiveMob: unknown;
|
|
84
|
+
nearbyHostileNames: string[];
|
|
85
|
+
nearbyPlayerNames: string[];
|
|
86
|
+
};
|
|
87
|
+
mode: ModeState;
|
|
88
|
+
mission: { current: MissionState | null; lastOutcome: MissionOutcome | null };
|
|
89
|
+
activeBehaviors: Array<
|
|
90
|
+
BehaviorStateInfo & { internalState: Record<string, unknown> }
|
|
91
|
+
>;
|
|
92
|
+
cooldowns: Record<string, number>;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type WorldSnapshot = BehaviorSnapshot;
|
|
96
|
+
|
|
97
|
+
export interface SnapshotCollectorOptions {
|
|
98
|
+
bus: MemoryBus;
|
|
99
|
+
engine: BehaviorEngine;
|
|
100
|
+
cooldowns: CooldownRegistry;
|
|
101
|
+
getContext: () => BehaviorContext | null;
|
|
102
|
+
getMission?: () => MissionState | null;
|
|
103
|
+
getLastOutcome?: () => MissionOutcome | null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class SnapshotCollector {
|
|
107
|
+
private seq = 0;
|
|
108
|
+
private readonly revisions = new SectionRevisionTracker();
|
|
109
|
+
|
|
110
|
+
constructor(private readonly opts: SnapshotCollectorOptions) {}
|
|
111
|
+
|
|
112
|
+
collect(): BehaviorSnapshot | null {
|
|
113
|
+
const ctx = this.opts.getContext();
|
|
114
|
+
if (!ctx) return null;
|
|
115
|
+
const bot: any = ctx.bot;
|
|
116
|
+
const states = this.opts.engine.getStates(ctx);
|
|
117
|
+
const all = this.opts.engine.getAllBehaviors();
|
|
118
|
+
const position = toPosition(bot.entity?.position);
|
|
119
|
+
const inventoryItems = collectInventory(bot);
|
|
120
|
+
const equipment = collectEquipment(bot);
|
|
121
|
+
const entities = collectEntities(bot);
|
|
122
|
+
const environment = collectEnvironment(bot);
|
|
123
|
+
const vitals = {
|
|
124
|
+
health: Number(bot.health ?? 0),
|
|
125
|
+
food: Number(bot.food ?? 0),
|
|
126
|
+
oxygen: Number(bot.oxygenLevel ?? 0),
|
|
127
|
+
};
|
|
128
|
+
const self = {
|
|
129
|
+
username: String(bot.username ?? "unknown"),
|
|
130
|
+
health: vitals.health,
|
|
131
|
+
food: vitals.food,
|
|
132
|
+
saturation: Number(bot.foodSaturation ?? 0),
|
|
133
|
+
oxygen: vitals.oxygen,
|
|
134
|
+
onGround: Boolean(bot.entity?.onGround),
|
|
135
|
+
velocity: toPosition(bot.entity?.velocity),
|
|
136
|
+
gameMode: String(bot.game?.gameMode ?? "unknown"),
|
|
137
|
+
experience: bot.experience ?? null,
|
|
138
|
+
};
|
|
139
|
+
const mission = {
|
|
140
|
+
current: this.opts.getMission?.() ?? null,
|
|
141
|
+
lastOutcome: this.opts.getLastOutcome?.() ?? null,
|
|
142
|
+
};
|
|
143
|
+
const inventory = {
|
|
144
|
+
items: inventoryItems,
|
|
145
|
+
emptySlots: getEmptySlots(bot),
|
|
146
|
+
full: getEmptySlots(bot) === 0,
|
|
147
|
+
};
|
|
148
|
+
const revisionValues = {
|
|
149
|
+
self,
|
|
150
|
+
inventory,
|
|
151
|
+
equipment,
|
|
152
|
+
entities,
|
|
153
|
+
environment,
|
|
154
|
+
mission,
|
|
155
|
+
};
|
|
156
|
+
const revisions = Object.fromEntries(
|
|
157
|
+
Object.entries(revisionValues).map(([key, value]) => [
|
|
158
|
+
key,
|
|
159
|
+
this.revisions.revision(key, value),
|
|
160
|
+
]),
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
seq: ++this.seq,
|
|
165
|
+
takenAt: Date.now(),
|
|
166
|
+
revisions,
|
|
167
|
+
self,
|
|
168
|
+
vitals,
|
|
169
|
+
position,
|
|
170
|
+
dimension: String(bot.game?.dimension ?? "overworld"),
|
|
171
|
+
heldItem: itemSnapshot(bot, bot.heldItem, bot.heldItem?.slot ?? null),
|
|
172
|
+
inventory,
|
|
173
|
+
equipment,
|
|
174
|
+
entities,
|
|
175
|
+
environment,
|
|
176
|
+
sensor: {
|
|
177
|
+
nearestHostile: this.opts.bus.get("nearestHostile") ?? null,
|
|
178
|
+
nearestPlayer: this.opts.bus.get("nearestPlayer") ?? null,
|
|
179
|
+
nearestCreeper: this.opts.bus.get("nearestCreeper") ?? null,
|
|
180
|
+
nearestPassiveMob: this.opts.bus.get("nearestPassiveMob") ?? null,
|
|
181
|
+
nearbyHostileNames: this.opts.bus.get("nearbyHostileNames") ?? [],
|
|
182
|
+
nearbyPlayerNames: this.opts.bus.get("nearbyPlayerNames") ?? [],
|
|
183
|
+
},
|
|
184
|
+
mode: this.opts.engine.modeState(),
|
|
185
|
+
mission,
|
|
186
|
+
activeBehaviors: states.map((state) => ({
|
|
187
|
+
...state,
|
|
188
|
+
internalState: this.contributesFor(all, state.name, ctx),
|
|
189
|
+
})),
|
|
190
|
+
cooldowns: this.opts.cooldowns.snapshot(),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private contributesFor(
|
|
195
|
+
all: Behavior[],
|
|
196
|
+
name: string,
|
|
197
|
+
ctx: BehaviorContext,
|
|
198
|
+
): Record<string, unknown> {
|
|
199
|
+
const behavior = all.find((candidate) => candidate.name === name);
|
|
200
|
+
if (!behavior) return {};
|
|
201
|
+
try {
|
|
202
|
+
return behavior.contributesState(ctx) ?? {};
|
|
203
|
+
} catch {
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function collectInventory(bot: any): ItemSnapshot[] {
|
|
210
|
+
const slots: any[] = bot.inventory?.slots ?? [];
|
|
211
|
+
return slots
|
|
212
|
+
.map((item, slot) => itemSnapshot(bot, item, slot))
|
|
213
|
+
.filter((item): item is ItemSnapshot => item !== null)
|
|
214
|
+
.sort((a, b) => (a.slot ?? 0) - (b.slot ?? 0));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function collectEquipment(bot: any): BehaviorSnapshot["equipment"] {
|
|
218
|
+
return {
|
|
219
|
+
hand: itemSnapshot(bot, bot.heldItem, bot.heldItem?.slot ?? null),
|
|
220
|
+
offHand: equipmentAt(bot, "off-hand"),
|
|
221
|
+
head: equipmentAt(bot, "head"),
|
|
222
|
+
torso: equipmentAt(bot, "torso"),
|
|
223
|
+
legs: equipmentAt(bot, "legs"),
|
|
224
|
+
feet: equipmentAt(bot, "feet"),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function equipmentAt(bot: any, destination: string): ItemSnapshot | null {
|
|
229
|
+
try {
|
|
230
|
+
const slot = bot.getEquipmentDestSlot?.(destination);
|
|
231
|
+
return slot == null
|
|
232
|
+
? null
|
|
233
|
+
: itemSnapshot(bot, bot.inventory?.slots?.[slot], slot);
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function itemSnapshot(
|
|
240
|
+
bot: any,
|
|
241
|
+
item: any,
|
|
242
|
+
slot: number | null,
|
|
243
|
+
): ItemSnapshot | null {
|
|
244
|
+
if (!item) return null;
|
|
245
|
+
const max = Number(bot.registry?.items?.[item.type]?.maxDurability ?? 0);
|
|
246
|
+
const used = Number(item.durabilityUsed ?? 0);
|
|
247
|
+
return {
|
|
248
|
+
slot,
|
|
249
|
+
name: String(item.name ?? "unknown").replace(/^minecraft:/, ""),
|
|
250
|
+
count: Number(item.count ?? 1),
|
|
251
|
+
...(max > 0
|
|
252
|
+
? {
|
|
253
|
+
durability: {
|
|
254
|
+
used,
|
|
255
|
+
max,
|
|
256
|
+
remaining: Math.max(0, max - used),
|
|
257
|
+
ratio: Math.max(0, max - used) / max,
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
: {}),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function collectEntities(bot: any): EntitySnapshot[] {
|
|
265
|
+
const source = Object.values(bot.entities ?? {}) as any[];
|
|
266
|
+
return source
|
|
267
|
+
.filter((entity) => entity && entity !== bot.entity && entity.position)
|
|
268
|
+
.map((entity) => {
|
|
269
|
+
const distance = entityDistance(bot.entity, entity);
|
|
270
|
+
const name = entityName(entity);
|
|
271
|
+
const playerEntry = Object.entries(bot.players ?? {}).find(
|
|
272
|
+
([, value]: any) => value?.entity?.id === entity.id,
|
|
273
|
+
);
|
|
274
|
+
const kind: EntitySnapshot["kind"] = playerEntry
|
|
275
|
+
? "player"
|
|
276
|
+
: isHostileEntity(entity)
|
|
277
|
+
? "hostile"
|
|
278
|
+
: isPassiveMob(entity)
|
|
279
|
+
? "passive"
|
|
280
|
+
: name === "item"
|
|
281
|
+
? "item"
|
|
282
|
+
: "other";
|
|
283
|
+
return {
|
|
284
|
+
id: Number(entity.id),
|
|
285
|
+
name,
|
|
286
|
+
kind,
|
|
287
|
+
username: playerEntry?.[0],
|
|
288
|
+
distance: Math.round(distance * 10) / 10,
|
|
289
|
+
position: toPosition(entity.position)!,
|
|
290
|
+
};
|
|
291
|
+
})
|
|
292
|
+
.filter((entity) => entity.distance <= 24)
|
|
293
|
+
.sort((a, b) => a.distance - b.distance || a.name.localeCompare(b.name))
|
|
294
|
+
.slice(0, 32);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function collectEnvironment(bot: any): BehaviorSnapshot["environment"] {
|
|
298
|
+
const position = bot.entity?.position;
|
|
299
|
+
const block = (dy: number) =>
|
|
300
|
+
position ? bot.blockAt(position.offset(0, dy, 0), false) : null;
|
|
301
|
+
const biome = position
|
|
302
|
+
? (bot.blockAt(position, false)?.biome?.name ?? null)
|
|
303
|
+
: null;
|
|
304
|
+
return {
|
|
305
|
+
timeOfDay: Number(bot.time?.timeOfDay ?? 0),
|
|
306
|
+
isDay: Boolean(bot.time?.isDay ?? true),
|
|
307
|
+
weather:
|
|
308
|
+
bot.thunderState > 0 ? "thunder" : bot.isRaining ? "rain" : "clear",
|
|
309
|
+
biome,
|
|
310
|
+
terrain: {
|
|
311
|
+
below: block(-1)?.name ?? null,
|
|
312
|
+
feet: block(0)?.name ?? null,
|
|
313
|
+
head: block(1)?.name ?? null,
|
|
314
|
+
nearbyInteresting: collectInterestingBlocks(bot),
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function collectInterestingBlocks(
|
|
320
|
+
bot: any,
|
|
321
|
+
): BehaviorSnapshot["environment"]["terrain"]["nearbyInteresting"] {
|
|
322
|
+
const origin = bot.entity?.position;
|
|
323
|
+
if (!origin || typeof bot.findBlocks !== "function") return [];
|
|
324
|
+
const interesting =
|
|
325
|
+
/(_ore$|_log$|crafting_table|furnace|chest|barrel|_bed$|water|lava|fire)/;
|
|
326
|
+
try {
|
|
327
|
+
const positions = bot.findBlocks({
|
|
328
|
+
matching: (block: any) => !!block && interesting.test(String(block.name)),
|
|
329
|
+
maxDistance: 16,
|
|
330
|
+
count: 48,
|
|
331
|
+
});
|
|
332
|
+
return positions
|
|
333
|
+
.map((position: any) => {
|
|
334
|
+
const block = bot.blockAt(position, false);
|
|
335
|
+
return {
|
|
336
|
+
name: String(block?.name ?? "unknown"),
|
|
337
|
+
distance: Math.round(origin.distanceTo(position) * 10) / 10,
|
|
338
|
+
position: toPosition(position)!,
|
|
339
|
+
};
|
|
340
|
+
})
|
|
341
|
+
.sort(
|
|
342
|
+
(a: any, b: any) =>
|
|
343
|
+
a.distance - b.distance || a.name.localeCompare(b.name),
|
|
344
|
+
);
|
|
345
|
+
} catch {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function getEmptySlots(bot: any): number {
|
|
351
|
+
const direct = bot.inventory?.emptySlotCount?.();
|
|
352
|
+
if (Number.isFinite(direct)) return Number(direct);
|
|
353
|
+
const slots = bot.inventory?.slots?.slice(9, 45) ?? [];
|
|
354
|
+
return slots.filter((item: any) => !item).length;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function toPosition(value: any): { x: number; y: number; z: number } | null {
|
|
358
|
+
if (!value) return null;
|
|
359
|
+
return { x: Number(value.x), y: Number(value.y), z: Number(value.z) };
|
|
360
|
+
}
|