rott/kf_sources/AcediaCore/Classes/LocalDatabaseInstance.uc
2026-07-14 20:27:09 +07:00

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
}