@lzimul/minecraft-mcp 1.0.4 → 1.0.5
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/LICENSE +21 -21
- package/README.md +238 -219
- package/build/config/server-info.js +4 -4
- package/build/config/storage.js +2 -1
- package/build/config/world-limits.js +2 -0
- package/build/enums/move-direction.enum.js +9 -0
- package/build/index.js +3 -1
- package/build/resources/dimension-resources.js +1 -1
- package/build/tools/entity-tools.js +318 -5
- package/build/tools/world-tools.js +388 -1
- package/build/utils/logger.js +1 -1
- package/build/world/block-spec.js +3 -1
- package/build/world/entity-inventory.js +21 -4
- package/build/world/simulated-dimension.js +135 -9
- package/build/world/stores/file-world-store.js +47 -39
- package/build/world/stores/memory-world-store.js +5 -0
- package/build/world/stores/serialized-entity.js +4 -3
- package/build/world/stores/serialized-snapshot.js +35 -0
- package/lib/app/server.d.ts.map +1 -1
- package/lib/config/dimensions.d.ts.map +1 -1
- package/lib/config/server-info.d.ts.map +1 -1
- package/lib/config/storage.d.ts.map +1 -1
- package/lib/config/world-limits.d.ts +2 -0
- package/lib/config/world-limits.d.ts.map +1 -1
- package/lib/data/entities.d.ts.map +1 -1
- package/lib/enums/bounding-box.enum.d.ts.map +1 -1
- package/lib/enums/dimension-type.enum.d.ts.map +1 -1
- package/lib/enums/entity-kind.enum.d.ts.map +1 -1
- package/lib/enums/icon-theme.enum.d.ts.map +1 -1
- package/lib/enums/inventory-section.enum.d.ts.map +1 -1
- package/lib/enums/move-direction.enum.d.ts +9 -0
- package/lib/enums/move-direction.enum.d.ts.map +1 -0
- package/lib/interfaces/block.interface.d.ts.map +1 -1
- package/lib/interfaces/data-entry.interface.d.ts.map +1 -1
- package/lib/interfaces/dimension.interface.d.ts.map +1 -1
- package/lib/interfaces/entity-inventory.interface.d.ts.map +1 -1
- package/lib/interfaces/entity-type.interface.d.ts.map +1 -1
- package/lib/interfaces/entity.interface.d.ts.map +1 -1
- package/lib/interfaces/item.interface.d.ts.map +1 -1
- package/lib/interfaces/placed-block.interface.d.ts.map +1 -1
- package/lib/interfaces/world-store.interface.d.ts +1 -0
- package/lib/interfaces/world-store.interface.d.ts.map +1 -1
- package/lib/resources/block-resources.d.ts.map +1 -1
- package/lib/resources/data-resources.d.ts.map +1 -1
- package/lib/resources/dimension-resources.d.ts.map +1 -1
- package/lib/resources/entity-resources.d.ts.map +1 -1
- package/lib/resources/index.d.ts.map +1 -1
- package/lib/resources/item-resources.d.ts.map +1 -1
- package/lib/schemas/block-schema.d.ts.map +1 -1
- package/lib/schemas/coordinate-schema.d.ts.map +1 -1
- package/lib/schemas/dimension-schema.d.ts.map +1 -1
- package/lib/schemas/entity-schema.d.ts.map +1 -1
- package/lib/schemas/item-schema.d.ts.map +1 -1
- package/lib/tools/block-tools.d.ts.map +1 -1
- package/lib/tools/entity-tools.d.ts.map +1 -1
- package/lib/tools/index.d.ts.map +1 -1
- package/lib/tools/item-tools.d.ts.map +1 -1
- package/lib/tools/result.d.ts.map +1 -1
- package/lib/tools/world-tools.d.ts.map +1 -1
- package/lib/utils/logger.d.ts.map +1 -1
- package/lib/world/block-spec.d.ts.map +1 -1
- package/lib/world/block-states.d.ts.map +1 -1
- package/lib/world/entity-inventory.d.ts.map +1 -1
- package/lib/world/item-spec.d.ts.map +1 -1
- package/lib/world/simulated-dimension.d.ts +25 -0
- package/lib/world/simulated-dimension.d.ts.map +1 -1
- package/lib/world/stores/file-world-store.d.ts +2 -2
- package/lib/world/stores/file-world-store.d.ts.map +1 -1
- package/lib/world/stores/index.d.ts.map +1 -1
- package/lib/world/stores/memory-world-store.d.ts +1 -0
- package/lib/world/stores/memory-world-store.d.ts.map +1 -1
- package/lib/world/stores/serialized-block.d.ts.map +1 -1
- package/lib/world/stores/serialized-entity.d.ts.map +1 -1
- package/lib/world/stores/serialized-snapshot.d.ts +6 -0
- package/lib/world/stores/serialized-snapshot.d.ts.map +1 -0
- package/lib/world/world.d.ts.map +1 -1
- package/package.json +113 -113
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
import * as z from 'zod/v4';
|
|
2
|
+
import { WORLD_LIMITS } from '../config/world-limits.js';
|
|
3
|
+
import { blockPositionShape } from '../schemas/coordinate-schema.js';
|
|
2
4
|
import { dimensionField } from '../schemas/dimension-schema.js';
|
|
5
|
+
import { entitySummarySchema, toEntitySummary } from '../schemas/entity-schema.js';
|
|
3
6
|
import { toErrorMessage } from '../utils/error-message.js';
|
|
4
|
-
import {
|
|
7
|
+
import { formatStates, isEmptyStates, sameStates } from '../world/block-states.js';
|
|
8
|
+
import { parseBlockSpec } from '../world/block-spec.js';
|
|
9
|
+
import { SNAPSHOT_FORMAT } from '../world/stores/serialized-snapshot.js';
|
|
10
|
+
import { coordinateRangeError, toolError } from './result.js';
|
|
5
11
|
const blockStatesSchema = z.record(z.string(), z.string());
|
|
12
|
+
const positionSchema = z.object({
|
|
13
|
+
x: z.number().int(),
|
|
14
|
+
y: z.number().int(),
|
|
15
|
+
z: z.number().int()
|
|
16
|
+
});
|
|
17
|
+
const blockTypeSchema = z
|
|
18
|
+
.string()
|
|
19
|
+
.trim()
|
|
20
|
+
.min(1)
|
|
21
|
+
.max(256)
|
|
22
|
+
.describe('Block id, such as "minecraft:stone" or "stone"; states are optional, for example "oak_stairs[facing=south]"');
|
|
23
|
+
function normalizeArea(from, to) {
|
|
24
|
+
return {
|
|
25
|
+
from: {
|
|
26
|
+
x: Math.min(from.x, to.x),
|
|
27
|
+
y: Math.min(from.y, to.y),
|
|
28
|
+
z: Math.min(from.z, to.z)
|
|
29
|
+
},
|
|
30
|
+
to: {
|
|
31
|
+
x: Math.max(from.x, to.x),
|
|
32
|
+
y: Math.max(from.y, to.y),
|
|
33
|
+
z: Math.max(from.z, to.z)
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
6
37
|
export function registerWorldTools(server, world) {
|
|
7
38
|
server.registerTool('getDimension', {
|
|
8
39
|
title: 'Get Dimension',
|
|
@@ -136,4 +167,360 @@ export function registerWorldTools(server, world) {
|
|
|
136
167
|
return toolError(`getDimensionBlocks failed: ${toErrorMessage(error)}`);
|
|
137
168
|
}
|
|
138
169
|
});
|
|
170
|
+
server.registerTool('getBlocksInArea', {
|
|
171
|
+
title: 'Get Blocks In Area',
|
|
172
|
+
description: 'List placed (non-air) blocks inside an inclusive box in one dimension, sorted by x, y and z. Corners may be supplied in any order. An optional blockType filter matches the block id and, when states are supplied, the exact state map. Results are paginated with limit and offset. Unmodified coordinates are air and are not listed.',
|
|
173
|
+
inputSchema: z.object({
|
|
174
|
+
dimension: dimensionField,
|
|
175
|
+
from: z.object(blockPositionShape),
|
|
176
|
+
to: z.object(blockPositionShape),
|
|
177
|
+
blockType: blockTypeSchema
|
|
178
|
+
.optional()
|
|
179
|
+
.describe('Optional block filter, such as "minecraft:oak_stairs" or "oak_stairs[facing=south]"'),
|
|
180
|
+
limit: z
|
|
181
|
+
.number()
|
|
182
|
+
.int()
|
|
183
|
+
.min(1)
|
|
184
|
+
.max(10_000)
|
|
185
|
+
.default(1_000)
|
|
186
|
+
.describe('Maximum number of matching blocks to return (1-10000, default 1000)'),
|
|
187
|
+
offset: z
|
|
188
|
+
.number()
|
|
189
|
+
.int()
|
|
190
|
+
.min(0)
|
|
191
|
+
.default(0)
|
|
192
|
+
.describe('Number of matching blocks to skip (default 0)')
|
|
193
|
+
}),
|
|
194
|
+
outputSchema: z.object({
|
|
195
|
+
dimension: z.string(),
|
|
196
|
+
displayName: z.string(),
|
|
197
|
+
from: positionSchema,
|
|
198
|
+
to: positionSchema,
|
|
199
|
+
blockType: z.string().nullable(),
|
|
200
|
+
total: z.number().int(),
|
|
201
|
+
offset: z.number().int(),
|
|
202
|
+
limit: z.number().int(),
|
|
203
|
+
returned: z.number().int(),
|
|
204
|
+
hasMore: z.boolean(),
|
|
205
|
+
blocks: z.array(z.object({
|
|
206
|
+
x: z.number().int(),
|
|
207
|
+
y: z.number().int(),
|
|
208
|
+
z: z.number().int(),
|
|
209
|
+
block: z.string(),
|
|
210
|
+
displayName: z.string(),
|
|
211
|
+
states: blockStatesSchema
|
|
212
|
+
}))
|
|
213
|
+
}),
|
|
214
|
+
annotations: {
|
|
215
|
+
readOnlyHint: true,
|
|
216
|
+
idempotentHint: true,
|
|
217
|
+
openWorldHint: false
|
|
218
|
+
}
|
|
219
|
+
}, async ({ dimension: dimensionType, from, to, blockType, limit, offset }) => {
|
|
220
|
+
try {
|
|
221
|
+
const dimension = world.getDimension(dimensionType);
|
|
222
|
+
if (!dimension.contains(from.x, from.y, from.z)) {
|
|
223
|
+
return coordinateRangeError(dimension.info, from.x, from.y, from.z);
|
|
224
|
+
}
|
|
225
|
+
if (!dimension.contains(to.x, to.y, to.z)) {
|
|
226
|
+
return coordinateRangeError(dimension.info, to.x, to.y, to.z);
|
|
227
|
+
}
|
|
228
|
+
const area = normalizeArea(from, to);
|
|
229
|
+
const spec = blockType ? parseBlockSpec(blockType) : null;
|
|
230
|
+
if (spec && !spec.ok) {
|
|
231
|
+
return toolError(spec.error);
|
|
232
|
+
}
|
|
233
|
+
const matches = dimension.sortedPlacedBlocks.filter(placed => {
|
|
234
|
+
if (placed.x < area.from.x ||
|
|
235
|
+
placed.x > area.to.x ||
|
|
236
|
+
placed.y < area.from.y ||
|
|
237
|
+
placed.y > area.to.y ||
|
|
238
|
+
placed.z < area.from.z ||
|
|
239
|
+
placed.z > area.to.z) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
if (!spec) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
return (placed.block.key === spec.block.key &&
|
|
246
|
+
(isEmptyStates(spec.states) ||
|
|
247
|
+
sameStates(placed.states, spec.states)));
|
|
248
|
+
});
|
|
249
|
+
const total = matches.length;
|
|
250
|
+
const page = matches.slice(offset, offset + limit);
|
|
251
|
+
const returned = page.length;
|
|
252
|
+
const hasMore = offset + returned < total;
|
|
253
|
+
const filter = spec
|
|
254
|
+
? `${spec.block.key}${formatStates(spec.states)}`
|
|
255
|
+
: null;
|
|
256
|
+
return {
|
|
257
|
+
content: [
|
|
258
|
+
{
|
|
259
|
+
type: 'text',
|
|
260
|
+
text: `Returned ${returned} of ${total} placed block(s) in ${dimension.info.displayName} from (${area.from.x}, ${area.from.y}, ${area.from.z}) to (${area.to.x}, ${area.to.y}, ${area.to.z})${filter ? ` matching ${filter}` : ''} (offset ${offset}, limit ${limit}${hasMore ? ', more available' : ''})`
|
|
261
|
+
}
|
|
262
|
+
],
|
|
263
|
+
structuredContent: {
|
|
264
|
+
dimension: dimension.info.key,
|
|
265
|
+
displayName: dimension.info.displayName,
|
|
266
|
+
from: area.from,
|
|
267
|
+
to: area.to,
|
|
268
|
+
blockType: filter,
|
|
269
|
+
total,
|
|
270
|
+
offset,
|
|
271
|
+
limit,
|
|
272
|
+
returned,
|
|
273
|
+
hasMore,
|
|
274
|
+
blocks: page.map(placed => ({
|
|
275
|
+
x: placed.x,
|
|
276
|
+
y: placed.y,
|
|
277
|
+
z: placed.z,
|
|
278
|
+
block: placed.block.key,
|
|
279
|
+
displayName: placed.block.displayName,
|
|
280
|
+
states: placed.states
|
|
281
|
+
}))
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
return toolError(`getBlocksInArea failed: ${toErrorMessage(error)}`);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
server.registerTool('fillBlocks', {
|
|
290
|
+
title: 'Fill Blocks',
|
|
291
|
+
description: 'Fill an inclusive box in one dimension with one block type and optional states. Corners may be supplied in any order. By default only air coordinates are filled; set overwrite to true to replace existing blocks. Use "minecraft:air" with overwrite to clear a region. The maximum volume per request is 16,384 blocks.',
|
|
292
|
+
inputSchema: z.object({
|
|
293
|
+
dimension: dimensionField,
|
|
294
|
+
from: z.object(blockPositionShape),
|
|
295
|
+
to: z.object(blockPositionShape),
|
|
296
|
+
blockType: blockTypeSchema,
|
|
297
|
+
overwrite: z
|
|
298
|
+
.boolean()
|
|
299
|
+
.default(false)
|
|
300
|
+
.describe('Replace existing non-air blocks when true; default false fills air only')
|
|
301
|
+
}),
|
|
302
|
+
outputSchema: z.object({
|
|
303
|
+
dimension: z.string(),
|
|
304
|
+
displayName: z.string(),
|
|
305
|
+
from: positionSchema,
|
|
306
|
+
to: positionSchema,
|
|
307
|
+
blockType: z.string(),
|
|
308
|
+
block: z.string(),
|
|
309
|
+
states: blockStatesSchema,
|
|
310
|
+
overwrite: z.boolean(),
|
|
311
|
+
volume: z.number().int(),
|
|
312
|
+
changed: z.number().int(),
|
|
313
|
+
placed: z.number().int(),
|
|
314
|
+
removed: z.number().int(),
|
|
315
|
+
skipped: z.number().int(),
|
|
316
|
+
modifiedBlockCount: z.number().int()
|
|
317
|
+
}),
|
|
318
|
+
annotations: {
|
|
319
|
+
destructiveHint: true,
|
|
320
|
+
idempotentHint: true,
|
|
321
|
+
openWorldHint: false
|
|
322
|
+
}
|
|
323
|
+
}, async ({ dimension: dimensionType, from, to, blockType, overwrite }) => {
|
|
324
|
+
try {
|
|
325
|
+
const dimension = world.getDimension(dimensionType);
|
|
326
|
+
if (!dimension.contains(from.x, from.y, from.z)) {
|
|
327
|
+
return coordinateRangeError(dimension.info, from.x, from.y, from.z);
|
|
328
|
+
}
|
|
329
|
+
if (!dimension.contains(to.x, to.y, to.z)) {
|
|
330
|
+
return coordinateRangeError(dimension.info, to.x, to.y, to.z);
|
|
331
|
+
}
|
|
332
|
+
const area = normalizeArea(from, to);
|
|
333
|
+
const volume = (area.to.x - area.from.x + 1) *
|
|
334
|
+
(area.to.y - area.from.y + 1) *
|
|
335
|
+
(area.to.z - area.from.z + 1);
|
|
336
|
+
if (!Number.isSafeInteger(volume) ||
|
|
337
|
+
volume > WORLD_LIMITS.maxFillVolume) {
|
|
338
|
+
return toolError(`The requested fill volume is too large (${volume}); the maximum is ${WORLD_LIMITS.maxFillVolume} blocks.`);
|
|
339
|
+
}
|
|
340
|
+
const spec = parseBlockSpec(blockType);
|
|
341
|
+
if (!spec.ok) {
|
|
342
|
+
return toolError(spec.error);
|
|
343
|
+
}
|
|
344
|
+
const isAirTarget = spec.block.key === 'minecraft:air';
|
|
345
|
+
if (isAirTarget && !isEmptyStates(spec.states)) {
|
|
346
|
+
return toolError('Air cannot carry block states. Use "minecraft:air" to remove blocks.');
|
|
347
|
+
}
|
|
348
|
+
let newBlocks = 0;
|
|
349
|
+
let skipped = 0;
|
|
350
|
+
for (let x = area.from.x; x <= area.to.x; x += 1) {
|
|
351
|
+
for (let y = area.from.y; y <= area.to.y; y += 1) {
|
|
352
|
+
for (let z = area.from.z; z <= area.to.z; z += 1) {
|
|
353
|
+
const existing = dimension.getBlockState(x, y, z);
|
|
354
|
+
const existingIsAir = existing.block.key === 'minecraft:air';
|
|
355
|
+
if (!overwrite && !existingIsAir) {
|
|
356
|
+
skipped += 1;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (isAirTarget) {
|
|
360
|
+
if (existingIsAir) {
|
|
361
|
+
skipped += 1;
|
|
362
|
+
}
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (!existingIsAir &&
|
|
366
|
+
existing.block.key === spec.block.key &&
|
|
367
|
+
sameStates(existing.states, spec.states)) {
|
|
368
|
+
skipped += 1;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (existingIsAir) {
|
|
372
|
+
newBlocks += 1;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (dimension.modifiedBlockCount + newBlocks >
|
|
378
|
+
WORLD_LIMITS.maxModifiedBlocks) {
|
|
379
|
+
return toolError(`The ${dimension.info.displayName} world would exceed the maximum of ${WORLD_LIMITS.maxModifiedBlocks} modified blocks.`);
|
|
380
|
+
}
|
|
381
|
+
let placed = 0;
|
|
382
|
+
let removed = 0;
|
|
383
|
+
let changed = 0;
|
|
384
|
+
for (let x = area.from.x; x <= area.to.x; x += 1) {
|
|
385
|
+
for (let y = area.from.y; y <= area.to.y; y += 1) {
|
|
386
|
+
for (let z = area.from.z; z <= area.to.z; z += 1) {
|
|
387
|
+
const existing = dimension.getBlockState(x, y, z);
|
|
388
|
+
const existingIsAir = existing.block.key === 'minecraft:air';
|
|
389
|
+
if (!overwrite && !existingIsAir) {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (isAirTarget) {
|
|
393
|
+
if (!existingIsAir) {
|
|
394
|
+
dimension.setBlock(x, y, z, spec.block, spec.states);
|
|
395
|
+
removed += 1;
|
|
396
|
+
changed += 1;
|
|
397
|
+
}
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (!existingIsAir &&
|
|
401
|
+
existing.block.key === spec.block.key &&
|
|
402
|
+
sameStates(existing.states, spec.states)) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
dimension.setBlock(x, y, z, spec.block, spec.states);
|
|
406
|
+
placed += 1;
|
|
407
|
+
changed += 1;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (changed > 0) {
|
|
412
|
+
try {
|
|
413
|
+
await dimension.flush();
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
return toolError(`The region was changed but could not be persisted to the world store: ${toErrorMessage(error)}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const normalizedType = `${spec.block.key}${formatStates(spec.states)}`;
|
|
420
|
+
return {
|
|
421
|
+
content: [
|
|
422
|
+
{
|
|
423
|
+
type: 'text',
|
|
424
|
+
text: `Filled ${changed} of ${volume} coordinate(s) in ${dimension.info.displayName} with ${normalizedType} (${placed} placed, ${removed} removed, ${skipped} skipped)`
|
|
425
|
+
}
|
|
426
|
+
],
|
|
427
|
+
structuredContent: {
|
|
428
|
+
dimension: dimension.info.key,
|
|
429
|
+
displayName: dimension.info.displayName,
|
|
430
|
+
from: area.from,
|
|
431
|
+
to: area.to,
|
|
432
|
+
blockType: normalizedType,
|
|
433
|
+
block: spec.block.key,
|
|
434
|
+
states: spec.states,
|
|
435
|
+
overwrite,
|
|
436
|
+
volume,
|
|
437
|
+
changed,
|
|
438
|
+
placed,
|
|
439
|
+
removed,
|
|
440
|
+
skipped,
|
|
441
|
+
modifiedBlockCount: dimension.modifiedBlockCount
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
return toolError(`fillBlocks failed: ${toErrorMessage(error)}`);
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
server.registerTool('getDimensionEntities', {
|
|
450
|
+
title: 'Get Dimension Entities',
|
|
451
|
+
description: 'List every entity and player in one dimension, sorted by UUID. Each row contains the entity UUID, kind, type, display name, name, position and dimension. Use getEntity or getInventory for one entity and its full inventory.',
|
|
452
|
+
inputSchema: z.object({
|
|
453
|
+
dimension: dimensionField
|
|
454
|
+
}),
|
|
455
|
+
outputSchema: z.object({
|
|
456
|
+
dimension: z.string(),
|
|
457
|
+
displayName: z.string(),
|
|
458
|
+
entityCount: z.number().int(),
|
|
459
|
+
playerCount: z.number().int(),
|
|
460
|
+
entities: z.array(entitySummarySchema)
|
|
461
|
+
}),
|
|
462
|
+
annotations: {
|
|
463
|
+
readOnlyHint: true,
|
|
464
|
+
idempotentHint: true,
|
|
465
|
+
openWorldHint: false
|
|
466
|
+
}
|
|
467
|
+
}, async ({ dimension: dimensionType }) => {
|
|
468
|
+
try {
|
|
469
|
+
const dimension = world.getDimension(dimensionType);
|
|
470
|
+
const entities = dimension.sortedPlacedEntities.map(toEntitySummary);
|
|
471
|
+
return {
|
|
472
|
+
content: [
|
|
473
|
+
{
|
|
474
|
+
type: 'text',
|
|
475
|
+
text: `${dimension.info.displayName} (${dimension.info.key}) holds ${entities.length} entities and ${dimension.playerCount} players`
|
|
476
|
+
}
|
|
477
|
+
],
|
|
478
|
+
structuredContent: {
|
|
479
|
+
dimension: dimension.info.key,
|
|
480
|
+
displayName: dimension.info.displayName,
|
|
481
|
+
entityCount: entities.length,
|
|
482
|
+
playerCount: dimension.playerCount,
|
|
483
|
+
entities
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
catch (error) {
|
|
488
|
+
return toolError(`getDimensionEntities failed: ${toErrorMessage(error)}`);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
server.registerTool('getDimensionRawFile', {
|
|
492
|
+
title: 'Get Dimension Raw File',
|
|
493
|
+
description: 'Return the raw JSON snapshot of one dimension, including its top-level blocks and entities arrays. The result is the on-disk JSON for file storage, or the equivalent serialized state for memory storage, and can be loaded directly by minecraft_world_viewer.html.',
|
|
494
|
+
inputSchema: z.object({
|
|
495
|
+
dimension: dimensionField
|
|
496
|
+
}),
|
|
497
|
+
outputSchema: z.object({
|
|
498
|
+
dimension: z.string(),
|
|
499
|
+
format: z.string(),
|
|
500
|
+
bytes: z.number().int(),
|
|
501
|
+
raw: z.string()
|
|
502
|
+
}),
|
|
503
|
+
annotations: {
|
|
504
|
+
readOnlyHint: true,
|
|
505
|
+
idempotentHint: true,
|
|
506
|
+
openWorldHint: false
|
|
507
|
+
}
|
|
508
|
+
}, async ({ dimension: dimensionType }) => {
|
|
509
|
+
try {
|
|
510
|
+
const dimension = world.getDimension(dimensionType);
|
|
511
|
+
const raw = await dimension.readRaw();
|
|
512
|
+
return {
|
|
513
|
+
content: [{ type: 'text', text: raw }],
|
|
514
|
+
structuredContent: {
|
|
515
|
+
dimension: dimension.info.key,
|
|
516
|
+
format: SNAPSHOT_FORMAT,
|
|
517
|
+
bytes: Buffer.byteLength(raw, 'utf8'),
|
|
518
|
+
raw
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
return toolError(`getDimensionRawFile failed: ${toErrorMessage(error)}`);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
139
526
|
}
|
package/build/utils/logger.js
CHANGED
|
@@ -16,7 +16,7 @@ const REQUESTED_LOG_LEVEL = process.env.MCP_LOG_LEVEL?.trim().toUpperCase();
|
|
|
16
16
|
const LOG_LEVEL = REQUESTED_LOG_LEVEL && VALID_LOG_LEVELS.has(REQUESTED_LOG_LEVEL)
|
|
17
17
|
? REQUESTED_LOG_LEVEL
|
|
18
18
|
: 'INFO';
|
|
19
|
-
const LOGGER_CATEGORY = 'lzimul
|
|
19
|
+
const LOGGER_CATEGORY = '@lzimul/minecraft-mcp';
|
|
20
20
|
function chalkLayout(event) {
|
|
21
21
|
const line = `${event.startTime.toISOString()} [${event.level.levelStr}] ${event.categoryName} - ${format(...event.data)}`;
|
|
22
22
|
switch (event.level.levelStr) {
|
|
@@ -26,6 +26,7 @@ export function parseBlockSpec(input) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
const states = {};
|
|
29
|
+
let stateCount = 0;
|
|
29
30
|
if (typeof rawStates === 'string') {
|
|
30
31
|
if (rawStates.trim().length === 0) {
|
|
31
32
|
return {
|
|
@@ -62,7 +63,8 @@ export function parseBlockSpec(input) {
|
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
65
|
states[propertyName] = propertyValue;
|
|
65
|
-
|
|
66
|
+
stateCount += 1;
|
|
67
|
+
if (stateCount > MAX_STATES) {
|
|
66
68
|
return {
|
|
67
69
|
ok: false,
|
|
68
70
|
error: `Too many block states in "${input}" (max ${MAX_STATES}).`
|
|
@@ -74,11 +74,28 @@ export function sameItemStack(a, b) {
|
|
|
74
74
|
return a !== null && b !== null && a.item === b.item && a.count === b.count;
|
|
75
75
|
}
|
|
76
76
|
export function inventoryUsedSlots(inventory) {
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
let used = 0;
|
|
78
|
+
for (const stack of inventory.hotbar) {
|
|
79
|
+
if (stack !== null) {
|
|
80
|
+
used += 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const stack of inventory.inventory) {
|
|
84
|
+
if (stack !== null) {
|
|
85
|
+
used += 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return used;
|
|
79
89
|
}
|
|
80
90
|
export function sectionUsedSlots(inventory, section) {
|
|
81
|
-
|
|
91
|
+
const slots = section === EInventorySection.Hotbar
|
|
82
92
|
? inventory.hotbar
|
|
83
|
-
: inventory.inventory
|
|
93
|
+
: inventory.inventory;
|
|
94
|
+
let used = 0;
|
|
95
|
+
for (const stack of slots) {
|
|
96
|
+
if (stack !== null) {
|
|
97
|
+
used += 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return used;
|
|
84
101
|
}
|
|
@@ -4,11 +4,30 @@ import { getBlockByName, MINECRAFT_BLOCKS } from '../data/blocks.js';
|
|
|
4
4
|
import { getEntityTypeByName } from '../data/entities.js';
|
|
5
5
|
import { EEntityKind } from '../enums/entity-kind.enum.js';
|
|
6
6
|
import { EInventorySection } from '../enums/inventory-section.enum.js';
|
|
7
|
+
import { EMoveDirection } from '../enums/move-direction.enum.js';
|
|
7
8
|
import { toErrorMessage } from '../utils/error-message.js';
|
|
8
9
|
import { logger } from '../utils/logger.js';
|
|
9
10
|
import { EMPTY_BLOCK_STATES, isEmptyStates, normalizeStates, sameStates } from './block-states.js';
|
|
10
|
-
import { createEmptyInventory, getInventorySlot, isSlotInRange, sameItemStack, slotCount, withInventorySlot } from './entity-inventory.js';
|
|
11
|
+
import { createEmptyInventory, getInventorySlot, inventoryUsedSlots, isSlotInRange, sameItemStack, slotCount, withInventorySlot } from './entity-inventory.js';
|
|
11
12
|
const AIR = getBlockByName('air') ?? MINECRAFT_BLOCKS[0];
|
|
13
|
+
function directionDelta(direction) {
|
|
14
|
+
switch (direction) {
|
|
15
|
+
case EMoveDirection.Forward:
|
|
16
|
+
return { x: 0, y: 0, z: 1 };
|
|
17
|
+
case EMoveDirection.Back:
|
|
18
|
+
return { x: 0, y: 0, z: -1 };
|
|
19
|
+
case EMoveDirection.Left:
|
|
20
|
+
return { x: -1, y: 0, z: 0 };
|
|
21
|
+
case EMoveDirection.Right:
|
|
22
|
+
return { x: 1, y: 0, z: 0 };
|
|
23
|
+
case EMoveDirection.Up:
|
|
24
|
+
return { x: 0, y: 1, z: 0 };
|
|
25
|
+
case EMoveDirection.Down:
|
|
26
|
+
return { x: 0, y: -1, z: 0 };
|
|
27
|
+
default:
|
|
28
|
+
throw new Error(`Unknown movement direction "${direction}".`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
12
31
|
function coordinateKey(x, y, z) {
|
|
13
32
|
return `${x},${y},${z}`;
|
|
14
33
|
}
|
|
@@ -71,6 +90,12 @@ export class SimulatedDimension {
|
|
|
71
90
|
return this.sortedEntitiesCache;
|
|
72
91
|
}
|
|
73
92
|
async load() {
|
|
93
|
+
if (this.timer !== null) {
|
|
94
|
+
clearTimeout(this.timer);
|
|
95
|
+
this.timer = null;
|
|
96
|
+
}
|
|
97
|
+
await this.queue;
|
|
98
|
+
this.dirty = false;
|
|
74
99
|
const stored = await this.store.load(this.info.type);
|
|
75
100
|
this.blocks.clear();
|
|
76
101
|
this.entities.clear();
|
|
@@ -96,17 +121,19 @@ export class SimulatedDimension {
|
|
|
96
121
|
entity.dimension !== this.info.type;
|
|
97
122
|
const overLimit = !duplicateId &&
|
|
98
123
|
this.entities.size >= WORLD_LIMITS.maxEntitiesPerDimension;
|
|
99
|
-
|
|
100
|
-
const duplicatePlayer = entity.kind === EEntityKind.Player &&
|
|
101
|
-
this.playersByName.has(playerName);
|
|
102
|
-
if (duplicateId || outOfRange || overLimit || duplicatePlayer) {
|
|
124
|
+
if (duplicateId || outOfRange || overLimit) {
|
|
103
125
|
skippedEntities++;
|
|
104
126
|
continue;
|
|
105
127
|
}
|
|
106
|
-
this.entities.set(entity.id, entity);
|
|
107
128
|
if (entity.kind === EEntityKind.Player) {
|
|
129
|
+
const playerName = entity.name.trim().toLowerCase();
|
|
130
|
+
if (this.playersByName.has(playerName)) {
|
|
131
|
+
skippedEntities++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
108
134
|
this.playersByName.set(playerName, entity);
|
|
109
135
|
}
|
|
136
|
+
this.entities.set(entity.id, entity);
|
|
110
137
|
}
|
|
111
138
|
if (skippedBlocks > 0) {
|
|
112
139
|
logger.warn(`Skipped ${skippedBlocks} invalid block(s) while loading ${this.info.key}`);
|
|
@@ -229,6 +256,101 @@ export class SimulatedDimension {
|
|
|
229
256
|
this.markDirty();
|
|
230
257
|
return { previous, entity: updated };
|
|
231
258
|
}
|
|
259
|
+
deleteEntity(id) {
|
|
260
|
+
const entity = this.entities.get(id);
|
|
261
|
+
if (!entity) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.entities.delete(id);
|
|
265
|
+
if (entity.kind === EEntityKind.Player) {
|
|
266
|
+
this.playersByName.delete(entity.name.trim().toLowerCase());
|
|
267
|
+
}
|
|
268
|
+
this.sortedEntitiesCache = null;
|
|
269
|
+
this.markDirty();
|
|
270
|
+
return entity;
|
|
271
|
+
}
|
|
272
|
+
clearInventory(id) {
|
|
273
|
+
const entity = this.entities.get(id);
|
|
274
|
+
if (!entity) {
|
|
275
|
+
throw new RangeError(`Unknown entity "${id}" in ${this.info.displayName}.`);
|
|
276
|
+
}
|
|
277
|
+
const clearedSlots = inventoryUsedSlots(entity.inventory);
|
|
278
|
+
if (clearedSlots === 0) {
|
|
279
|
+
return { clearedSlots, entity };
|
|
280
|
+
}
|
|
281
|
+
const updated = {
|
|
282
|
+
...entity,
|
|
283
|
+
inventory: createEmptyInventory()
|
|
284
|
+
};
|
|
285
|
+
this.entities.set(id, updated);
|
|
286
|
+
if (updated.kind === EEntityKind.Player) {
|
|
287
|
+
this.playersByName.set(updated.name.trim().toLowerCase(), updated);
|
|
288
|
+
}
|
|
289
|
+
this.sortedEntitiesCache = null;
|
|
290
|
+
this.markDirty();
|
|
291
|
+
return { clearedSlots, entity: updated };
|
|
292
|
+
}
|
|
293
|
+
swapInventorySlots(id, fromSection, fromSlot, toSection, toSlot) {
|
|
294
|
+
const entity = this.entities.get(id);
|
|
295
|
+
if (!entity) {
|
|
296
|
+
throw new RangeError(`Unknown entity "${id}" in ${this.info.displayName}.`);
|
|
297
|
+
}
|
|
298
|
+
if (!isSlotInRange(fromSection, fromSlot)) {
|
|
299
|
+
throw new RangeError(`Slot ${fromSlot} is out of range for the ${fromSection} (0-${slotCount(fromSection) - 1}).`);
|
|
300
|
+
}
|
|
301
|
+
if (!isSlotInRange(toSection, toSlot)) {
|
|
302
|
+
throw new RangeError(`Slot ${toSlot} is out of range for the ${toSection} (0-${slotCount(toSection) - 1}).`);
|
|
303
|
+
}
|
|
304
|
+
const previousFrom = getInventorySlot(entity.inventory, fromSection, fromSlot);
|
|
305
|
+
const previousTo = getInventorySlot(entity.inventory, toSection, toSlot);
|
|
306
|
+
const sameSlot = fromSection === toSection && fromSlot === toSlot;
|
|
307
|
+
if (sameSlot || sameItemStack(previousFrom, previousTo)) {
|
|
308
|
+
return { previousFrom, previousTo, entity };
|
|
309
|
+
}
|
|
310
|
+
let inventory = withInventorySlot(entity.inventory, fromSection, fromSlot, previousTo);
|
|
311
|
+
inventory = withInventorySlot(inventory, toSection, toSlot, previousFrom);
|
|
312
|
+
const updated = { ...entity, inventory };
|
|
313
|
+
this.entities.set(id, updated);
|
|
314
|
+
if (updated.kind === EEntityKind.Player) {
|
|
315
|
+
this.playersByName.set(updated.name.trim().toLowerCase(), updated);
|
|
316
|
+
}
|
|
317
|
+
this.sortedEntitiesCache = null;
|
|
318
|
+
this.markDirty();
|
|
319
|
+
return { previousFrom, previousTo, entity: updated };
|
|
320
|
+
}
|
|
321
|
+
moveEntity(id, direction) {
|
|
322
|
+
const entity = this.entities.get(id);
|
|
323
|
+
if (!entity) {
|
|
324
|
+
throw new RangeError(`Unknown entity "${id}" in ${this.info.displayName}.`);
|
|
325
|
+
}
|
|
326
|
+
const delta = directionDelta(direction);
|
|
327
|
+
return this.setEntityPosition(id, entity.x + delta.x, entity.y + delta.y, entity.z + delta.z);
|
|
328
|
+
}
|
|
329
|
+
setEntityPosition(id, x, y, z) {
|
|
330
|
+
const entity = this.entities.get(id);
|
|
331
|
+
if (!entity) {
|
|
332
|
+
throw new RangeError(`Unknown entity "${id}" in ${this.info.displayName}.`);
|
|
333
|
+
}
|
|
334
|
+
if (!this.contains(x, y, z)) {
|
|
335
|
+
throw new RangeError(`Position (${x}, ${y}, ${z}) is out of range in ${this.info.displayName} (Y: ${this.info.minY} to ${this.info.maxY - 1}).`);
|
|
336
|
+
}
|
|
337
|
+
const previous = {
|
|
338
|
+
x: entity.x,
|
|
339
|
+
y: entity.y,
|
|
340
|
+
z: entity.z
|
|
341
|
+
};
|
|
342
|
+
if (entity.x === x && entity.y === y && entity.z === z) {
|
|
343
|
+
return { previous, entity };
|
|
344
|
+
}
|
|
345
|
+
const updated = { ...entity, x, y, z };
|
|
346
|
+
this.entities.set(id, updated);
|
|
347
|
+
if (updated.kind === EEntityKind.Player) {
|
|
348
|
+
this.playersByName.set(updated.name.trim().toLowerCase(), updated);
|
|
349
|
+
}
|
|
350
|
+
this.sortedEntitiesCache = null;
|
|
351
|
+
this.markDirty();
|
|
352
|
+
return { previous, entity: updated };
|
|
353
|
+
}
|
|
232
354
|
setBlock(x, y, z, block, states = EMPTY_BLOCK_STATES) {
|
|
233
355
|
const key = coordinateKey(x, y, z);
|
|
234
356
|
const existing = this.blocks.get(key);
|
|
@@ -254,6 +376,10 @@ export class SimulatedDimension {
|
|
|
254
376
|
this.markDirty();
|
|
255
377
|
return previous;
|
|
256
378
|
}
|
|
379
|
+
async readRaw() {
|
|
380
|
+
await this.flush();
|
|
381
|
+
return this.store.readRaw(this.info.type);
|
|
382
|
+
}
|
|
257
383
|
async flush() {
|
|
258
384
|
if (this.timer !== null) {
|
|
259
385
|
clearTimeout(this.timer);
|
|
@@ -268,8 +394,8 @@ export class SimulatedDimension {
|
|
|
268
394
|
}
|
|
269
395
|
this.dirty = false;
|
|
270
396
|
const snapshot = {
|
|
271
|
-
blocks: this.
|
|
272
|
-
entities: this.
|
|
397
|
+
blocks: this.sortedPlacedBlocks,
|
|
398
|
+
entities: this.sortedPlacedEntities
|
|
273
399
|
};
|
|
274
400
|
const run = this.queue.then(async () => {
|
|
275
401
|
await this.store.save(this.info.type, snapshot);
|
|
@@ -295,7 +421,7 @@ export class SimulatedDimension {
|
|
|
295
421
|
}
|
|
296
422
|
this.timer = setTimeout(() => {
|
|
297
423
|
this.timer = null;
|
|
298
|
-
void this.flush().catch(
|
|
424
|
+
void this.flush().catch(error => {
|
|
299
425
|
logger.error(`Failed to persist ${this.info.key}: ${toErrorMessage(error)}`);
|
|
300
426
|
});
|
|
301
427
|
}, this.writeDelayMs);
|