358 lines
12 KiB
Ucode
358 lines
12 KiB
Ucode
/**
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
*/
|
|
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<DBTask> 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
|
|
} |