mc-anvil
Version:
A Typescript library for reading Minecraft Anvil format files and Minecraft NBT format files in the browser.
200 lines (168 loc) • 7.66 kB
text/typescript
import { ResizableBinaryWriter } from "../util";
import { inflate } from 'pako';
import { ListPayload, TagData, TagPayload, TagType } from "./types";
export const LIST_INDEX = /^[\[][0-9]+[\]]$/;
export function parseCompoundListIndex(value: string): number {
if (value.match(LIST_INDEX) !== null) return +value.slice(1, value.length - 1);
return +value;
}
export function findChildTag(tag: TagData, f: (x: TagData) => boolean): TagData | undefined {
if (tag.type === TagType.COMPOUND) return (tag.data as TagData[]).find(f);
}
export function findChildTagIndex(tag: TagData, f: (x: TagData) => boolean): number | undefined {
if (tag.type === TagType.COMPOUND) return (tag.data as TagData[]).findIndex(f);
}
export function findCompoundListChildren(tag: TagData, f: (x: TagData) => boolean): (TagData | undefined)[] | undefined {
if (tag.type === TagType.LIST && (tag.data as ListPayload).subType === TagType.COMPOUND)
return (tag.data as ListPayload).data.map( x => (x as TagData[]).find(f));
}
export function findChildTagAtPath(path: string, tag?: TagData): TagData | undefined {
const p = path.split('/');
for (let i = 0; i < p.length; ++i) {
if (!tag || !tag.type) return;
if (tag.type === TagType.COMPOUND)
tag = findChildTag(tag, x => x.name === p[i]);
else if (tag.type === TagType.LIST && (tag.data as ListPayload).subType === TagType.COMPOUND) {
const data = ((tag.data as ListPayload).data as TagData[][])[parseCompoundListIndex(p[i])];
tag = data ? { type: TagType.COMPOUND, name: "", data } : undefined;
}
}
return tag;
}
export function parent(path: string): string {
const p = path.split("/");
return p.slice(0, p.length - 1).join("/");
}
export function baseName(path: string): string {
const p = path.split("/");
return p[p.length - 1];
}
function tryInflate(buffer: ArrayBuffer): ArrayBuffer {
try {
const b = inflate(new Uint8Array(buffer));
if (!b) throw new Error("not compressed");
return b.buffer;
} catch (e) {
return buffer;
}
}
export class NBTParser extends ResizableBinaryWriter {
private verbose?: boolean;
constructor(data: ArrayBuffer, verbose?: boolean) {
super(tryInflate(data));
this.verbose = verbose;
}
private tagReaders: Map<TagType, () => TagPayload> = new Map([
[ TagType.END, () => null ],
[ TagType.BYTE, this.getByte.bind(this) ],
[ TagType.BYTE_ARRAY, this.getByteArrayTag.bind(this) ],
[ TagType.SHORT, this.getShort.bind(this) ],
[ TagType.INT, this.getInt.bind(this) ],
[ TagType.INT_ARRAY, this.getIntArrayTag.bind(this) ],
[ TagType.LONG, this.getInt64.bind(this) ],
[ TagType.LONG_ARRAY, this.getLongArrayTag.bind(this) ],
[ TagType.FLOAT, this.getFloat.bind(this) ],
[ TagType.DOUBLE, this.getDouble.bind(this) ],
[ TagType.STRING, this.getStringTag.bind(this) ],
[ TagType.COMPOUND, this.getCompoundTag.bind(this) ],
[ TagType.LIST, this.getListTag.bind(this) ]
]);
private tagWriters: Map<TagType, (value?: any) => void> = new Map([
[ TagType.END, () => {} ],
[ TagType.BYTE, this.setByte.bind(this) as (value?: any) => void ],
[ TagType.BYTE_ARRAY, this.setByteArrayTag.bind(this) as (value?: any) => void ],
[ TagType.SHORT, this.setShort.bind(this) as (value?: any) => void ],
[ TagType.INT, this.setInt.bind(this) as (value?: any) => void ],
[ TagType.INT_ARRAY, this.setIntArrayTag.bind(this) as (value?: any) => void ],
[ TagType.LONG, this.setInt64LE.bind(this) as (value?: any) => void ],
[ TagType.LONG_ARRAY, this.setLongArrayTag.bind(this) as (value?: any) => void ],
[ TagType.FLOAT, this.setFloat.bind(this) as (value?: any) => void ],
[ TagType.DOUBLE, this.setDouble.bind(this) as (value?: any) => void ],
[ TagType.STRING, this.setStringTag.bind(this) as (value?: any) => void ],
[ TagType.COMPOUND, this.setCompoundTag.bind(this) as (value?: any) => void ],
[ TagType.LIST, this.setListTag.bind(this) as (value?: any) => void ]
]);
private getNumberArrayTag(reader: () => number): TagPayload {
const data: number[] = [];
const length = this.getInt();
for (let i = 0; i < length; ++i) data.push(reader());
return data;
}
private setNumberArrayTag(value: number[], writer: (value: number) => void) {
this.setInt(value.length);
value.forEach(writer);
}
private getByteArrayTag(): TagPayload {
return this.getNumberArrayTag(this.getByte.bind(this));
}
private setByteArrayTag(value: number[]) {
this.setNumberArrayTag(value, this.setByte.bind(this));
}
private getIntArrayTag(): TagPayload {
return this.getNumberArrayTag(this.getInt.bind(this));
}
private setIntArrayTag(value: number[]) {
this.setNumberArrayTag(value, this.setInt.bind(this));
}
private getLongArrayTag(): TagPayload {
const length = this.getInt();
const r = this.view.buffer.slice(this.position, this.position + length * 8);
this.position += length * 8;
return r;
}
private setLongArrayTag(value: ArrayBuffer) {
this.setInt(value.byteLength / 8);
this.setArrayBuffer(value);
}
private getStringTag(): TagPayload {
const length = this.getUShort();
return this.getFixedLengthString(length);
}
private setStringTag(value: string) {
this.setUShort(value.length);
this.setFixedLengthString(value);
}
private getListTag(): TagPayload {
const subType = this.getByte();
const length = this.getInt();
const reader = this.tagReaders.get(subType);
const data: TagPayload[] = [];
if (reader === undefined) throw new Error(`Invalid NBT tag ID ${subType} for list tag`);
for (let i = 0; i < length; ++i) data.push(reader());
return { subType, data };
}
private setListTag(value: { subType: number, data: TagPayload[] }) {
const writer = this.tagWriters.get(value.subType);
if (writer === undefined) throw new Error(`Invalid NBT tag ID ${value.subType} for list tag`);
this.setByte(value.subType);
this.setInt(value.data.length);
value.data.forEach(writer);
}
private getCompoundTag(): TagPayload {
const tags: TagData[] = [];
do {
tags.push(this.getTag());
} while (tags[tags.length - 1].type !== TagType.END);
return tags;
}
private setCompoundTag(value: TagData[]) {
value.forEach(this.setTag.bind(this));
if (value[value.length - 1]?.type !== TagType.END) this.setTag({ type: TagType.END, name: "", data: null });
}
getTag() {
const type = this.getByte();
const nameLength = type !== TagType.END ? this.getUShort() : 0;
const name = this.getFixedLengthString(nameLength);
const reader = this.tagReaders.get(type);
if (reader === undefined) throw new Error(`Invalid NBT tag ID ${type}`);
return { type, name, data: reader() };
}
setTag(value: TagData) {
const writer = this.tagWriters.get(value.type);
if (writer === undefined) throw new Error(`Invalid NBT tag ID ${value.type}`);
this.setByte(value.type);
if (value.type !== TagType.END) this.setUShort(value.name.length);
this.setFixedLengthString(value.name);
writer(value.data);
}
}