UNPKG

mcbe-leveldb

Version:

A utility module for easily working with Minecraft Bedrock Edition world data.

1,193 lines (1,159 loc) 83.3 kB
import NBT, { type Compound } from "prismarine-nbt"; import type { OmitNeverValueKeys, VerifyConstraint } from "./types.js"; function toPrimitive(tag: any): number { switch (tag.type) { case "byte": case "short": case "int": case "float": case "double": return tag.value; case "long": return Number(toLong(tag.value)); default: throw new SyntaxError("Cannot convert to primitive: " + tag.type); } } function uuidToIntArray(uuidStr: string): number[] { const hex: string = uuidStr.replace(/-/g, ""); if (hex.length !== 32) throw new SyntaxError("Invalid UUID: " + uuidStr, { cause: { position: 0, stack: [ { input: uuidStr, positionInInput: 0, function: "uuidToIntArray", error: { type: "InvalidUUID", uuid: uuidStr }, }, ], } as const satisfies SNBTParseErrorCause, }); const arr: number[] = []; for (let i: number = 0; i < 16; i += 4) { arr.push((parseInt(hex.slice(i, i + 4), 16) << 16) >> 16); // 32-bit signed chunks } return arr; } /** * The type of an SNBT parse error. */ export type SNBTParseErrorType = | NonNullable< Extract< SNBTParseErrorCauseStackItem, { /** * @ignore */ error?: any; } >["error"] >["type"] | "ExpectedEndOfInput" | "ExpectedCompoundOrList"; /** * Maps SNBT parse error types to error codes. */ export const SNBTParseErrorTypeToCode = { /** * An error that occurred because a disallowed type was found in a typed array. */ DisallowedTypeInTypedArray: "disallowed-type-in-typed-array", /** * An error that occurred because an expected compound or list was not found. */ ExpectedCompoundOrList: "expected-compound-or-list", /** * An error that occurred because an argument to a function was invalid. */ InvalidArgumentToFunction: "invalid-argument-to-function", /** * An error that occurred because an SNBT key was invalid. */ InvalidSNBTKey: "invalid-snbt-key", /** * An error that occurred because an SNBT string was invalid. */ InvalidSNBTString: "invalid-snbt-string", /** * An error that occurred because a UUID was invalid. */ InvalidUUID: "invalid-uuid", /** * An error that occurred because an unsupported function was called. */ UnsupportedFunction: "unsupported-function", /** * An error that occurred because an unsupported SNBT primitive was found. */ UnsupportedSNBTPrimitive: "unsupported-snbt-primitive", /** * An error that occurred because a list contained multiple types. */ MixedListTypesNotAllowed: "mixed-list-types-not-allowed", /** * An error that occurred because an unsupported type was found in a typed array. */ UnsupportedTypeInTypedArray: "unsupported-type-in-typed-array", /** * An error that occurred because the end of the input was expected. */ ExpectedEndOfInput: "expected-end-of-input", } as const satisfies Record<SNBTParseErrorType, string>; /** * The namespace of SNBT parse errors. */ export const SNBTParseErrorDisplayNamespace = "mcbe-leveldb"; /** * A stack item in an SNBT parse error. */ export type SNBTParseErrorCauseStackItem = | { /** * The position of the error in the input. */ positionInInput: number; /** * The input. */ input: any; /** * The function associated with this stack item. */ function: "parseSNBTPrimitive"; /** * The error associated with this stack item. */ error?: | { /** * The type of this error. */ type: "InvalidArgumentToFunction"; /** * The name of the function. */ functionName: "bool" | "uuid"; /** * The argument that caused this error. */ argument: string; } | { /** * The type of this error. */ type: "UnsupportedFunction"; /** * The name of the unsupported function. */ functionName: string; } | { /** * The type of this error. */ type: "UnsupportedSNBTPrimitive"; /** * The raw SNBT primitive value that caused this error. */ raw: any; } | { /** * The type of this error. */ type: "InvalidSNBTString"; /** * The raw SNBT string value that caused this error. */ raw: any; }; } | { /** * The position of the error in the input. */ positionInInput: number; /** * The input. */ input: string; /** * The function associated with this stack item. */ function: "uuidToIntArray"; /** * The error associated with this stack item. */ error: { /** * The type of this error. */ type: "InvalidUUID"; /** * The invalid UUID that caused this error. */ uuid: string; }; } | { /** * The position of the error in the input item. */ positionInInputItem: number; /** * The input items (the items in the typed array). */ inputItems: string[]; /** * The input item associated with this stack item. */ inputItem: string; /** * The index of the input item associated with this stack item. */ inputItemIndex: number; /** * The type of the typed array associated with this stack item. */ arrayType: `${TypedArrayLetterTypes.B | TypedArrayLetterTypes.S | TypedArrayLetterTypes.I | TypedArrayLetterTypes.L}`; /** * The function associated with this stack item. */ function: "parseTypedArray"; /** * The error associated with this stack item. */ error?: | { /** * The type of this error. */ type: "DisallowedTypeInTypedArray"; /** * The type of the item that caused this error. */ itemType: `${TypedArrayLetterTypes.B | TypedArrayLetterTypes.S | TypedArrayLetterTypes.I | TypedArrayLetterTypes.L}`; /** * The allowed types. */ allowedTypes: `${TypedArrayLetterTypes.B | TypedArrayLetterTypes.S | TypedArrayLetterTypes.I | TypedArrayLetterTypes.L}`[]; } | { /** * The type of this error. */ type: "UnsupportedTypeInTypedArray"; /** * Should only be undefined if the item is unable to be parsed. */ itemType?: `${NBT.TagType}` | undefined; }; } | { /** * The position of the error in the input item. */ positionInInputItem: number; /** * The input items (the items in the list). */ inputItems: string[]; /** * The input item associated with this stack item. */ inputItem: string; /** * The index of the input item associated with this stack item. */ inputItemIndex: number; /** * The function associated with this stack item. */ function: "parseList"; /** * The error associated with this stack item. */ error?: { /** * The type of this error. */ type: "MixedListTypesNotAllowed"; /** * The type of the item that caused this error. */ itemType: `${NBT.TagType}`; /** * The detected type of the item that caused this error. */ item: NBT.Tags[NBT.TagType]; /** * The detected type of the list. */ detectedArrayType: `${NBT.TagType}`; }; } | { /** * The position of the error in the input. */ positionInInput: number; /** * The input. */ input: string; /** * The property key associated with this stack item. */ key?: string; /** * The function associated with this stack item. */ function: "extractSNBT" | "parseSNBTCompoundString"; /** * The error associated with this stack item. */ error?: { /** * The type of this error. */ type: "InvalidSNBTKey"; /** * The raw SNBT key value that caused this error. */ raw: any; }; }; /** * The cause of an SNBT parse error. */ export interface SNBTParseErrorCause { /** * The position of the error in the input. */ position: number; /** * The stack trace of the error. */ stack: [initialError: SNBTParseErrorCauseStackItem, ...outerStack: SNBTParseErrorCauseStackItem[]]; } /** * Represents an error that occurred while parsing SNBT. * * @template T Whether the error's full stack has been resolved. */ export interface SNBTParseError<T extends boolean = boolean> extends Error { /** * The cause of the error. */ cause: SNBTParseErrorCause; /** * Whether the error's full stack has been resolved. */ isResolved: T; /** * The original input that caused the error. */ originalInput: T extends true ? string : null; /** * Gets the position of the error in the input. * * @returns The position of the error in the input. */ getErrorPosition(): T extends true ? [line: number, column: number] : null; /** * Gets the end position of the error in the input. * * @returns The end position of the error in the input. */ getErrorEndPosition(): T extends true ? [line: number, column: number] : null; /** * Gets the position of the error in the input with an offset. * * @param offset The offset to add to the error position. * @returns The position of the error in the input. */ getErrorPositionWithOffset(offset: number): T extends true ? [line: number, column: number] : null; /** * Gets the position of the error in the input for a stack item. * * @param stackItem The stack item to get the error position for. * @returns The position of the error in the input. */ getErrorPositionForStackItem(stackItem: SNBTParseErrorCauseStackItem): [line: number, column: number]; } /** * The options for an SNBT parse error. */ export interface SNBTParseErrorOptions extends ErrorOptions { /** * The cause of the error. */ cause: SNBTParseErrorCause; /** * Whether the error's full stack has been resolved. * * Only set this to true if this was from a function that was not called by another SNBT parser function, if the SNBT parser has more to the stack, this should be false. * * @defaul false */ resolved?: boolean | undefined; /** * Only set this if {@link resolved} is also true, this is the original input of the SNBT parser function. */ originalInput?: string | undefined; } /** * The constructor for an SNBT parse error. */ export interface SNBTParseErrorConstructor extends Omit<ErrorConstructor, "prototype"> { /** * Creates a new SNBTParseError. * * @param message The message of the error. * @param options The options for the error. * @returns A new SNBTParseError. */ new (message: string, options: SNBTParseErrorOptions): SNBTParseError; /** * Creates a new SNBTParseError. * * @param message The message of the error. * @param options The options for the error. * @returns A new SNBTParseError. */ (message: string, options: SNBTParseErrorOptions): SNBTParseError; /** * The prototype of an SNBTParseError. */ prototype: SNBTParseError; } /** * An SNBT parse error. */ export var SNBTParseError: SNBTParseErrorConstructor = new Proxy( /** * An SNBT parse error. */ class SNBTParseError extends Error implements SNBTParseError { public declare cause: SNBTParseErrorCause; public isResolved: boolean = false; public originalInput: string | null = null; public constructor(message: string, options: SNBTParseErrorOptions) { if (arguments.length !== 2) throw new TypeError(`Incorrect number of arguments to constructor, expected 2 but got ${arguments.length} instead.`); if (typeof message !== "string") throw new TypeError(`Expected args[0] (message) to be a string but got ${typeof message} instead.`); if (typeof options !== "object" || options === null) throw new TypeError(`Expected args[1] (options) to be an object but got ${options === null ? "null" : typeof options} instead.`); if (typeof options.cause !== "object" || options.cause === null) throw new TypeError(`Expected options.cause to be an object but got ${options === null ? "null" : typeof options.cause} instead.`); super(message, options); this.isResolved = options.resolved ?? false; this.originalInput = options.originalInput ?? null; } public getErrorPosition(): [line: number, column: number] | null { if (!this.isResolved || this.originalInput === null) return null; const cause: SNBTParseErrorCause = this.cause; const position: SNBTParseErrorCause["position"] = cause.position; const input: string = this.originalInput; return [input.slice(0, position).split("\n").length, position - input.slice(0, position).lastIndexOf("\n") + 1]; } public getErrorEndPosition(): [line: number, column: number] | null { if (!this.isResolved || this.originalInput === null) return null; const cause: SNBTParseErrorCause = this.cause; let offset: number = 0; const stackItem: SNBTParseErrorCauseStackItem = cause.stack[0]; stackItemFunctionSwitcher: switch (stackItem.function) { case "extractSNBT": switch (stackItem.error?.type) { case "InvalidSNBTKey": offset = stackItem.error.raw.length; break stackItemFunctionSwitcher; default: offset = stackItem.input.length - stackItem.positionInInput; break stackItemFunctionSwitcher; } case "parseList": switch (stackItem.error?.type) { case "MixedListTypesNotAllowed": offset = stackItem.inputItem.length - stackItem.positionInInputItem; break stackItemFunctionSwitcher; default: offset = stackItem.inputItem.length - stackItem.positionInInputItem; break stackItemFunctionSwitcher; } case "parseSNBTCompoundString": switch (stackItem.error?.type) { case "InvalidSNBTKey": offset = stackItem.error.raw.length; break stackItemFunctionSwitcher; default: offset = stackItem.input.length - stackItem.positionInInput; break stackItemFunctionSwitcher; } case "parseSNBTPrimitive": switch (stackItem.error?.type) { case "InvalidArgumentToFunction": offset = stackItem.error.argument.length; break stackItemFunctionSwitcher; case "UnsupportedFunction": offset = stackItem.error.functionName.length; break stackItemFunctionSwitcher; case "UnsupportedSNBTPrimitive": offset = stackItem.error.raw.length; break stackItemFunctionSwitcher; case "InvalidSNBTString": offset = stackItem.error.raw.length; break stackItemFunctionSwitcher; default: offset = stackItem.input.length - stackItem.positionInInput; break stackItemFunctionSwitcher; } case "parseTypedArray": switch (stackItem.error?.type) { case "DisallowedTypeInTypedArray": offset = stackItem.inputItem.length - stackItem.positionInInputItem; break stackItemFunctionSwitcher; case "UnsupportedTypeInTypedArray": offset = stackItem.inputItem.length - stackItem.positionInInputItem; break stackItemFunctionSwitcher; default: offset = stackItem.inputItem.length - stackItem.positionInInputItem; break stackItemFunctionSwitcher; } case "uuidToIntArray": offset = stackItem.error.uuid.length; break; } const position: SNBTParseErrorCause["position"] = cause.position + offset; const input: string = this.originalInput; return [input.slice(0, position).split("\n").length, position - input.slice(0, position).lastIndexOf("\n") + 1]; } public getErrorPositionWithOffset(offset: number): [line: number, column: number] | null { if (!this.isResolved || this.originalInput === null) return null; const cause: SNBTParseErrorCause = this.cause; const position: SNBTParseErrorCause["position"] = cause.position + offset; const input: string = this.originalInput; return [input.slice(0, position).split("\n").length, position - input.slice(0, position).lastIndexOf("\n") + 1]; } public getErrorPositionForStackItem(stackItem: SNBTParseErrorCauseStackItem): [line: number, column: number] { const input: string = "input" in stackItem ? stackItem.input : stackItem.inputItem; const position: number = "positionInInput" in stackItem ? stackItem.positionInInput : stackItem.positionInInputItem; return [input.slice(0, position).split("\n").length, position - input.slice(0, position).lastIndexOf("\n") + 1]; } } as unknown as SNBTParseErrorConstructor, { apply(target: SNBTParseErrorConstructor, thisArg: any, argumentsList: [any, any]): SNBTParseError { return new target(...argumentsList); }, } ); /** * Options for parsing an SNBT-like primitive. * * @internal */ interface ParseSNBTPrimitiveOptions extends ParseSNBTBaseOptions {} /** * Parses an SNBT-like primitive. * * @internal */ function parseSNBTPrimitive( raw: any, options: ParseSNBTPrimitiveOptions & { keepGoingAfterError: true } ): { value?: NBT.Tags[NBT.TagType]; errors: SNBTParseError[] }; /** * Parses an SNBT-like primitive. * * @internal */ function parseSNBTPrimitive(raw: any, options?: ParseSNBTPrimitiveOptions & { keepGoingAfterError?: false }): NBT.Tags[NBT.TagType]; /** * Parses an SNBT-like primitive. * * @internal */ function parseSNBTPrimitive(raw: any, options?: ParseSNBTPrimitiveOptions): NBT.Tags[NBT.TagType] | { value?: NBT.Tags[NBT.TagType]; errors: SNBTParseError[] }; function parseSNBTPrimitive( raw: any, options: ParseSNBTPrimitiveOptions = {} ): NBT.Tags[NBT.TagType] | { value?: NBT.Tags[NBT.TagType]; errors: SNBTParseError[] } { const errors: SNBTParseError[] = []; function structureResult(val?: NBT.Tags[NBT.TagType]): ReturnType<typeof parseSNBTPrimitive> { if (options.keepGoingAfterError) return { value: val, errors }; return val!; } try { if (typeof raw === "string") { const originalInput: string = raw; raw = raw.trim(); const funcMatch: RegExpMatchArray | null = raw.match(/^(\w+)\((.*)\)$/s); if (funcMatch) { const fn: string = funcMatch[1]!; const arg: string = funcMatch[2]!.trim(); switch (fn) { case "bool": { const baseVal = parseSNBTPrimitive(arg, options); if ("errors" in baseVal) { baseVal.errors.forEach((err: SNBTParseError): void => { err.cause.stack.push({ input: originalInput, positionInInput: originalInput.indexOf(raw) + fn.length + 1, function: "parseSNBTPrimitive", }); err.cause.position += originalInput.indexOf(raw) + fn.length + 1; }); errors.push(...baseVal.errors); } const val = "errors" in baseVal ? baseVal.value : baseVal; if (!val) return structureResult(); if (val.type === "short" || val.type === "int" || val.type === "long" || val.type === "float" || val.type === "double") { const num: number = Number(toPrimitive(val)); return structureResult(NBT.byte(num === 0 ? 0 : 1)); } if (val.type === "byte") return val; throw new SNBTParseError(`Invalid argument to bool(): ${arg}`, { cause: { position: originalInput.indexOf(raw) + fn.length + 1, stack: [ { input: originalInput, positionInInput: originalInput.indexOf(raw) + fn.length + 1, function: "parseSNBTPrimitive", error: { type: "InvalidArgumentToFunction", functionName: "bool", argument: arg }, }, ], }, }); } case "uuid": { const uuidStr: string = arg.replace(/^["']|["']$/g, ""); try { const uuidIntArray: number[] = uuidToIntArray(uuidStr); return structureResult(NBT.intArray(uuidIntArray)); } catch (e) { if (e instanceof Error && e.cause) { (e.cause as SNBTParseErrorCause).stack.push({ positionInInput: originalInput.indexOf(raw) + fn.length + 1, input: originalInput, function: "parseSNBTPrimitive", error: { type: "InvalidArgumentToFunction", functionName: "uuid", argument: arg, }, }); (e.cause as SNBTParseErrorCause).position += originalInput.indexOf(raw) + fn.length + 1; } throw e; } } default: throw ( (new ReferenceError(`Unsupported SNBT function: ${fn}`), { cause: { position: originalInput.indexOf(raw) + fn.length + 1, stack: [ { input: originalInput, positionInInput: originalInput.indexOf(raw) + fn.length + 1, function: "parseSNBTPrimitive", error: { type: "UnsupportedFunction", functionName: fn }, }, ], } as const satisfies SNBTParseErrorCause, }) ); } } const arrMatch: RegExpMatchArray | null = raw.match(/^\[(B|S|I|L);\s*(.*?)\]$/is); if (arrMatch) { const type = arrMatch[1]!.toUpperCase() as "B" | "S" | "I" | "L"; const items: string[] = arrMatch[2]!.split(/\s*,\s*/).filter(Boolean); const baseVal = parseTypedArray(type, items, options); if ("errors" in baseVal) { baseVal.errors.forEach((err: SNBTParseError): void => { const lastStackItem = err.cause.stack.at(-1)! as Extract<SNBTParseErrorCauseStackItem, { function: "parseTypedArray" }>; const baseOffset: number = arrMatch[2]!.match(new RegExp(`^(.*?\\s*(,\\s*|$)){${lastStackItem.inputItemIndex}}`))?.[0]?.length ?? 0; err.cause.stack.push({ input: originalInput, positionInInput: originalInput.indexOf(raw) + raw.indexOf(arrMatch[2]) + baseOffset, function: "parseSNBTPrimitive", }); err.cause.position += originalInput.indexOf(raw) + raw.indexOf(arrMatch[2]) + baseOffset; }); errors.push(...baseVal.errors); } return structureResult("errors" in baseVal ? baseVal.value : baseVal); } const listMatch: RegExpMatchArray | null = raw.match(/^\[\s*(.*?)\]$/is); if (listMatch) { const baseVal = extractSNBT(raw, options); if ("errors" in baseVal) { baseVal.errors.forEach((err: SNBTParseError): void => { err.cause.stack.push({ input: originalInput, positionInInput: originalInput.indexOf(raw), function: "parseSNBTPrimitive", }); err.cause.position += originalInput.indexOf(raw); }); errors.push(...baseVal.errors); } return structureResult("errors" in baseVal ? baseVal.value : baseVal.value); } const match: RegExpMatchArray | null = raw.match(/^([-+]?0x[\da-fA-F]+|0b[01]+|[-+]?\d*\.?\d*(?:[eE][-+]?\d+)?)([bsilfdBSILFD])?$/i); if (match) { const numStr: string = match[1]!; const suffix: string | undefined = match[2]?.toLowerCase(); let value: number | bigint; if (numStr.startsWith("0x")) value = parseInt(numStr, 16); else if (numStr.startsWith("0b")) value = parseInt(numStr.slice(2), 2); else if (suffix === "l") value = BigInt(numStr); else value = Number(numStr); switch (suffix) { case "b": return structureResult(NBT.byte(Number(value))); case "s": return structureResult(NBT.short(Number(value))); case "i": return structureResult(NBT.int(Number(value))); case "l": return structureResult(NBT.long(toLongParts(BigInt(value)))); case "f": return structureResult(NBT.float(Number(value))); case "d": return structureResult(NBT.double(Number(value))); default: if (typeof value === "bigint") return structureResult(NBT.long(toLongParts(value))); if (Number.isInteger(value)) return structureResult(NBT.int(value)); return structureResult(NBT.double(value)); } } if (["true", "false"].includes(raw)) return structureResult(NBT.byte(+(raw === "true"))); try { return structureResult({ type: "string", value: parseFormattedString(raw), }); } catch { const error = new SNBTParseError("Invalid SNBT string: " + raw, { cause: { position: originalInput.indexOf(raw), stack: [ { function: "parseSNBTPrimitive", input: originalInput, positionInInput: originalInput.indexOf(raw), error: { type: "InvalidSNBTString", raw, }, }, ], }, }); if (options.keepGoingAfterError) errors.push(error); else throw error; return structureResult(); } } if (typeof raw === "number") { if (raw % 1 !== 0) return structureResult(NBT.double(raw)); return structureResult(NBT.int(raw)); } if (typeof raw === "bigint") return structureResult(NBT.long(toLongParts(raw))); if (typeof raw === "boolean") return structureResult(NBT.byte(+raw)); throw new SNBTParseError("Unsupported SNBT primitive: " + raw, { cause: { position: 0, stack: [ { function: "parseSNBTPrimitive", input: raw, positionInInput: 0, error: { type: "UnsupportedSNBTPrimitive", raw, }, }, ], }, }); } catch (e) { if (options.keepGoingAfterError && e instanceof SNBTParseError) errors.push(e); else throw e; return structureResult(); } } /** * The letter types of typed arrays. * * @internal */ enum TypedArrayLetterTypes { /** * A byte array. */ B = "byte", /** * A short array. */ S = "short", /** * An integer array. */ I = "int", /** * A long array. */ L = "long", } /** * A typed array. * * @internal */ type TypedArray = NBT.Tags[NBT.TagType.ByteArray | NBT.TagType.ShortArray | NBT.TagType.IntArray | NBT.TagType.LongArray]; function parseTypedArray( type: "B" | "S" | "I" | "L", items: string[], options: ParseSNBTBaseOptions & { keepGoingAfterError: true } ): { value: TypedArray; errors: SNBTParseError[] }; function parseTypedArray(type: "B" | "S" | "I" | "L", items: string[], options?: ParseSNBTBaseOptions & { keepGoingAfterError?: false }): TypedArray; function parseTypedArray( type: "B" | "S" | "I" | "L", items: string[], options?: ParseSNBTBaseOptions ): TypedArray | { value: TypedArray; errors: SNBTParseError[] }; function parseTypedArray( type: "B" | "S" | "I" | "L", items: string[], options: ParseSNBTBaseOptions = {} ): TypedArray | { value: TypedArray; errors: SNBTParseError[] } { const errors: SNBTParseError[] = []; const values: any[] = []; let i: number = 0; for (const x of items) { try { if (/^-?\d+b$/i.test(x)) { const n: number = Number(x.slice(0, -1)); if (type === "L") values.push(toLongParts(BigInt(n))); else if (!["B", "S", "I", "L"].includes(type)) throw new SNBTParseError(`Byte value not allowed in ${TypedArrayLetterTypes[type]} array: ${x}`, { cause: { position: 0, stack: [ { function: "parseTypedArray", inputItems: items, inputItem: x, inputItemIndex: i, positionInInputItem: 0, arrayType: TypedArrayLetterTypes[type], error: { type: "DisallowedTypeInTypedArray", allowedTypes: [TypedArrayLetterTypes.B, TypedArrayLetterTypes.S, TypedArrayLetterTypes.I, TypedArrayLetterTypes.L], itemType: NBT.TagType.Byte, }, }, ], }, }); else values.push(n); } else if (/^-?\d+s$/i.test(x)) { const n: number = Number(x.slice(0, -1)); if (type === "L") values.push(toLongParts(BigInt(n))); else if (!["S", "I", "L"].includes(type)) throw new SNBTParseError(`Short value not allowed in ${TypedArrayLetterTypes[type]} array: ${x}`, { cause: { position: 0, stack: [ { function: "parseTypedArray", inputItems: items, inputItem: x, inputItemIndex: i, positionInInputItem: 0, arrayType: TypedArrayLetterTypes[type], error: { type: "DisallowedTypeInTypedArray", allowedTypes: [TypedArrayLetterTypes.B, TypedArrayLetterTypes.S, TypedArrayLetterTypes.I, TypedArrayLetterTypes.L], itemType: NBT.TagType.Short, }, }, ], }, }); else values.push(n); } else if (/^-?\d+l$/i.test(x)) { if (type !== "L") throw new SNBTParseError(`Long value not allowed in ${TypedArrayLetterTypes[type]} array: ${x}`, { cause: { position: 0, stack: [ { function: "parseTypedArray", inputItems: items, inputItem: x, inputItemIndex: i, positionInInputItem: 0, arrayType: TypedArrayLetterTypes[type], error: { type: "DisallowedTypeInTypedArray", allowedTypes: [TypedArrayLetterTypes.B, TypedArrayLetterTypes.S, TypedArrayLetterTypes.I, TypedArrayLetterTypes.L], itemType: NBT.TagType.Long, }, }, ], }, }); values.push(parseLong(x)); } else if (/^-?\d+i?$/.test(x)) { const n: number = x.toLowerCase().endsWith("i") ? Number(x.slice(0, -1)) : Number(x); if (type === "L") values.push(toLongParts(BigInt(n))); else if (!["I", "L"].includes(type)) throw new SNBTParseError(`Int value not allowed in ${TypedArrayLetterTypes[type]} array: ${x}`, { cause: { position: 0, stack: [ { function: "parseTypedArray", inputItems: items, inputItem: x, inputItemIndex: i, positionInInputItem: 0, arrayType: TypedArrayLetterTypes[type], error: { type: "DisallowedTypeInTypedArray", allowedTypes: [TypedArrayLetterTypes.B, TypedArrayLetterTypes.S, TypedArrayLetterTypes.I, TypedArrayLetterTypes.L], itemType: NBT.TagType.Int, }, }, ], }, }); else values.push(n); } else if (["true", "false"].includes(x)) { values.push(+(x === "true")); } else { throw new SNBTParseError(`Unsupported value in ${TypedArrayLetterTypes[type]} array: ${x}`, { cause: { position: 0, stack: [ { function: "parseTypedArray", inputItems: items, inputItem: x, inputItemIndex: i, positionInInputItem: 0, arrayType: TypedArrayLetterTypes[type], error: { type: "UnsupportedTypeInTypedArray", itemType: ((): `${NBT.TagType}` | undefined => { try { return parseSNBTPrimitive(x).type; } catch { return undefined; } })(), }, }, ], }, }); } } catch (e) { if (options.keepGoingAfterError && e instanceof SNBTParseError) errors.push(e); else throw e; } i++; } let result: TypedArray; switch (type) { case "B": result = NBT.byteArray(values as number[]); break; case "S": result = NBT.shortArray(values as number[]); break; case "I": result = NBT.intArray(values as number[]); break; case "L": result = NBT.longArray(values as [high: number, low: number][]); break; } return options.keepGoingAfterError ? { value: result, errors } : result; } interface ParseListOptions extends ParseSNBTBaseOptions {} function parseList(items: string[], options: ParseListOptions & { keepGoingAfterError: true }): { value: NBT.List<NBT.TagType>; errors: SNBTParseError[] }; function parseList(items: string[], options?: ParseListOptions & { keepGoingAfterError?: false }): NBT.List<NBT.TagType>; function parseList(items: string[], options?: ParseListOptions): NBT.List<NBT.TagType> | { value: NBT.List<NBT.TagType>; errors: SNBTParseError[] }; function parseList(items: string[], options: ParseListOptions = {}): NBT.List<NBT.TagType> | { value: NBT.List<NBT.TagType>; errors: SNBTParseError[] } { console.log(items, items.length); const errors: SNBTParseError[] = []; let values: NBT.Tags[NBT.TagType][] = []; let valueIndices: number[] = []; let i: number = 0; for (const x of items) { try { if (x.trim().startsWith("{")) { const baseVal = parseSNBTCompoundString(x, options); if ("errors" in baseVal) { baseVal.errors.forEach((err) => { err.cause.stack.push({ inputItem: x, inputItemIndex: i, inputItems: items, positionInInputItem: 0, function: "parseList", }); err.cause.position += 0; }); errors.push(...baseVal.errors); } values.push("errors" in baseVal ? baseVal.value : baseVal); } else { const baseVal = parseSNBTPrimitive(x, options); if ("errors" in baseVal) { baseVal.errors.forEach((err) => { err.cause.stack.push({ inputItem: x, inputItemIndex: i, inputItems: items, positionInInputItem: 0, function: "parseList", }); err.cause.position += 0; }); errors.push(...baseVal.errors); } const val = "errors" in baseVal ? baseVal.value : baseVal; if (val !== undefined) values.push(val); } valueIndices.push(i); } catch (e) { if (options.keepGoingAfterError && e instanceof SNBTParseError) errors.push(e); else throw e; } i++; } let type: `${NBT.TagType}` | undefined = values[0]?.type; if ( type && !values.every((v, i) => { if (v.type === type) { return true; } if (!(options.mixedListsAllowed ?? true)) { if (!options.keepGoingAfterError) throw new SNBTParseError("Mixed list types not allowed.", { cause: { position: 0, stack: [ { function: "parseList", inputItems: items, inputItem: items[i]!, inputItemIndex: valueIndices[i]!, positionInInputItem: 0, error: { type: "MixedListTypesNotAllowed", itemType: v.type, item: v, detectedArrayType: type, }, }, ], }, }); } return false; }) ) { if (!(options.mixedListsAllowed ?? true) && options.keepGoingAfterError) { const numOfEachType = values.reduce((acc, v) => ({ ...acc, [v.type]: (acc[v.type] ?? 0) + 1 }), {} as Record<`${NBT.TagType}`, number>); const mostCommonType = Object.entries(numOfEachType).reduce((a, b) => (a[1] > b[1] ? a : b))[0] as `${NBT.TagType}`; for (let i: number = 0; i < values.length; i++) { if (values[i]!.type === mostCommonType) continue; errors.push( new SNBTParseError("Mixed list types not allowed.", { cause: { position: 0, stack: [ { function: "parseList", inputItems: items, inputItem: items[i]!, inputItemIndex: valueIndices[i]!, positionInInputItem: 0, error: { type: "MixedListTypesNotAllowed", itemType: values[i]!.type, item: values[i]!, detectedArrayType: type, }, }, ], }, }) ); } } if (options.convertMixedListsToCompoundLists ?? true) values = values.map((v, i) => ({ type: NBT.TagType.Compound, value: { "": v } })); } const result = NBT.list({ type: values[0]?.type ?? "end", value: values.map((v) => v.value) }) as any; return options.keepGoingAfterError ? { value: result, errors } : result; } /** * Options for parsing SNBT. */ export interface ParseSNBTBaseOptions { /** * Whether to allow lists of mixed types. * * @default true */ mixedListsAllowed?: boolean; /** * Whether to convert lists of mixed types to compound lists. * * @default true */ convertMixedListsToCompoundLists?: boolean; /** * Whether the function is being called from within an inner stack of a parent SNBT parser function. * * This is internal only and should not be specified by users. * * @internal * @ignore * * @default false */ isInnerStack?: boolean; /** * Whether to keep parsing the SNBT string even if there are errors. * * It will cause the function to return an object containing the errors, as well as the parts of the value that were able to be extracted. * * @default false */ keepGoingAfterError?: boolean; /** * Whether to stop parsing the SNBT string at a negative depth. * * @default true */ stopAtNegativeDepth?: boolean; } /** * The result of the {@link parseSNBTCompoundString} function when {@link ParseSNBTBaseOptions.keepGoingAfterError} is `true`. */ export type ParseSNBTCompoundStringResultWithErrors = { /**