/** * Implementation of Acedia's `Database` interface for locally stored * databases. * This class SHOULD NOT be deallocated manually. * This name was chosen so that more readable `LocalDatabase` could be * used in config for defining local databases through per-object-config. * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * * Acedia is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3 of the License, or * (at your option) any later version. * * Acedia is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ class LocalDatabaseInstance extends Database; /** * `LocalDatabaseInstance` implements `Database` interface for * local databases, however most of the work (everything related to actually * performing operations) is handled by `DBRecord` class. * This class' purpose is to: * 1. Managing updating information stored on the disk: it has to make * sure that saving is (eventually) done after every update, but not * too often, since it is an expensive operation; * 2. Making sure handlers for database queries are called (eventually). * First point is done via starting a "cooldown" timer after every disk * update that will count time until the next one. Second is done by storing * `DBTask`, generated by last database query and making it call it's handler * at the start of next tick. * * Why do we wait until the next tick? * Factually, every `LocalDatabaseInstance`'s query is completed immediately. * However, `Database`'s interface is designed to be used like so: * `db.ReadData(...).connect = handler;` where `handler` for query is assigned * AFTER it was filed to the database. Therefore, we cannot call `handler` * inside `ReadData()` and wait until next tick instead. * We could have allowed for immediate query response if we either * requested that handler was somehow set before the query or by providing * a method to immediately call handlers for queries users have made so far. * We avoided these solutions because we intend Acedia's `Database` interface * to be used in the same way regardless of whether server admins have chosen * to use local or remote databases. And neither of these solutions would have * worked with inherently asynchronous remote databases. That is why we instead * opted to use a more convenient interface * `db.ReadData(...).connect = handler;` and have both databases behave * the same way - with somewhat delayed response from the database. * If you absolutely must force your local database to have an immediate * response, then you can do it like so: * ```unrealscript * local DBTask task; * ... * task = db.ReadData(...); * task.connect = handler; * task.TryCompleting(); * ``` * However this method is not recommended and will never be a part of * a stable interface. */ // Reference to the `LocalDatabase` config object, corresponding to // this database var private LocalDatabase configEntry; // Reference to the `DBRecord` that stores root object of this database var private DBRecord rootRecord; // Remembers whether we've made a request for the disk access to the scheduler, // to avoid sending multiple ones. var private bool pendingDiskUpdate; // Last to-be-completed task added to this database var private DBTask lastTask; // Remember task's life version to make sure we still have the correct copy var private int lastTaskLifeVersion; protected function Constructor() { __level().unreal_api().OnTick(self).connect = CompleteAllTasks; } protected function Finalizer() { // Defaulting variables is not necessary, since this class does not // use object pool. CompleteAllTasks(); WriteToDisk(); rootRecord = none; __level().unreal_api().OnTick(self).Disconnect(); configEntry = none; } // It only has parameters so that it can be used as a `Tick()` event handler. private final function CompleteAllTasks( optional float delta, optional float dilationCoefficient) { if (lastTask != none && lastTask.GetLifeVersion() == lastTaskLifeVersion) { lastTask.TryCompleting(self); } lastTask = none; lastTaskLifeVersion = -1; } private final function ScheduleDiskUpdate() { if (!pendingDiskUpdate) { pendingDiskUpdate = true; _.scheduler.RequestDiskAccess(self).connect = WriteToDisk; } } public final function WriteToDisk() { local string packageName; if (!pendingDiskUpdate) { return; } pendingDiskUpdate = false; if (configEntry != none) { packageName = _.text.IntoString(configEntry.GetPackageName()); } if (packageName != "") { __level().unreal_api().GetGameType().SavePackage(packageName); } } private final function DBTask MakeNewTask(class newTaskClass) { local DBTask newTask; if (lastTask != none && lastTask.GetLifeVersion() != lastTaskLifeVersion) { lastTask = none; lastTaskLifeVersion = -1; } newTask = DBTask(_.memory.Allocate(newTaskClass)); newTask.SetPreviousTask(lastTask); lastTask = newTask; lastTaskLifeVersion = lastTask.GetLifeVersion(); return newTask; } private function bool ValidatePointer( BaseJSONPointer pointer, DBTask relevantTask, int requestID) { if (pointer != none) { return true; } relevantTask.SetResult(DBR_InvalidPointer, requestID); return false; } private function bool ValidateRootRecord(DBTask relevantTask, int requestID) { if (rootRecord != none) { return true; } relevantTask.SetResult(DBR_InvalidDatabase, requestID); return false; } public function DBReadTask ReadData( BaseJSONPointer pointer, optional bool makeMutable, optional int requestID) { local AcediaObject queryResult; local DBReadTask readTask; readTask = DBReadTask(MakeNewTask(class'DBReadTask')); if (!ValidatePointer(pointer, readTask, requestID)) return readTask; if (!ValidateRootRecord(readTask, requestID)) return readTask; if (rootRecord.LoadObject(pointer, queryResult, makeMutable)) { readTask.SetReadData(queryResult); readTask.SetResult(DBR_Success, requestID); } else { readTask.SetResult(DBR_InvalidPointer, requestID); _.memory.Free(queryResult); // just in case } return readTask; } public function DBWriteTask WriteData( BaseJSONPointer pointer, AcediaObject data, optional int requestID) { local bool isDataStorable; local DBWriteTask writeTask; writeTask = DBWriteTask(MakeNewTask(class'DBWriteTask')); if (!ValidatePointer(pointer, writeTask, requestID)) return writeTask; if (!ValidateRootRecord(writeTask, requestID)) return writeTask; // We can only write JSON array as the root value if (data != none && pointer.GetLength() <= 0) { isDataStorable = (data.class == class'HashTable'); } else { isDataStorable = _.json.IsCompatible(data); } if (!isDataStorable) { writeTask.SetResult(DBR_InvalidData, requestID); return writeTask; } if (rootRecord.SaveObject(pointer, data)) { writeTask.SetResult(DBR_Success, requestID); ScheduleDiskUpdate(); } else { writeTask.SetResult(DBR_InvalidPointer, requestID); } return writeTask; } public function DBRemoveTask RemoveData( BaseJSONPointer pointer, optional int requestID) { local DBRemoveTask removeTask; removeTask = DBRemoveTask(MakeNewTask(class'DBRemoveTask')); if (!ValidatePointer(pointer, removeTask, requestID)) return removeTask; if (!ValidateRootRecord(removeTask, requestID)) return removeTask; if (pointer.GetLength() == 0) { rootRecord.EmptySelf(); removeTask.SetResult(DBR_Success, requestID); return removeTask; } if (rootRecord.RemoveObject(pointer)) { removeTask.SetResult(DBR_Success, requestID); ScheduleDiskUpdate(); } else { removeTask.SetResult(DBR_InvalidPointer, requestID); } return removeTask; } public function DBCheckTask CheckDataType( BaseJSONPointer pointer, optional int requestID) { local DBCheckTask checkTask; checkTask = DBCheckTask(MakeNewTask(class'DBCheckTask')); if (!ValidatePointer(pointer, checkTask, requestID)) return checkTask; if (!ValidateRootRecord(checkTask, requestID)) return checkTask; checkTask.SetDataType(rootRecord.GetObjectType(pointer)); checkTask.SetResult(DBR_Success, requestID); return checkTask; } public function DBSizeTask GetDataSize( BaseJSONPointer pointer, optional int requestID) { local DBSizeTask sizeTask; sizeTask = DBSizeTask(MakeNewTask(class'DBSizeTask')); if (!ValidatePointer(pointer, sizeTask, requestID)) return sizeTask; if (!ValidateRootRecord(sizeTask, requestID)) return sizeTask; sizeTask.SetDataSize(rootRecord.GetObjectSize(pointer)); sizeTask.SetResult(DBR_Success, requestID); return sizeTask; } public function DBKeysTask GetDataKeys( BaseJSONPointer pointer, optional int requestID) { local ArrayList keys; local DBKeysTask keysTask; keysTask = DBKeysTask(MakeNewTask(class'DBKeysTask')); if (!ValidatePointer(pointer, keysTask, requestID)) return keysTask; if (!ValidateRootRecord(keysTask, requestID)) return keysTask; keys = rootRecord.GetObjectKeys(pointer); keysTask.SetDataKeys(keys); if (keys == none) { keysTask.SetResult(DBR_InvalidData, requestID); } else { keysTask.SetResult(DBR_Success, requestID); } return keysTask; } public function DBIncrementTask IncrementData( BaseJSONPointer pointer, AcediaObject increment, optional int requestID) { local DBQueryResult queryResult; local DBIncrementTask incrementTask; incrementTask = DBIncrementTask(MakeNewTask(class'DBIncrementTask')); if (!ValidatePointer(pointer, incrementTask, requestID)) { return incrementTask; } if (!ValidateRootRecord(incrementTask, requestID)) { return incrementTask; } queryResult = rootRecord.IncrementObject(pointer, increment); incrementTask.SetResult(queryResult, requestID); if (queryResult == DBR_Success) { ScheduleDiskUpdate(); } return incrementTask; } /** * Initializes caller database with prepared config and root objects. * * This is internal method and should not be called outside of `DBAPI`. */ public final function Initialize(LocalDatabase config, DBRecord root) { if (configEntry != none) return; if (config == none) return; configEntry = config; rootRecord = root; ScheduleDiskUpdate(); } /** * Returns config object that describes caller database. * * @return Config object that describes caller database. * returned value is the same value caller database uses, * it IS NOT a copy and SHOULD NOT be deallocated or deleted. */ public final function LocalDatabase GetConfig() { return configEntry; } defaultproperties { usesObjectPool = false }