Add user groups and data API #8
Merged
dkanus
merged 20 commits from feature_user_groups
into develop
2 years ago
59 changed files with 9471 additions and 404 deletions
@ -0,0 +1,5 @@
|
||||
; Define all databases you want Acedia to use here. |
||||
; For simply making default Acedia configs work, set `createIfMissing` below |
||||
; to `true`. |
||||
[Database LocalDatabase] |
||||
createIfMissing=false |
@ -0,0 +1,35 @@
|
||||
; Acedia requires adding its own `GameRules` to listen to many different |
||||
; game events. |
||||
|
||||
; In this config you can setup Acedia's user groups and persistent data |
||||
; storage. Enabling this feature automatically enables user group support, |
||||
; while persistent data is optional. |
||||
; Databases can be configured in `AcediaDB.ini`. |
||||
[default Users] |
||||
; Configures whether to use database (and which) for storing user groups. |
||||
; Set `useDatabaseForGroupsData` to `false` if you want to define which users |
||||
; belong to what groups inside this config. |
||||
useDatabaseForGroupsData=true |
||||
groupsDatabaseLink=[local]Database:/group_data |
||||
; Configures whether persistent data should be additionally used. |
||||
; It can only be stored inside a database. |
||||
usePersistentData=true |
||||
persistentDataDatabaseLink=[local]Database:/user_data |
||||
; Available groups. Only used if `useDatabaseForGroupsData` is set to `false`. |
||||
localUserGroup=admin |
||||
localUserGroup=moderator |
||||
localUserGroup=trusted |
||||
|
||||
; These groups definitions only work in case you're using a config with |
||||
; `useDatabaseForGroupsData` set to `false`. Simply add new `user=` record, |
||||
; specifying SteamIDs of the players, e.g. `user=76561197960287930`. |
||||
; You can also optionally specify a human-readable lable for the SteamID after |
||||
; slash "/", e.g. `user=76561197960287930/gabe`. |
||||
[admin UserGroup] |
||||
;user= |
||||
|
||||
[moderator UserGroup] |
||||
;user= |
||||
|
||||
[trusted UserGroup] |
||||
;user= |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,791 @@
|
||||
/** |
||||
* Auxiliary object for simplifying working with databases. |
||||
* Copyright 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 DBConnection extends AcediaObject |
||||
dependson(Database) |
||||
dependson(DBCache); |
||||
|
||||
/** |
||||
* # `DBConnection` |
||||
* |
||||
* Auxiliary object for simplifying working with databases. |
||||
* `Database` class has a rather simple interface and there are several issues |
||||
* that constantly arise when trying to use it: |
||||
* |
||||
* 1. If one tries to read/write data from/to specific location in |
||||
* the database, then `JSONPointer` has to be kept in addition to |
||||
* the `Database` reference at all times; |
||||
* 2. One has to perform initial checks about whether database can even be |
||||
* connected to, if at desired location there is a proper data |
||||
* structure, etc.. If one also wants to start using data before |
||||
* database's response or after its failure, then the same work of |
||||
* duplication that data locally must be performed. |
||||
* 3. Instead of immediate operations, database operations are delayed and |
||||
* user has to handle their results asynchronously in separate methods. |
||||
* |
||||
* `DBConnection` takes care of these issues by providing you synchronous |
||||
* methods for accessing cached version of the data at the given location that |
||||
* is duplicated to the database as soon as possible (and even if its no longer |
||||
* possible in the case of a failure). |
||||
* `DBConnection` makes immediate changes on the local cache and reports |
||||
* about possible failures with database later through signals `OnEditResult()` |
||||
* (reports about success of writing operations) and `OnStateChanged()` |
||||
* (reports about state changes of connected database, including complete |
||||
* failures). |
||||
* The only reading of database's values occurs at the moment of connecting |
||||
* to it, after that all the data is read from the local cache. |
||||
* Possible `DBConnection` states include: |
||||
* |
||||
* * `DBCS_Idle` - database was created, but not yet connected; |
||||
* * `DBCS_Connecting` - `Connect()` method was successfully called, but |
||||
* its result is still unknown; |
||||
* * `DBCS_Connected` - database is connected and properly working; |
||||
* * `DBCS_Disconnected` - database was manually disconnected and now |
||||
* operates solely on the local cache. Once disconnected `DBConnection` |
||||
* cannot be reconnected - create a new one instead. |
||||
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason |
||||
* can be found out with `GetError()` method: |
||||
* |
||||
* * `FDE_None` - no error has yet occurred; |
||||
* * `FDE_CannotReadRootData` - root data couldn't be read from |
||||
* the database, most likely because of the invalid `JSONPointer` |
||||
* for the root value; |
||||
* * `FDE_UnexpectedRootData` - root data was read from the database, |
||||
* but has an unexpected format. `DBConnection` expects either |
||||
* JSON object or array (can be specified which one) and this error |
||||
* occurs if its not found at specified database's location; |
||||
* * `FDE_Unknown` - database returned `DBR_InvalidDatabase` result |
||||
* for one of the queries. This is likely to happen when database |
||||
* is damaged. More precise details depend on the implementation, |
||||
* but result boils down to database being unusable. |
||||
* |
||||
* ## Usage |
||||
* |
||||
* Usage is straightforward: |
||||
* |
||||
* 1. Initialize with appropriate database by calling `Initialize()`; |
||||
* 2. Start connecting to it by calling `Connect()`; |
||||
* 3. Use `ReadDataByJSON()`/`WriteDataByJSON()` to read/write into |
||||
* the connected database; |
||||
* |
||||
* You can use it transparently even if database connection fails, but if you |
||||
* need to handle such failure - connect to the `OnStateChanged()` signal for |
||||
* tracking `DBCS_FaultyDatabase` state and to `OnEditResult()` for tracking |
||||
* success of writing operations. |
||||
* |
||||
* ## Implementation |
||||
* |
||||
* The brunt of work is done by `DBCache` and most of the logic in this |
||||
* class is for tracking state of the connection to the database and then |
||||
* reporting these changes through its own signals. |
||||
* The most notable hidden functionality is tracking requests by ID - |
||||
* `DBConnection` makes each request with unique ID and then stores them inside |
||||
* `requestIDs` (and in case of write requests - along with corresponding |
||||
* `JSONPointer` inside `queuedPointers`). This is necessary because: |
||||
* |
||||
* 1. Even if `DBConnection` gets reallocated - as far as UnrealScript is |
||||
* concerned it is still the same object, so the responses we no longer |
||||
* care about will still arrive. Keeping track of the IDs that interest |
||||
* us inside `requestIDs` allows us to filter out responses we no |
||||
* longer care about; |
||||
* 2. Tracking corresponding (to the IDs) `queuedPointers` also allow us |
||||
* to know responses to which writing requests we've received. |
||||
* |
||||
* ## Remarks |
||||
* |
||||
* Currently `DBConnection` doesn't support important feature of *incrementing* |
||||
* data that allows several sources to safely change the same value |
||||
* asynchronously. We're skipping on it right now to save time as its not |
||||
* really currently needed, however it will be added in the future. |
||||
*/ |
||||
|
||||
enum DBConnectionState |
||||
{ |
||||
// `DBConnection` was created, but didn't yet attempt to connect |
||||
// to database |
||||
DBCS_Idle, |
||||
// `DBConnection` is currently connecting |
||||
DBCS_Connecting, |
||||
// `DBConnection` has already connected without errors |
||||
DBCS_Connected, |
||||
// `DBConnection` was manually disconnected |
||||
DBCS_Disconnected, |
||||
// `DBConnection` was disconnected because of the database error, |
||||
// @see `FaultyDatabaseError` for more. |
||||
DBCS_FaultyDatabase |
||||
}; |
||||
// Current connection state |
||||
var private DBConnectionState currentState; |
||||
|
||||
enum FaultyDatabaseError |
||||
{ |
||||
// No error has occurred yet |
||||
FDE_None, |
||||
// Root data isn't available |
||||
FDE_CannotReadRootData, |
||||
// Root data has incorrect format |
||||
FDE_UnexpectedRootData, |
||||
// Some internal error in database has occurred |
||||
FDE_Unknown |
||||
}; |
||||
// Reason for why current state is in `DBCS_FaultyDatabase` state; |
||||
// `FDE_None` if `DBConnection` is in any other state. |
||||
var private FaultyDatabaseError dbFailureReason; |
||||
// Keeps track whether root value read from the database was of the correct |
||||
// type. Only relevant after database tried connecting (i.e. it is in states |
||||
// `DBCS_Connected`, `DBCS_Disconnected` or `DBCS_FaultyDatabase`). |
||||
// This variable helps us determine whether error should be |
||||
// `FDE_CannotReadRootData` or `FDE_UnexpectedRootData`. |
||||
var private bool rootIsOfExpectedType; |
||||
|
||||
// `Database` + `JSONPointer` combo that point at the data we want to |
||||
// connect to |
||||
var private Database dbInstance; |
||||
var private JSONPointer rootPointer; |
||||
// Local, cached version of that data |
||||
var private DBCache localCache; |
||||
|
||||
// This is basically an array of (`int`, `JSONPointer`) pairs for tracking |
||||
// database requests of interest |
||||
var private array<int> requestIDs; |
||||
var private array<JSONPointer> queuedPointers; |
||||
// Next usable ID. `DBConnection` is expected to always use unique IDs. |
||||
var private int nextRequestID; |
||||
|
||||
|
||||
var private DBConnection_StateChanged_Signal onStateChangedSignal; |
||||
var private DBConnection_EditResult_Signal onEditResultSignal; |
||||
|
||||
var private LoggerAPI.Definition errDoubleInitialization; |
||||
|
||||
/** |
||||
* Signal that will be emitted whenever `DBConnection` changes state. |
||||
* List of the available states: |
||||
* |
||||
* * `DBCS_Idle` - database was created, but not yet connected; |
||||
* * `DBCS_Connecting` - `Connect()` method was successfully called, but |
||||
* its result is still unknown; |
||||
* * `DBCS_Connected` - database is connected and properly working; |
||||
* * `DBCS_Disconnected` - database was manually disconnected and now |
||||
* operates solely on the local cache. Once disconnected `DBConnection` |
||||
* cannot be reconnected - create a new one instead. |
||||
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason |
||||
* can be found out with `GetError()` method. |
||||
* |
||||
* This method *is not* called when `DBConnection` is deallocated. |
||||
* |
||||
* [Signature] |
||||
* void <slot>( |
||||
* DBConnection instance, |
||||
* DBConnectionState oldState, |
||||
* DBConnectionState newState) |
||||
* |
||||
* @param instance Instance of the `DBConnection` that has changed state. |
||||
* @param oldState State it was previously in. |
||||
* @param oldState New state. |
||||
*/ |
||||
/* SIGNAL */ |
||||
public final function DBConnection_StateChanged_Slot OnStateChanged( |
||||
AcediaObject receiver) |
||||
{ |
||||
return DBConnection_StateChanged_Slot(onStateChangedSignal |
||||
.NewSlot(receiver)); |
||||
} |
||||
|
||||
/** |
||||
* Signal that will be emitted whenever `DBConnection` receives response from |
||||
* connected database about success of writing operation. |
||||
* |
||||
* Responses to old requests can still be received even if database got |
||||
* disconnected. |
||||
* |
||||
* Any emissions of this signal when the database's state is `DBCS_Connecting` |
||||
* correspond to reapplying edits made prior connection was established. |
||||
* |
||||
* [Signature] |
||||
* void <slot>(JSONPointer editLocation, bool isSuccessful) |
||||
* |
||||
* @param editLocation Location of the writing operation this is |
||||
* a response to. |
||||
* @param isSuccessful Whether writing operation ended in the success. |
||||
*/ |
||||
/* SIGNAL */ |
||||
public final function DBConnection_EditResult_Slot OnEditResult( |
||||
AcediaObject receiver) |
||||
{ |
||||
return DBConnection_EditResult_Slot(onEditResultSignal.NewSlot(receiver)); |
||||
} |
||||
|
||||
protected function Constructor() |
||||
{ |
||||
localCache = DBCache(_.memory.Allocate(class'DBCache')); |
||||
onStateChangedSignal = DBConnection_StateChanged_Signal( |
||||
_.memory.Allocate(class'DBConnection_StateChanged_Signal')); |
||||
onEditResultSignal = DBConnection_EditResult_Signal( |
||||
_.memory.Allocate(class'DBConnection_EditResult_Signal')); |
||||
} |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
rootIsOfExpectedType = false; |
||||
currentState = DBCS_Idle; |
||||
_.memory.Free(dbInstance); |
||||
_.memory.Free(rootPointer); |
||||
_.memory.Free(localCache); |
||||
dbInstance = none; |
||||
rootPointer = none; |
||||
localCache = none; |
||||
_.memory.FreeMany(queuedPointers); |
||||
queuedPointers.length = 0; |
||||
requestIDs.length = 0; |
||||
// Free signals |
||||
_.memory.Free(onStateChangedSignal); |
||||
_.memory.Free(onEditResultSignal); |
||||
onStateChangedSignal = none; |
||||
onEditResultSignal = none; |
||||
} |
||||
|
||||
/** |
||||
* Initializes `DBConnection` with database and location to which it must be |
||||
* connected. |
||||
* |
||||
* For the initialization to be successful `DBConnection` must not yet be |
||||
* initialized and `initDatabase` be not `none`. |
||||
* |
||||
* To check whether caller `DBConnection` is initialized |
||||
* @see `IsInitialized()`. |
||||
* |
||||
* @param initDatabase Database with data we want to connect to. |
||||
* @param initRootPointer Location of said data in the given database. |
||||
* If `none` is specified, uses root object of the database. |
||||
* @return `true` if initialization was successful and `false` otherwise. |
||||
*/ |
||||
public final function bool Initialize( |
||||
Database initDatabase, |
||||
optional JSONPointer initRootPointer) |
||||
{ |
||||
if (IsInitialized()) return false; |
||||
if (initDatabase == none) return false; |
||||
if (!initDatabase.IsAllocated()) return false; |
||||
|
||||
dbInstance = initDatabase; |
||||
dbInstance.NewRef(); |
||||
if (initRootPointer != none) { |
||||
rootPointer = initRootPointer.Copy(); |
||||
} |
||||
else { |
||||
rootPointer = _.json.Pointer(); |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Reads data from the `DBConnection` at the location defined by the given |
||||
* `JSONPointer`. |
||||
* |
||||
* If data was initialized with non-empty location for the root data, then |
||||
* actual returned data's location in the database is defined by appending |
||||
* given `pointer` to that root pointer. |
||||
* |
||||
* Data is actually always read from the local cache and, therefore, we can |
||||
* read data we've written via `DBConnection` even without actually connecting |
||||
* to the database. |
||||
* |
||||
* @param pointer Location from which to read the data. |
||||
* @return Data recorded for the given `JSONPointer`. `none` if it is missing. |
||||
*/ |
||||
public final function AcediaObject ReadDataByJSON(JSONPointer pointer) |
||||
{ |
||||
return localCache.Read(pointer); |
||||
} |
||||
|
||||
/** |
||||
* Writes given data into the `DBConnection` at the location defined by |
||||
* the given `JSONPointer`. |
||||
* |
||||
* If data was initialized with non-empty location for the root data, then |
||||
* actual location for writing data in the database is defined by appending |
||||
* given `pointer` to that root pointer. |
||||
* |
||||
* Data is actually always also written into the local cache, even when |
||||
* there is no connection to the database. Once connection is made - all valid |
||||
* changes will be duplicated into it. |
||||
* Success of failure of actually making changes into the database can be |
||||
* tracked with `OnEditResult()` signal. |
||||
* |
||||
* This operation also returns immediate indication of whether it has |
||||
* failed *locally*. This can happen when trying to perform operation |
||||
* impossible for the local cache. For example, we cannot write any data at |
||||
* location "/a/b/c" for the JSON object "{"a":45.6}". |
||||
* If operation ended in failure locally, then change to database won't |
||||
* even be attempted. |
||||
* |
||||
* @param pointer Location into which to write the data. |
||||
* @param data Data to write into the connection. |
||||
* @return `true` on success and `false` on failure. `true` is required for |
||||
* the writing database request to be made. |
||||
*/ |
||||
public final function bool WriteDataByJSON( |
||||
JSONPointer pointer, |
||||
AcediaObject data) |
||||
{ |
||||
if (pointer == none) { |
||||
return false; |
||||
} |
||||
if (localCache.Write(pointer, data)) |
||||
{ |
||||
ModifyDataInDatabase(pointer, data, false); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Increments given data into the `DBConnection` at the location defined by |
||||
* the given `JSONPointer`. |
||||
* |
||||
* If data was initialized with non-empty location for the root data, then |
||||
* actual location for incrementing data in the database is defined by |
||||
* appending given `pointer` to that root pointer. |
||||
* |
||||
* Data is actually always also incremented into the local cache, even when |
||||
* there is no connection to the database. Once connection is made - all valid |
||||
* changes will be duplicated into it. |
||||
* Success of failure of actually making changes into the database can be |
||||
* tracked with `OnEditResult()` signal. |
||||
* |
||||
* This operation also returns immediate indication of whether it has |
||||
* failed *locally*. This can happen when trying to perform operation |
||||
* impossible for the local cache. For example, we cannot increment any data at |
||||
* location "/a/b/c" for the JSON object "{"a":45.6}". |
||||
* If operation ended in failure locally, then change to database won't |
||||
* even be attempted. |
||||
* |
||||
* @param pointer Location at which to increment the data. |
||||
* @param data Data with which to increment value inside the connection. |
||||
* @return `true` on success and `false` on failure. `true` is required for |
||||
* the incrementing database request to be made. |
||||
*/ |
||||
public final function bool IncrementDataByJSON( |
||||
JSONPointer pointer, |
||||
AcediaObject data) |
||||
{ |
||||
if (pointer == none) { |
||||
return false; |
||||
} |
||||
if (localCache.Increment(pointer, data)) |
||||
{ |
||||
ModifyDataInDatabase(pointer, data, true); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
/** |
||||
* Removes data from the `DBConnection` at the location defined by the given |
||||
* `JSONPointer`. |
||||
* |
||||
* If data was initialized with non-empty location for the root data, then |
||||
* actual location at which to remove data in the database is defined by |
||||
* appending given `pointer` to that root pointer. |
||||
* |
||||
* Data is actually always also removed from the local cache, even when |
||||
* there is no connection to the database. Once connection is made - all valid |
||||
* changes will be duplicated into it. |
||||
* Success of failure of actually making changes into the database can be |
||||
* tracked with `OnEditResult()` signal. |
||||
* |
||||
* This operation also returns immediate indication of whether it has |
||||
* failed *locally*. |
||||
* If operation ended in failure locally, then change to database won't |
||||
* even be attempted. |
||||
* |
||||
* @param pointer Location at which to remove data. |
||||
* @return `true` on success and `false` on failure. `true` is required for |
||||
* the removal database request to be made. |
||||
*/ |
||||
public final function bool RemoveDataByJSON(JSONPointer pointer) |
||||
{ |
||||
if (pointer == none) { |
||||
return false; |
||||
} |
||||
if (localCache.Remove(pointer)) |
||||
{ |
||||
RemoveDataInDatabase(pointer); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private final function ModifyDataInDatabase( |
||||
JSONPointer pointer, |
||||
AcediaObject data, |
||||
bool increment) |
||||
{ |
||||
local JSONPointer dataPointer; |
||||
|
||||
if (currentState != DBCS_Connected) { |
||||
return; |
||||
} |
||||
dataPointer = rootPointer.Copy(); |
||||
dataPointer.Append(pointer); |
||||
// `dataPointer` is consumed by `RegisterNextRequestID()` method |
||||
if (increment) |
||||
{ |
||||
dbInstance |
||||
.IncrementData( |
||||
dataPointer, |
||||
data, |
||||
RegisterNextRequestID(dataPointer)) |
||||
.connect = EditDataHandler; |
||||
} |
||||
else |
||||
{ |
||||
dbInstance |
||||
.WriteData(dataPointer, data, RegisterNextRequestID(dataPointer)) |
||||
.connect = EditDataHandler; |
||||
} |
||||
} |
||||
|
||||
private final function RemoveDataInDatabase(JSONPointer pointer) |
||||
{ |
||||
local JSONPointer dataPointer; |
||||
|
||||
if (currentState != DBCS_Connected) { |
||||
return; |
||||
} |
||||
dataPointer = rootPointer.Copy(); |
||||
dataPointer.Append(pointer); |
||||
// `dataPointer` is consumed by `RegisterNextRequestID()` method |
||||
dbInstance |
||||
.RemoveData(dataPointer, RegisterNextRequestID(dataPointer)) |
||||
.connect = EditDataHandler; |
||||
} |
||||
|
||||
/** |
||||
* Checks caller `DBConnection` was successfully initialized. |
||||
* |
||||
* @return `true` if caller `DBConnection` was initialized and `false` |
||||
* otherwise. |
||||
*/ |
||||
public final function bool IsInitialized() |
||||
{ |
||||
return (dbInstance != none); |
||||
} |
||||
|
||||
/** |
||||
* Returns current state of the connection of `DBConnection` to the database |
||||
* it was initialized with. |
||||
* |
||||
* @see `OnStateChanged()` for more information about connection states. |
||||
* @return Current connection state. |
||||
*/ |
||||
public final function DBConnectionState GetConnectionState() |
||||
{ |
||||
return currentState; |
||||
} |
||||
|
||||
/** |
||||
* Checks whether caller `DBConnection` is currently connected without errors |
||||
* to the database it was initialized with. |
||||
* |
||||
* @return `true` if caller `DBConnection` is connected to the database and |
||||
* `false` otherwise. |
||||
*/ |
||||
public final function bool IsConnected() |
||||
{ |
||||
return (currentState == DBCS_Connected); |
||||
} |
||||
|
||||
/** |
||||
* Checks whether an error has occurred with connection to the database. |
||||
* |
||||
* `DBConnection` can get disconnected from database manually and without |
||||
* any errors, so, if you simply want to check whether connection exists, |
||||
* @see `IsConnected()` or @see `GetConnectionState()`. |
||||
* To obtain more detailed information @see `GetError()`. |
||||
* |
||||
* @return `true` if there were no error thus far and `false` otherwise. |
||||
*/ |
||||
public final function bool IsOk() |
||||
{ |
||||
return (dbFailureReason == FDE_None); |
||||
} |
||||
|
||||
/** |
||||
* Returns error that has occurred during connection. |
||||
* |
||||
* @return Error that has occurred during connection to the database, |
||||
* `FDE_None` if there was no errors. |
||||
*/ |
||||
public final function FaultyDatabaseError GetError() |
||||
{ |
||||
return dbFailureReason; |
||||
} |
||||
|
||||
private final function ChangeState(DBConnectionState newState) |
||||
{ |
||||
local DBConnectionState oldState; |
||||
|
||||
oldState = currentState; |
||||
currentState = newState; |
||||
onStateChangedSignal.Emit(self, oldState, newState); |
||||
} |
||||
|
||||
/** |
||||
* Attempts connection to the database caller `DBConnection` was initialized |
||||
* with. Result isn't immediate and can be tracked with `OnStateChanged()` |
||||
* signal. |
||||
* |
||||
* Connection checks whether data by the initialization address can be read and |
||||
* has proper type (by default JSON object, but JSON array can be used |
||||
* instead). |
||||
* |
||||
* Whether connection is successfully established isn't known at the moment |
||||
* this function returns. User `OnStateChanged()` to track that. |
||||
* |
||||
* @param expectArray Set this to `true` if the expected root value is |
||||
* JSON array. |
||||
*/ |
||||
public final function Connect(optional bool expectArray) |
||||
{ |
||||
local Collection incrementObject; |
||||
|
||||
if (!IsInitialized()) return; |
||||
if (currentState != DBCS_Idle) return; |
||||
|
||||
if (expectArray) { |
||||
incrementObject = _.collections.EmptyArrayList(); |
||||
} |
||||
else { |
||||
incrementObject = _.collections.EmptyHashTable(); |
||||
} |
||||
dbInstance.IncrementData( |
||||
rootPointer, |
||||
incrementObject, |
||||
RegisterNextRequestID()).connect = IncrementCheckHandler; |
||||
incrementObject.FreeSelf(); |
||||
// Copy of the `rootPointer` is consumed by `RegisterNextRequestID()` |
||||
// method |
||||
dbInstance.ReadData(rootPointer,, RegisterNextRequestID(rootPointer.Copy())) |
||||
.connect = InitialLoadingHandler; |
||||
ChangeState(DBCS_Connecting); |
||||
} |
||||
|
||||
/** |
||||
* Disconnects `DBConnection` from its database, preventing its further |
||||
* updates. |
||||
* |
||||
* Database can only be disconnected if connection was at least initialized |
||||
* (state isn't `DBCS_Idle`) and no error has yet occurred (state isn't |
||||
* `DBCS_FaultyDatabase`). |
||||
* |
||||
* @return `true` if `DBConnection` was disconnected from the database and |
||||
* `false` otherwise (including if it already was disconnected). |
||||
*/ |
||||
public final function bool Disconnect() |
||||
{ |
||||
if ( currentState != DBCS_FaultyDatabase |
||||
&& currentState != DBCS_Idle |
||||
&& currentState != DBCS_Disconnected) |
||||
{ |
||||
ChangeState(DBCS_Disconnected); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private final function int RegisterNextRequestID( |
||||
optional /*take*/ JSONPointer relativePointer) |
||||
{ |
||||
if (relativePointer != none) { |
||||
queuedPointers[queuedPointers.length] = relativePointer; |
||||
} |
||||
else { |
||||
queuedPointers[queuedPointers.length] = _.json.Pointer(); |
||||
} |
||||
requestIDs[requestIDs.length] = nextRequestID; |
||||
nextRequestID += 1; |
||||
return (nextRequestID - 1); |
||||
} |
||||
|
||||
private final function JSONPointer FetchRequestPointer(int requestID) |
||||
{ |
||||
local int i; |
||||
local JSONPointer result; |
||||
|
||||
while (i < requestIDs.length) |
||||
{ |
||||
if (requestIDs[i] < requestID) |
||||
{ |
||||
// We receive all requests in order, so if `requestID` is higher |
||||
// than IDs of some other requests - it means that they are older, |
||||
// lost requests |
||||
_.memory.Free(queuedPointers[i]); |
||||
queuedPointers.Remove(i, 1); |
||||
requestIDs.Remove(i, 1); |
||||
} |
||||
if (requestIDs[i] == requestID) |
||||
{ |
||||
result = queuedPointers[i]; |
||||
queuedPointers.Remove(i, 1); |
||||
requestIDs.Remove(i, 1); |
||||
return result; |
||||
} |
||||
i += 1; |
||||
} |
||||
return none; |
||||
} |
||||
|
||||
private final function bool FetchIfRequestStillValid(int requestID) |
||||
{ |
||||
local JSONPointer result; |
||||
|
||||
result = FetchRequestPointer(requestID); |
||||
if (result != none) |
||||
{ |
||||
_.memory.Free(result); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private final function IncrementCheckHandler( |
||||
Database.DBQueryResult result, |
||||
Database source, |
||||
int requestID) |
||||
{ |
||||
if (!FetchIfRequestStillValid(requestID)) { |
||||
return; |
||||
} |
||||
// If we could successfully increment value with appropriate JSON value, |
||||
// then its type is correct |
||||
rootIsOfExpectedType = (result == DBR_Success); |
||||
} |
||||
|
||||
private final function InitialLoadingHandler( |
||||
Database.DBQueryResult result, |
||||
/*take*/ AcediaObject data, |
||||
Database source, |
||||
int requestID) |
||||
{ |
||||
local int i; |
||||
local array<DBCache.PendingEdit> completedEdits; |
||||
|
||||
if (!FetchIfRequestStillValid(requestID)) |
||||
{ |
||||
_.memory.Free(data); |
||||
return; |
||||
} |
||||
if (HandleInitializationError(result)) |
||||
{ |
||||
_.memory.Free(data); |
||||
return; |
||||
} |
||||
completedEdits = localCache.SetRealData(data); |
||||
for (i = 0; i < completedEdits.length; i += 1) |
||||
{ |
||||
if (completedEdits[i].successful) |
||||
{ |
||||
if (completedEdits[i].type == DBCET_Remove) { |
||||
RemoveDataInDatabase(completedEdits[i].location); |
||||
} |
||||
else |
||||
{ |
||||
ModifyDataInDatabase( |
||||
completedEdits[i].location, |
||||
completedEdits[i].data, |
||||
completedEdits[i].type == DBCET_Increment); |
||||
} |
||||
} |
||||
else { |
||||
onEditResultSignal.Emit(completedEdits[i].location, false); |
||||
} |
||||
_.memory.Free(completedEdits[i].location); |
||||
_.memory.Free(completedEdits[i].data); |
||||
} |
||||
_.memory.Free(data); |
||||
ChangeState(DBCS_Connected); |
||||
} |
||||
|
||||
// Return `true` if further initialization must be stopped. |
||||
private final function bool HandleInitializationError( |
||||
Database.DBQueryResult result) |
||||
{ |
||||
// Get disconnected before even response has even arrived |
||||
if (currentState == DBCS_Disconnected) { |
||||
return true; |
||||
} |
||||
if (currentState == DBCS_Connected) |
||||
{ |
||||
_.logger.Auto(errDoubleInitialization).Arg(rootPointer.ToText()); |
||||
return true; |
||||
} |
||||
if (result == DBR_InvalidDatabase) |
||||
{ |
||||
dbFailureReason = FDE_Unknown; |
||||
ChangeState(DBCS_FaultyDatabase); |
||||
return true; |
||||
} |
||||
if (result != DBR_Success) |
||||
{ |
||||
dbFailureReason = FDE_CannotReadRootData; |
||||
ChangeState(DBCS_FaultyDatabase); |
||||
return true; |
||||
} |
||||
if (!rootIsOfExpectedType) |
||||
{ |
||||
dbFailureReason = FDE_UnexpectedRootData; |
||||
ChangeState(DBCS_FaultyDatabase); |
||||
return true; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private final function EditDataHandler( |
||||
Database.DBQueryResult result, |
||||
Database source, |
||||
int requestID) |
||||
{ |
||||
local JSONPointer relatedPointer; |
||||
|
||||
relatedPointer = FetchRequestPointer(requestID); |
||||
if (relatedPointer == none) { |
||||
return; |
||||
} |
||||
if (result == DBR_InvalidDatabase) |
||||
{ |
||||
dbFailureReason = FDE_Unknown; |
||||
ChangeState(DBCS_FaultyDatabase); |
||||
relatedPointer.FreeSelf(); |
||||
return; |
||||
} |
||||
if (result == DBR_Success) { |
||||
onEditResultSignal.Emit(relatedPointer, true); |
||||
} |
||||
else { |
||||
onEditResultSignal.Emit(relatedPointer, false); |
||||
} |
||||
relatedPointer.FreeSelf(); |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
errDoubleInitialization = (l=LOG_Error,m="`DBConnection` connected to \"%1\" was double-initialized. This SHOULD NOT happen. Please report this bug.") |
||||
} |
@ -0,0 +1,40 @@
|
||||
/** |
||||
* Signal class for `DBConnections`'s `OnEditResult()` signal. |
||||
* Copyright 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 DBConnection_EditResult_Signal extends Signal |
||||
dependson(DBConnection); |
||||
|
||||
public final function Emit(JSONPointer editLocation, bool isSuccessful) |
||||
{ |
||||
local Slot nextSlot; |
||||
StartIterating(); |
||||
nextSlot = GetNextSlot(); |
||||
while (nextSlot != none) |
||||
{ |
||||
DBConnection_EditResult_Slot(nextSlot) |
||||
.connect(editLocation, isSuccessful); |
||||
nextSlot = GetNextSlot(); |
||||
} |
||||
CleanEmptySlots(); |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
relatedSlotClass = class'DBConnection_EditResult_Slot' |
||||
} |
@ -0,0 +1,41 @@
|
||||
/** |
||||
* Slot class for `DBConnections`'s `OnEditResult()` signal. |
||||
* Copyright 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 DBConnection_EditResult_Slot extends Slot |
||||
dependson(DBConnection); |
||||
|
||||
delegate connect(JSONPointer editLocation, bool isSuccessful) |
||||
{ |
||||
DummyCall(); |
||||
} |
||||
|
||||
protected function Constructor() |
||||
{ |
||||
connect = none; |
||||
} |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
super.Finalizer(); |
||||
connect = none; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -0,0 +1,43 @@
|
||||
/** |
||||
* Signal class for `DBConnections`'s `OnStatusChanged()` signal. |
||||
* Copyright 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 DBConnection_StateChanged_Signal extends Signal |
||||
dependson(DBConnection); |
||||
|
||||
public final function Emit( |
||||
DBConnection instance, |
||||
DBConnection.DBConnectionState oldState, |
||||
DBConnection.DBConnectionState newState) |
||||
{ |
||||
local Slot nextSlot; |
||||
StartIterating(); |
||||
nextSlot = GetNextSlot(); |
||||
while (nextSlot != none) |
||||
{ |
||||
DBConnection_StateChanged_Slot(nextSlot) |
||||
.connect(instance, oldState, newState); |
||||
nextSlot = GetNextSlot(); |
||||
} |
||||
CleanEmptySlots(); |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
relatedSlotClass = class'DBConnection_StateChanged_Slot' |
||||
} |
@ -0,0 +1,44 @@
|
||||
/** |
||||
* Slot class for `DBConnections`'s `OnStatusChanged()` signal. |
||||
* Copyright 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 DBConnection_StateChanged_Slot extends Slot |
||||
dependson(DBConnection); |
||||
|
||||
delegate connect( |
||||
DBConnection instance, |
||||
DBConnection.DBConnectionState oldState, |
||||
DBConnection.DBConnectionState newState) |
||||
{ |
||||
DummyCall(); |
||||
} |
||||
|
||||
protected function Constructor() |
||||
{ |
||||
connect = none; |
||||
} |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
super.Finalizer(); |
||||
connect = none; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -0,0 +1,389 @@
|
||||
/** |
||||
* Set of tests for `DBConnection` and `DBCache` classes. |
||||
* Copyright 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 TEST_DBConnection extends TestCase |
||||
abstract; |
||||
|
||||
var DBCache cache; |
||||
|
||||
protected static function TESTS() |
||||
{ |
||||
Context("Testing how `DBCache` handles data writes and reads when" |
||||
@ "the actual database's data is yet to be downloaded."); |
||||
Test_DBCache_BasicWriteRead_DuringLoading(); |
||||
Test_DBCache_AllOperations_DuringLoading(); |
||||
Context("Testing how `DBCache` handles applying pending writes after real" |
||||
@ "data was set."); |
||||
Test_DBCache_ApplyingPendingEdits(); |
||||
Test_DBCache_ApplyingPendingEdit_AllOperations(); |
||||
Context("Testing how `DBCache` handles data writes and reads when" |
||||
@ "the actual database's data is already setup."); |
||||
Test_DBCache_BasicWriteRead_AfterLoading(); |
||||
Test_DBCache_AllOperations_AfterLoading(); |
||||
} |
||||
|
||||
/* Creates following object: |
||||
{ |
||||
"A": "simpleValue", |
||||
"B": { |
||||
"A": [true, { |
||||
"A": "simpleValue", |
||||
"B": 11.12, |
||||
"": [true, null, "huh"] |
||||
}, "huh"], |
||||
"B": -13.95 |
||||
}, |
||||
"C": -5, |
||||
"D": [] |
||||
} */ |
||||
protected static function HashTable MakeTestJSONObject() |
||||
{ |
||||
local ArrayList outerArray, innerArray; |
||||
local HashTable result, outerObject, innerObject; |
||||
|
||||
innerArray = __().collections.EmptyArrayList(); |
||||
innerArray.AddBool(true).AddItem(none).AddString("huh"); |
||||
innerObject = __().collections.EmptyHashTable(); |
||||
innerObject.SetString(P("A"), "simpleValue"); |
||||
innerObject.SetFloat(P("B"), 11.12).SetItem(P(""), innerArray); |
||||
outerArray = __().collections.EmptyArrayList(); |
||||
outerArray.AddBool(true).AddItem(innerObject).AddString("huh"); |
||||
outerObject = __().collections.EmptyHashTable(); |
||||
outerObject.SetItem(P("A"), outerArray).SetFloat(P("B"), -13.95); |
||||
result = __().collections.EmptyHashTable(); |
||||
result.SetString(P("A"), "simpleValue"); |
||||
result.SetItem(P("B"), outerObject); |
||||
result.SetInt(P("C"), -5); |
||||
result.SetItem(P("D"), __().collections.EmptyArrayList()); |
||||
return result; |
||||
} |
||||
|
||||
protected static function CheckLocation(string pointer, string expectedResult) |
||||
{ |
||||
local AcediaObject value; |
||||
|
||||
value = default.cache |
||||
.Read(__().json.Pointer(__().text.FromString(pointer))); |
||||
if (BaseText(value) != none) |
||||
{ |
||||
TEST_ExpectTrue( |
||||
__().json.Print(value).ToString() |
||||
== ("\"" $ expectedResult $ "\"")); |
||||
} |
||||
else { |
||||
TEST_ExpectTrue(__().json.Print(value).ToString() == expectedResult); |
||||
} |
||||
} |
||||
|
||||
// This is a rather extensive and long test. |
||||
// Despite its length, I've chosen to not break it up into smaller parts, |
||||
// since the same value `default.cache` is changed and built up all the way |
||||
// through this method. |
||||
// Trying to break it up into smaller and simpler tests would only mean |
||||
// putting less stress of `DBCache` than we currently do. |
||||
protected static function Test_DBCache_BasicWriteRead_DuringLoading() |
||||
{ |
||||
local AcediaObject complexData; |
||||
|
||||
Issue("Simple write/read sequence not working correctly."); |
||||
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||
complexData = MakeTestJSONObject(); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/just/some/place/far/away")), |
||||
complexData); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/just/another/place/not/near")), |
||||
complexData); |
||||
CheckLocation("/just/another/place/not/near/A", "simpleValue"); |
||||
CheckLocation("/just/some/place/far/away/B/A/2", "huh"); |
||||
CheckLocation("/just/another/place/not/near/B/A/1//2", "huh"); |
||||
CheckLocation("/just/some/place/far/away/B/A/1/", "[true,null,\"huh\"]"); |
||||
|
||||
Issue("Data is read incorrectly after being overridden."); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/just/another/place/not/near/B/A/1//1")), |
||||
__().box.bool(false)); |
||||
CheckLocation( |
||||
"/just/another/place/not/near/B/A/1/", |
||||
"[true,false,\"huh\"]"); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/just/another/place/not/near/B/A")), |
||||
__().box.int(121)); |
||||
CheckLocation("/just/another/place/not/near/B/A", "121"); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/just/some/place/far/away/B/A/1/B")), |
||||
complexData); |
||||
CheckLocation("/just/some/place/far/away/B/A/1/B/B/A/0", "true"); |
||||
default.cache.Write( |
||||
__().json.Pointer( |
||||
P("/just/some/place/far/away/C/inside_the_number!/hey")), |
||||
__().box.float(1.1)); |
||||
CheckLocation("/just/some/place/far/away/C/inside_the_number!/hey", "null"); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/just/some/place/far/away/D/7/hey")), |
||||
__().box.int(-345)); |
||||
CheckLocation("/just/some/place/far/away/D", "[]"); |
||||
CheckLocation("/just/some/place/far/away/D/7/hey", "-345"); |
||||
default.cache.Write( |
||||
__().json.Pointer( |
||||
P("/just/some/place/far/away/D/inside_the_array!/hey")), |
||||
__().box.float(1.1)); |
||||
CheckLocation("/just/some/place/far/away/D/inside_the_array!/hey", "null"); |
||||
CheckLocation("/just/some/place/far/away/D", "[]"); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/just/some/place/far/away/D/-")), |
||||
__().box.bool(true)); |
||||
CheckLocation("/just/some/place/far/away/D", "[true]"); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/just/some/place/far/away/D/7")), |
||||
__().box.bool(true)); |
||||
CheckLocation( |
||||
"/just/some/place/far/away/D", |
||||
"[true,null,null,null,null,null,null,true]"); |
||||
Issue("Writing at the end of a JSON array several times doesn't correctly" |
||||
@ "keep all values."); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/just/some/place/far/away/D/-")), |
||||
__().box.int(13524)); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/just/some/place/far/away/D/-")), none); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/just/some/place/far/away/D/-")), |
||||
__().box.int(121)); |
||||
CheckLocation( |
||||
"/just/some/place/far/away/D", |
||||
"[true,null,null,null,null,null,null,true,13524,null,121]"); |
||||
} |
||||
|
||||
protected static function Test_DBCache_AllOperations_DuringLoading() |
||||
{ |
||||
local AcediaObject complexData; |
||||
local ArrayList newArrayData; |
||||
|
||||
newArrayData = __().collections.EmptyArrayList(); |
||||
newArrayData = newArrayData.AddInt(45); |
||||
newArrayData = newArrayData.AddItem(none); |
||||
newArrayData = newArrayData.AddString("lol"); |
||||
|
||||
Issue("Increment/remove/read sequence not working correctly."); |
||||
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||
complexData = MakeTestJSONObject(); |
||||
default.cache.Increment(__().json.Pointer(P("")), complexData); |
||||
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||
__().text.FromString("oi")); |
||||
default.cache.Remove(__().json.Pointer(P("/B/A/1/B"))); |
||||
default.cache.Remove(__().json.Pointer(P("/B/A/1/"))); |
||||
default.cache.Increment(__().json.Pointer(P("/B/A")), newArrayData); |
||||
default.cache.Increment(__().json.Pointer(P("/C")), __().box.float(34.5)); |
||||
default.cache.Increment(__().json.Pointer(P("/C")), __().box.bool(true)); |
||||
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData); |
||||
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData); |
||||
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData); |
||||
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||
__().text.FromString("! Yeah!")); |
||||
// Override all increments! |
||||
default.cache.Write(__().json.Pointer(P("/D")), newArrayData); |
||||
CheckLocation("/B/A/1/A", "simpleValueoi! Yeah!"); |
||||
CheckLocation("/B/A", |
||||
"[true,{\"A\":\"simpleValueoi! Yeah!\"},\"huh\",45,null,\"lol\"]"); |
||||
CheckLocation("/C", "29.5"); |
||||
CheckLocation("/D", "[45,null,\"lol\"]"); |
||||
} |
||||
|
||||
protected static function Test_DBCache_ApplyingPendingEdits() |
||||
{ |
||||
SubTest_DBCache_ApplyingPendingEdits_Simple(); |
||||
SubTest_DBCache_ApplyingPendingEdits_Complex(); |
||||
} |
||||
|
||||
protected static function SubTest_DBCache_ApplyingPendingEdits_Simple() |
||||
{ |
||||
local int i; |
||||
local array<DBCache.PendingEdit> result; |
||||
|
||||
Issue("Pending writes successfully apply in simple JSON object case."); |
||||
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("")), |
||||
__().collections.EmptyHashTable()); |
||||
default.cache.Write(__().json.Pointer(P("/hey")), __().box.int(476)); |
||||
default.cache.Write(__().json.Pointer(P("/hope")), none); |
||||
default.cache.Write(__().json.Pointer(P("/-")), __().ref.float(324.3)); |
||||
result = default.cache.SetRealData(__().text.FromString("woah")); |
||||
for (i = 0; i < result.length; i += 1) { |
||||
TEST_ExpectTrue(result[i].successful); |
||||
} |
||||
CheckLocation("/hey", "476"); |
||||
CheckLocation("/hope", "null"); |
||||
CheckLocation("/-", "324.3"); |
||||
|
||||
Issue("Pending don't fail in a simple case of writing into JSON string."); |
||||
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||
default.cache.Write(__().json.Pointer(P("/hey")), __().box.int(476)); |
||||
default.cache.Write(__().json.Pointer(P("/hope")), none); |
||||
default.cache.Write(__().json.Pointer(P("/-")), __().ref.float(324.3)); |
||||
result = default.cache.SetRealData(__().text.FromString("woah")); |
||||
for (i = 0; i < result.length; i += 1) { |
||||
TEST_ExpectFalse(result[i].successful); |
||||
} |
||||
CheckLocation("/hey", "null"); |
||||
CheckLocation("/hope", "null"); |
||||
CheckLocation("/-", "null"); |
||||
} |
||||
|
||||
protected static function SubTest_DBCache_ApplyingPendingEdits_Complex() |
||||
{ |
||||
local array<DBCache.PendingEdit> result; |
||||
|
||||
Issue("Pending writes incorrectly apply in complex case."); |
||||
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||
default.cache.Write(__().json.Pointer(P("/B/A/1/B")), __().box.int(777)); |
||||
default.cache.Write(__().json.Pointer(P("/B/A/-")), __().box.bool(true)); |
||||
default.cache.Write(__().json.Pointer(P("/D/5")), __().box.float(1.1)); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/new")), |
||||
__().collections.EmptyHashTable()); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/new/sub")), |
||||
__().text.FromString("!SubString!")); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/D/impossiburu")), |
||||
__().text.FromString("!SubString!")); |
||||
result = default.cache.SetRealData(MakeTestJSONObject()); |
||||
CheckLocation("/B/A/1/B", "777"); |
||||
CheckLocation("/B/A/3", "true"); |
||||
CheckLocation("/D/5", "1.1"); |
||||
CheckLocation("/D/2", "null"); |
||||
CheckLocation("/new", "{\"sub\":\"!SubString!\"}"); |
||||
TEST_ExpectTrue(result[0].successful); |
||||
TEST_ExpectTrue(result[1].successful); |
||||
TEST_ExpectTrue(result[2].successful); |
||||
TEST_ExpectTrue(result[3].successful); |
||||
TEST_ExpectTrue(result[4].successful); |
||||
TEST_ExpectFalse(result[5].successful); |
||||
} |
||||
|
||||
protected static function Test_DBCache_ApplyingPendingEdit_AllOperations() |
||||
{ |
||||
local ArrayList newArrayData; |
||||
local array<DBCache.PendingEdit> result; |
||||
|
||||
newArrayData = __().collections.EmptyArrayList(); |
||||
newArrayData = newArrayData.AddInt(45).AddItem(none).AddString("lol"); |
||||
|
||||
Issue("Pending increments and removals incorrectly apply in complex case."); |
||||
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||
__().text.FromString("oi")); |
||||
default.cache.Remove(__().json.Pointer(P("/B/A/1/B"))); |
||||
default.cache.Remove(__().json.Pointer(P("/B/A/1/"))); |
||||
default.cache.Increment(__().json.Pointer(P("/B/A")), newArrayData); |
||||
default.cache.Increment(__().json.Pointer(P("/C")), __().box.float(34.5)); |
||||
default.cache.Increment(__().json.Pointer(P("/C")), __().box.bool(true)); |
||||
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData); |
||||
default.cache.Increment(__().json.Pointer(P("/D")), newArrayData); |
||||
default.cache.Remove(__().json.Pointer(P("/B/A/Y"))); |
||||
default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||
__().text.FromString("! Yeah!")); |
||||
default.cache.Write(__().json.Pointer(P("/D")), newArrayData); |
||||
result = default.cache.SetRealData(MakeTestJSONObject()); |
||||
TEST_ExpectTrue(result.length == 9); |
||||
TEST_ExpectTrue(result[0].successful); |
||||
TEST_ExpectTrue(result[1].successful); |
||||
TEST_ExpectTrue(result[2].successful); |
||||
TEST_ExpectTrue(result[3].successful); |
||||
TEST_ExpectTrue(result[4].successful); |
||||
TEST_ExpectFalse(result[5].successful); |
||||
TEST_ExpectFalse(result[6].successful); |
||||
TEST_ExpectTrue(result[7].successful); |
||||
TEST_ExpectTrue(result[8].successful); |
||||
} |
||||
|
||||
protected static function Test_DBCache_BasicWriteRead_AfterLoading() |
||||
{ |
||||
local AcediaObject complexData; |
||||
|
||||
Issue("Simple write/read sequence not working."); |
||||
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||
complexData = MakeTestJSONObject(); |
||||
TEST_ExpectTrue(default.cache.SetRealData(complexData).length == 0); |
||||
default.cache.Write(__().json.Pointer(P("/B/A/1/B")), __().box.int(777)); |
||||
default.cache.Write(__().json.Pointer(P("/B/A/-")), __().box.bool(true)); |
||||
default.cache.Write(__().json.Pointer(P("/D/5")), __().box.float(1.1)); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/new")), |
||||
__().collections.EmptyHashTable()); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/new/sub")), |
||||
__().text.FromString("!SubString!")); |
||||
default.cache.Write( |
||||
__().json.Pointer(P("/D/impossiburu")), |
||||
__().text.FromString("!SubString!")); |
||||
CheckLocation("/B/A/1/B", "777"); |
||||
CheckLocation("/B/A/3", "true"); |
||||
CheckLocation("/D/5", "1.1"); |
||||
CheckLocation("/new", "{\"sub\":\"!SubString!\"}"); |
||||
|
||||
Issue("Simple write/read are affecting data when they shouldn't."); |
||||
CheckLocation("/D/2", "null"); |
||||
default.cache.Write(__().json.Pointer(P("")), __().box.float(1.1)); |
||||
default.cache.Write(__().json.Pointer(P("/hm?")), __().box.float(2.2)); |
||||
CheckLocation("", "1.1"); |
||||
} |
||||
|
||||
protected static function Test_DBCache_AllOperations_AfterLoading() |
||||
{ |
||||
local ArrayList newArrayData; |
||||
|
||||
newArrayData = __().collections.EmptyArrayList(); |
||||
newArrayData = newArrayData.AddInt(45).AddItem(none).AddString("lol"); |
||||
|
||||
Issue("Increment/remove/read sequence not working correctly."); |
||||
default.cache = DBCache(__().memory.Allocate(class'DBCache')); |
||||
default.cache.SetRealData(MakeTestJSONObject()); |
||||
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||
__().text.FromString("oi"))); |
||||
TEST_ExpectTrue(default.cache.Remove(__().json.Pointer(P("/B/A/1/B")))); |
||||
TEST_ExpectTrue(default.cache.Remove(__().json.Pointer(P("/B/A/1/")))); |
||||
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A")), |
||||
newArrayData)); |
||||
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/C")), |
||||
__().box.float(34.5))); |
||||
TEST_ExpectFalse(default.cache.Increment(__().json.Pointer(P("/C")), |
||||
__().box.bool(true))); |
||||
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/D")), |
||||
newArrayData)); |
||||
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/D")), |
||||
newArrayData)); |
||||
TEST_ExpectFalse(default.cache.Remove(__().json.Pointer(P("/B/A/Y")))); |
||||
TEST_ExpectTrue(default.cache.Increment(__().json.Pointer(P("/B/A/1/A")), |
||||
__().text.FromString("! Yeah!"))); |
||||
default.cache.Write(__().json.Pointer(P("/D")), newArrayData); |
||||
CheckLocation("/B/A/1/A", "simpleValueoi! Yeah!"); |
||||
CheckLocation("/B/A", |
||||
"[true,{\"A\":\"simpleValueoi! Yeah!\"},\"huh\",45,null,\"lol\"]"); |
||||
CheckLocation("/C", "29.5"); |
||||
CheckLocation("/D", "[45,null,\"lol\"]"); |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
caseGroup = "Database" |
||||
caseName = "DBConnection related tests" |
||||
} |
@ -0,0 +1,655 @@
|
||||
/** |
||||
* Command for displaying help information about registered Acedia's commands. |
||||
* Copyright 2022-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 ACommandUserGroups extends Command |
||||
dependson(Users_Feature); |
||||
|
||||
protected function BuildData(CommandDataBuilder builder) |
||||
{ |
||||
builder.Name(P("usergroups")) |
||||
.Group(P("admin")) |
||||
.Summary(P("User groups management.")) |
||||
.Describe(P("Allows to add/remove user groups and users to these:" |
||||
@ "groups. Changes made by it will always affect current session," |
||||
@ "but might fail to be saved in case user groups are stored in" |
||||
@ "a database that is either corrupted or in read-only mode.")); |
||||
builder.SubCommand(P("list")) |
||||
.Describe(P("Lists specified groups along with users that belong to" |
||||
@ "them. If no groups were specified at all - lists all available" |
||||
@ "groups.")) |
||||
.OptionalParams() |
||||
.ParamTextList(P("groups")); |
||||
builder.SubCommand(P("add")) |
||||
.Describe(P("Adds a new group")) |
||||
.ParamText(P("group_name")); |
||||
builder.SubCommand(P("remove")) |
||||
.Describe(P("Removes a group")) |
||||
.ParamText(P("group_name")); |
||||
builder.SubCommand(P("addplayer")) |
||||
.Describe(F("Adds new user to the group, specified by the player" |
||||
@ "selector. Can add several players at once." |
||||
@ "Allows to also optionally specify annotation" |
||||
@ "(human-readable name) that can be thought of as" |
||||
@ "a {$TextEmphasis comment}. If annotation isn't specified" |
||||
@ "current nickname will be used as one.")) |
||||
.ParamText(P("group_name")) |
||||
.ParamPlayers(P("player_selector")) |
||||
.OptionalParams() |
||||
.ParamText(P("annotation")); |
||||
builder.SubCommand(P("removeplayer")) |
||||
.Describe(P("Removes user from the group, specified by player selector." |
||||
@ "Can remove several players at once.")) |
||||
.ParamText(P("group_name")) |
||||
.ParamPlayers(P("player_selector")); |
||||
builder.SubCommand(P("adduser")) |
||||
.Describe(F("Adds new user to the group. Allows to also optionally" |
||||
@ "specify annotation (human-readable name) that can be thought of" |
||||
@ "as a {$TextEmphasis comment}.")) |
||||
.ParamText(P("group_name")) |
||||
.ParamText(P("user_id")) |
||||
.OptionalParams() |
||||
.ParamText(P("annotation")); |
||||
builder.SubCommand(P("removeuser")) |
||||
.Describe(P("Removes user from the group. User can be specified by both" |
||||
@ "user's id or annotation, with id taking priority.")) |
||||
.ParamText(P("group_name")) |
||||
.ParamText(P("user_name")); |
||||
builder.Option(P("force")) |
||||
.Describe(P("Allows to force usage of invalid user IDs.")); |
||||
} |
||||
|
||||
protected function Executed(CallData arguments, EPlayer instigator) |
||||
{ |
||||
local bool forceOption; |
||||
local Text groupName, userID, userName, annotation; |
||||
local ArrayList players, groups; |
||||
|
||||
groupName = arguments.parameters.GetText(P("group_name")); |
||||
// For parameters named `user_id`, can only be ID |
||||
userID = arguments.parameters.GetText(P("user_id")); |
||||
// For parameters named `user_id`, can be either ID or annotation |
||||
userName = arguments.parameters.GetText(P("user_name")); |
||||
annotation = arguments.parameters.GetText(P("annotation")); |
||||
// An array of players that can be specified for some commands |
||||
players = arguments.parameters.GetArrayList(P("player_selector")); |
||||
groups = arguments.parameters.GetArrayList(P("groups")); |
||||
forceOption = arguments.options.HasKey(P("force")); |
||||
if (arguments.subCommandName.IsEmpty()) { |
||||
DisplayUserGroups(); |
||||
} |
||||
else if (arguments.subCommandName.Compare(P("list"), SCASE_SENSITIVE)) { |
||||
DisplayUserGroupsWithUsers(groups); |
||||
} |
||||
else if (arguments.subCommandName.Compare(P("add"), SCASE_SENSITIVE)) { |
||||
AddGroup(groupName); |
||||
} |
||||
else if (arguments.subCommandName.Compare(P("remove"), SCASE_SENSITIVE)) { |
||||
RemoveGroup(groupName); |
||||
} |
||||
else if (arguments.subCommandName.Compare(P("adduser"), SCASE_SENSITIVE)) { |
||||
AddOrAnnotateUser(groupName, userID, annotation, forceOption); |
||||
} |
||||
else if (arguments.subCommandName.Compare(P("removeuser"), SCASE_SENSITIVE)) |
||||
{ |
||||
RemoveUser(groupName, userName); |
||||
} |
||||
else if (arguments.subCommandName.Compare(P("addplayer"), SCASE_SENSITIVE)) { |
||||
AddOrAnnotatePlayers(groupName, players, annotation); |
||||
} |
||||
else if (arguments.subCommandName |
||||
.Compare(P("removeplayer"), SCASE_SENSITIVE)) |
||||
{ |
||||
RemovePlayers(groupName, players); |
||||
} |
||||
_.memory.Free(groupName); |
||||
_.memory.Free(userID); |
||||
_.memory.Free(userName); |
||||
_.memory.Free(annotation); |
||||
_.memory.Free(players); |
||||
_.memory.Free(groups); |
||||
} |
||||
|
||||
private function bool ValidateGroupExistence(BaseText groupName) |
||||
{ |
||||
if (_.users.IsGroupExisting(groupName)) { |
||||
return true; |
||||
} |
||||
callerConsole |
||||
.Write(P("Group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.UseColorOnce(_.color.TextFailure) |
||||
.Write(P(" doesn't exists")) |
||||
.WriteLine(P("!")); |
||||
return false; |
||||
} |
||||
|
||||
private function bool ValidateUserID(BaseText textUserID) |
||||
{ |
||||
local int i; |
||||
|
||||
if (textUserID == none) { |
||||
return false; |
||||
} |
||||
if (textUserID.IsEmpty()) |
||||
{ |
||||
callerConsole.WriteLine(F("Valid User ID" |
||||
@ "{$TextFailure shouldn't be empty}," |
||||
@ "use {$TextEmphasis --force} flag if you want to enforce" |
||||
@ "using it.")); |
||||
return false; |
||||
} |
||||
for (i = 0; i < textUserID.GetLength(); i += 1) |
||||
{ |
||||
if (!_.text.IsDigit(textUserID.GetCharacter(i))) |
||||
{ |
||||
callerConsole.WriteLine(F("Valid User ID" |
||||
@ "{$TextFailure should consist only of digits}," |
||||
@ "use {$TextEmphasis --force} flag if you want" |
||||
@ "to enforce using it.")); |
||||
return false; |
||||
} |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
private function bool TryAddingUserID( |
||||
BaseText groupName, |
||||
UserID userID, |
||||
BaseText userSpecifiedID) |
||||
{ |
||||
if (_.users.IsUserIDInGroup(userID, groupName)) |
||||
{ |
||||
callerConsole |
||||
.Write(P("User id specified as ")) |
||||
.UseColorOnce(_.color.Gray) |
||||
.Write(userSpecifiedID) |
||||
.UseColorOnce(_.color.TextFailure) |
||||
.Write(P(" is already in the group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.WriteLine(P("!")); |
||||
} |
||||
else if (_.users.AddUserIDToGroup(userID, groupName)) |
||||
{ |
||||
callerConsole |
||||
.Write(F("{$TextPositive Added} user id specified as ")) |
||||
.UseColorOnce(_.color.Gray) |
||||
.Write(userSpecifiedID) |
||||
.Write(P(" to the group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.WriteLine(P("!")); |
||||
} |
||||
else |
||||
{ |
||||
callerConsole |
||||
.UseColorOnce(_.color.TextFailure) |
||||
.Write(P("Failed (for unknown reason)")) |
||||
.Write(P(" to add user id ")) |
||||
.UseColorOnce(_.color.Gray).Write(userSpecifiedID) |
||||
.Write(P(" to the group ")) |
||||
.UseColorOnce(_.color.TextEmphasis).Write(groupName) |
||||
.WriteLine(P("!")); |
||||
return false; |
||||
} |
||||
return true; |
||||
} |
||||
|
||||
private function DisplayAnnotation( |
||||
BaseText userSpecifiedName, |
||||
BaseText groupName, |
||||
BaseText annotation) |
||||
{ |
||||
callerConsole |
||||
.Write(P("Annotation for user id specified as ")) |
||||
.UseColorOnce(_.color.Gray) |
||||
.Write(userSpecifiedName) |
||||
.UseColorOnce(_.color.TextPositive) |
||||
.Write(P(" in the group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.Write(P(" is set to ")) |
||||
.UseColorOnce(_.color.TextNeutral) |
||||
.WriteLine(annotation); |
||||
} |
||||
|
||||
private function AddOrAnnotateUser( |
||||
BaseText groupName, |
||||
BaseText textUserID, |
||||
BaseText annotation, |
||||
bool forceOption) |
||||
{ |
||||
local UserID id; |
||||
|
||||
if (groupName == none) return; |
||||
if (textUserID == none) return; |
||||
if (!ValidateGroupExistence(groupName)) return; |
||||
if (!forceOption && !ValidateUserID(textUserID)) return; |
||||
|
||||
id = UserID(_.memory.Allocate(class'UserID')); |
||||
id.Initialize(textUserID); |
||||
if (!TryAddingUserID(groupName, id, textUserID) || annotation == none) |
||||
{ |
||||
_.memory.Free(id); |
||||
return; |
||||
} |
||||
_.users.SetAnnotationForUserID(groupName, id, annotation); |
||||
_.memory.Free(id); |
||||
DisplayAnnotation(textUserID, groupName, annotation); |
||||
} |
||||
|
||||
private function AddOrAnnotatePlayers( |
||||
BaseText groupName, |
||||
ArrayList players, |
||||
BaseText annotation) |
||||
{ |
||||
local int i; |
||||
local BaseText playerName, nextAnnotation; |
||||
local EPlayer nextPlayer; |
||||
local UserID nextID; |
||||
|
||||
if (groupName == none) return; |
||||
if (players == none) return; |
||||
if (!ValidateGroupExistence(groupName)) return; |
||||
|
||||
for (i = 0; i < players.GetLength(); i += 1) |
||||
{ |
||||
nextPlayer = EPlayer(players.GetItem(i)); |
||||
if (nextPlayer == none) { |
||||
continue; |
||||
} |
||||
playerName = nextPlayer.GetName(); |
||||
nextID = nextPlayer.GetUserID(); |
||||
if (TryAddingUserID(groupName, nextID, playerName)) |
||||
{ |
||||
if (annotation == none) { |
||||
nextAnnotation = playerName; |
||||
} |
||||
else { |
||||
nextAnnotation = annotation; |
||||
} |
||||
_.users.SetAnnotationForUserID(groupName, nextID, nextAnnotation); |
||||
DisplayAnnotation(playerName, groupName, nextAnnotation); |
||||
_.memory.Free(nextID); |
||||
nextAnnotation = none; |
||||
} |
||||
_.memory.Free(nextPlayer); |
||||
_.memory.Free(playerName); |
||||
_.memory.Free(nextID); |
||||
nextPlayer = none; |
||||
playerName = none; |
||||
nextID = none; |
||||
} |
||||
} |
||||
|
||||
private function TryRemovingUserID( |
||||
BaseText groupName, |
||||
UserID idToRemove, |
||||
BaseText userSpecifiedName) |
||||
{ |
||||
local Text idAsText; |
||||
|
||||
idAsText = idToRemove.GetUniqueID(); |
||||
if (_.users.RemoveUserIDFromGroup(idToRemove, groupName)) |
||||
{ |
||||
callerConsole |
||||
.Write(F("{$TextNegative Removed} user ")) |
||||
.UseColorOnce(_.color.Gray) |
||||
.Write(userSpecifiedName) |
||||
.Write(P(" (with id ")) |
||||
.UseColorOnce(_.color.Gray) |
||||
.Write(idAsText) |
||||
.Write(P(") from the group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.WriteLine(P("!")); |
||||
} |
||||
else |
||||
{ |
||||
callerConsole |
||||
.UseColorOnce(_.color.TextFailure) |
||||
.Write(P("Failed (for unknown reason)")) |
||||
.Write(P("to remove user with id ")) |
||||
.UseColorOnce(_.color.Gray) |
||||
.Write(idAsText) |
||||
.Write(P(" from the group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.WriteLine(P(".")); |
||||
} |
||||
_.memory.Free(idAsText); |
||||
} |
||||
|
||||
private function bool RemoveUsersByAnnotation( |
||||
BaseText groupName, |
||||
BaseText userName) |
||||
{ |
||||
local int i; |
||||
local bool removedUser; |
||||
local array<Users_Feature.AnnotatedUserID> annotatedUsers; |
||||
|
||||
annotatedUsers = _.users.GetAnnotatedGroupMembers(groupName); |
||||
for (i = 0; i < annotatedUsers.length; i += 1) |
||||
{ |
||||
if (userName.Compare(annotatedUsers[i].annotation, SCASE_INSENSITIVE)) |
||||
{ |
||||
TryRemovingUserID(groupName, annotatedUsers[i].id, userName); |
||||
removedUser = true; |
||||
} |
||||
} |
||||
for (i = 0; i < annotatedUsers.length; i += 1) |
||||
{ |
||||
_.memory.Free(annotatedUsers[i].id); |
||||
_.memory.Free(annotatedUsers[i].annotation); |
||||
} |
||||
return removedUser; |
||||
} |
||||
|
||||
private function RemoveUser(BaseText groupName, BaseText userName) |
||||
{ |
||||
local bool matchedUserName; |
||||
local UserID idFromName; |
||||
|
||||
if (groupName == none) return; |
||||
if (userName == none) return; |
||||
if (!ValidateGroupExistence(groupName)) return; |
||||
|
||||
idFromName = UserID(_.memory.Allocate(class'UserID')); |
||||
idFromName.Initialize(userName); |
||||
if ( idFromName.IsInitialized() |
||||
&& _.users.IsUserIDInGroup(idFromName, groupName)) |
||||
{ |
||||
TryRemovingUserID(groupName, idFromName, userName); |
||||
matchedUserName = true; |
||||
} |
||||
else { |
||||
matchedUserName = RemoveUsersByAnnotation(groupName, userName); |
||||
} |
||||
_.memory.Free(idFromName); |
||||
if (!matchedUserName) |
||||
{ |
||||
callerConsole |
||||
.Write(P("User ")) |
||||
.UseColorOnce(_.color.Gray) |
||||
.Write(userName) |
||||
.UseColorOnce(_.color.TextFailure) |
||||
.Write(P(" doesn't belong to the group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.WriteLine(P("!")); |
||||
} |
||||
} |
||||
|
||||
private function RemovePlayers(BaseText groupName, ArrayList players) |
||||
{ |
||||
local int i; |
||||
local Text playerName; |
||||
local EPlayer nextPlayer; |
||||
local UserID nextID; |
||||
|
||||
if (groupName == none) return; |
||||
if (players == none) return; |
||||
if (!ValidateGroupExistence(groupName)) return; |
||||
|
||||
for (i = 0; i < players.GetLength(); i += 1) |
||||
{ |
||||
nextPlayer = EPlayer(players.GetItem(i)); |
||||
if (nextPlayer == none) { |
||||
continue; |
||||
} |
||||
playerName = nextPlayer.GetName(); |
||||
nextID = nextPlayer.GetUserID(); |
||||
if (!_.users.IsUserIDInGroup(nextID, groupName)) |
||||
{ |
||||
callerConsole |
||||
.Write(P("Player ")) |
||||
.UseColorOnce(_.color.Gray) |
||||
.Write(playerName) |
||||
.Write(F(" {$TextFailure doesn't belong} to the group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.WriteLine(P("!")); |
||||
} |
||||
else { |
||||
TryRemovingUserID(groupName, nextID, playerName); |
||||
} |
||||
_.memory.Free(nextPlayer); |
||||
_.memory.Free(playerName); |
||||
_.memory.Free(nextID); |
||||
nextPlayer = none; |
||||
playerName = none; |
||||
nextID = none; |
||||
} |
||||
} |
||||
|
||||
private function AddGroup(BaseText groupName) |
||||
{ |
||||
if (_.users.IsGroupExisting(groupName)) |
||||
{ |
||||
callerConsole |
||||
.Write(P("Group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.UseColorOnce(_.color.TextNegative) |
||||
.Write(P(" already exists")) |
||||
.WriteLine(P("!")); |
||||
return; |
||||
} |
||||
if (_.users.AddGroup(groupName)) |
||||
{ |
||||
callerConsole |
||||
.Write(P("Group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.UseColorOnce(_.color.TextPositive) |
||||
.Write(P(" was added")) |
||||
.WriteLine(P("!")); |
||||
} |
||||
else |
||||
{ |
||||
callerConsole |
||||
.UseColorOnce(_.color.TextFailure) |
||||
.Write(P("Cannot add")) |
||||
.Write(P(" group with a name ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.WriteLine(P(" for unknown reason.")); |
||||
} |
||||
} |
||||
|
||||
private function RemoveGroup(BaseText groupName) |
||||
{ |
||||
if (!_.users.IsGroupExisting(groupName)) |
||||
{ |
||||
callerConsole |
||||
.Write(P("Group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.UseColorOnce(_.color.TextNegative) |
||||
.Write(P(" doesn't exists")) |
||||
.WriteLine(P("!")); |
||||
return; |
||||
} |
||||
if (_.users.RemoveGroup(groupName)) |
||||
{ |
||||
callerConsole |
||||
.Write(P("Group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.UseColorOnce(_.color.TextPositive) |
||||
.Write(P(" was removed")) |
||||
.WriteLine(P("!")); |
||||
} |
||||
else |
||||
{ |
||||
callerConsole |
||||
.UseColorOnce(_.color.TextFailure) |
||||
.Write(P("Cannot remove")) |
||||
.Write(P(" group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(groupName) |
||||
.WriteLine(P(" for unknown reason.")); |
||||
} |
||||
} |
||||
|
||||
private function DisplayUserGroups() |
||||
{ |
||||
local int i; |
||||
local array<Text> availableGroups; |
||||
|
||||
if (!ValidateUsersFeature()) { |
||||
return; |
||||
} |
||||
availableGroups = _.users.GetAvailableGroups(); |
||||
if (availableGroups.length <= 0) |
||||
{ |
||||
callerConsole.WriteLine(F("{$TextNegative No user groups}" |
||||
@ "currently available.")); |
||||
return; |
||||
} |
||||
callerConsole |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(P("Available user groups")) |
||||
.Write(P(": ")); |
||||
for (i = 0; i < availableGroups.length; i += 1) |
||||
{ |
||||
if (i > 0) { |
||||
callerConsole.Write(P(", ")); |
||||
} |
||||
callerConsole.Write(availableGroups[i]); |
||||
} |
||||
callerConsole.Flush(); |
||||
_.memory.FreeMany(availableGroups); |
||||
} |
||||
|
||||
private function bool ValidateUsersFeature() |
||||
{ |
||||
if (class'Users_Feature'.static.IsEnabled()) { |
||||
return true; |
||||
} |
||||
callerConsole |
||||
.UseColorOnce(_.color.TextFailure) |
||||
.WriteLine(P("`Users_Feature` is currently disabled.")); |
||||
return false; |
||||
} |
||||
|
||||
private function bool IsGroupSpecified( |
||||
ArrayList specifiedGroups, |
||||
BaseText groupToCheck) |
||||
{ |
||||
local int i; |
||||
local int length; |
||||
local Text nextGroup; |
||||
|
||||
if (groupToCheck == none) return false; |
||||
if (specifiedGroups == none) return true; |
||||
length = groupToCheck.GetLength(); |
||||
if (length <= 0) return true; |
||||
|
||||
for (i = 0; i < length; i += 1) |
||||
{ |
||||
nextGroup = specifiedGroups.GetText(i); |
||||
if (groupToCheck.Compare(nextGroup, SCASE_INSENSITIVE)) |
||||
{ |
||||
nextGroup.FreeSelf(); |
||||
return true; |
||||
} |
||||
_.memory.Free(nextGroup); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private function DisplayUserGroupsWithUsers(ArrayList specifiedGroups) |
||||
{ |
||||
local int i; |
||||
local bool displayedGroup; |
||||
local array<Text> availableGroups; |
||||
|
||||
if (!ValidateUsersFeature()) { |
||||
return; |
||||
} |
||||
availableGroups = _.users.GetAvailableGroups(); |
||||
if (availableGroups.length <= 0) |
||||
{ |
||||
callerConsole.WriteLine(F("{$TextNegative No user groups}" |
||||
@ "currently available.")); |
||||
return; |
||||
} |
||||
for (i = 0; i < availableGroups.length; i += 1) |
||||
{ |
||||
if (IsGroupSpecified(specifiedGroups, availableGroups[i])) |
||||
{ |
||||
displayedGroup = true; |
||||
callerConsole |
||||
.Write(P("User group ")) |
||||
.UseColorOnce(_.color.TextEmphasis) |
||||
.Write(availableGroups[i]) |
||||
.WriteLine(P(":")); |
||||
DisplayUsersFor(availableGroups[i]); |
||||
} |
||||
} |
||||
callerConsole.Flush(); |
||||
_.memory.FreeMany(availableGroups); |
||||
if (!displayedGroup && specifiedGroups != none) { |
||||
callerConsole.WriteLine(F("{$TextFailure No valid groups} specified!")); |
||||
} |
||||
} |
||||
|
||||
private function DisplayUsersFor(Text groupName) |
||||
{ |
||||
local int i; |
||||
local Text nextID; |
||||
local array<Users_Feature.AnnotatedUserID> annotatedUsers; |
||||
|
||||
annotatedUsers = _.users.GetAnnotatedGroupMembers(groupName); |
||||
if (annotatedUsers.length <= 0) |
||||
{ |
||||
callerConsole.WriteBlock(P("No users")); |
||||
return; |
||||
} |
||||
for (i = 0; i < annotatedUsers.length; i += 1) |
||||
{ |
||||
if (annotatedUsers[i].id == none) { |
||||
continue; |
||||
} |
||||
nextID = annotatedUsers[i].id.GetUniqueID(); |
||||
if (annotatedUsers[i].annotation != none) |
||||
{ |
||||
callerConsole |
||||
.Write(nextID) |
||||
.UseColorOnce(_.color.TextNeutral) |
||||
.Write(P(" aka ")) |
||||
.WriteBlock(annotatedUsers[i].annotation); |
||||
} |
||||
else { |
||||
callerConsole.WriteBlock(nextID); |
||||
} |
||||
_.memory.Free(nextID); |
||||
} |
||||
for (i = 0; i < annotatedUsers.length; i += 1) |
||||
{ |
||||
_.memory.Free(annotatedUsers[i].id); |
||||
_.memory.Free(annotatedUsers[i].annotation); |
||||
} |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -0,0 +1,40 @@
|
||||
/** |
||||
* Signal class for `PersistentDataManager`'s `OnPersistentDataReady()` signal. |
||||
* Copyright 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 PersistentDataManager_OnPersistentDataReady_Signal extends Signal |
||||
dependson(DBConnection); |
||||
|
||||
public final function Emit(UserID id, bool online) |
||||
{ |
||||
local Slot nextSlot; |
||||
StartIterating(); |
||||
nextSlot = GetNextSlot(); |
||||
while (nextSlot != none) |
||||
{ |
||||
PersistentDataManager_OnPersistentDataReady_Slot(nextSlot) |
||||
.connect(id, online); |
||||
nextSlot = GetNextSlot(); |
||||
} |
||||
CleanEmptySlots(); |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
relatedSlotClass = class'PersistentDataManager_OnPersistentDataReady_Slot' |
||||
} |
@ -0,0 +1,41 @@
|
||||
/** |
||||
* Slot class for `PersistentDataManager`'s `OnPersistentDataReady()` signal. |
||||
* Copyright 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 PersistentDataManager_OnPersistentDataReady_Slot extends Slot |
||||
dependson(DBConnection); |
||||
|
||||
delegate connect(UserID id, bool online) |
||||
{ |
||||
DummyCall(); |
||||
} |
||||
|
||||
protected function Constructor() |
||||
{ |
||||
connect = none; |
||||
} |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
super.Finalizer(); |
||||
connect = none; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -0,0 +1,407 @@
|
||||
/** |
||||
* This tool is for simplifying writing and reading persistent user data. |
||||
* All it requires is a setup of database + json pointer to data and it will |
||||
* take care of data caching and database connection. |
||||
* Copyright 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 PersistentDataManager extends AcediaObject; |
||||
|
||||
/** |
||||
* # `PersistentDataManager` |
||||
* |
||||
* This tool is for simplifying writing and reading persistent user data. |
||||
* All it requires is a setup of database + json pointer to data and it will |
||||
* take care of data caching and database connection. |
||||
* |
||||
* ## Usage |
||||
* |
||||
* Create an instance and use `Setup()` to connect to the database with |
||||
* persistent data. You can use `Setup()` again on the same object to setup |
||||
* a different database as a source. All data will be automatically reloaded. |
||||
* After that you can use `GetPersistentData()`/`SetPersistentData()` to |
||||
* read/write persistent data for the particular user. |
||||
* Since loading data from the database takes time, you don't have an |
||||
* immediate access to it. |
||||
* But you can use `_.users.OnPersistentDataAvailable()` signal to track |
||||
* whenever new user data from database becomes available. However, you can |
||||
* start writing persistent data (and reading what you've wrote) at any time it |
||||
* - these changes will be reapplied whenever data is actually loaded from |
||||
* database. |
||||
* |
||||
* ## Implementation |
||||
* |
||||
* Implementation consists of simply creating `DBConnection` for every user |
||||
* and storing them in the `HashTable` that maps user IDs into those |
||||
* `DBConnection`s. |
||||
* We also maintain a reverse map to figure out what `DBConnection` belongs |
||||
* to what user when connection signals an update. We borrow the signal that |
||||
* `UsersAPI` provides to inform everyone interested about which users |
||||
* have updated. |
||||
*/ |
||||
|
||||
var private bool initialized; |
||||
var private Database database; |
||||
var private JSONPointer rootPointer; |
||||
var private HashTable userToConnection, connectionToUser; |
||||
|
||||
var private PersistentDataManager_OnPersistentDataReady_Signal onPersistentDataReadySignal; |
||||
|
||||
protected function Constructor() |
||||
{ |
||||
_.players.OnNewPlayer(self).connect = ConnectPersistentData; |
||||
onPersistentDataReadySignal = _.users._getOnReadySignal(); |
||||
} |
||||
|
||||
protected function Finalizer() |
||||
{ |
||||
Reset(); |
||||
_.players.OnNewPlayer(self).Disconnect(); |
||||
} |
||||
|
||||
private final function Reset() |
||||
{ |
||||
_.memory.Free(database); |
||||
_.memory.Free(rootPointer); |
||||
_.memory.Free(userToConnection); |
||||
_.memory.Free(connectionToUser); |
||||
_.memory.Free(onPersistentDataReadySignal); |
||||
database = none; |
||||
rootPointer = none; |
||||
userToConnection = none; |
||||
connectionToUser = none; |
||||
onPersistentDataReadySignal = none; |
||||
initialized = false; |
||||
} |
||||
|
||||
/** |
||||
* Sets up database and location inside it as a source of users' persistent |
||||
* data. |
||||
* |
||||
* Must be successfully called at least once for the caller |
||||
* `PersistentDataManager` to be usable. |
||||
* |
||||
* @param db Database inside which persistent data is stored. |
||||
* @param location Location inside specified database to the root of |
||||
* persistent data. |
||||
* @return `true` if setup was successful (requires both arguments to be not |
||||
* `none`) and `false` otherwise. |
||||
*/ |
||||
public final function bool Setup(Database db, JSONPointer location) |
||||
{ |
||||
if (db == none) return false; |
||||
if (location == none) return false; |
||||
|
||||
Reset(); |
||||
database = db; |
||||
database.NewRef(); |
||||
rootPointer = location.Copy(); |
||||
userToConnection = _.collections.EmptyHashTable(); |
||||
connectionToUser = _.collections.EmptyHashTable(); |
||||
// Using `userToConnection` as an empty hash table, not related to its |
||||
// actual meaning |
||||
database.IncrementData(location, userToConnection); |
||||
initialized = true; |
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Reads specified named persistent data for the specified group. |
||||
* |
||||
* @param id ID of the user to read persistent data from. |
||||
* @param groupName Group to which this persistent data belongs to. |
||||
* Groups are used as namespaces to avoid duplicate persistent variables |
||||
* between mods. If your mod needs several subgroups, its recommended to |
||||
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and |
||||
* "MyAwesomeMod.enemies". |
||||
* @param dataName Name of persistent data variable to read inside |
||||
* `groupName` persistent data group. Not `none` value must be provided. |
||||
* @param data Data to set as persistent value. Must be |
||||
* JSON-compatible. If `none` is passed, returns the all data for |
||||
* the given group. |
||||
* @return Data read from the persistent variable. `none` in case of any kind |
||||
* of failure. |
||||
*/ |
||||
public final function AcediaObject GetPersistentData( |
||||
UserID id, |
||||
BaseText groupName, |
||||
optional BaseText dataName) |
||||
{ |
||||
local AcediaObject result; |
||||
local Text textID; |
||||
local JSONPointer location; |
||||
local DBConnection relevantConnection; |
||||
|
||||
if (!initialized) return none; |
||||
if (id == none) return none; |
||||
if (groupName == none) return none; |
||||
|
||||
textID = id.GetUniqueID(); |
||||
relevantConnection = DBConnection(userToConnection.GetItem(textID)); |
||||
textID.FreeSelf(); |
||||
if (relevantConnection != none) |
||||
{ |
||||
location = _.json.Pointer(); |
||||
location.Push(groupName); |
||||
if (dataName != none) { |
||||
location.Push(dataName); |
||||
} |
||||
result = relevantConnection.ReadDataByJSON(location); |
||||
relevantConnection.FreeSelf(); |
||||
location.FreeSelf(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Writes specified named persistent data for the specified group. |
||||
* |
||||
* @param id ID of the user to change persistent data of. |
||||
* @param groupName Group to which this persistent data belongs to. |
||||
* Groups are used as namespaces to avoid duplicate persistent variables |
||||
* between mods. If your mod needs several subgroups, its recommended to |
||||
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and |
||||
* "MyAwesomeMod.enemies". |
||||
* @param dataName Name of persistent data variable to change inside |
||||
* `groupName` persistent data group. |
||||
* @param data Data to set as persistent value. Must be |
||||
* JSON-compatible. |
||||
* @return `true` if change succeeded in local cached version of database with |
||||
* persistent values and `false` otherwise. Such local changes can |
||||
* potentially be not applied to the actual database. But successful local |
||||
* changes should persist for the game session. |
||||
*/ |
||||
public final function bool WritePersistentData( |
||||
UserID id, |
||||
BaseText groupName, |
||||
BaseText dataName, |
||||
AcediaObject data) |
||||
{ |
||||
local bool result; |
||||
local Text textID; |
||||
local JSONPointer location; |
||||
local DBConnection relevantConnection; |
||||
local HashTable emptyObject; |
||||
|
||||
if (!initialized) return false; |
||||
if (id == none) return false; |
||||
if (groupName == none) return false; |
||||
if (dataName == none) return false; |
||||
|
||||
textID = id.GetUniqueID(); |
||||
relevantConnection = DBConnection(userToConnection.GetItem(textID)); |
||||
textID.FreeSelf(); |
||||
if (relevantConnection != none) |
||||
{ |
||||
emptyObject = _.collections.EmptyHashTable(); |
||||
location = _.json.Pointer(); |
||||
location.Push(groupName); |
||||
relevantConnection.IncrementDataByJSON(location, emptyObject); |
||||
location.Push(dataName); |
||||
result = relevantConnection.WriteDataByJSON(location, data); |
||||
relevantConnection.FreeSelf(); |
||||
location.FreeSelf(); |
||||
emptyObject.FreeSelf(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Increments specified named persistent data for the specified group. |
||||
* |
||||
* @param id ID of the user to change persistent data of. |
||||
* @param groupName Group to which this persistent data belongs to. |
||||
* Groups are used as namespaces to avoid duplicate persistent variables |
||||
* between mods. If your mod needs several subgroups, its recommended to |
||||
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and |
||||
* "MyAwesomeMod.enemies". |
||||
* @param dataName Name of persistent data variable to change inside |
||||
* `groupName` persistent data group. |
||||
* @param data Data by which to increment existing persistent value. |
||||
* Must be JSON-compatible. |
||||
* @return `true` if change succeeded in local cached version of database with |
||||
* persistent values and `false` otherwise. Such local changes can |
||||
* potentially be not applied to the actual database. But successful local |
||||
* changes should persist for the game session. |
||||
*/ |
||||
public final function bool IncrementPersistentData( |
||||
UserID id, |
||||
BaseText groupName, |
||||
BaseText dataName, |
||||
AcediaObject data) |
||||
{ |
||||
local bool result; |
||||
local Text textID; |
||||
local JSONPointer location; |
||||
local DBConnection relevantConnection; |
||||
|
||||
if (!initialized) return false; |
||||
if (id == none) return false; |
||||
if (groupName == none) return false; |
||||
if (dataName == none) return false; |
||||
|
||||
textID = id.GetUniqueID(); |
||||
relevantConnection = DBConnection(userToConnection.GetItem(textID)); |
||||
textID.FreeSelf(); |
||||
if (relevantConnection != none) |
||||
{ |
||||
location = _.json.Pointer(); |
||||
location.Push(groupName).Push(dataName); |
||||
result = relevantConnection.IncrementDataByJSON(location, data); |
||||
relevantConnection.FreeSelf(); |
||||
location.FreeSelf(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Removes specified named persistent data for the specified group. |
||||
* |
||||
* @param id ID of the user to remove persistent data of. |
||||
* @param groupName Group to which this persistent data belongs to. |
||||
* Groups are used as namespaces to avoid duplicate persistent variables |
||||
* between mods. If your mod needs several subgroups, its recommended to |
||||
* use the same prefix for them, e.g. "MyAwesomeMod.economy" and |
||||
* "MyAwesomeMod.enemies". |
||||
* @param dataName Name of persistent data variable to remove inside |
||||
* `groupName` persistent data group. |
||||
* @return `true` if removal succeeded in local cached version of database with |
||||
* persistent values and `false` otherwise. Such local changes can |
||||
* potentially be not applied to the actual database. But successful local |
||||
* changes should persist for the game session. |
||||
*/ |
||||
public final function bool RemovePersistentData( |
||||
UserID id, |
||||
BaseText groupName, |
||||
BaseText dataName) |
||||
{ |
||||
local bool result; |
||||
local Text textID; |
||||
local JSONPointer location; |
||||
local DBConnection relevantConnection; |
||||
|
||||
if (!initialized) return false; |
||||
if (id == none) return false; |
||||
if (groupName == none) return false; |
||||
if (dataName == none) return false; |
||||
|
||||
textID = id.GetUniqueID(); |
||||
relevantConnection = DBConnection(userToConnection.GetItem(textID)); |
||||
textID.FreeSelf(); |
||||
if (relevantConnection != none) |
||||
{ |
||||
location = _.json.Pointer(); |
||||
location.Push(groupName).Push(dataName); |
||||
result = relevantConnection.RemoveDataByJSON(location); |
||||
relevantConnection.FreeSelf(); |
||||
location.FreeSelf(); |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Connects and starts synchronizing persistent data for the given player. |
||||
* |
||||
* @param player Player to synchronize persistent data for. |
||||
*/ |
||||
public final function ConnectPersistentData(EPlayer player) |
||||
{ |
||||
local UserID playerID; |
||||
|
||||
if (initialized && player != none) |
||||
{ |
||||
playerID = player.GetUserID(); |
||||
ConnectPersistentDataByID(playerID); |
||||
_.memory.Free(playerID); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Connects and starts synchronizing persistent data for the player given by |
||||
* their ID. |
||||
* |
||||
* @param id User ID for which to synchronize persistent data from |
||||
* the database. |
||||
*/ |
||||
public final function ConnectPersistentDataByID(UserID id) |
||||
{ |
||||
local Text textID; |
||||
local DBConnection newConnection; |
||||
|
||||
if (!initialized) return; |
||||
if (id == none) return; |
||||
|
||||
textID = id.GetUniqueID(); |
||||
if (userToConnection.HasKey(textID)) |
||||
{ |
||||
_.memory.Free(textID); |
||||
return; |
||||
} |
||||
rootPointer.Push(textID); |
||||
newConnection = DBConnection(_.memory.Allocate(class'DBConnection')); |
||||
newConnection.Initialize(database, rootPointer); |
||||
_.memory.Free(rootPointer.Pop()); |
||||
newConnection.Connect(); |
||||
userToConnection.SetItem(textID, newConnection); |
||||
connectionToUser.SetItem(newConnection, textID); |
||||
newConnection.OnStateChanged(self).connect = UserUpdated; |
||||
textID.FreeSelf(); |
||||
newConnection.FreeSelf(); |
||||
} |
||||
|
||||
private final function UserUpdated( |
||||
DBConnection instance, |
||||
DBConnection.DBConnectionState oldState, |
||||
DBConnection.DBConnectionState newState) |
||||
{ |
||||
local UserID id; |
||||
|
||||
if (!initialized) return; |
||||
if (newState == DBCS_Connecting) return; |
||||
if (onPersistentDataReadySignal == none) return; |
||||
if (!onPersistentDataReadySignal.IsAllocated()) return; |
||||
|
||||
id = UserID(connectionToUser.GetItem(instance)); |
||||
if (id != none) |
||||
{ |
||||
onPersistentDataReadySignal.Emit(id, newState == DBCS_Connected); |
||||
id.FreeSelf(); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Attempts to start persistent data synchronization for all players currently |
||||
* on the server. |
||||
*/ |
||||
public final function LoadCurrentPlayers() |
||||
{ |
||||
local int i; |
||||
local array<EPlayer> currentPlayers; |
||||
|
||||
if (initialized) |
||||
{ |
||||
currentPlayers = _.players.GetAll(); |
||||
for (i = 0; i < currentPlayers.length; i += 1) { |
||||
ConnectPersistentData(currentPlayers[i]); |
||||
} |
||||
_.memory.FreeMany(currentPlayers); |
||||
} |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
} |
@ -0,0 +1,70 @@
|
||||
/** |
||||
* Acedia's class for defining user group in config files. |
||||
* Copyright 2022 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 UserGroup extends AcediaConfig |
||||
perobjectconfig |
||||
config(AcediaUsers); |
||||
|
||||
var public config array<string> user; |
||||
|
||||
protected function HashTable ToData() |
||||
{ |
||||
local int i; |
||||
local HashTable data; |
||||
local ArrayList wrappedUserArray; |
||||
|
||||
data = __().collections.EmptyHashTable(); |
||||
wrappedUserArray = __().collections.EmptyArrayList(); |
||||
for (i = 0; i < user.length; i += 1) { |
||||
wrappedUserArray.AddString(user[i]); |
||||
} |
||||
data.SetItem(P("user"), wrappedUserArray); |
||||
wrappedUserArray.FreeSelf(); |
||||
return data; |
||||
} |
||||
|
||||
protected function FromData(HashTable source) |
||||
{ |
||||
local int i; |
||||
local ArrayList wrappedUserArray; |
||||
|
||||
DefaultIt(); |
||||
if (source == none) { |
||||
return; |
||||
} |
||||
wrappedUserArray = source.GetArrayList(P("user")); |
||||
if (wrappedUserArray == none) { |
||||
return; |
||||
} |
||||
for (i = 0; i < wrappedUserArray.GetLength(); i += 1) { |
||||
user[user.length] = wrappedUserArray.GetString(i); |
||||
} |
||||
wrappedUserArray.FreeSelf(); |
||||
} |
||||
|
||||
protected function DefaultIt() |
||||
{ |
||||
user.length = 0; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
configName = "AcediaUsers" |
||||
supportsDataConversion = true |
||||
} |
@ -0,0 +1,99 @@
|
||||
/** |
||||
* Config object for `Users_Feature`. |
||||
* Copyright 2022 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 Users extends FeatureConfig |
||||
perobjectconfig |
||||
config(AcediaUsers); |
||||
|
||||
var public config bool usePersistentData; |
||||
var public config string persistentDataDatabaseLink; |
||||
var public config bool useDatabaseForGroupsData; |
||||
var public config string groupsDatabaseLink; |
||||
var public config array<string> localUserGroup; |
||||
|
||||
protected function HashTable ToData() |
||||
{ |
||||
local int i; |
||||
local HashTable data; |
||||
local ArrayList userGroupList; |
||||
|
||||
data = __().collections.EmptyHashTable(); |
||||
data.SetBool(P("usePersistentData"), usePersistentData); |
||||
data.SetString(P("persistentDataDatabaseLink"), persistentDataDatabaseLink); |
||||
data.SetBool(P("useDatabaseForGroupsData"), useDatabaseForGroupsData); |
||||
data.SetString(P("groupsDatabaseLink"), groupsDatabaseLink); |
||||
userGroupList = _.collections.EmptyArrayList(); |
||||
for (i = 0; i < localUserGroup.length; i += 1) { |
||||
userGroupList.AddString(localUserGroup[i]); |
||||
} |
||||
data.SetItem(P("userGroups"), userGroupList); |
||||
userGroupList.FreeSelf(); |
||||
return data; |
||||
} |
||||
|
||||
protected function FromData(HashTable source) |
||||
{ |
||||
local int i; |
||||
local ArrayList userGroupList; |
||||
|
||||
if (source == none) { |
||||
return; |
||||
} |
||||
usePersistentData = source.GetBool(P("usePersistentData")); |
||||
persistentDataDatabaseLink = source.GetString( |
||||
P("persistentDataDatabaseLink"), |
||||
"[local]database:/persistent_data"); |
||||
useDatabaseForGroupsData = source.GetBool(P("useDatabaseForGroupsData")); |
||||
groupsDatabaseLink = source.GetString( |
||||
P("groupsDatabaseLink"), |
||||
"[local]database:/groups_data"); |
||||
userGroupList = source.GetArrayList(P("userGroups")); |
||||
localUserGroup.length = 0; |
||||
if (userGroupList == none) { |
||||
return; |
||||
} |
||||
for (i = 0; i < userGroupList.GetLength(); i += 1) { |
||||
localUserGroup[localUserGroup.length] = userGroupList.GetString(i); |
||||
} |
||||
userGroupList.FreeSelf(); |
||||
} |
||||
|
||||
protected function DefaultIt() |
||||
{ |
||||
usePersistentData = false; |
||||
persistentDataDatabaseLink = "[local]database:/persistent_data"; |
||||
useDatabaseForGroupsData = false; |
||||
groupsDatabaseLink = "[local]database:/groups_data"; |
||||
localUserGroup.length = 0; |
||||
localUserGroup[0] = "admin"; |
||||
localUserGroup[1] = "moderator"; |
||||
localUserGroup[2] = "trusted"; |
||||
} |
||||
|
||||
defaultproperties |
||||
{ |
||||
configName = "AcediaUsers" |
||||
usePersistentData = false |
||||
persistentDataDatabaseLink = "[local]database:/persistent_data" |
||||
useDatabaseForGroupsData = false |
||||
groupsDatabaseLink = "[local]database:/groups_data" |
||||
localUserGroup(0) = "admin" |
||||
localUserGroup(1) = "moderator" |
||||
localUserGroup(2) = "trusted" |
||||
} |
Loading…
Reference in new issue