Path: blob/master/src/library/classes/databaseBuilder.ts
1784 views
/* eslint-disable @typescript-eslint/ban-types */1/* eslint-disable @typescript-eslint/no-explicit-any */2import { Entity, World, world } from "@minecraft/server";3import { Database } from "../@types/classes/databaseBuilder";4import { contentLog } from "@notbeer-api";56const databases: { [k: string]: DatabaseImpl<any> } = {};7const parsers: ((key: string, value: any, databaseName: string) => any)[] = [];89function parseJSON(databaseName: string, json: string) {10return JSON.parse(json, (key, value) => {11for (const parser of parsers) value = parser(key, value, databaseName);12return value;13});14}1516function getDatabaseKey(name: string, provider?: World | Entity) {17return name + "//" + (provider instanceof Entity ? provider.id : "world");18}1920class DatabaseManager {21load<T extends object = { [key: string]: any }>(name: string, provider: World | Entity = world) {22const key = getDatabaseKey(name, provider);23if (!databases[key]) {24databases[key] = new DatabaseImpl<T>(name, provider);25databases[key].load();26}27return <Database<T>>databases[key];28}2930delete(name: string, provider: World | Entity = world) {31const key = getDatabaseKey(name, provider);32const database = databases[key] ?? new DatabaseImpl<any>(name, provider);33if (database.isValid) database.delete();34delete databases[key];35}3637find(regexp: RegExp, provider: World | Entity = world) {38return provider.getDynamicPropertyIds().filter((name) => name.match(regexp));39}4041getRawData(name: string, provider: World | Entity = world) {42const key = getDatabaseKey(name, provider);43const database = databases[key] ?? new DatabaseImpl<any>(name, provider);44return database.rawData;45}4647addParser(parser: (key: string, value: any, database: string) => any) {48parsers.push(parser);49}50}5152export const Databases = new DatabaseManager();5354class DatabaseImpl<T extends object = { [key: string]: any }> implements Database<T> {55private _data: T = <any>{};56private loaded = false;57private valid = true;5859constructor(60private name: string,61private provider: World | Entity = world62) {}6364get data() {65if (!this.valid) throw new Error(`Can't get data from invalid database "${this.name}".`);66if (!this.loaded) this.load();67return this._data;68}6970set data(value: T) {71if (!this.valid) throw new Error(`Can't set data on invalid database "${this.name}".`);72this.loaded = true;73this._data = value;74}7576get rawData(): string | undefined {77let data: string | undefined;78data = <string>this.provider.getDynamicProperty(this.name);79let page: string | undefined;80let i = 2;81while (data && (page = <string>this.provider.getDynamicProperty(`__page${i++}__` + this.name))) data += page;82return data;83}8485get isLoaded() {86return this.loaded;87}8889get isValid() {90return this.valid;91}9293clear(): void {94if (!this.valid) throw new Error(`Can't clear data from invalid database "${this.name}".`);95if (!this.loaded) this.load();96this._data = {} as T;97}9899save(): void {100if (!this.valid) throw new Error(`Can't save data to invalid database "${this.name}".`);101102const data = JSON.stringify(this._data);103// Try smaller divisions of data until the right number of pages is found.104// 50 subdivions allow for a little more than 1.5 MB per database.105divisions: for (let i = 1; i <= 50; i++) {106let page: number | undefined = undefined;107const stepSize = Math.ceil(data.length / i);108for (let j = 0; j < data.length; j += stepSize) {109try {110this.provider.setDynamicProperty((page ? `__page${page}__` : "") + this.name, data.slice(j, j + stepSize));111page = (page ?? 1) + 1;112} catch {113continue divisions;114}115}116// Remove unused pages117while (this.provider.getDynamicProperty(`__page${page}__` + this.name)) {118this.provider.setDynamicProperty(`__page${page!++}__` + this.name, undefined);119}120this.loaded = true;121return;122}123124contentLog.error(`Failed to save database ${this.name} to ${this.provider instanceof Entity ? this.provider.nameTag ?? this.provider.id : "the world"}`);125contentLog.debug(contentLog.stack());126}127128load() {129if (!this.valid) throw new Error(`Can't load data from invalid database "${this.name}".`);130if (this.loaded) return;131try {132this._data = parseJSON(this.name, this.rawData ?? "{}");133} catch (err) {134contentLog.error(`Failed to load database ${this.name} from ${this.provider instanceof Entity ? this.provider.nameTag ?? this.provider.id : "the world"}`);135if (err) contentLog.debug(err, err.stack);136return;137}138this.loaded = true;139}140141unload() {142if (!this.valid) throw new Error(`Can't unload invalid database "${this.name}".`);143this.loaded = false;144this._data = undefined;145Databases.delete(this.name, this.provider);146}147148delete() {149if (!this.valid) throw new Error(`Can't delete invalid database "${this.name}".`);150this.provider.setDynamicProperty(this.name, undefined);151let page = 2;152while (this.provider.getDynamicProperty(`__page${page}__` + this.name)) {153this.provider.setDynamicProperty(`__page${page++}__` + this.name, undefined);154}155this.valid = false;156Databases.delete(this.name, this.provider);157}158159toJSON() {160const json: any = { __dbName__: this.name };161if (this.provider instanceof Entity) json.__dbProvider__ = this.provider.id;162return json;163}164}165166Databases.addParser((_, value) => {167if (!value || typeof value !== "object" || !value.__dbName__) return value;168const provider = value.__dbProvider__ ? world.getEntity(value.__dbProvider__) : world;169const key = getDatabaseKey(value.__dbName__, provider);170if (!databases[key]) databases[key] = new DatabaseImpl(value.__dbName__, provider);171return databases[key];172});173174175