@james-pre/mc-admin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE.md +675 -0
  2. package/README.md +15 -0
  3. package/dist/buffers.d.ts +1 -0
  4. package/dist/buffers.js +1 -0
  5. package/dist/cli.d.ts +6 -0
  6. package/dist/cli.js +110 -0
  7. package/dist/common/buffers.d.ts +8 -0
  8. package/dist/common/buffers.js +47 -0
  9. package/dist/common/chunk.d.ts +41 -0
  10. package/dist/common/chunk.js +31 -0
  11. package/dist/common/index.d.ts +7 -0
  12. package/dist/common/index.js +7 -0
  13. package/dist/common/level.d.ts +21 -0
  14. package/dist/common/level.js +16 -0
  15. package/dist/common/log.d.ts +13 -0
  16. package/dist/common/log.js +10 -0
  17. package/dist/common/nbt.d.ts +85 -0
  18. package/dist/common/nbt.js +179 -0
  19. package/dist/common/rcon.d.ts +58 -0
  20. package/dist/common/rcon.js +154 -0
  21. package/dist/common/region.d.ts +52 -0
  22. package/dist/common/region.js +128 -0
  23. package/dist/common/snbt.d.ts +98 -0
  24. package/dist/common/snbt.js +332 -0
  25. package/dist/common/tsconfig.tsbuildinfo +1 -0
  26. package/dist/config.d.ts +20 -0
  27. package/dist/config.js +12 -0
  28. package/dist/index.d.ts +10 -0
  29. package/dist/index.js +10 -0
  30. package/dist/level.d.ts +53 -0
  31. package/dist/level.js +136 -0
  32. package/dist/log.d.ts +18 -0
  33. package/dist/log.js +137 -0
  34. package/dist/main.d.ts +2 -0
  35. package/dist/main.js +9 -0
  36. package/dist/nbt.d.ts +1 -0
  37. package/dist/nbt.js +1 -0
  38. package/dist/prune.d.ts +85 -0
  39. package/dist/prune.js +201 -0
  40. package/dist/rcon.d.ts +12 -0
  41. package/dist/rcon.js +19 -0
  42. package/dist/region.d.ts +1 -0
  43. package/dist/region.js +1 -0
  44. package/dist/snbt.d.ts +14 -0
  45. package/dist/snbt.js +63 -0
  46. package/dist/tsconfig.tsbuildinfo +1 -0
  47. package/dist/utils.d.ts +9 -0
  48. package/dist/utils.js +52 -0
  49. package/package.json +68 -0
@@ -0,0 +1,58 @@
1
+ export declare const enum PacketType {
2
+ /** A command's output. */
3
+ Value = 0,
4
+ /** Run a command. */
5
+ Command = 2,
6
+ /** Whether a login was accepted. */
7
+ AuthResponse = 2,
8
+ /** Log in. */
9
+ AuthRequest = 3
10
+ }
11
+ export interface Packet {
12
+ id: number;
13
+ type: PacketType;
14
+ body: string;
15
+ }
16
+ /** A request, ready to hand to a transport. */
17
+ export declare function encode(id: number, type: PacketType, body: string): Uint8Array<ArrayBuffer>;
18
+ export interface ConnectionOptions {
19
+ /**
20
+ * If set, resolve a command as soon as the first response packet is received rather than collecting the whole response.
21
+ */
22
+ shortCommandOutput?: boolean;
23
+ }
24
+ /** A command waiting on the server. */
25
+ interface Pending extends PromiseWithResolvers<string> {
26
+ id: number;
27
+ received: string[];
28
+ endId: number | null;
29
+ }
30
+ /** A client RCON session */
31
+ export declare class Connection {
32
+ protected readonly options: ConnectionOptions;
33
+ protected readonly pending: Map<number, Pending>;
34
+ protected auth: PromiseWithResolvers<void> | null;
35
+ protected nextId: number;
36
+ protected readonly writer: WritableStreamDefaultWriter<Uint8Array>;
37
+ /** Settles when the server's half of the stream ends */
38
+ readonly closed: Promise<void>;
39
+ /**
40
+ * @param stream The transport. Use Node.js `Socket`'s with `Duplex.toWeb`.
41
+ */
42
+ constructor(stream: ReadableWritablePair<Uint8Array, Uint8Array>, options?: ConnectionOptions);
43
+ /** Pull packets until the server stops sending them. */
44
+ protected read(readable: ReadableStream<Uint8Array>): Promise<void>;
45
+ protected buffer: Uint8Array<ArrayBuffer>;
46
+ /** Every whole packet the bytes received so far complete. */
47
+ protected push(chunk: Uint8Array): Generator<Packet>;
48
+ protected accept(packet: Packet): void;
49
+ /** Log in. Replies are treated as auth results until this settles. */
50
+ authenticate(password: string): Promise<void>;
51
+ /** Run a command and wait for its output. */
52
+ command(text: string): Promise<string>;
53
+ /** Close this end of the stream, leaving whatever owns the transport to tear it down. */
54
+ close(): Promise<void>;
55
+ /** Fail everything still in flight, e.g. once the socket closes. */
56
+ abort(reason: Error): void;
57
+ }
58
+ export {};
@@ -0,0 +1,154 @@
1
+ import { decodeUTF8, encodeUTF8 } from 'utilium';
2
+ /* eslint-disable @typescript-eslint/no-duplicate-enum-values */
3
+ export var PacketType;
4
+ (function (PacketType) {
5
+ /** A command's output. */
6
+ PacketType[PacketType["Value"] = 0] = "Value";
7
+ /** Run a command. */
8
+ PacketType[PacketType["Command"] = 2] = "Command";
9
+ /** Whether a login was accepted. */
10
+ PacketType[PacketType["AuthResponse"] = 2] = "AuthResponse";
11
+ /** Log in. */
12
+ PacketType[PacketType["AuthRequest"] = 3] = "AuthRequest";
13
+ })(PacketType || (PacketType = {}));
14
+ /** Every packet opens with three `int32`s and ends with two nulls terminating the body. */
15
+ const headerSize = 12;
16
+ /** `length` counts everything after itself, never its own four bytes. */
17
+ const lengthSize = 4;
18
+ /** What `length` covers besides the body: `id`, `type`, and the two nulls. */
19
+ const overhead = 10;
20
+ /** A request, ready to hand to a transport. */
21
+ export function encode(id, type, body) {
22
+ const bytes = encodeUTF8(body);
23
+ const packet = new Uint8Array(headerSize + bytes.byteLength + 2);
24
+ const view = new DataView(packet.buffer);
25
+ view.setInt32(0, overhead + bytes.byteLength, true);
26
+ view.setInt32(lengthSize, id, true);
27
+ view.setInt32(lengthSize + 4, type, true);
28
+ packet.set(bytes, headerSize);
29
+ return packet;
30
+ }
31
+ /** A client RCON session */
32
+ export class Connection {
33
+ options;
34
+ pending = new Map();
35
+ auth = null;
36
+ nextId = 1;
37
+ writer;
38
+ /** Settles when the server's half of the stream ends */
39
+ closed;
40
+ /**
41
+ * @param stream The transport. Use Node.js `Socket`'s with `Duplex.toWeb`.
42
+ */
43
+ constructor(stream, options = {}) {
44
+ this.options = options;
45
+ this.writer = stream.writable.getWriter();
46
+ this.closed = this.read(stream.readable);
47
+ // Keep a lone failure from surfacing as an unhandled rejection; callers still see it.
48
+ this.closed.catch(() => { });
49
+ }
50
+ /** Pull packets until the server stops sending them. */
51
+ async read(readable) {
52
+ const reader = readable.getReader();
53
+ try {
54
+ for (let next = await reader.read(); !next.done; next = await reader.read())
55
+ for (const packet of this.push(next.value))
56
+ this.accept(packet);
57
+ }
58
+ catch (error) {
59
+ this.abort(error instanceof Error ? error : new Error(String(error)));
60
+ throw error;
61
+ }
62
+ this.abort(new Error('the connection closed'));
63
+ }
64
+ buffer = new Uint8Array(0);
65
+ /** Every whole packet the bytes received so far complete. */
66
+ *push(chunk) {
67
+ const grown = new Uint8Array(this.buffer.byteLength + chunk.byteLength);
68
+ grown.set(this.buffer);
69
+ grown.set(chunk, this.buffer.byteLength);
70
+ this.buffer = grown;
71
+ // Every packet is at least `headerSize + 2` bytes long.
72
+ // Waiting for a whole header before reading `length` can't stall a packet that has fully arrived.
73
+ while (this.buffer.byteLength >= headerSize) {
74
+ const view = new DataView(this.buffer.buffer, this.buffer.byteOffset, this.buffer.byteLength);
75
+ const length = view.getInt32(0, true);
76
+ if (length < overhead)
77
+ throw new Error(`RCON packet claims an impossible length (${length})`);
78
+ const end = lengthSize + length;
79
+ if (this.buffer.byteLength < end)
80
+ return;
81
+ yield {
82
+ id: view.getInt32(lengthSize, true),
83
+ type: view.getInt32(lengthSize + 4, true),
84
+ body: decodeUTF8(this.buffer.subarray(headerSize, end - 2)),
85
+ };
86
+ this.buffer = this.buffer.subarray(end);
87
+ }
88
+ }
89
+ accept(packet) {
90
+ if (this.auth) {
91
+ if (packet.type !== PacketType.AuthResponse)
92
+ return;
93
+ const auth = this.auth;
94
+ this.auth = null;
95
+ if (packet.id === -1)
96
+ auth.reject(new Error('authentication failed (bad password)'));
97
+ else
98
+ auth.resolve();
99
+ return;
100
+ }
101
+ const pending = this.pending.get(packet.id);
102
+ if (!pending)
103
+ return;
104
+ if (pending.endId === null) {
105
+ this.pending.delete(pending.id);
106
+ pending.resolve(packet.body);
107
+ return;
108
+ }
109
+ if (packet.id !== pending.endId) {
110
+ pending.received.push(packet.body);
111
+ return;
112
+ }
113
+ this.pending.delete(pending.id);
114
+ this.pending.delete(pending.endId);
115
+ pending.resolve(pending.received.join(''));
116
+ }
117
+ /** Log in. Replies are treated as auth results until this settles. */
118
+ async authenticate(password) {
119
+ if (this.auth)
120
+ throw new Error('already authenticating');
121
+ const auth = Promise.withResolvers();
122
+ this.auth = auth;
123
+ await this.writer.write(encode(this.nextId++, PacketType.AuthRequest, password));
124
+ await auth.promise;
125
+ }
126
+ /** Run a command and wait for its output. */
127
+ async command(text) {
128
+ const id = this.nextId++;
129
+ const pending = { ...Promise.withResolvers(), id, received: [], endId: null };
130
+ this.pending.set(id, pending);
131
+ if (!this.options.shortCommandOutput) {
132
+ pending.endId = this.nextId++;
133
+ this.pending.set(pending.endId, pending);
134
+ }
135
+ await this.writer.write(encode(id, PacketType.Command, text));
136
+ if (pending.endId !== null)
137
+ await this.writer.write(encode(pending.endId, PacketType.Command, ''));
138
+ const result = await pending.promise;
139
+ return result.trimEnd();
140
+ }
141
+ /** Close this end of the stream, leaving whatever owns the transport to tear it down. */
142
+ async close() {
143
+ await this.writer.close();
144
+ }
145
+ /** Fail everything still in flight, e.g. once the socket closes. */
146
+ abort(reason) {
147
+ const auth = this.auth;
148
+ this.auth = null;
149
+ auth?.reject(reason);
150
+ for (const pending of this.pending.values())
151
+ pending.reject(reason);
152
+ this.pending.clear();
153
+ }
154
+ }
@@ -0,0 +1,52 @@
1
+ import * as chunk from './chunk.js';
2
+ import type { RegionFile } from './level.js';
3
+ /** Region files are addressed in 4 KiB sectors. */
4
+ export declare const sectorSize = 4096;
5
+ /** Chunks per region, per axis. */
6
+ export declare const regionSize = 32;
7
+ /** Chunks per region. */
8
+ export declare const chunkCount: number;
9
+ /** The region coordinates a file name encodes, or null if it isn't a region file. */
10
+ export declare function parseName(name: string): {
11
+ x: number;
12
+ z: number;
13
+ } | null;
14
+ export declare class Region {
15
+ readonly file?: RegionFile | undefined;
16
+ readonly data: Uint8Array<ArrayBuffer>;
17
+ protected readonly view: DataView;
18
+ constructor(data: BufferSource, file?: RegionFile | undefined);
19
+ /**
20
+ * Whether the file is too short to hold the header.
21
+ *
22
+ * The server creates region files before it has anything to put in them, so a zero-length or
23
+ * truncated file is normal rather than damage.
24
+ */
25
+ get empty(): boolean;
26
+ /** Where chunk `index` lives, or null when the region has never stored it. */
27
+ entry(index: number): chunk.Entry | null;
28
+ /** Where the chunk at region-local coordinates lives, or null when the region lacks it. */
29
+ at(x: number, z: number): chunk.Entry | null;
30
+ /** Every chunk the region actually stores. */
31
+ entries(): Generator<chunk.Entry>;
32
+ /**
33
+ * A chunk's record, still compressed.
34
+ *
35
+ * @throws When the header points outside the file, which means the region is truncated.
36
+ */
37
+ raw(entry: chunk.Entry): chunk.Raw;
38
+ /** A chunk's NBT. */
39
+ chunk(entry: chunk.Entry): Promise<chunk.Parsed>;
40
+ /**
41
+ * Every chunk's NBT.
42
+ *
43
+ * A single bad chunk ends the iteration, since there is no way to report it otherwise. To
44
+ * survive damaged regions, walk {@link entries} and call {@link chunk} inside a try/catch.
45
+ */
46
+ chunksUnsafe(): AsyncGenerator<chunk.Parsed>;
47
+ /**
48
+ * Every chunk's NBT.
49
+ */
50
+ chunks(onError?: (error: Error, entry: chunk.Entry) => void): AsyncGenerator<chunk.Parsed>;
51
+ filterChunks(predicate: (chunk: chunk.Parsed) => boolean): Promise<chunk.Parsed[]>;
52
+ }
@@ -0,0 +1,128 @@
1
+ import { toBytes } from './buffers.js';
2
+ import * as chunk from './chunk.js';
3
+ import { parse } from './nbt.js';
4
+ /** Region files are addressed in 4 KiB sectors. */
5
+ export const sectorSize = 4096;
6
+ /** Chunks per region, per axis. */
7
+ export const regionSize = 32;
8
+ /** Chunks per region. */
9
+ export const chunkCount = regionSize * regionSize;
10
+ const namePattern = /^r\.(-?\d+)\.(-?\d+)\.mca$/;
11
+ /** The region coordinates a file name encodes, or null if it isn't a region file. */
12
+ export function parseName(name) {
13
+ const match = namePattern.exec(name);
14
+ return match ? { x: Number(match[1]), z: Number(match[2]) } : null;
15
+ }
16
+ export class Region {
17
+ file;
18
+ data;
19
+ view;
20
+ constructor(data, file) {
21
+ this.file = file;
22
+ this.data = toBytes(data);
23
+ this.view = new DataView(this.data.buffer, this.data.byteOffset, this.data.byteLength);
24
+ }
25
+ /**
26
+ * Whether the file is too short to hold the header.
27
+ *
28
+ * The server creates region files before it has anything to put in them, so a zero-length or
29
+ * truncated file is normal rather than damage.
30
+ */
31
+ get empty() {
32
+ return this.data.byteLength < sectorSize * 2;
33
+ }
34
+ /** Where chunk `index` lives, or null when the region has never stored it. */
35
+ entry(index) {
36
+ if (index < 0 || index >= chunkCount)
37
+ throw new RangeError(`chunk index ${index} is outside the region`);
38
+ if (this.empty)
39
+ return null;
40
+ // A location packs a 3-byte sector offset with a 1-byte sector count.
41
+ const location = this.view.getUint32(index * 4);
42
+ if (location === 0)
43
+ return null;
44
+ return {
45
+ index,
46
+ x: index % regionSize,
47
+ z: Math.floor(index / regionSize),
48
+ offset: (location >>> 8) * sectorSize,
49
+ sectors: location & 0xff,
50
+ timestamp: this.view.getInt32(sectorSize + index * 4),
51
+ };
52
+ }
53
+ /** Where the chunk at region-local coordinates lives, or null when the region lacks it. */
54
+ at(x, z) {
55
+ return this.entry(x + z * regionSize);
56
+ }
57
+ /** Every chunk the region actually stores. */
58
+ *entries() {
59
+ for (let index = 0; index < chunkCount; index++) {
60
+ const entry = this.entry(index);
61
+ if (entry)
62
+ yield entry;
63
+ }
64
+ }
65
+ /**
66
+ * A chunk's record, still compressed.
67
+ *
68
+ * @throws When the header points outside the file, which means the region is truncated.
69
+ */
70
+ raw(entry) {
71
+ const { offset } = entry;
72
+ if (offset + 5 > this.data.byteLength)
73
+ throw new RangeError(`chunk ${entry.x},${entry.z} starts past the end of the region`);
74
+ // The stored length counts the compression byte that follows it.
75
+ const end = offset + 4 + this.view.getUint32(offset);
76
+ if (end > this.data.byteLength)
77
+ throw new RangeError(`chunk ${entry.x},${entry.z} runs past the end of the region`);
78
+ const flags = this.view.getUint8(offset + 4);
79
+ return {
80
+ ...entry,
81
+ compression: flags & ~chunk.externalFlag,
82
+ external: (flags & chunk.externalFlag) !== 0,
83
+ data: this.data.subarray(offset + 5, end),
84
+ };
85
+ }
86
+ /** A chunk's NBT. */
87
+ async chunk(entry) {
88
+ const raw = this.raw(entry);
89
+ return { ...raw, tag: parse(await chunk.payload(raw)).tag };
90
+ }
91
+ /**
92
+ * Every chunk's NBT.
93
+ *
94
+ * A single bad chunk ends the iteration, since there is no way to report it otherwise. To
95
+ * survive damaged regions, walk {@link entries} and call {@link chunk} inside a try/catch.
96
+ */
97
+ async *chunksUnsafe() {
98
+ for (const entry of this.entries())
99
+ yield await this.chunk(entry);
100
+ }
101
+ /**
102
+ * Every chunk's NBT.
103
+ */
104
+ async *chunks(onError) {
105
+ for (const entry of this.entries()) {
106
+ try {
107
+ yield await this.chunk(entry);
108
+ }
109
+ catch (e) {
110
+ onError?.(e, entry);
111
+ }
112
+ }
113
+ }
114
+ async filterChunks(predicate) {
115
+ const chunks = [];
116
+ for (const entry of this.entries()) {
117
+ try {
118
+ const chunk = await this.chunk(entry);
119
+ if (predicate(chunk))
120
+ chunks.push(chunk);
121
+ }
122
+ catch {
123
+ // Ignore chunks that can't be read
124
+ }
125
+ }
126
+ return chunks;
127
+ }
128
+ }
@@ -0,0 +1,98 @@
1
+ import type { Tag } from './nbt.js';
2
+ import { TagType } from './nbt.js';
3
+ export interface ParseOptions {
4
+ /**
5
+ * Accept unquoted words as string values, e.g. `{id:stone}`.
6
+ *
7
+ * Off by default, because {@link scan} relies on the strictness: without it, Brigadier's
8
+ * parse-error pointer `<--[HERE]` reads as a one-element list of the string `HERE` and gets
9
+ * treated like data. Minecraft always quotes the strings it prints, so nothing is lost.
10
+ */
11
+ bareStrings?: boolean;
12
+ }
13
+ export declare class SnbtError extends SyntaxError {
14
+ /** Index into the source where parsing gave up. */
15
+ readonly position: number;
16
+ constructor(message: string,
17
+ /** Index into the source where parsing gave up. */
18
+ position: number);
19
+ }
20
+ /** @internal */
21
+ export declare const escapes: Record<string, string>;
22
+ /** @internal */
23
+ export declare const suffixTags: Record<string, TagType>;
24
+ /** The letter printed after a number of each type. Ints get nothing. */
25
+ export declare const tagSuffixes: {
26
+ 1: string;
27
+ 2: string;
28
+ 4: string;
29
+ 5: string;
30
+ 6: string;
31
+ };
32
+ /** The marker a typed array leads with, and the suffix its items carry. */
33
+ export declare const arrayMarkers: {
34
+ 7: string[];
35
+ 11: string[];
36
+ 12: string[];
37
+ };
38
+ /** @internal */
39
+ export declare class Parser {
40
+ protected readonly source: string;
41
+ offset: number;
42
+ protected readonly bareStrings: boolean;
43
+ constructor(source: string, options?: ParseOptions);
44
+ protected error(message: string, position?: number): never;
45
+ protected get current(): string;
46
+ protected skipSpace(): void;
47
+ protected take(literal: string): boolean;
48
+ protected expect(literal: string): void;
49
+ protected match(pattern: RegExp): RegExpExecArray | null;
50
+ /** True when the character after a `length`-long match would continue the word. */
51
+ protected continues(length: number): boolean;
52
+ /** The character(s) a backslash stands for. */
53
+ protected escape(): string;
54
+ /** A quoted string, or null when the cursor isn't on a quote. */
55
+ protected quoted(): string | null;
56
+ /** A compound key: quoted, or a bare word. */
57
+ protected key(): string;
58
+ /** A number with its optional type suffix, or null when the cursor isn't on one. */
59
+ protected number(): Tag | null;
60
+ /** `true` and `false` are how the game prints bytes it knows are flags. */
61
+ protected boolean(): Tag | null;
62
+ /** An unsuffixed number is an int, or a double once it has a fraction or an exponent. */
63
+ protected numeric(digits: string, suffix: string, position: number): Tag;
64
+ /** Integer types reject a fractional or exponential literal, e.g. `1.5b`. */
65
+ protected whole(digits: string, position: number): number;
66
+ /** The integer an array item carries, whatever suffix it was written with. */
67
+ protected arrayItem(tag: Tag, position: number): bigint;
68
+ protected compound(): Tag;
69
+ /** A list, or one of the typed arrays that lead with their element type: `[I; 1, 2]`. */
70
+ protected list(): Tag;
71
+ /** The body of `[B;…]`, `[I;…]` or `[L;…]`, whose marker has already been consumed. */
72
+ protected array(marker: string): Tag;
73
+ value(): Tag;
74
+ /** One value and nothing else, ignoring the whitespace around it. */
75
+ document(): Tag;
76
+ }
77
+ /**
78
+ * Parse a whole SNBT document.
79
+ *
80
+ * @throws SnbtError when the source isn't well-formed SNBT.
81
+ */
82
+ export declare function parse(source: string, options?: ParseOptions): Tag;
83
+ export interface Span {
84
+ /** Index of the value's first character. */
85
+ start: number;
86
+ /** Index just past its last character. */
87
+ end: number;
88
+ tag: Tag;
89
+ }
90
+ /**
91
+ * Parse the value starting at `start`, returning it with the index just past it, or null when
92
+ * what's there isn't well-formed SNBT. Unlike {@link parse}, trailing text is fine.
93
+ */
94
+ export declare function parseAt(source: string, start?: number, options?: ParseOptions): Span | null;
95
+ /**
96
+ * Find the SNBT embedded in prose.
97
+ */
98
+ export declare function scan(text: string): Generator<Span>;