@tendrock/database
Version:
A database lib under the Tendrock ecosystem for Minecraft Bedrock Edition Script API
466 lines (465 loc) • 18.8 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import { Block, Entity, EntityInitializationCause, ItemStack, system, world, World } from "@minecraft/server";
import { BlockDatabase, EntityDatabase, ItemStackDatabase, WorldDatabase } from "../impl";
import { UniqueIdUtils } from "../helper/UniqueIdUtils";
import { BetterSet, SetMap } from "@tenolib/map";
import { Utils } from "../helper/Utils";
import { DatabaseTypes } from "../DatabaseTypes";
import { ConstructorRegistryImpl } from "../instance/ConstructorRegistry";
import { LocationUtils } from "@tendrock/location-id";
import { DynamicPropertySerializer } from "../DynamicPropertySerializer";
export class DatabaseManager {
constructor() {
this._databaseManagerMap = new Map();
this._eventCallbackMap = new SetMap();
this._changingEntityDatabaseBuffer = new Map();
this._isInitialized = false;
this._flushInterval = 3 * 60 * 20;
this._autoUpdateSourceEntity = true;
this._autoFlush = true;
this._blockDatabaseMap = new Map();
this._itemDatabaseMap = new Map();
this._entityDatabaseMap = new Map();
this._blockInitialIdListMap = new SetMap();
this._worldInitialIdList = [];
this._isFlushing = false;
this._dirtyDatabaseList = new BetterSet();
this._dirtyDatabaseBuffer = new BetterSet();
this._triggerStartupEventWhenSystemStartup();
this._loadWorldDynamicPropertiesWhenWorldLoad();
this._flushDataWhenPlayerLeave();
}
_triggerStartupEventWhenSystemStartup() {
const callback = system.beforeEvents.startup.subscribe(() => {
this._doStartup();
system.beforeEvents.startup.unsubscribe(callback);
});
}
*_loadAndParseWorldDynamicPropertiesGenerator() {
for (const id of world.getDynamicPropertyIds()) {
const { lid, dataIdentifier } = DynamicPropertySerializer.Instance.deserializePropertyId(id);
if (lid) {
this._addBlockDataId(lid, id, dataIdentifier);
}
else {
this._addWorldDataId(id, dataIdentifier);
}
yield;
}
}
_loadWorldDynamicProperties() {
return __awaiter(this, void 0, void 0, function* () {
yield Utils.runJob(this._loadAndParseWorldDynamicPropertiesGenerator());
yield Utils.runJob(this._initWorldBlockDataGenerator());
this._startAutoFlushTask();
this._isInitialized = true;
this._doReady();
});
}
_loadWorldDynamicPropertiesWhenWorldLoad() {
const callback = world.afterEvents.worldLoad.subscribe(() => {
this._loadWorldDynamicProperties().then(() => world.afterEvents.worldLoad.unsubscribe(callback));
});
}
_addBlockDataId(lid, propertyId, dataId) {
this._blockInitialIdListMap.addValue(lid, [propertyId, dataId]);
}
_addWorldDataId(propertyId, dataId) {
if (!this._worldInitialIdList) {
throw new Error("World data id list is used and frozen.");
}
this._worldInitialIdList.push([propertyId, dataId]);
}
*_initWorldDataGenerator() {
if (this._worldInitialIdList) {
this._worldDatabase = WorldDatabase.create(this, world, this._worldInitialIdList);
this._worldInitialIdList = undefined;
yield;
}
}
*_initBlockDataGenerator() {
if (this._blockInitialIdListMap.size > 0) {
for (const [lid, set] of this._blockInitialIdListMap) {
const blockDatabase = BlockDatabase.create(this, lid, set);
this._blockDatabaseMap.set(lid, blockDatabase);
yield;
}
this._blockInitialIdListMap.clear();
}
}
*_initWorldBlockDataGenerator() {
yield* this._initWorldDataGenerator();
yield* this._initBlockDataGenerator();
}
_doStartup() {
var _a;
if (this._isInitialized) {
throw new Error('DatabaseManager is already startup');
}
const event = { constructorRegistry: ConstructorRegistryImpl.Instance.getRegistry() };
(_a = this._eventCallbackMap.get('whenStartup')) === null || _a === void 0 ? void 0 : _a.forEach(callback => callback(event));
this._eventCallbackMap.delete('whenStartup');
}
whenStartup(callback) {
if (this._isInitialized) {
throw new Error('DatabaseManager is already startup');
}
this._eventCallbackMap.addValue('whenStartup', callback);
return () => {
this._eventCallbackMap.deleteValue('whenStartup', callback);
};
}
_doReady() {
var _a;
(_a = this._eventCallbackMap.get('whenReady')) === null || _a === void 0 ? void 0 : _a.forEach(callback => callback());
this._eventCallbackMap.delete('whenReady');
}
whenReady(callback) {
if (this._isInitialized) {
callback();
return undefined;
}
this._eventCallbackMap.addValue('whenReady', callback);
return () => {
this._eventCallbackMap.deleteValue('whenReady', callback);
};
}
isReady() {
return this._isInitialized;
}
_markDirty(runtimeId, dataBase) {
Utils.assertInvokedByTendrock(runtimeId);
const dirtyDatabases = this._isFlushing ? this._dirtyDatabaseBuffer : this._dirtyDatabaseList;
// console.log('dirty database list before mark dirty: ', JSON.stringify(dirtyDatabases.map(db => db.getUid())));
if (dirtyDatabases.includes(dataBase)) {
return;
}
const uniqueId = dataBase.getUid();
// If database is removed or not exist, skip.
if (!this._blockDatabaseMap.has(uniqueId) && !this._entityDatabaseMap.has(uniqueId) &&
!this._itemDatabaseMap.has(uniqueId) && this._worldDatabase !== dataBase) {
return;
}
dirtyDatabases.push(dataBase);
}
setFlushInterval(interval, flush = true) {
this._flushInterval = interval;
if (flush) {
this.flush();
}
this._startAutoFlushTask();
}
getFlushInterval() {
return this._flushInterval;
}
setAutoFlush(value = true) {
this._autoFlush = value;
if (value) {
this._clearFlushJobIfPresent();
}
else {
this._startAutoFlushTask();
}
}
autoFlush() {
return this._autoFlush;
}
setAutoUpdateSourceEntity(value = true) {
this._autoUpdateSourceEntity = value;
}
autoUpdateSourceEntity() {
return this._autoUpdateSourceEntity;
}
*_flushDatabase(database) {
if (!database) {
return;
}
database._beginFlush(UniqueIdUtils.RuntimeId);
const dirtyIdList = database._getDirtyDataIdList(UniqueIdUtils.RuntimeId);
// console.log(`flush database "${database.getUid()}: " `, JSON.stringify(dirtyIdList))
for (const identifier of dirtyIdList) {
if (database.size() <= 0 && dirtyIdList.length <= 0) {
yield;
break;
}
const value = database.get(identifier);
// console.log(`flush ${identifier}`, JSON.stringify(value));
database._saveData(UniqueIdUtils.RuntimeId, identifier, value);
yield;
}
// console.log(`flush ${database._getDirtyDataIdList(UniqueIdUtils.RuntimeId).length} data`);
database._endFlush(UniqueIdUtils.RuntimeId);
}
*_flushDataGenerator() {
this._beginFlush();
// console.log('start flush')
const databaseValues = this.getDirtyDatabaseList();
if (databaseValues.length > 0) {
for (const database of databaseValues) {
yield* this._flushDatabase(database);
}
}
this._endFlush();
// console.log('flush end')
}
_flushDatabaseSync(database, flushAllDirtyData) {
database._beginFlush(UniqueIdUtils.RuntimeId);
const dirtyIdList = flushAllDirtyData ? database._getAllDirtyDataIdList(UniqueIdUtils.RuntimeId) : database._getDirtyDataIdList(UniqueIdUtils.RuntimeId);
// console.log(`flush ${dirtyIdList.length} data`)
for (const identifier of dirtyIdList) {
if (database.size() <= 0 && dirtyIdList.length <= 0) {
break;
}
const value = database.get(identifier);
// console.log(`flush ${identifier}`, JSON.stringify(value));
database._saveData(UniqueIdUtils.RuntimeId, identifier, value);
}
database._endFlush(UniqueIdUtils.RuntimeId);
// console.log(`database "${database.getUid()}" flushed`);
}
_flushSyncImpl(includeBuffer = false) {
if (!this.isReady())
return;
this._beginFlush();
const databaseValues = includeBuffer ? this.getAllDirtyDatabaseList() : this.getDirtyDatabaseList();
// console.log('flush database when shutdown, dirty database count: ', databaseValues.length)
if (databaseValues.length <= 0) {
return;
}
for (const database of databaseValues) {
this._flushDatabaseSync(database, includeBuffer);
}
this._endFlush(includeBuffer);
// console.log('databases flushed');
}
flushSync() {
this._flushSyncImpl(false);
}
_flushWhenShutdown() {
this._flushSyncImpl(true);
}
flush() {
if (!this.isReady())
return;
system.runJob(this._flushDataGenerator());
}
_flushDataWhenPlayerLeave() {
world.beforeEvents.playerLeave.subscribe(({ player }) => {
if (world.getAllPlayers().length === 1) {
this._flushWhenShutdown();
}
else {
this._flushDatabase(this.get(player));
}
});
}
_clearFlushJobIfPresent() {
if (this._autoFlushTaskId !== undefined) {
system.clearJob(this._autoFlushTaskId);
}
}
_startAutoFlushTask() {
this._clearFlushJobIfPresent();
if (!this._autoFlush)
return;
this._autoFlushTaskId = system.runInterval(() => {
this.flush();
}, this._flushInterval);
}
_beginFlush() {
this._isFlushing = true;
}
_endFlush(allDirtyDataFlushed = false) {
if (allDirtyDataFlushed) {
this._dirtyDatabaseList = new BetterSet();
this._dirtyDatabaseBuffer = new BetterSet();
this._isFlushing = false;
}
else {
this._dirtyDatabaseList = this._dirtyDatabaseBuffer;
this._isFlushing = false;
this._dirtyDatabaseBuffer = new BetterSet();
}
}
// ---------------------------------------------------------
_prepare(gameObject) {
if (typeof gameObject === 'string' || gameObject instanceof Block) {
const uniqueId = UniqueIdUtils.getBlockUniqueId(gameObject);
const databaseMap = this._blockDatabaseMap;
const databaseType = BlockDatabase;
return { uniqueId, databaseMap, databaseType };
}
else if (gameObject instanceof Entity) {
const uniqueId = UniqueIdUtils.getEntityUniqueId(gameObject);
const databaseMap = this._entityDatabaseMap;
const databaseType = EntityDatabase;
return { uniqueId, databaseMap, databaseType };
}
else if (gameObject instanceof ItemStack) {
const uniqueId = UniqueIdUtils.getItemUniqueId(gameObject);
const databaseMap = this._itemDatabaseMap;
const databaseType = ItemStackDatabase;
return { uniqueId, databaseMap, databaseType };
}
else if (gameObject instanceof World) {
const databaseType = WorldDatabase;
return { uniqueId: undefined, databaseMap: undefined, databaseType };
}
else {
throw new Error(`Invalid game object type.`);
}
}
createIfAbsent(gameObject) {
const { uniqueId, databaseMap, databaseType } = this._prepare(gameObject);
// Is world database
if (!uniqueId || !databaseMap) {
if (this._worldDatabase) {
return this._worldDatabase;
}
this._worldDatabase = WorldDatabase.create(this, world);
return this._worldDatabase;
}
let database = databaseMap.get(uniqueId);
if (database) {
return database;
}
database = databaseType.create(this, gameObject);
databaseMap.set(uniqueId, database);
return database;
}
get(gameObject) {
const { uniqueId, databaseMap } = this._prepare(gameObject);
if (!uniqueId || !databaseMap) {
return undefined;
}
return databaseMap.get(uniqueId);
}
remove(gameObject, clearProperty = false) {
const { uniqueId, databaseMap } = this._prepare(gameObject);
if (!uniqueId || !databaseMap) {
return;
}
const database = databaseMap.get(uniqueId);
if (!database) {
return;
}
if (clearProperty) {
database.clear();
this._dirtyDatabaseList.delete(database);
this._dirtyDatabaseBuffer.delete(database);
}
databaseMap.delete(uniqueId);
}
getDatabaseList(type) {
if (type === DatabaseTypes.World) {
return [this._worldDatabase];
}
else if (type === DatabaseTypes.Block) {
return Array.from(this._blockDatabaseMap.values());
}
else if (type === DatabaseTypes.Item) {
return Array.from(this._itemDatabaseMap.values());
}
else if (type === DatabaseTypes.Entity) {
return Array.from(this._entityDatabaseMap.values());
}
else {
throw new Error(`Invalid database type.`);
}
}
getWorldDatabase() {
return this._worldDatabase;
}
_addDatabase(runtimeId, database) {
Utils.assertInvokedByTendrock(runtimeId);
const { uniqueId, databaseMap } = this._prepare(database.getGameObject());
if (!databaseMap || !uniqueId) {
return;
}
if (databaseMap.has(uniqueId)) {
return;
}
databaseMap.set(uniqueId, database);
}
setData(gameObject, identifier, value) {
this.createIfAbsent(gameObject).set(identifier, value);
}
getData(gameObject, identifier) {
var _a;
return (_a = this.get(gameObject)) === null || _a === void 0 ? void 0 : _a.get(identifier);
}
deleteData(gameObject, identifier) {
const database = this.get(gameObject);
if (!database)
return false;
return database.delete(identifier);
}
buildDataInstanceIfPresent(gameObject, identifier, objectConstructor, options) {
const database = this.get(gameObject);
return database === null || database === void 0 ? void 0 : database.buildInstanceIfPresent(identifier, objectConstructor, options);
}
getBuiltDataInstance(gameObject, identifier) {
const database = this.get(gameObject);
return database === null || database === void 0 ? void 0 : database.getBuiltInstance(identifier);
}
createDataInstanceIfAbsent(gameObject, identifier, objectConstructor, options) {
const database = this.createIfAbsent(gameObject);
return database.createInstanceIfAbsent(identifier, objectConstructor, options);
}
getDirtyDatabaseList() {
return this._dirtyDatabaseList;
}
getAllDirtyDatabaseList() {
return this._dirtyDatabaseList.concat(this._dirtyDatabaseBuffer);
}
// --------------------------------------------------------
_setChangingEntityDatabaseBuffer(runtimeId, locationId, entityDatabase) {
Utils.assertInvokedByTendrock(runtimeId);
// console.log(`set changing entity database buffer: ${locationId}`)
this._changingEntityDatabaseBuffer.set(locationId, entityDatabase);
}
_getChangingEntityDatabaseBuffer(runtimeId, locationId) {
Utils.assertInvokedByTendrock(runtimeId);
return {
entityDatabase: this._changingEntityDatabaseBuffer.get(locationId),
cleanBuffer: () => {
this._changingEntityDatabaseBuffer.delete(locationId);
}
};
}
}
DatabaseManager.Instance = new DatabaseManager();
export const databaseManager = DatabaseManager.Instance;
world.beforeEvents.entityRemove.subscribe(({ removedEntity }) => {
if (!databaseManager.autoUpdateSourceEntity())
return;
const removedEntityDatabase = databaseManager.get(removedEntity);
if (!removedEntityDatabase)
return;
const locationId = LocationUtils.getLocationId(Object.assign(Object.assign({}, removedEntity.location), { dimension: removedEntity.dimension }), true);
databaseManager._setChangingEntityDatabaseBuffer(UniqueIdUtils.RuntimeId, locationId, removedEntityDatabase);
system.runTimeout(() => {
databaseManager._getChangingEntityDatabaseBuffer(UniqueIdUtils.RuntimeId, locationId).cleanBuffer();
}, 3);
});
world.afterEvents.entitySpawn.subscribe(({ entity, cause }) => {
if (!databaseManager.autoUpdateSourceEntity())
return;
if (cause !== EntityInitializationCause.Event && cause !== EntityInitializationCause.Transformed)
return;
const locationId = LocationUtils.getLocationId(Object.assign(Object.assign({}, entity.location), { dimension: entity.dimension }), true);
// console.log('entity spawned: ', locationId)
const { cleanBuffer, entityDatabase } = databaseManager._getChangingEntityDatabaseBuffer(UniqueIdUtils.RuntimeId, locationId);
if (entityDatabase) {
entityDatabase._setEntity(UniqueIdUtils.RuntimeId, entity);
cleanBuffer();
}
});