@lzimul/minecraft-mcp 0.0.0-stage → 1.0.1
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 -0
- package/README.md +163 -2
- package/SECURITY.md +21 -0
- package/build/app/server.js +10 -0
- package/build/config/dimensions.js +27 -0
- package/build/config/server-info.js +25 -0
- package/build/config/storage.js +15 -0
- package/build/config/world-limits.js +7 -0
- package/build/data/blocks.js +17532 -0
- package/build/data/items.js +11194 -0
- package/build/data/version.js +1 -0
- package/build/enums/bounding-box.enum.js +5 -0
- package/build/enums/dimension-type.enum.js +6 -0
- package/build/enums/icon-theme.enum.js +5 -0
- package/build/index.js +47 -0
- package/build/interfaces/block-states.interface.js +1 -0
- package/build/interfaces/block.interface.js +1 -0
- package/build/interfaces/data-entry.interface.js +1 -0
- package/build/interfaces/dimension.interface.js +1 -0
- package/build/interfaces/item.interface.js +1 -0
- package/build/interfaces/placed-block.interface.js +1 -0
- package/build/interfaces/world-store.interface.js +1 -0
- package/build/resources/block-resources.js +17 -0
- package/build/resources/data-resources.js +55 -0
- package/build/resources/dimension-resources.js +70 -0
- package/build/resources/error-content.js +12 -0
- package/build/resources/index.js +9 -0
- package/build/resources/item-resources.js +17 -0
- package/build/schemas/block-schema.js +27 -0
- package/build/schemas/coordinate-schema.js +12 -0
- package/build/schemas/dimension-schema.js +6 -0
- package/build/schemas/item-schema.js +28 -0
- package/build/tools/block-tools.js +145 -0
- package/build/tools/index.js +6 -0
- package/build/tools/item-tools.js +50 -0
- package/build/tools/result.js +11 -0
- package/build/utils/error-message.js +25 -0
- package/build/world/block-spec.js +74 -0
- package/build/world/block-states.js +43 -0
- package/build/world/simulated-dimension.js +129 -0
- package/build/world/stores/file-world-store.js +103 -0
- package/build/world/stores/index.js +9 -0
- package/build/world/stores/memory-world-store.js +14 -0
- package/build/world/stores/serialized-block.js +53 -0
- package/build/world/world.js +39 -0
- package/lib/app/server.d.ts +4 -0
- package/lib/app/server.d.ts.map +1 -0
- package/lib/config/dimensions.d.ts +3 -0
- package/lib/config/dimensions.d.ts.map +1 -0
- package/lib/config/server-info.d.ts +15 -0
- package/lib/config/server-info.d.ts.map +1 -0
- package/lib/config/storage.d.ts +8 -0
- package/lib/config/storage.d.ts.map +1 -0
- package/lib/config/world-limits.d.ts +8 -0
- package/lib/config/world-limits.d.ts.map +1 -0
- package/lib/data/blocks.d.ts +5 -0
- package/lib/data/blocks.d.ts.map +1 -0
- package/lib/data/items.d.ts +5 -0
- package/lib/data/items.d.ts.map +1 -0
- package/lib/data/version.d.ts +2 -0
- package/lib/data/version.d.ts.map +1 -0
- package/lib/enums/bounding-box.enum.d.ts +5 -0
- package/lib/enums/bounding-box.enum.d.ts.map +1 -0
- package/lib/enums/dimension-type.enum.d.ts +6 -0
- package/lib/enums/dimension-type.enum.d.ts.map +1 -0
- package/lib/enums/icon-theme.enum.d.ts +5 -0
- package/lib/enums/icon-theme.enum.d.ts.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/interfaces/block-states.interface.d.ts +2 -0
- package/lib/interfaces/block-states.interface.d.ts.map +1 -0
- package/lib/interfaces/block.interface.d.ts +14 -0
- package/lib/interfaces/block.interface.d.ts.map +1 -0
- package/lib/interfaces/data-entry.interface.d.ts +7 -0
- package/lib/interfaces/data-entry.interface.d.ts.map +1 -0
- package/lib/interfaces/dimension.interface.d.ts +10 -0
- package/lib/interfaces/dimension.interface.d.ts.map +1 -0
- package/lib/interfaces/item.interface.d.ts +8 -0
- package/lib/interfaces/item.interface.d.ts.map +1 -0
- package/lib/interfaces/placed-block.interface.d.ts +10 -0
- package/lib/interfaces/placed-block.interface.d.ts.map +1 -0
- package/lib/interfaces/world-store.interface.d.ts +7 -0
- package/lib/interfaces/world-store.interface.d.ts.map +1 -0
- package/lib/resources/block-resources.d.ts +3 -0
- package/lib/resources/block-resources.d.ts.map +1 -0
- package/lib/resources/data-resources.d.ts +15 -0
- package/lib/resources/data-resources.d.ts.map +1 -0
- package/lib/resources/dimension-resources.d.ts +5 -0
- package/lib/resources/dimension-resources.d.ts.map +1 -0
- package/lib/resources/error-content.d.ts +8 -0
- package/lib/resources/error-content.d.ts.map +1 -0
- package/lib/resources/index.d.ts +4 -0
- package/lib/resources/index.d.ts.map +1 -0
- package/lib/resources/item-resources.d.ts +3 -0
- package/lib/resources/item-resources.d.ts.map +1 -0
- package/lib/schemas/block-schema.d.ts +17 -0
- package/lib/schemas/block-schema.d.ts.map +1 -0
- package/lib/schemas/coordinate-schema.d.ts +4 -0
- package/lib/schemas/coordinate-schema.d.ts.map +1 -0
- package/lib/schemas/dimension-schema.d.ts +5 -0
- package/lib/schemas/dimension-schema.d.ts.map +1 -0
- package/lib/schemas/item-schema.d.ts +14 -0
- package/lib/schemas/item-schema.d.ts.map +1 -0
- package/lib/tools/block-tools.d.ts +4 -0
- package/lib/tools/block-tools.d.ts.map +1 -0
- package/lib/tools/index.d.ts +4 -0
- package/lib/tools/index.d.ts.map +1 -0
- package/lib/tools/item-tools.d.ts +3 -0
- package/lib/tools/item-tools.d.ts.map +1 -0
- package/lib/tools/result.d.ts +14 -0
- package/lib/tools/result.d.ts.map +1 -0
- package/lib/utils/error-message.d.ts +2 -0
- package/lib/utils/error-message.d.ts.map +1 -0
- package/lib/world/block-spec.d.ts +12 -0
- package/lib/world/block-spec.d.ts.map +1 -0
- package/lib/world/block-states.d.ts +8 -0
- package/lib/world/block-states.d.ts.map +1 -0
- package/lib/world/simulated-dimension.d.ts +27 -0
- package/lib/world/simulated-dimension.d.ts.map +1 -0
- package/lib/world/stores/file-world-store.d.ts +14 -0
- package/lib/world/stores/file-world-store.d.ts.map +1 -0
- package/lib/world/stores/index.d.ts +3 -0
- package/lib/world/stores/index.d.ts.map +1 -0
- package/lib/world/stores/memory-world-store.d.ts +9 -0
- package/lib/world/stores/memory-world-store.d.ts.map +1 -0
- package/lib/world/stores/serialized-block.d.ts +12 -0
- package/lib/world/stores/serialized-block.d.ts.map +1 -0
- package/lib/world/world.d.ts +13 -0
- package/lib/world/world.d.ts.map +1 -0
- package/package.json +107 -4
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { getBlockByName } from '../data/blocks.js';
|
|
2
|
+
import { isSafeStateName, isSafeStateValue, normalizeStates } from './block-states.js';
|
|
3
|
+
const SPEC = /^(?:([a-z0-9_.-]+):)?([a-z0-9_./-]+)(?:\[([^\]]*)\])?$/;
|
|
4
|
+
const PROPERTY = /^([a-z0-9_]+)=([a-z0-9_.-]+)$/;
|
|
5
|
+
const MAX_STATES = 32;
|
|
6
|
+
export function parseBlockSpec(input) {
|
|
7
|
+
const spec = input.trim().toLowerCase();
|
|
8
|
+
const match = SPEC.exec(spec);
|
|
9
|
+
if (!match) {
|
|
10
|
+
return {
|
|
11
|
+
ok: false,
|
|
12
|
+
error: `Invalid block "${input}". Expected an id such as "minecraft:stone" or "minecraft:oak_stairs[facing=south]".`
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const namespace = match[1];
|
|
16
|
+
const name = match[2];
|
|
17
|
+
const rawStates = match[3];
|
|
18
|
+
let block = getBlockByName(namespace ? `${namespace}:${name}` : name);
|
|
19
|
+
if (!block && !namespace) {
|
|
20
|
+
block = getBlockByName(`minecraft:${name}`);
|
|
21
|
+
}
|
|
22
|
+
if (!block) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
error: `Unknown block "${input}". Use a block id such as "minecraft:stone"; see the resource lzimul://minecraft/blocks for the full list.`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const states = {};
|
|
29
|
+
if (typeof rawStates === 'string') {
|
|
30
|
+
if (rawStates.trim().length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
error: `Empty block state list in "${input}". Use "minecraft:oak_stairs[facing=south]" or omit the brackets.`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
for (const part of rawStates.split(',')) {
|
|
37
|
+
const property = PROPERTY.exec(part.trim());
|
|
38
|
+
if (!property) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: `Invalid block state "${part.trim()}" in "${input}". Expected "property=value".`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const propertyName = property[1];
|
|
45
|
+
const propertyValue = property[2];
|
|
46
|
+
if (!isSafeStateName(propertyName)) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
error: `Unsupported block state name "${propertyName}" in "${input}".`
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (!isSafeStateValue(propertyValue)) {
|
|
53
|
+
return {
|
|
54
|
+
ok: false,
|
|
55
|
+
error: `Unsupported block state value "${propertyValue}" for "${propertyName}" in "${input}".`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (Object.hasOwn(states, propertyName)) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: `Duplicate block state "${propertyName}" in "${input}".`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
states[propertyName] = propertyValue;
|
|
65
|
+
}
|
|
66
|
+
if (Object.keys(states).length > MAX_STATES) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: `Too many block states in "${input}" (max ${MAX_STATES}).`
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, block, states: normalizeStates(states) };
|
|
74
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const EMPTY_BLOCK_STATES = Object.freeze({});
|
|
2
|
+
const MAX_STATE_NAME_LENGTH = 64;
|
|
3
|
+
const MAX_STATE_VALUE_LENGTH = 64;
|
|
4
|
+
const SAFE_STATE_NAME = /^[a-z0-9_]+$/;
|
|
5
|
+
const SAFE_STATE_VALUE = /^[a-z0-9_.-]+$/;
|
|
6
|
+
const RESERVED_STATE_NAMES = new Set(['__proto__', 'constructor', 'prototype']);
|
|
7
|
+
export function isSafeStateName(name) {
|
|
8
|
+
const normalized = name.toLowerCase();
|
|
9
|
+
return (normalized.length > 0 &&
|
|
10
|
+
normalized.length <= MAX_STATE_NAME_LENGTH &&
|
|
11
|
+
SAFE_STATE_NAME.test(normalized) &&
|
|
12
|
+
!RESERVED_STATE_NAMES.has(normalized));
|
|
13
|
+
}
|
|
14
|
+
export function isSafeStateValue(value) {
|
|
15
|
+
const normalized = value.toLowerCase();
|
|
16
|
+
return (normalized.length > 0 &&
|
|
17
|
+
normalized.length <= MAX_STATE_VALUE_LENGTH &&
|
|
18
|
+
SAFE_STATE_VALUE.test(normalized));
|
|
19
|
+
}
|
|
20
|
+
export function normalizeStates(states) {
|
|
21
|
+
if (!states) {
|
|
22
|
+
return EMPTY_BLOCK_STATES;
|
|
23
|
+
}
|
|
24
|
+
const entries = Object.entries(states)
|
|
25
|
+
.filter(([key, value]) => typeof value === 'string' &&
|
|
26
|
+
isSafeStateName(key) &&
|
|
27
|
+
isSafeStateValue(value))
|
|
28
|
+
.map(([key, value]) => [key.toLowerCase(), value.toLowerCase()])
|
|
29
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
30
|
+
return Object.fromEntries(entries);
|
|
31
|
+
}
|
|
32
|
+
export function sameStates(a, b) {
|
|
33
|
+
const entries = Object.entries(a);
|
|
34
|
+
return (entries.length === Object.keys(b).length &&
|
|
35
|
+
entries.every(([key, value]) => b[key] === value));
|
|
36
|
+
}
|
|
37
|
+
export function formatStates(states) {
|
|
38
|
+
const entries = Object.entries(states).sort(([a], [b]) => a.localeCompare(b));
|
|
39
|
+
if (entries.length === 0) {
|
|
40
|
+
return '';
|
|
41
|
+
}
|
|
42
|
+
return `[${entries.map(([key, value]) => `${key}=${value}`).join(',')}]`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { WORLD_LIMITS } from '../config/world-limits.js';
|
|
2
|
+
import { getBlockByName, MINECRAFT_BLOCKS } from '../data/blocks.js';
|
|
3
|
+
import { toErrorMessage } from '../utils/error-message.js';
|
|
4
|
+
import { EMPTY_BLOCK_STATES, normalizeStates, sameStates } from './block-states.js';
|
|
5
|
+
const AIR = getBlockByName('air') ?? MINECRAFT_BLOCKS[0];
|
|
6
|
+
function coordinateKey(x, y, z) {
|
|
7
|
+
return `${x},${y},${z}`;
|
|
8
|
+
}
|
|
9
|
+
export class SimulatedDimension {
|
|
10
|
+
info;
|
|
11
|
+
blocks = new Map();
|
|
12
|
+
store;
|
|
13
|
+
writeDelayMs;
|
|
14
|
+
timer = null;
|
|
15
|
+
queue = Promise.resolve();
|
|
16
|
+
savePromise = null;
|
|
17
|
+
dirty = false;
|
|
18
|
+
constructor(info, store, writeDelayMs = 25) {
|
|
19
|
+
this.info = info;
|
|
20
|
+
this.store = store;
|
|
21
|
+
this.writeDelayMs = writeDelayMs;
|
|
22
|
+
}
|
|
23
|
+
get modifiedBlockCount() {
|
|
24
|
+
return this.blocks.size;
|
|
25
|
+
}
|
|
26
|
+
get placedBlocks() {
|
|
27
|
+
return [...this.blocks.values()];
|
|
28
|
+
}
|
|
29
|
+
async load() {
|
|
30
|
+
const stored = await this.store.load(this.info.type);
|
|
31
|
+
let skipped = 0;
|
|
32
|
+
for (const placed of stored) {
|
|
33
|
+
const key = coordinateKey(placed.x, placed.y, placed.z);
|
|
34
|
+
const isNew = !this.blocks.has(key);
|
|
35
|
+
const outOfRange = !this.contains(placed.x, placed.y, placed.z);
|
|
36
|
+
const overLimit = isNew && this.blocks.size >= WORLD_LIMITS.maxModifiedBlocks;
|
|
37
|
+
if (outOfRange || overLimit) {
|
|
38
|
+
skipped++;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
this.blocks.set(key, placed);
|
|
42
|
+
}
|
|
43
|
+
if (skipped > 0) {
|
|
44
|
+
console.error(`[lzimul-minecraft-mcp] Skipped ${skipped} invalid block(s) while loading ${this.info.key}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
contains(x, y, z) {
|
|
48
|
+
return (y >= this.info.minY &&
|
|
49
|
+
y < this.info.maxY &&
|
|
50
|
+
x >= WORLD_LIMITS.minHorizontal &&
|
|
51
|
+
x <= WORLD_LIMITS.maxHorizontal &&
|
|
52
|
+
z >= WORLD_LIMITS.minHorizontal &&
|
|
53
|
+
z <= WORLD_LIMITS.maxHorizontal);
|
|
54
|
+
}
|
|
55
|
+
canSetBlock(x, y, z) {
|
|
56
|
+
return (this.blocks.has(coordinateKey(x, y, z)) ||
|
|
57
|
+
this.blocks.size < WORLD_LIMITS.maxModifiedBlocks);
|
|
58
|
+
}
|
|
59
|
+
getBlock(x, y, z) {
|
|
60
|
+
return this.blocks.get(coordinateKey(x, y, z))?.block ?? AIR;
|
|
61
|
+
}
|
|
62
|
+
getStates(x, y, z) {
|
|
63
|
+
return (this.blocks.get(coordinateKey(x, y, z))?.states ?? EMPTY_BLOCK_STATES);
|
|
64
|
+
}
|
|
65
|
+
setBlock(x, y, z, block, states = EMPTY_BLOCK_STATES) {
|
|
66
|
+
const key = coordinateKey(x, y, z);
|
|
67
|
+
const existing = this.blocks.get(key);
|
|
68
|
+
const previous = existing?.block ?? AIR;
|
|
69
|
+
const normalized = normalizeStates(states);
|
|
70
|
+
if (previous.key === block.key &&
|
|
71
|
+
sameStates(existing?.states ?? EMPTY_BLOCK_STATES, normalized)) {
|
|
72
|
+
return previous;
|
|
73
|
+
}
|
|
74
|
+
const isRemoval = block.key === AIR.key && Object.keys(normalized).length === 0;
|
|
75
|
+
if (!existing &&
|
|
76
|
+
!isRemoval &&
|
|
77
|
+
this.blocks.size >= WORLD_LIMITS.maxModifiedBlocks) {
|
|
78
|
+
throw new RangeError(`The ${this.info.displayName} world already holds the maximum of ${WORLD_LIMITS.maxModifiedBlocks} modified blocks.`);
|
|
79
|
+
}
|
|
80
|
+
if (isRemoval) {
|
|
81
|
+
this.blocks.delete(key);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
this.blocks.set(key, { x, y, z, block, states: normalized });
|
|
85
|
+
}
|
|
86
|
+
this.dirty = true;
|
|
87
|
+
this.scheduleFlush();
|
|
88
|
+
return previous;
|
|
89
|
+
}
|
|
90
|
+
async flush() {
|
|
91
|
+
if (this.timer !== null) {
|
|
92
|
+
clearTimeout(this.timer);
|
|
93
|
+
this.timer = null;
|
|
94
|
+
}
|
|
95
|
+
while (this.savePromise !== null) {
|
|
96
|
+
await this.savePromise;
|
|
97
|
+
}
|
|
98
|
+
if (!this.dirty) {
|
|
99
|
+
await this.queue;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.dirty = false;
|
|
103
|
+
const snapshot = this.placedBlocks;
|
|
104
|
+
const run = this.queue.then(async () => {
|
|
105
|
+
await this.store.save(this.info.type, snapshot);
|
|
106
|
+
});
|
|
107
|
+
this.queue = run.catch(() => {
|
|
108
|
+
this.dirty = true;
|
|
109
|
+
});
|
|
110
|
+
this.savePromise = run;
|
|
111
|
+
try {
|
|
112
|
+
await run;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
this.savePromise = null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
scheduleFlush() {
|
|
119
|
+
if (this.timer !== null) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.timer = setTimeout(() => {
|
|
123
|
+
this.timer = null;
|
|
124
|
+
void this.flush().catch((error) => {
|
|
125
|
+
console.error(`[lzimul-minecraft-mcp] Failed to persist ${this.info.key}: ${toErrorMessage(error)}`);
|
|
126
|
+
});
|
|
127
|
+
}, this.writeDelayMs);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { WORLD_LIMITS } from '../../config/world-limits.js';
|
|
5
|
+
import { toErrorMessage } from '../../utils/error-message.js';
|
|
6
|
+
import { fromSerialized, toSerialized } from './serialized-block.js';
|
|
7
|
+
const FILE_FORMAT = 'lzimul-world/2';
|
|
8
|
+
const MAX_SNAPSHOT_BYTES = 128 * 1024 * 1024;
|
|
9
|
+
function warn(message) {
|
|
10
|
+
console.error(`[lzimul-minecraft-mcp] ${message}`);
|
|
11
|
+
}
|
|
12
|
+
export class FileWorldStore {
|
|
13
|
+
directory;
|
|
14
|
+
directoryReady = false;
|
|
15
|
+
constructor(directory) {
|
|
16
|
+
this.directory = directory;
|
|
17
|
+
}
|
|
18
|
+
async ensureDirectory() {
|
|
19
|
+
if (!this.directoryReady) {
|
|
20
|
+
await mkdir(this.directory, { recursive: true });
|
|
21
|
+
this.directoryReady = true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
fileFor(type) {
|
|
25
|
+
return path.join(this.directory, `${type}.json`);
|
|
26
|
+
}
|
|
27
|
+
async load(type) {
|
|
28
|
+
const file = this.fileFor(type);
|
|
29
|
+
let raw;
|
|
30
|
+
try {
|
|
31
|
+
const info = await stat(file);
|
|
32
|
+
if (info.size > MAX_SNAPSHOT_BYTES) {
|
|
33
|
+
await this.quarantine(file, `snapshot is larger than ${MAX_SNAPSHOT_BYTES} bytes`);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
raw = await readFile(file, 'utf8');
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code === 'ENOENT') {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`Cannot read world snapshot ${file}: ${toErrorMessage(error)}`, { cause: error });
|
|
43
|
+
}
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(raw);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
await this.quarantine(file, 'invalid JSON');
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
if (!parsed || !Array.isArray(parsed.blocks)) {
|
|
53
|
+
await this.quarantine(file, 'missing blocks array');
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
if (parsed.blocks.length > WORLD_LIMITS.maxModifiedBlocks) {
|
|
57
|
+
await this.quarantine(file, `${parsed.blocks.length} blocks exceed the limit of ${WORLD_LIMITS.maxModifiedBlocks}`);
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return parsed.blocks.flatMap(entry => {
|
|
61
|
+
const block = fromSerialized(entry);
|
|
62
|
+
if (block) {
|
|
63
|
+
return [block];
|
|
64
|
+
}
|
|
65
|
+
warn(`Skipped malformed block entry in ${file}`);
|
|
66
|
+
return [];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async save(type, blocks) {
|
|
70
|
+
if (blocks.length > WORLD_LIMITS.maxModifiedBlocks) {
|
|
71
|
+
throw new RangeError(`World snapshot has ${blocks.length} blocks, above the limit of ${WORLD_LIMITS.maxModifiedBlocks}`);
|
|
72
|
+
}
|
|
73
|
+
const file = this.fileFor(type);
|
|
74
|
+
try {
|
|
75
|
+
await this.ensureDirectory();
|
|
76
|
+
const sorted = blocks.toSorted((a, b) => a.x - b.x || a.y - b.y || a.z - b.z);
|
|
77
|
+
const payload = {
|
|
78
|
+
format: FILE_FORMAT,
|
|
79
|
+
dimension: `minecraft:${type}`,
|
|
80
|
+
updatedAt: new Date().toISOString(),
|
|
81
|
+
modifiedBlockCount: sorted.length,
|
|
82
|
+
blocks: sorted.map(toSerialized)
|
|
83
|
+
};
|
|
84
|
+
const temporary = `${file}.tmp`;
|
|
85
|
+
await writeFile(temporary, `${JSON.stringify(payload)}\n`, 'utf8');
|
|
86
|
+
await rename(temporary, file);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
this.directoryReady = false;
|
|
90
|
+
throw new Error(`Failed to write world snapshot ${file}: ${toErrorMessage(error)}`, { cause: error });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
async quarantine(file, reason) {
|
|
94
|
+
const backup = `${file}.corrupt-${randomUUID()}`;
|
|
95
|
+
try {
|
|
96
|
+
await rename(file, backup);
|
|
97
|
+
warn(`World file ${file} is unusable (${reason}); moved it to ${backup}`);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
warn(`World file ${file} is unusable (${reason}); could not move it: ${toErrorMessage(error)}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { WORLD_STORAGE } from '../../config/storage.js';
|
|
2
|
+
import { FileWorldStore } from './file-world-store.js';
|
|
3
|
+
import { MemoryWorldStore } from './memory-world-store.js';
|
|
4
|
+
export function createWorldStore() {
|
|
5
|
+
if (WORLD_STORAGE.kind === 'memory') {
|
|
6
|
+
return new MemoryWorldStore();
|
|
7
|
+
}
|
|
8
|
+
return new FileWorldStore(WORLD_STORAGE.directory);
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { WORLD_LIMITS } from '../../config/world-limits.js';
|
|
2
|
+
export class MemoryWorldStore {
|
|
3
|
+
worlds = new Map();
|
|
4
|
+
async load(type) {
|
|
5
|
+
const stored = this.worlds.get(type);
|
|
6
|
+
return stored ? [...stored] : [];
|
|
7
|
+
}
|
|
8
|
+
async save(type, blocks) {
|
|
9
|
+
if (blocks.length > WORLD_LIMITS.maxModifiedBlocks) {
|
|
10
|
+
throw new RangeError(`World snapshot has ${blocks.length} blocks, above the limit of ${WORLD_LIMITS.maxModifiedBlocks}`);
|
|
11
|
+
}
|
|
12
|
+
this.worlds.set(type, [...blocks]);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { getBlockByName } from '../../data/blocks.js';
|
|
2
|
+
import { normalizeStates } from '../block-states.js';
|
|
3
|
+
export function toSerialized(placed) {
|
|
4
|
+
const states = normalizeStates(placed.states);
|
|
5
|
+
if (Object.keys(states).length === 0) {
|
|
6
|
+
return {
|
|
7
|
+
x: placed.x,
|
|
8
|
+
y: placed.y,
|
|
9
|
+
z: placed.z,
|
|
10
|
+
block: placed.block.key
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
x: placed.x,
|
|
15
|
+
y: placed.y,
|
|
16
|
+
z: placed.z,
|
|
17
|
+
block: placed.block.key,
|
|
18
|
+
states
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function readStates(value) {
|
|
22
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
const states = {};
|
|
26
|
+
for (const [key, propertyValue] of Object.entries(value)) {
|
|
27
|
+
if (typeof propertyValue === 'string' && propertyValue.length > 0) {
|
|
28
|
+
states[key] = propertyValue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return normalizeStates(states);
|
|
32
|
+
}
|
|
33
|
+
export function fromSerialized(entry) {
|
|
34
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const record = entry;
|
|
38
|
+
const { x, y, z, block } = record;
|
|
39
|
+
if (typeof x !== 'number' ||
|
|
40
|
+
typeof y !== 'number' ||
|
|
41
|
+
typeof z !== 'number' ||
|
|
42
|
+
!Number.isInteger(x) ||
|
|
43
|
+
!Number.isInteger(y) ||
|
|
44
|
+
!Number.isInteger(z) ||
|
|
45
|
+
typeof block !== 'string') {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const resolved = getBlockByName(block.toLowerCase());
|
|
49
|
+
if (!resolved) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return { x, y, z, block: resolved, states: readStates(record.states) };
|
|
53
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DIMENSIONS } from '../config/dimensions.js';
|
|
2
|
+
import { toErrorMessage } from '../utils/error-message.js';
|
|
3
|
+
import { SimulatedDimension } from './simulated-dimension.js';
|
|
4
|
+
export class World {
|
|
5
|
+
dimensions;
|
|
6
|
+
constructor(dimensions) {
|
|
7
|
+
this.dimensions = dimensions;
|
|
8
|
+
}
|
|
9
|
+
static async load(store, writeDelayMs = 25) {
|
|
10
|
+
const entries = await Promise.all(DIMENSIONS.map(async (dimension) => {
|
|
11
|
+
const simulated = new SimulatedDimension(dimension, store, writeDelayMs);
|
|
12
|
+
try {
|
|
13
|
+
await simulated.load();
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
throw new Error(`Failed to load the ${dimension.displayName} world: ${toErrorMessage(error)}`, { cause: error });
|
|
17
|
+
}
|
|
18
|
+
return [dimension.type, simulated];
|
|
19
|
+
}));
|
|
20
|
+
return new World(new Map(entries));
|
|
21
|
+
}
|
|
22
|
+
static listDimensions() {
|
|
23
|
+
return DIMENSIONS;
|
|
24
|
+
}
|
|
25
|
+
getDimension(type) {
|
|
26
|
+
const dimension = this.dimensions.get(type);
|
|
27
|
+
if (!dimension) {
|
|
28
|
+
throw new Error(`Unknown dimension "${type}". Use "overworld", "the_nether" or "the_end".`);
|
|
29
|
+
}
|
|
30
|
+
return dimension;
|
|
31
|
+
}
|
|
32
|
+
async flush() {
|
|
33
|
+
const results = await Promise.allSettled([...this.dimensions.values()].map(dimension => dimension.flush()));
|
|
34
|
+
const failures = results.filter((result) => result.status === 'rejected');
|
|
35
|
+
if (failures.length > 0) {
|
|
36
|
+
throw new Error(failures.map(failure => toErrorMessage(failure.reason)).join('; '));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAMzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE/C,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,CAKpD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dimensions.d.ts","sourceRoot":"","sources":["../../src/config/dimensions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sCAAsC,CAAC;AAEvE,eAAO,MAAM,UAAU,EAAE,SAAS,UAAU,EAyB3C,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { EIconTheme } from '../enums/icon-theme.enum.js';
|
|
2
|
+
export declare const SERVER_INFO: {
|
|
3
|
+
name: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description: string;
|
|
6
|
+
version: string;
|
|
7
|
+
websiteUrl: string;
|
|
8
|
+
icons: {
|
|
9
|
+
src: string;
|
|
10
|
+
mimeType: string;
|
|
11
|
+
sizes: string[];
|
|
12
|
+
theme: EIconTheme;
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=server-info.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-info.d.ts","sourceRoot":"","sources":["../../src/config/server-info.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAEzD,eAAO,MAAM,WAAW;;;;;;;;;;;;CAoBvB,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type EWorldStorageKind = 'file' | 'memory';
|
|
2
|
+
export interface IWorldStorageConfig {
|
|
3
|
+
readonly kind: EWorldStorageKind;
|
|
4
|
+
readonly directory: string;
|
|
5
|
+
readonly writeDelayMs: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const WORLD_STORAGE: IWorldStorageConfig;
|
|
8
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/config/storage.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,CAAC;AAElD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IACjC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAeD,eAAO,MAAM,aAAa,EAAE,mBAI3B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"world-limits.d.ts","sourceRoot":"","sources":["../../src/config/world-limits.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY;;;;;;CAOf,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IBlock } from '../interfaces/block.interface.js';
|
|
2
|
+
export declare const MINECRAFT_BLOCKS: readonly IBlock[];
|
|
3
|
+
export declare const MINECRAFT_BLOCK_COUNT: number;
|
|
4
|
+
export declare function getBlockByName(name: string): IBlock | undefined;
|
|
5
|
+
//# sourceMappingURL=blocks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../../src/data/blocks.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kCAAkC,CAAC;AAE/D,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAiniB7C,CAAC;AAEF,eAAO,MAAM,qBAAqB,QAA0B,CAAC;AAQ7D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE/D"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IItem } from '../interfaces/item.interface.js';
|
|
2
|
+
export declare const MINECRAFT_ITEMS: readonly IItem[];
|
|
3
|
+
export declare const MINECRAFT_ITEM_COUNT: number;
|
|
4
|
+
export declare function getItemByName(name: string): IItem | undefined;
|
|
5
|
+
//# sourceMappingURL=items.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"items.d.ts","sourceRoot":"","sources":["../../src/data/items.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iCAAiC,CAAC;AAE7D,eAAO,MAAM,eAAe,EAAE,SAAS,KAAK,EAg7V3C,CAAC;AAEF,eAAO,MAAM,oBAAoB,QAAyB,CAAC;AAQ3D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,CAE7D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/data/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bounding-box.enum.d.ts","sourceRoot":"","sources":["../../src/enums/bounding-box.enum.ts"],"names":[],"mappings":"AAAA,oBAAY,YAAY;IACtB,KAAK,UAAU;IACf,KAAK,UAAU;CAChB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dimension-type.enum.d.ts","sourceRoot":"","sources":["../../src/enums/dimension-type.enum.ts"],"names":[],"mappings":"AAAA,oBAAY,cAAc;IACxB,SAAS,cAAc;IACvB,SAAS,eAAe;IACxB,MAAM,YAAY;CACnB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icon-theme.enum.d.ts","sourceRoot":"","sources":["../../src/enums/icon-theme.enum.ts"],"names":[],"mappings":"AAAA,oBAAY,UAAU;IACpB,KAAK,UAAU;IACf,IAAI,SAAS;CACd"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-states.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/block-states.interface.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EBoundingBox } from '../enums/bounding-box.enum.js';
|
|
2
|
+
import type { IDataEntry } from './data-entry.interface.js';
|
|
3
|
+
export interface IBlock extends IDataEntry {
|
|
4
|
+
readonly hardness: number;
|
|
5
|
+
readonly resistance: number;
|
|
6
|
+
readonly stackSize: number;
|
|
7
|
+
readonly diggable: boolean;
|
|
8
|
+
readonly material: string;
|
|
9
|
+
readonly transparent: boolean;
|
|
10
|
+
readonly emitLight: number;
|
|
11
|
+
readonly filterLight: number;
|
|
12
|
+
readonly boundingBox: EBoundingBox;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=block.interface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/block.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,MAAM,WAAW,MAAO,SAAQ,UAAU;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,YAAY,CAAC;CACpC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-entry.interface.d.ts","sourceRoot":"","sources":["../../src/interfaces/data-entry.interface.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { EDimensionType } from '../enums/dimension-type.enum.js';
|
|
2
|
+
export interface IDimension {
|
|
3
|
+
readonly key: string;
|
|
4
|
+
readonly type: EDimensionType;
|
|
5
|
+
readonly displayName: string;
|
|
6
|
+
readonly minY: number;
|
|
7
|
+
readonly maxY: number;
|
|
8
|
+
readonly hasSkylight: boolean;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=dimension.interface.d.ts.map
|