Move most files to other Acedia packages
This commit is contained in:
parent
e513749119
commit
c42edfb88f
93 changed files with 85 additions and 20255 deletions
|
|
@ -1,45 +0,0 @@
|
|||
/**
|
||||
* Actor base class to be used to Acedia instead of an `Actor`.
|
||||
* The only difference is defined `_` member that provides convenient access to
|
||||
* Acedia's API.
|
||||
* It isn't guaranteed that `default._` will be defined for `AcediaActor`s.
|
||||
* Copyright 2020 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 AcediaActor extends Actor
|
||||
abstract;
|
||||
|
||||
var protected Global _;
|
||||
|
||||
public final function Text T(string string)
|
||||
{
|
||||
return _.text.FromString(string);
|
||||
}
|
||||
|
||||
event PreBeginPlay()
|
||||
{
|
||||
super.PreBeginPlay();
|
||||
if (_ == none)
|
||||
{
|
||||
_ = Global(class'Global'.static.GetInstance());
|
||||
default._ = _;
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
/**
|
||||
* Object base class to be used to Acedia instead of an `Object`.
|
||||
* The only difference is defined `_` member that provides convenient access to
|
||||
* Acedia's API.
|
||||
* Since `Global` is an actor, we wish to avoid storing it's instance in
|
||||
* the object because it can mess with garbage collection on level change.
|
||||
* So we provide an accessor function `_()` instead.
|
||||
* Copyright 2020 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 AcediaObject extends Object
|
||||
abstract;
|
||||
|
||||
public final function Text T(string string)
|
||||
{
|
||||
return _().text.FromString(string);
|
||||
}
|
||||
|
||||
public static final function Global _()
|
||||
{
|
||||
return Global(class'Global'.static.GetInstance());
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
/**
|
||||
* Facilitates some core replicated functions between client and server.
|
||||
* Copyright 2019 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 AcediaReplicationInfo extends ReplicationInfo;
|
||||
|
||||
var public PlayerController linkOwner;
|
||||
|
||||
replication
|
||||
{
|
||||
reliable if (role == ROLE_Authority)
|
||||
linkOwner;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
/**
|
||||
* A class, implementing a hash-table-based dictionary for quick access to
|
||||
* aliases' values.
|
||||
* It does not support dynamic hash table capacity change and
|
||||
* requires to set the size upfront.
|
||||
* Copyright 2020 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 AliasHash extends AcediaObject
|
||||
dependson(AliasSource)
|
||||
config(AcediaSystem);
|
||||
|
||||
// Reasonable lower and upper limits on hash table capacity,
|
||||
// that will be enforced if user requires something outside those bounds
|
||||
var private config const int MINIMUM_CAPACITY;
|
||||
var private config const int MAXIMUM_CAPACITY;
|
||||
|
||||
// Bucket of alias-value pairs, with the same alias hash.
|
||||
struct PairBucket
|
||||
{
|
||||
var array<AliasSource.AliasValuePair> pairs;
|
||||
};
|
||||
var private array<PairBucket> hashTable;
|
||||
|
||||
/**
|
||||
* Initializes caller `AliasHash`.
|
||||
*
|
||||
* Calling this function again will clear all existing data and will create
|
||||
* a brand new hash table.
|
||||
*
|
||||
* @param desiredCapacity Desired capacity of the underlying hash table.
|
||||
* Will be clamped between `MINIMUM_CAPACITY` and `MAXIMUM_CAPACITY`.
|
||||
* Not specifying anything as this parameter creates a hash table of
|
||||
* size `MINIMUM_CAPACITY`.
|
||||
* @return A reference to a caller object to allow for function chaining.
|
||||
*/
|
||||
public final function AliasHash Initialize(optional int desiredCapacity)
|
||||
{
|
||||
desiredCapacity = Clamp(desiredCapacity, MINIMUM_CAPACITY,
|
||||
MAXIMUM_CAPACITY);
|
||||
hashTable.length = 0;
|
||||
hashTable.length = desiredCapacity;
|
||||
return self;
|
||||
}
|
||||
|
||||
// Helper method that is needed as a replacement for `%`, since it is
|
||||
// an operation on `float`s in UnrealScript and does not have enough precision
|
||||
// to work with hashes.
|
||||
// Assumes positive input.
|
||||
private function int Remainder(int number, int divisor)
|
||||
{
|
||||
local int quotient;
|
||||
quotient = number / divisor;
|
||||
return (number - quotient * divisor);
|
||||
}
|
||||
|
||||
// Finds indices for:
|
||||
// 1. Bucked that contains specified alias (`bucketIndex`);
|
||||
// 2. Pair for specified alias in the bucket's collection (`pairIndex`).
|
||||
// `bucketIndex` is always found,
|
||||
// `pairIndex` is valid iff method returns `true`.
|
||||
private final function bool FindPairIndices(
|
||||
string alias,
|
||||
out int bucketIndex,
|
||||
out int pairIndex)
|
||||
{
|
||||
local int i;
|
||||
local array<AliasSource.AliasValuePair> bucketPairs;
|
||||
// `Locs()` is used because aliases are case-insensitive.
|
||||
bucketIndex = _().text.GetHash(Locs(alias));
|
||||
if (bucketIndex < 0) {
|
||||
bucketIndex *= -1;
|
||||
}
|
||||
bucketIndex = Remainder(bucketIndex, hashTable.length);
|
||||
// Check if bucket actually has given alias.
|
||||
bucketPairs = hashTable[bucketIndex].pairs;
|
||||
for (i = 0; i < bucketPairs.length; i += 1)
|
||||
{
|
||||
if (bucketPairs[i].alias ~= alias)
|
||||
{
|
||||
pairIndex = i;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a value for a given alias.
|
||||
*
|
||||
* @param alias Alias for which we need to find a value.
|
||||
* Aliases are case-insensitive.
|
||||
* @param value If given alias is present in caller `AliasHash`, -
|
||||
* it's value will be written in this variable.
|
||||
* Otherwise value is undefined.
|
||||
* @return `true` if we found value, `false` otherwise.
|
||||
*/
|
||||
public final function bool Find(string alias, out string value)
|
||||
{
|
||||
local int bucketIndex;
|
||||
local int pairIndex;
|
||||
if (FindPairIndices(alias, bucketIndex, pairIndex))
|
||||
{
|
||||
value = hashTable[bucketIndex].pairs[pairIndex].value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if caller `AliasHash` contains given alias.
|
||||
*
|
||||
* @param alias Alias to check for belonging to caller `AliasHash`.
|
||||
* Aliases are case-insensitive.
|
||||
* @return `true` if caller `AliasHash` contains the value for a given alias
|
||||
* and `false` otherwise.
|
||||
*/
|
||||
public final function bool Contains(string alias)
|
||||
{
|
||||
local int bucketIndex;
|
||||
local int pairIndex;
|
||||
return FindPairIndices(alias, bucketIndex, pairIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts new record for alias `alias` for value of `value`.
|
||||
*
|
||||
* If there is already a value for a given `alias` - it will be overwritten.
|
||||
*
|
||||
* @param alias Alias to insert. Aliases are case-insensitive.
|
||||
* @param value Value for a given alias to store.
|
||||
* @return A reference to a caller object to allow for function chaining.
|
||||
*/
|
||||
public final function AliasHash Insert(string alias, string value)
|
||||
{
|
||||
local int bucketIndex;
|
||||
local int pairIndex;
|
||||
local AliasSource.AliasValuePair newRecord;
|
||||
newRecord.value = value;
|
||||
newRecord.alias = alias;
|
||||
if (!FindPairIndices(alias, bucketIndex, pairIndex)) {
|
||||
pairIndex = hashTable[bucketIndex].pairs.length;
|
||||
}
|
||||
hashTable[bucketIndex].pairs[pairIndex] = newRecord;
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts new record for alias `alias` for value of `value`.
|
||||
*
|
||||
* If there is already a value for a given `alias`, - new value will be
|
||||
* discarded and `AliasHash` will not be changed.
|
||||
*
|
||||
* @param alias Alias to insert. Aliases are case-insensitive.
|
||||
* @param value Value for a given alias to store.
|
||||
* @param existingValue Value that will correspond to a given alias after
|
||||
* this method's execution. If insertion was successful - given `value`,
|
||||
* otherwise (if there already was a record for an `alias`)
|
||||
* it will return value that already existed in caller `AliasHash`.
|
||||
* @return `true` if given alias-value pair was inserted and `false` otherwise.
|
||||
*/
|
||||
public final function bool InsertIfMissing(
|
||||
string alias,
|
||||
string value,
|
||||
out string existingValue)
|
||||
{
|
||||
local int bucketIndex;
|
||||
local int pairIndex;
|
||||
local AliasSource.AliasValuePair newRecord;
|
||||
newRecord.value = value;
|
||||
newRecord.alias = alias;
|
||||
existingValue = value;
|
||||
if (FindPairIndices(alias, bucketIndex, pairIndex)) {
|
||||
existingValue = hashTable[bucketIndex].pairs[pairIndex].value;
|
||||
return false;
|
||||
}
|
||||
pairIndex = hashTable[bucketIndex].pairs.length;
|
||||
hashTable[bucketIndex].pairs[pairIndex] = newRecord;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes record, corresponding to a given alias `alias`.
|
||||
*
|
||||
* @param alias Alias for which all records must be removed.
|
||||
* @return `true` if record was removed, `false` if id did not
|
||||
* (can only happen when `AliasHash` did not have any records for `alias`).
|
||||
*/
|
||||
public final function bool Remove(string alias)
|
||||
{
|
||||
local int bucketIndex;
|
||||
local int pairIndex;
|
||||
if (FindPairIndices(alias, bucketIndex, pairIndex)) {
|
||||
hashTable[bucketIndex].pairs.Remove(pairIndex, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
MINIMUM_CAPACITY = 10
|
||||
MAXIMUM_CAPACITY = 100000
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
/**
|
||||
* Service that handles pending saving of aliases data into configs.
|
||||
* Adding aliases into `AliasSource`s causes corresponding configs to update.
|
||||
* This service allows to delay and spread config rewrites over time,
|
||||
* which should help in case someone dynamically adds a lot of
|
||||
* different aliases.
|
||||
* Copyright 2020 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 AliasService extends Service
|
||||
config(AcediaSystem);
|
||||
|
||||
// Objects for which we are yet to write configs
|
||||
var private array<AliasSource> sourcesPendingToSave;
|
||||
var private array<Aliases> aliasesPendingToSave;
|
||||
// How often should we do it.
|
||||
// Negative or zero values would be reset to `0.05`.
|
||||
var public config const float saveInterval;
|
||||
|
||||
// To avoid creating yet another object for aliases system we will
|
||||
// keep config variable pointing to weapon, color, etc. `AliasSource`
|
||||
// subclasses here. It's not the best regarding separation of responsibility,
|
||||
// but should make config files less fragmented.
|
||||
// Changing these allows you to change in what sources `AliasesAPI`
|
||||
// looks for weapon and color aliases.
|
||||
var public config const class<AliasSource> weaponAliasesSource;
|
||||
var public config const class<AliasSource> colorAliasesSource;
|
||||
|
||||
protected function OnLaunch()
|
||||
{
|
||||
local float actualInterval;
|
||||
actualInterval = saveInterval;
|
||||
if (actualInterval <= 0)
|
||||
{
|
||||
actualInterval = 0.05;
|
||||
}
|
||||
SetTimer(actualInterval, true);
|
||||
}
|
||||
|
||||
protected function OnShutdown()
|
||||
{
|
||||
SaveAllPendingObjects();
|
||||
}
|
||||
|
||||
public final function PendingSaveSource(AliasSource sourceToSave)
|
||||
{
|
||||
local int i;
|
||||
if (sourceToSave == none) return;
|
||||
// Starting searching from the end of an array will make situations when
|
||||
// we add several aliases to a single source in a row more efficient.
|
||||
for (i = sourcesPendingToSave.length - 1;i >= 0; i -= 1) {
|
||||
if (sourcesPendingToSave[i] == sourceToSave) return;
|
||||
}
|
||||
sourcesPendingToSave[sourcesPendingToSave.length] = sourceToSave;
|
||||
}
|
||||
|
||||
public final function PendingSaveObject(Aliases objectToSave)
|
||||
{
|
||||
local int i;
|
||||
if (objectToSave == none) return;
|
||||
// Starting searching from the end of an array will make situations when
|
||||
// we add several aliases to a single `Aliases` object in a row
|
||||
// more efficient.
|
||||
for (i = aliasesPendingToSave.length - 1;i >= 0; i -= 1) {
|
||||
if (aliasesPendingToSave[i] == objectToSave) return;
|
||||
}
|
||||
aliasesPendingToSave[aliasesPendingToSave.length] = objectToSave;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces saving of the next object (either `AliasSource` or `Aliases`)
|
||||
* in queue to the config file.
|
||||
*
|
||||
* Does not reset the timer until next saving.
|
||||
*/
|
||||
private final function DoSaveNextPendingObject()
|
||||
{
|
||||
if (sourcesPendingToSave.length > 0)
|
||||
{
|
||||
if (sourcesPendingToSave[0] != none) {
|
||||
sourcesPendingToSave[0].SaveConfig();
|
||||
}
|
||||
sourcesPendingToSave.Remove(0, 1);
|
||||
return;
|
||||
}
|
||||
if (aliasesPendingToSave.length > 0)
|
||||
{
|
||||
aliasesPendingToSave[0].SaveOrClear();
|
||||
aliasesPendingToSave.Remove(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces saving of all objects (both `AliasSource`s or `Aliases`s) in queue
|
||||
* to their config files.
|
||||
*/
|
||||
private final function SaveAllPendingObjects()
|
||||
{
|
||||
local int i;
|
||||
for (i = 0; i < sourcesPendingToSave.length; i += 1) {
|
||||
if (sourcesPendingToSave[i] == none) continue;
|
||||
sourcesPendingToSave[i].SaveConfig();
|
||||
}
|
||||
for (i = 0; i < aliasesPendingToSave.length; i += 1) {
|
||||
aliasesPendingToSave[i].SaveOrClear();
|
||||
}
|
||||
sourcesPendingToSave.length = 0;
|
||||
aliasesPendingToSave.length = 0;
|
||||
}
|
||||
|
||||
event Timer()
|
||||
{
|
||||
DoSaveNextPendingObject();
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
saveInterval = 0.05
|
||||
weaponAliasesSource = class'WeaponAliasSource'
|
||||
colorAliasesSource = class'ColorAliasSource'
|
||||
}
|
||||
|
|
@ -1,379 +0,0 @@
|
|||
/**
|
||||
* Aliases allow users to define human-readable and easier to use
|
||||
* "synonyms" to some symbol sequences (mainly names of UnrealScript classes).
|
||||
* This class implements an alias database that stores aliases inside
|
||||
* standard config ini-files.
|
||||
* Several `AliasSource`s are supposed to exist separately, each storing
|
||||
* aliases of particular kind: for weapon, zeds, colors, etc..
|
||||
* Copyright 2020 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 AliasSource extends Singleton
|
||||
config(AcediaAliases);
|
||||
|
||||
// Name of the configurational file (without extension) where
|
||||
// this `AliasSource`'s data will be stored.
|
||||
var private const string configName;
|
||||
|
||||
// (Sub-)class of `Aliases` objects that this `AliasSource` uses to store
|
||||
// aliases in per-object-config manner.
|
||||
// Leaving this variable `none` will produce an `AliasSource` that can
|
||||
// only store aliases in form of `record=(alias="...",value="...")`.
|
||||
var public const class<Aliases> aliasesClass;
|
||||
// Storage for all objects of `aliasesClass` class in the config.
|
||||
// Exists after `OnCreated()` event and is maintained up-to-date at all times.
|
||||
var private array<Aliases> loadedAliasObjects;
|
||||
|
||||
// Links alias to a value.
|
||||
// An array of these structures (without duplicate `alias` records) defines
|
||||
// a function from the space of aliases to the space of values.
|
||||
struct AliasValuePair
|
||||
{
|
||||
var string alias;
|
||||
var string value;
|
||||
};
|
||||
// Aliases data for saving and loading on a disk (ini-file).
|
||||
// Name is chosen to make configurational files more readable.
|
||||
var private config array<AliasValuePair> record;
|
||||
// Hash table for a faster access to value by alias' name.
|
||||
// It contains same records as `record` array + aliases from
|
||||
// `loadedAliasObjects` objects when there are no duplicate aliases.
|
||||
// Otherwise only stores first loaded alias.
|
||||
var private AliasHash hash;
|
||||
|
||||
|
||||
// How many times bigger capacity of `hash` should be, compared to amount of
|
||||
// initially loaded data from a config.
|
||||
var private const float HASH_TABLE_SCALE;
|
||||
|
||||
// Load and hash all the data `AliasSource` creation.
|
||||
protected function OnCreated()
|
||||
{
|
||||
local int entriesAmount;
|
||||
if (!AssertAliasesClassIsOwnedByMe()) {
|
||||
return;
|
||||
}
|
||||
// Load and hash
|
||||
entriesAmount = LoadData();
|
||||
hash = AliasHash(_.memory.Allocate(class'AliasHash'));
|
||||
hash.Initialize(int(entriesAmount * HASH_TABLE_SCALE));
|
||||
HashValidAliases();
|
||||
}
|
||||
|
||||
// Ensures invariant of our `Aliases` class only belonging to us by
|
||||
// itself ourselves otherwise.
|
||||
private final function bool AssertAliasesClassIsOwnedByMe()
|
||||
{
|
||||
if (aliasesClass == none) return true;
|
||||
if (aliasesClass.default.sourceClass == class) return true;
|
||||
_.logger.Failure("`AliasSource`-`Aliases` class pair is incorrectly"
|
||||
@ "setup for source `" $ string(class) $ "`. Omitting it.");
|
||||
Destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
// This method loads all the defined aliases from the config file and
|
||||
// returns how many entries are there are total.
|
||||
// Does not change data, including fixing duplicates.
|
||||
private final function int LoadData()
|
||||
{
|
||||
local int i;
|
||||
local int entriesAmount;
|
||||
local array<string> objectNames;
|
||||
entriesAmount = record.length;
|
||||
if (aliasesClass == none) {
|
||||
return entriesAmount;
|
||||
}
|
||||
objectNames =
|
||||
GetPerObjectNames(configName, string(aliasesClass.name), MaxInt);
|
||||
loadedAliasObjects.length = objectNames.length;
|
||||
for (i = 0; i < objectNames.length; i += 1)
|
||||
{
|
||||
loadedAliasObjects[i] = new(none, objectNames[i]) aliasesClass;
|
||||
entriesAmount += loadedAliasObjects[i].GetAliases().length;
|
||||
}
|
||||
return entriesAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply checks if given alias is present in caller `AliasSource`.
|
||||
*
|
||||
* @param alias Alias to check, case-insensitive.
|
||||
* @return `true` if present, `false` otherwise.
|
||||
*/
|
||||
public function bool ContainsAlias(string alias)
|
||||
{
|
||||
return hash.Contains(alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to look up a value, stored for given alias in caller `AliasSource` and
|
||||
* reports error upon failure.
|
||||
*
|
||||
* Also see `Try()` method.
|
||||
*
|
||||
* @param alias Alias, for which method will attempt to look up a value.
|
||||
* Case-insensitive.
|
||||
* @param value If passed `alias` was recorded in caller `AliasSource`,
|
||||
* it's corresponding value will be written in this variable.
|
||||
* Otherwise value is undefined.
|
||||
* @return `true` if lookup was successful (alias present in 'AliasSource`)
|
||||
* and correct value was written into `value`, `false` otherwise.
|
||||
*/
|
||||
public function bool Resolve(string alias, out string value)
|
||||
{
|
||||
return hash.Find(alias, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to look up a value, stored for given alias in caller `AliasSource` and
|
||||
* silently returns given `alias` value upon failure.
|
||||
*
|
||||
* Also see `Resolve()` method.
|
||||
*
|
||||
* @param alias Alias, for which method will attempt to look up a value.
|
||||
* Case-insensitive.
|
||||
* @return Value corresponding to a given alias, if it was present in
|
||||
* caller `AliasSource` and value of `alias` parameter instead.
|
||||
*/
|
||||
public function string Try(string alias)
|
||||
{
|
||||
local string result;
|
||||
if (hash.Find(alias, result)) {
|
||||
return result;
|
||||
}
|
||||
return alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds another alias to the caller `AliasSource`.
|
||||
* If alias with the same name as `aliasToAdd` already exists, -
|
||||
* method overwrites it.
|
||||
*
|
||||
* Can fail iff `aliasToAdd` is an invalid alias.
|
||||
*
|
||||
* When adding alias to an object (`saveInObject == true`) alias `aliasToAdd`
|
||||
* will be altered by changing any ':' inside it into a '.'.
|
||||
* This is a necessary measure to allow storing class names in
|
||||
* config files via per-object-config.
|
||||
*
|
||||
* NOTE: This call will cause update of an ini-file. That update can be
|
||||
* slightly delayed, so do not make assumptions about it's immediacy.
|
||||
*
|
||||
* NOTE #2: Removing alias would require this method to go through the
|
||||
* whole `AliasSource` to remove possible duplicates.
|
||||
* This means that unless you can guarantee that there is no duplicates, -
|
||||
* performing a lot of alias additions during run-time can be costly.
|
||||
*
|
||||
* @param aliasToAdd Alias that you want to add to caller source.
|
||||
* Alias names are case-insensitive.
|
||||
* @param aliasValue Intended value of this alias.
|
||||
* @param saveInObject Setting this to `true` will make `AliasSource` save
|
||||
* given alias in per-object-config storage, while keeping it at default
|
||||
* `false` will just add alias to the `record=` storage.
|
||||
* If caller `AliasSource` does not support per-object-config storage, -
|
||||
* this flag will be ignores.
|
||||
* @return `true` if alias was added and `false` otherwise (alias was invalid).
|
||||
*/
|
||||
public final function bool AddAlias(
|
||||
string aliasToAdd,
|
||||
string aliasValue,
|
||||
optional bool saveInObject)
|
||||
{
|
||||
local AliasValuePair newPair;
|
||||
if (_.alias.IsAliasValid(aliasToAdd)) {
|
||||
return false;
|
||||
}
|
||||
if (hash.Contains(aliasToAdd)) {
|
||||
RemoveAlias(aliasToAdd);
|
||||
}
|
||||
// We might not be able to use per-object-config storage
|
||||
if (saveInObject && aliasesClass == none) {
|
||||
saveInObject = false;
|
||||
_.logger.Warning("Cannot save alias in object for source `"
|
||||
$ string(class)
|
||||
$ "`, because it does not have appropriate `Aliases` class setup.");
|
||||
}
|
||||
// Save
|
||||
if (saveInObject) {
|
||||
GetAliasesObjectWithValue(aliasValue).AddAlias(aliasToAdd);
|
||||
}
|
||||
else
|
||||
{
|
||||
newPair.alias = aliasToAdd;
|
||||
newPair.value = aliasValue;
|
||||
record[record.length] = newPair;
|
||||
}
|
||||
hash.Insert(aliasToAdd, aliasValue);
|
||||
AliasService(class'AliasService'.static.Require()).PendingSaveSource(self);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes alias (all records with it, in case of duplicates) from
|
||||
* the caller `AliasSource`.
|
||||
*
|
||||
* Cannot fail.
|
||||
*
|
||||
* NOTE: This call will cause update of an ini-file. That update can be
|
||||
* slightly delayed, so do not make assumptions about it's immediacy.
|
||||
*
|
||||
* NOTE #2: removing alias requires this method to go through the
|
||||
* whole `AliasSource` to remove possible duplicates, which can make
|
||||
* performing a lot of alias removal during run-time costly.
|
||||
*
|
||||
* @param aliasToRemove Alias that you want to remove from caller source.
|
||||
*/
|
||||
public final function RemoveAlias(string aliasToRemove)
|
||||
{
|
||||
local int i;
|
||||
local bool removedAliasFromRecord;
|
||||
hash.Remove(aliasToRemove);
|
||||
while (i < record.length)
|
||||
{
|
||||
if (record[i].alias ~= aliasToRemove)
|
||||
{
|
||||
record.Remove(i, 1);
|
||||
removedAliasFromRecord = true;
|
||||
}
|
||||
else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < loadedAliasObjects.length; i += 1) {
|
||||
loadedAliasObjects[i].RemoveAlias(aliasToRemove);
|
||||
}
|
||||
if (removedAliasFromRecord)
|
||||
{
|
||||
AliasService(class'AliasService'.static.Require())
|
||||
.PendingSaveSource(self);
|
||||
}
|
||||
}
|
||||
|
||||
// Performs initial hashing of every record with valid alias.
|
||||
// In case of duplicate or invalid aliases - method will skip them
|
||||
// and log warnings.
|
||||
private final function HashValidAliases()
|
||||
{
|
||||
if (hash == none) {
|
||||
_.logger.Warning("Alias source `" $ string(class) $ "` called"
|
||||
$ "`HashValidAliases()` function without creating an `AliasHasher`"
|
||||
$ "instance first. This should not have happened.");
|
||||
return;
|
||||
}
|
||||
HashValidAliasesFromRecord();
|
||||
HashValidAliasesFromPerObjectConfig();
|
||||
}
|
||||
|
||||
private final function LogDuplicateAliasWarning(
|
||||
string alias,
|
||||
string existingValue)
|
||||
{
|
||||
_.logger.Warning("Alias source `" $ string(class)
|
||||
$ "` has duplicate record for alias \"" $ alias
|
||||
$ "\". This is likely due to an erroneous config. \"" $ existingValue
|
||||
$ "\" value will be used.");
|
||||
}
|
||||
|
||||
private final function LogInvalidAliasWarning(string invalidAlias)
|
||||
{
|
||||
_.logger.Warning("Alias source `" $ string(class)
|
||||
$ "` contains invalid alias name \"" $ invalidAlias
|
||||
$ "\". This alias will not be loaded.");
|
||||
}
|
||||
|
||||
private final function HashValidAliasesFromRecord()
|
||||
{
|
||||
local int i;
|
||||
local bool isDuplicate;
|
||||
local string existingValue;
|
||||
for (i = 0; i < record.length; i += 1)
|
||||
{
|
||||
if (!_.alias.IsAliasValid(record[i].alias))
|
||||
{
|
||||
LogInvalidAliasWarning(record[i].alias);
|
||||
continue;
|
||||
}
|
||||
isDuplicate = !hash.InsertIfMissing(record[i].alias, record[i].value,
|
||||
existingValue);
|
||||
if (isDuplicate) {
|
||||
LogDuplicateAliasWarning(record[i].alias, existingValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final function HashValidAliasesFromPerObjectConfig()
|
||||
{
|
||||
local int i, j;
|
||||
local bool isDuplicate;
|
||||
local string existingValue;
|
||||
local string objectValue;
|
||||
local array<string> objectAliases;
|
||||
for (i = 0; i < loadedAliasObjects.length; i += 1)
|
||||
{
|
||||
objectValue = loadedAliasObjects[i].GetValue();
|
||||
objectAliases = loadedAliasObjects[i].GetAliases();
|
||||
for (j = 0; j < objectAliases.length; j += 1)
|
||||
{
|
||||
if (!_.alias.IsAliasValid(objectAliases[j]))
|
||||
{
|
||||
LogInvalidAliasWarning(objectAliases[j]);
|
||||
continue;
|
||||
}
|
||||
isDuplicate = !hash.InsertIfMissing(objectAliases[j], objectValue,
|
||||
existingValue);
|
||||
if (isDuplicate) {
|
||||
LogDuplicateAliasWarning(objectAliases[j], existingValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tries to find a loaded `Aliases` config object that stores aliases for
|
||||
// the given value. If such object does not exists - creates a new one.
|
||||
private final function Aliases GetAliasesObjectWithValue(string value)
|
||||
{
|
||||
local int i;
|
||||
local Aliases newAliasesObject;
|
||||
// This method only makes sense if this `AliasSource` supports
|
||||
// per-object-config storage.
|
||||
if (aliasesClass == none)
|
||||
{
|
||||
_.logger.Warning("`GetAliasesObjectForValue()` function was called for "
|
||||
$ "alias source with `aliasesClass == none`."
|
||||
$ "This should not happen.");
|
||||
return none;
|
||||
}
|
||||
for (i = 0; i < loadedAliasObjects.length; i += 1)
|
||||
{
|
||||
if (loadedAliasObjects[i].GetValue() ~= value) {
|
||||
return loadedAliasObjects[i];
|
||||
}
|
||||
}
|
||||
newAliasesObject = new(none, value) aliasesClass;
|
||||
loadedAliasObjects[loadedAliasObjects.length] = newAliasesObject;
|
||||
return newAliasesObject;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
// Source main parameters
|
||||
configName = "AcediaAliases"
|
||||
aliasesClass = class'Aliases'
|
||||
// HashTable twice the size of data entries should do it
|
||||
HASH_TABLE_SCALE = 2.0
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
/**
|
||||
* This is a simple helper object for `AliasSource` that can store
|
||||
* an array of aliases in config files in a per-object-config manner.
|
||||
* One `Aliases` object can store several aliases for a single value.
|
||||
* It is recommended that you do not try to access these objects directly.
|
||||
* Class name `Aliases` is chosen to make configuration files
|
||||
* more readable.
|
||||
* It's only interesting function is storing '.'s as ':' in it's config,
|
||||
* which is necessary to allow storing aliases for class names via
|
||||
* these objects (since UnrealScript's cannot handle '.'s in object's names
|
||||
* in it's configs).
|
||||
* Copyright 2020 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 Aliases extends AcediaObject
|
||||
perObjectConfig
|
||||
config(AcediaAliases);
|
||||
|
||||
// Link to the `AliasSource` that uses `Aliases` objects of this class.
|
||||
// To ensure that any `Aliases` sub-class only belongs to one `AliasSource`.
|
||||
var public const class<AliasSource> sourceClass;
|
||||
|
||||
// Aliases, recorded by this `Aliases` object that all mean the same value,
|
||||
// defined by this object's name `string(self.name)`.
|
||||
var protected config array<string> alias;
|
||||
|
||||
// Since '.'s in values are converted into ':' for storage purposes,
|
||||
// we need methods to convert between "storage" and "actual" value version.
|
||||
// `ToStorageVersion()` and `ToActualVersion()` do that.
|
||||
private final function string ToStorageVersion(string actualValue)
|
||||
{
|
||||
return Repl(actualValue, ".", ":");
|
||||
}
|
||||
|
||||
private final function string ToActualVersion(string storageValue)
|
||||
{
|
||||
return Repl(storageValue, ":", ".");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns value that caller's `Aliases` object's aliases point to.
|
||||
*
|
||||
* @return Value, stored by this object.
|
||||
*/
|
||||
public final function string GetValue()
|
||||
{
|
||||
return ToActualVersion(string(self.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array of aliases that caller `Aliases` tells us point to it's value.
|
||||
*
|
||||
* @return Array of all aliases, stored by caller `Aliases` object.
|
||||
*/
|
||||
public final function array<string> GetAliases()
|
||||
{
|
||||
return alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* [For inner use by `AliasSource`] Adds new alias to this object.
|
||||
*
|
||||
* Does no duplicates checks through for it's `AliasSource` and
|
||||
* neither it updates relevant `AliasHash`,
|
||||
* but will prevent adding duplicate records inside it's own storage.
|
||||
*
|
||||
* @param aliasToAdd Alias to add to caller `Aliases` object.
|
||||
*/
|
||||
public final function AddAlias(string aliasToAdd)
|
||||
{
|
||||
local int i;
|
||||
for (i = 0; i < alias.length; i += 1) {
|
||||
if (alias[i] ~= aliasToAdd) return;
|
||||
}
|
||||
alias[alias.length] = ToStorageVersion(aliasToAdd);
|
||||
AliasService(class'AliasService'.static.Require())
|
||||
.PendingSaveObject(self);
|
||||
}
|
||||
|
||||
/**
|
||||
* [For inner use by `AliasSource`] Removes alias from this object.
|
||||
*
|
||||
* Does not update relevant `AliasHash`.
|
||||
*
|
||||
* Will prevent adding duplicate records inside it's own storage.
|
||||
*
|
||||
* @param aliasToRemove Alias to remove from caller `Aliases` object.
|
||||
*/
|
||||
public final function RemoveAlias(string aliasToRemove)
|
||||
{
|
||||
local int i;
|
||||
local bool removedAlias;
|
||||
while (i < alias.length)
|
||||
{
|
||||
if (alias[i] ~= aliasToRemove)
|
||||
{
|
||||
alias.Remove(i, 1);
|
||||
removedAlias = true;
|
||||
}
|
||||
else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
if (removedAlias)
|
||||
{
|
||||
AliasService(class'AliasService'.static.Require())
|
||||
.PendingSaveObject(self);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If this object still has any alias records, - forces a rewrite of it's data
|
||||
* into the config file, otherwise - removes it's record entirely.
|
||||
*/
|
||||
public final function SaveOrClear()
|
||||
{
|
||||
if (alias.length <= 0) {
|
||||
ClearConfig();
|
||||
}
|
||||
else {
|
||||
SaveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
sourceClass = class'AliasSource'
|
||||
}
|
||||
|
|
@ -1,233 +0,0 @@
|
|||
/**
|
||||
* Provides convenient access to Aliases-related functions.
|
||||
* Copyright 2019 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 AliasesAPI extends Singleton;
|
||||
|
||||
/**
|
||||
* Checks that passed value is a valid alias name.
|
||||
*
|
||||
* A valid name is any name consisting out of 128 ASCII symbols.
|
||||
*
|
||||
* @param aliasToCheck Alias to check for validity.
|
||||
* @return `true` if `aliasToCheck` is a valid alias and `false` otherwise.
|
||||
*/
|
||||
public final function bool IsAliasValid(string aliasToCheck)
|
||||
{
|
||||
return _.text.IsASCIIString(aliasToCheck);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an easier access to the instance of the `AliasSource` of
|
||||
* the given class.
|
||||
*
|
||||
* Can fail if `customSourceClass` is incorrectly defined.
|
||||
*
|
||||
* @param customSourceClass Class of the source we want.
|
||||
* @return Instance of the requested `AliasSource`,
|
||||
* `none` if `customSourceClass` is incorrectly defined.
|
||||
*/
|
||||
public final function AliasSource GetCustomSource(
|
||||
class<AliasSource> customSourceClass)
|
||||
{
|
||||
return AliasSource(customSourceClass.static.GetInstance(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `AliasSource` that is designated in configuration files as
|
||||
* a source for weapon aliases.
|
||||
*
|
||||
* NOTE: while by default weapon aliases source will contain only weapon
|
||||
* aliases, you should not assume that. Acedia allows admins to store all
|
||||
* the aliases in the same config.
|
||||
*
|
||||
* @return Reference to the `AliasSource` that contains weapon aliases.
|
||||
* Can return `none` if no source for weapons was configured or
|
||||
* the configured source is incorrectly defined.
|
||||
*/
|
||||
public final function AliasSource GetWeaponSource()
|
||||
{
|
||||
local AliasSource weaponSource;
|
||||
local class<AliasSource> sourceClass;
|
||||
sourceClass = class'AliasService'.default.weaponAliasesSource;
|
||||
if (sourceClass == none) {
|
||||
_.logger.Failure("No weapon aliases source configured for Acedia's"
|
||||
@ "alias API. Error is most likely cause by erroneous config.");
|
||||
return none;
|
||||
}
|
||||
weaponSource = AliasSource(sourceClass.static.GetInstance(true));
|
||||
if (weaponSource == none) {
|
||||
_.logger.Failure("`AliasSource` class `" $ string(sourceClass) $ "` is"
|
||||
@ "configured to store weapon aliases, but it seems to be invalid."
|
||||
@ "This is a bug and not configuration file problem, but issue"
|
||||
@ "might be avoided by using a different `AliasSource`.");
|
||||
return none;
|
||||
}
|
||||
return weaponSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `AliasSource` that is designated in configuration files as
|
||||
* a source for color aliases.
|
||||
*
|
||||
* NOTE: while by default color aliases source will contain only color aliases,
|
||||
* you should not assume that. Acedia allows admins to store all the aliases
|
||||
* in the same config.
|
||||
*
|
||||
* @return Reference to the `AliasSource` that contains color aliases.
|
||||
* Can return `none` if no source for colors was configured or
|
||||
* the configured source is incorrectly defined.
|
||||
*/
|
||||
public final function AliasSource GetColorSource()
|
||||
{
|
||||
local AliasSource colorSource;
|
||||
local class<AliasSource> sourceClass;
|
||||
sourceClass = class'AliasService'.default.colorAliasesSource;
|
||||
if (sourceClass == none) {
|
||||
_.logger.Failure("No color aliases source configured for Acedia's"
|
||||
@ "alias API. Error is most likely cause by erroneous config.");
|
||||
return none;
|
||||
}
|
||||
colorSource = AliasSource(sourceClass.static.GetInstance(true));
|
||||
if (colorSource == none) {
|
||||
_.logger.Failure("`AliasSource` class `" $ string(sourceClass) $ "` is"
|
||||
@ "configured to store color aliases, but it seems to be invalid."
|
||||
@ "This is a bug and not configuration file problem, but issue"
|
||||
@ "might be avoided by using a different `AliasSource`.");
|
||||
return none;
|
||||
}
|
||||
return colorSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to look up a value, stored for given alias in an `AliasSource`
|
||||
* configured to store weapon aliases. Reports error on failure.
|
||||
*
|
||||
* Lookup of alias can fail if either alias does not exist in weapon alias
|
||||
* source or weapon alias source itself does not exist
|
||||
* (due to either faulty configuration or incorrect definition).
|
||||
* To determine if weapon alias source exists you can check
|
||||
* `_.alias.GetWeaponSource()` value.
|
||||
*
|
||||
* Also see `TryWeapon()` method.
|
||||
*
|
||||
* @param alias Alias, for which method will attempt to look up a value.
|
||||
* Case-insensitive.
|
||||
* @param value If passed `alias` was recorded as a weapon alias,
|
||||
* it's corresponding value will be written in this variable.
|
||||
* Otherwise value is undefined.
|
||||
* @return `true` if lookup was successful and `false` otherwise.
|
||||
*/
|
||||
public final function bool ResolveWeapon(string alias, out string result)
|
||||
{
|
||||
local AliasSource source;
|
||||
source = GetWeaponSource();
|
||||
if (source != none) {
|
||||
return source.Resolve(alias, result);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to look up a value, stored for given alias in an `AliasSource`
|
||||
* configured to store weapon aliases and silently returns given `alias`
|
||||
* value upon failure.
|
||||
*
|
||||
* Lookup of alias can fail if either alias does not exist in weapon alias
|
||||
* source or weapon alias source itself does not exist
|
||||
* (due to either faulty configuration or incorrect definition).
|
||||
* To determine if weapon alias source exists you can check
|
||||
* `_.alias.GetWeaponSource()` value.
|
||||
*
|
||||
* Also see `ResolveWeapon()` method.
|
||||
*
|
||||
* @param alias Alias, for which method will attempt to look up a value.
|
||||
* Case-insensitive.
|
||||
* @return Weapon value corresponding to a given alias, if it was present in
|
||||
* the weapon alias source and value of `alias` parameter instead.
|
||||
*/
|
||||
public function string TryWeapon(string alias)
|
||||
{
|
||||
local AliasSource source;
|
||||
source = GetWeaponSource();
|
||||
if (source != none) {
|
||||
return source.Try(alias);
|
||||
}
|
||||
return alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to look up a value, stored for given alias in an `AliasSource`
|
||||
* configured to store color aliases. Reports error on failure.
|
||||
*
|
||||
* Lookup of alias can fail if either alias does not exist in color alias
|
||||
* source or color alias source itself does not exist
|
||||
* (due to either faulty configuration or incorrect definition).
|
||||
* To determine if color alias source exists you can check
|
||||
* `_.alias.GetColorSource()` value.
|
||||
*
|
||||
* Also see `TryColor()` method.
|
||||
*
|
||||
* @param alias Alias, for which method will attempt to look up a value.
|
||||
* Case-insensitive.
|
||||
* @param value If passed `alias` was recorded as a color alias,
|
||||
* it's corresponding value will be written in this variable.
|
||||
* Otherwise value is undefined.
|
||||
* @return `true` if lookup was successful and `false` otherwise.
|
||||
*/
|
||||
public final function bool ResolveColor(string alias, out string result)
|
||||
{
|
||||
local AliasSource source;
|
||||
source = GetColorSource();
|
||||
if (source != none) {
|
||||
return source.Resolve(alias, result);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to look up a value, stored for given alias in an `AliasSource`
|
||||
* configured to store color aliases and silently returns given `alias`
|
||||
* value upon failure.
|
||||
*
|
||||
* Lookup of alias can fail if either alias does not exist in color alias
|
||||
* source or color alias source itself does not exist
|
||||
* (due to either faulty configuration or incorrect definition).
|
||||
* To determine if color alias source exists you can check
|
||||
* `_.alias.GetColorSource()` value.
|
||||
*
|
||||
* Also see `ResolveColor()` method.
|
||||
*
|
||||
* @param alias Alias, for which method will attempt to look up a value.
|
||||
* Case-insensitive.
|
||||
* @return Color value corresponding to a given alias, if it was present in
|
||||
* the color alias source and value of `alias` parameter instead.
|
||||
*/
|
||||
public function string TryColor(string alias)
|
||||
{
|
||||
local AliasSource source;
|
||||
source = GetColorSource();
|
||||
if (source != none) {
|
||||
return source.Try(alias);
|
||||
}
|
||||
return alias;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Source intended for color aliases.
|
||||
* Copyright 2020 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 ColorAliasSource extends AliasSource
|
||||
config(AcediaAliases_Colors);
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaAliases_Colors"
|
||||
aliasesClass = class'ColorAliases'
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Per-object-configuration intended for color aliases.
|
||||
* Copyright 2020 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 ColorAliases extends Aliases
|
||||
perObjectConfig
|
||||
config(AcediaAliases_Colors);
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
sourceClass = class'ColorAliasSource'
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Source intended for weapon aliases.
|
||||
* Copyright 2020 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 WeaponAliasSource extends AliasSource
|
||||
config(AcediaAliases_Weapons);
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaAliases_Weapons"
|
||||
aliasesClass = class'WeaponAliases'
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Per-object-configuration intended for weapon aliases.
|
||||
* Copyright 2020 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 WeaponAliases extends Aliases
|
||||
perObjectConfig
|
||||
config(AcediaAliases_Weapons);
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
sourceClass = class'WeaponAliasSource'
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Source intended for testing aliases.
|
||||
* Copyright 2020 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 MockAliasSource extends AliasSource
|
||||
config(AcediaAliases_Tests);
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
configName = "AcediaAliases_Tests"
|
||||
aliasesClass = class'MockAliases'
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* Per-object-configuration intended for testing aliases.
|
||||
* Copyright 2020 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 MockAliases extends Aliases
|
||||
perObjectConfig
|
||||
config(AcediaAliases_Tests);
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
sourceClass = class'MockAliasSource'
|
||||
}
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
/**
|
||||
* Set of tests for Aliases system.
|
||||
* Copyright 2020 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_Aliases extends TestCase
|
||||
abstract;
|
||||
|
||||
protected static function TESTS()
|
||||
{
|
||||
Test_AliasHash();
|
||||
Test_AliasLoading();
|
||||
}
|
||||
|
||||
protected static function Test_AliasLoading()
|
||||
{
|
||||
Context("Testing loading aliases from a mock object `MockAliasSource`.");
|
||||
SubTest_AliasLoadingCorrect();
|
||||
SubTest_AliasLoadingIncorrect();
|
||||
}
|
||||
|
||||
protected static function SubTest_AliasLoadingCorrect()
|
||||
{
|
||||
local AliasSource source;
|
||||
local string outValue;
|
||||
|
||||
Issue("`Resolve()` fails to return alias that should be loaded.");
|
||||
source = _().alias.GetCustomSource(class'MockAliasSource');
|
||||
TEST_ExpectTrue(source.Resolve("Global", outValue));
|
||||
TEST_ExpectTrue(outValue == "value");
|
||||
TEST_ExpectTrue(source.Resolve("ford", outValue));
|
||||
TEST_ExpectTrue(outValue == "car");
|
||||
|
||||
Issue("`Try()` fails to return alias that should be loaded.");
|
||||
TEST_ExpectTrue(source.Try("question") == "response");
|
||||
TEST_ExpectTrue(source.Try("delorean") == "car");
|
||||
|
||||
Issue("`ContainsAlias()` reports alias, that should be present,"
|
||||
@ "as missing.");
|
||||
TEST_ExpectTrue(source.ContainsAlias("Global"));
|
||||
TEST_ExpectTrue(source.ContainsAlias("audi"));
|
||||
|
||||
Issue("Aliases in per-object-configs incorrectly handle ':'.");
|
||||
TEST_ExpectTrue(source.Try("HardToBeAGod") == "sci.fi");
|
||||
|
||||
Issue("Aliases with empty values in alias name or their value are handled"
|
||||
@ "incorrectly.");
|
||||
TEST_ExpectTrue(source.Try("") == "empty");
|
||||
TEST_ExpectTrue(source.Try("also") == "");
|
||||
}
|
||||
|
||||
protected static function SubTest_AliasLoadingIncorrect()
|
||||
{
|
||||
local AliasSource source;
|
||||
local string outValue;
|
||||
Context("Testing loading aliases from a mock object `MockAliasSource`.");
|
||||
Issue("`AliasAPI` cannot return value custom source.");
|
||||
source = _().alias.GetCustomSource(class'MockAliasSource');
|
||||
TEST_ExpectNotNone(source);
|
||||
|
||||
Issue("`Resolve()` reports success of finding inexistent alias.");
|
||||
source = _().alias.GetCustomSource(class'MockAliasSource');
|
||||
TEST_ExpectFalse(source.Resolve("noSuchThing", outValue));
|
||||
|
||||
Issue("`Try()` does not return given value for non-existent alias.");
|
||||
TEST_ExpectTrue(source.Try("TheHellIsThis") == "TheHellIsThis");
|
||||
|
||||
Issue("`ContainsAlias()` reports inexistent alias as present.");
|
||||
TEST_ExpectFalse(source.ContainsAlias("FordК"));
|
||||
}
|
||||
|
||||
protected static function Test_AliasHash()
|
||||
{
|
||||
Context("Testing `AliasHasher`.");
|
||||
SubTest_AliasHashInsertingRemoval();
|
||||
}
|
||||
|
||||
protected static function SubTest_AliasHashInsertingRemoval()
|
||||
{
|
||||
local AliasHash hasher;
|
||||
local string outValue;
|
||||
hasher = new class'AliasHash';
|
||||
hasher.Initialize();
|
||||
Issue("`AliasHash` cannot properly store added aliases.");
|
||||
hasher.Insert("alias", "value").Insert("one", "more");
|
||||
TEST_ExpectTrue(hasher.Contains("alias"));
|
||||
TEST_ExpectTrue(hasher.Contains("one"));
|
||||
TEST_ExpectTrue(hasher.Find("alias", outValue));
|
||||
TEST_ExpectTrue(outValue == "value");
|
||||
TEST_ExpectTrue(hasher.Find("one", outValue));
|
||||
TEST_ExpectTrue(outValue == "more");
|
||||
|
||||
Issue("`AliasHash` reports hashing aliases that never were hashed.");
|
||||
TEST_ExpectFalse(hasher.Contains("alia"));
|
||||
|
||||
Issue("`AliasHash` cannot properly remove stored aliases.");
|
||||
hasher.Remove("alias");
|
||||
TEST_ExpectFalse(hasher.Contains("alias"));
|
||||
TEST_ExpectTrue(hasher.Contains("one"));
|
||||
TEST_ExpectFalse(hasher.Find("alias", outValue));
|
||||
outValue = "wrong";
|
||||
TEST_ExpectTrue(hasher.Find("one", outValue));
|
||||
TEST_ExpectTrue(outValue == "more");
|
||||
|
||||
Issue("`InsertIfMissing()` function cannot properly store added aliases.");
|
||||
TEST_ExpectTrue(hasher.InsertIfMissing("another", "var", outValue));
|
||||
TEST_ExpectTrue(hasher.Find("another", outValue));
|
||||
TEST_ExpectTrue(outValue == "var");
|
||||
|
||||
Issue("`InsertIfMissing()` function incorrectly resolves a conflict with"
|
||||
@ "an existing value.");
|
||||
TEST_ExpectFalse(hasher.InsertIfMissing("one", "something", outValue));
|
||||
TEST_ExpectTrue(outValue == "more");
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
caseName = "Aliases"
|
||||
}
|
||||
|
|
@ -1,812 +0,0 @@
|
|||
/**
|
||||
* API that provides functions for working with color.
|
||||
* It has a wide range of pre-defined colors and some functions that
|
||||
* allow to quickly assemble color from rgb(a) values, parse it from
|
||||
* a `Text`/string or load it from an alias.
|
||||
* Copyright 2020 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 ColorAPI extends Singleton
|
||||
dependson(Parser)
|
||||
config(AcediaSystem);
|
||||
|
||||
/**
|
||||
* Enumeration for ways to represent `Color` as a `string`.
|
||||
*/
|
||||
enum ColorDisplayType
|
||||
{
|
||||
// Hex format; for pink: #ffc0cb
|
||||
CLRDISPLAY_HEX,
|
||||
// RGB format; for pink: rgb(255,192,203)
|
||||
CLRDISPLAY_RGB,
|
||||
// RGBA format; for opaque pink: rgb(255,192,203,255)
|
||||
CLRDISPLAY_RGBA,
|
||||
// RGB format with tags; for pink: rgb(r=255,g=192,b=203)
|
||||
CLRDISPLAY_RGB_TAG,
|
||||
// RGBA format with tags; for pink: rgb(r=255,g=192,b=203,a=255)
|
||||
CLRDISPLAY_RGBA_TAG
|
||||
};
|
||||
|
||||
// Some useful predefined color values.
|
||||
// They are marked as `config` to allow server admins to mess about with
|
||||
// colors if they want to.
|
||||
// Pink colors
|
||||
var public config const Color Pink;
|
||||
var public config const Color LightPink;
|
||||
var public config const Color HotPink;
|
||||
var public config const Color DeepPink;
|
||||
var public config const Color PaleVioletRed;
|
||||
var public config const Color MediumVioletRed;
|
||||
// Red colors
|
||||
var public config const Color LightSalmon;
|
||||
var public config const Color Salmon;
|
||||
var public config const Color DarkSalmon;
|
||||
var public config const Color LightCoral;
|
||||
var public config const Color IndianRed;
|
||||
var public config const Color Crimson;
|
||||
var public config const Color Firebrick;
|
||||
var public config const Color DarkRed;
|
||||
var public config const Color Red;
|
||||
// Orange colors
|
||||
var public config const Color OrangeRed;
|
||||
var public config const Color Tomato;
|
||||
var public config const Color Coral;
|
||||
var public config const Color DarkOrange;
|
||||
var public config const Color Orange;
|
||||
// Yellow colors
|
||||
var public config const Color Yellow;
|
||||
var public config const Color LightYellow;
|
||||
var public config const Color LemonChiffon;
|
||||
var public config const Color LightGoldenrodYellow;
|
||||
var public config const Color PapayaWhip;
|
||||
var public config const Color Moccasin;
|
||||
var public config const Color PeachPuff;
|
||||
var public config const Color PaleGoldenrod;
|
||||
var public config const Color Khaki;
|
||||
var public config const Color DarkKhaki;
|
||||
var public config const Color Gold;
|
||||
// Brown colors
|
||||
var public config const Color Cornsilk;
|
||||
var public config const Color BlanchedAlmond;
|
||||
var public config const Color Bisque;
|
||||
var public config const Color NavajoWhite;
|
||||
var public config const Color Wheat;
|
||||
var public config const Color Burlywood;
|
||||
var public config const Color TanColor; // `Tan()` already taken by a function
|
||||
var public config const Color RosyBrown;
|
||||
var public config const Color SandyBrown;
|
||||
var public config const Color Goldenrod;
|
||||
var public config const Color DarkGoldenrod;
|
||||
var public config const Color Peru;
|
||||
var public config const Color Chocolate;
|
||||
var public config const Color SaddleBrown;
|
||||
var public config const Color Sienna;
|
||||
var public config const Color Brown;
|
||||
var public config const Color Maroon;
|
||||
// Green colors
|
||||
var public config const Color DarkOliveGreen;
|
||||
var public config const Color Olive;
|
||||
var public config const Color OliveDrab;
|
||||
var public config const Color YellowGreen;
|
||||
var public config const Color LimeGreen;
|
||||
var public config const Color Lime;
|
||||
var public config const Color LawnGreen;
|
||||
var public config const Color Chartreuse;
|
||||
var public config const Color GreenYellow;
|
||||
var public config const Color SpringGreen;
|
||||
var public config const Color MediumSpringGreen;
|
||||
var public config const Color LightGreen;
|
||||
var public config const Color PaleGreen;
|
||||
var public config const Color DarkSeaGreen;
|
||||
var public config const Color MediumAquamarine;
|
||||
var public config const Color MediumSeaGreen;
|
||||
var public config const Color SeaGreen;
|
||||
var public config const Color ForestGreen;
|
||||
var public config const Color Green;
|
||||
var public config const Color DarkGreen;
|
||||
// Cyan colors
|
||||
var public config const Color Aqua;
|
||||
var public config const Color Cyan;
|
||||
var public config const Color LightCyan;
|
||||
var public config const Color PaleTurquoise;
|
||||
var public config const Color Aquamarine;
|
||||
var public config const Color Turquoise;
|
||||
var public config const Color MediumTurquoise;
|
||||
var public config const Color DarkTurquoise;
|
||||
var public config const Color LightSeaGreen;
|
||||
var public config const Color CadetBlue;
|
||||
var public config const Color DarkCyan;
|
||||
var public config const Color Teal;
|
||||
// Blue colors
|
||||
var public config const Color LightSteelBlue;
|
||||
var public config const Color PowderBlue;
|
||||
var public config const Color LightBlue;
|
||||
var public config const Color SkyBlue;
|
||||
var public config const Color LightSkyBlue;
|
||||
var public config const Color DeepSkyBlue;
|
||||
var public config const Color DodgerBlue;
|
||||
var public config const Color CornflowerBlue;
|
||||
var public config const Color SteelBlue;
|
||||
var public config const Color RoyalBlue;
|
||||
var public config const Color Blue;
|
||||
var public config const Color MediumBlue;
|
||||
var public config const Color DarkBlue;
|
||||
var public config const Color Navy;
|
||||
var public config const Color MidnightBlue;
|
||||
// Purple, violet, and magenta colors
|
||||
var public config const Color Lavender;
|
||||
var public config const Color Thistle;
|
||||
var public config const Color Plum;
|
||||
var public config const Color Violet;
|
||||
var public config const Color Orchid;
|
||||
var public config const Color Fuchsia;
|
||||
var public config const Color Magenta;
|
||||
var public config const Color MediumOrchid;
|
||||
var public config const Color MediumPurple;
|
||||
var public config const Color BlueViolet;
|
||||
var public config const Color DarkViolet;
|
||||
var public config const Color DarkOrchid;
|
||||
var public config const Color DarkMagenta;
|
||||
var public config const Color Purple;
|
||||
var public config const Color Indigo;
|
||||
var public config const Color DarkSlateBlue;
|
||||
var public config const Color SlateBlue;
|
||||
var public config const Color MediumSlateBlue;
|
||||
// White colors
|
||||
var public config const Color White;
|
||||
var public config const Color Snow;
|
||||
var public config const Color Honeydew;
|
||||
var public config const Color MintCream;
|
||||
var public config const Color Azure;
|
||||
var public config const Color AliceBlue;
|
||||
var public config const Color GhostWhite;
|
||||
var public config const Color WhiteSmoke;
|
||||
var public config const Color Seashell;
|
||||
var public config const Color Beige;
|
||||
var public config const Color OldLace;
|
||||
var public config const Color FloralWhite;
|
||||
var public config const Color Ivory;
|
||||
var public config const Color AntiqueWhite;
|
||||
var public config const Color Linen;
|
||||
var public config const Color LavenderBlush;
|
||||
var public config const Color MistyRose;
|
||||
// Gray and black colors
|
||||
var public config const Color Gainsboro;
|
||||
var public config const Color LightGray;
|
||||
var public config const Color Silver;
|
||||
var public config const Color DarkGray;
|
||||
var public config const Color Gray;
|
||||
var public config const Color DimGray;
|
||||
var public config const Color LightSlateGray;
|
||||
var public config const Color SlateGray;
|
||||
var public config const Color DarkSlateGray;
|
||||
var public config const Color Eigengrau;
|
||||
var public config const Color Black;
|
||||
|
||||
// Escape code point is used to change output's color and is used in
|
||||
// Unreal Engine's `string`s.
|
||||
var private const int CODEPOINT_ESCAPE;
|
||||
var private const int CODEPOINT_SMALL_A;
|
||||
|
||||
/**
|
||||
* Creates opaque color from (red, green, blue) triplet.
|
||||
*
|
||||
* @param red Red component, range from 0 to 255.
|
||||
* @param green Green component, range from 0 to 255.
|
||||
* @param blue Blue component, range from 0 to 255.
|
||||
* @return `Color` with specified red, green and blue component and
|
||||
* alpha component of `255`.
|
||||
*/
|
||||
public final function Color RGB(byte red, byte green, byte blue)
|
||||
{
|
||||
local Color result;
|
||||
result.r = red;
|
||||
result.g = green;
|
||||
result.b = blue;
|
||||
result.a = 255;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates color from (red, green, blue, alpha) quadruplet.
|
||||
*
|
||||
* @param red Red component, range from 0 to 255.
|
||||
* @param green Green component, range from 0 to 255.
|
||||
* @param blue Blue component, range from 0 to 255.
|
||||
* @param alpha Alpha component, range from 0 to 255.
|
||||
* @return `Color` with specified red, green, blue and alpha component.
|
||||
*/
|
||||
public final function Color RGBA(byte red, byte green, byte blue, byte alpha)
|
||||
{
|
||||
local Color result;
|
||||
result.r = red;
|
||||
result.g = green;
|
||||
result.b = blue;
|
||||
result.a = alpha;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two colors for exact equality of red, green and blue components.
|
||||
* Alpha component is ignored.
|
||||
*
|
||||
* @param color1 Color to compare
|
||||
* @param color2 Color to compare
|
||||
* @return `true` if colors' red, green and blue components are equal
|
||||
* and `false` otherwise.
|
||||
*/
|
||||
public final function bool AreEqual(Color color1, Color color2, optional bool fixColors)
|
||||
{
|
||||
if (fixColors) {
|
||||
color1 = FixColor(color1);
|
||||
color2 = FixColor(color2);
|
||||
}
|
||||
if (color1.r != color2.r) return false;
|
||||
if (color1.g != color2.g) return false;
|
||||
if (color1.b != color2.b) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two colors for exact equality of red, green, blue
|
||||
* and alpha components.
|
||||
*
|
||||
* @param color1 Color to compare
|
||||
* @param color2 Color to compare
|
||||
* @return `true` if colors' red, green, blue and alpha components are equal
|
||||
* and `false` otherwise.
|
||||
*/
|
||||
public final function bool AreEqualWithAlpha(Color color1, Color color2, optional bool fixColors)
|
||||
{
|
||||
if (fixColors) {
|
||||
color1 = FixColor(color1);
|
||||
color2 = FixColor(color2);
|
||||
}
|
||||
if (color1.r != color2.r) return false;
|
||||
if (color1.g != color2.g) return false;
|
||||
if (color1.b != color2.b) return false;
|
||||
if (color1.a != color2.a) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Killing floor's standard methods of rendering colored `string`s
|
||||
* make use of inserting 4-byte sequence into them: first bytes denotes
|
||||
* the start of the sequence, 3 following bytes denote rgb color components.
|
||||
* Unfortunately these methods also have issues with rendering `string`s
|
||||
* if you specify certain values (`0` and `10`) of rgb color components.
|
||||
*
|
||||
* This function "fixes" components by replacing them with close and valid
|
||||
* color component values (adds `1` to the component).
|
||||
*/
|
||||
public final function byte FixColorComponent(byte colorComponent)
|
||||
{
|
||||
if (colorComponent == 0 || colorComponent == 10)
|
||||
{
|
||||
return colorComponent + 1;
|
||||
}
|
||||
return colorComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Killing floor's standard methods of rendering colored `string`s
|
||||
* make use of inserting 4-byte sequence into them: first bytes denotes
|
||||
* the start of the sequence, 3 following bytes denote rgb color components.
|
||||
* Unfortunately these methods also have issues with rendering `string`s
|
||||
* if you specify certain values (`0` and `10`) as rgb color components.
|
||||
*
|
||||
* This function "fixes" given `Color`'s components by replacing them with
|
||||
* close and valid color values (using `FixColorComponent()` method),
|
||||
* resulting in a `Color` that looks almost the same, but is suitable to be
|
||||
* included into 4-byte color change sequence.
|
||||
*
|
||||
* Since alpha component is never used in color-change sequences,
|
||||
* it is never affected.
|
||||
*/
|
||||
public final function Color FixColor(Color colorToFix)
|
||||
{
|
||||
colorToFix.r = FixColorComponent(colorToFix.r);
|
||||
colorToFix.g = FixColorComponent(colorToFix.g);
|
||||
colorToFix.b = FixColorComponent(colorToFix.b);
|
||||
return colorToFix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 4-gyte sequence for color change to a given color.
|
||||
*
|
||||
* To make returned tag work in most sequences, the value of given color is
|
||||
* auto "fixed" (see `FixColor()` for details).
|
||||
* There is an option to skip color fixing, but method will still change
|
||||
* `0` components to `1`, since they cannot otherwise be used in a tag at all.
|
||||
*
|
||||
* Also see `GetColorTagRGB()`.
|
||||
*
|
||||
* @param colorToUse Color to which tag must change the text.
|
||||
* It's alpha value (`colorToUse.a`) is discarded.
|
||||
* @param doNotFixComponents Minimizes changes to color components
|
||||
* (only allows to change `0` components to `1` before creating a tag).
|
||||
* @return `string` containing 4-byte sequence that will swap text's color to
|
||||
* a given one in standard Unreal Engine's UI.
|
||||
*/
|
||||
public final function string GetColorTag(
|
||||
Color colorToUse,
|
||||
optional bool doNotFixComponents)
|
||||
{
|
||||
if (!doNotFixComponents) {
|
||||
colorToUse = FixColor(colorToUse);
|
||||
}
|
||||
colorToUse.r = Max(1, colorToUse.r);
|
||||
colorToUse.g = Max(1, colorToUse.g);
|
||||
colorToUse.b = Max(1, colorToUse.b);
|
||||
return Chr(CODEPOINT_ESCAPE)
|
||||
$ Chr(colorToUse.r)
|
||||
$ Chr(colorToUse.g)
|
||||
$ Chr(colorToUse.b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns 4-gyte sequence for color change to a given color.
|
||||
*
|
||||
* To make returned tag work in most sequences, the value of given color is
|
||||
* auto "fixed" (see `FixColor()` for details).
|
||||
* There is an option to skip color fixing, but method will still change
|
||||
* `0` components to `1`, since they cannot otherwise be used in a tag at all.
|
||||
*
|
||||
* Also see `GetColorTag()`.
|
||||
*
|
||||
* @param red Red component of color to which tag must
|
||||
* change the text.
|
||||
* @param green Green component of color to which tag must
|
||||
* change the text.
|
||||
* @param blue Blue component of color to which tag must
|
||||
* change the text.
|
||||
* @param doNotFixComponents Minimizes changes to color components
|
||||
* (only allows to change `0` components to `1` before creating a tag).
|
||||
* @return `string` containing 4-byte sequence that will swap text's color to
|
||||
* a given one in standard Unreal Engine's UI.
|
||||
*/
|
||||
public final function string GetColorTagRGB(
|
||||
int red,
|
||||
int green,
|
||||
int blue,
|
||||
optional bool doNotFixComponents)
|
||||
{
|
||||
if (!doNotFixComponents)
|
||||
{
|
||||
red = FixColorComponent(red);
|
||||
green = FixColorComponent(green);
|
||||
blue = FixColorComponent(blue);
|
||||
}
|
||||
red = Max(1, red);
|
||||
green = Max(1, green);
|
||||
blue = Max(1, blue);
|
||||
return Chr(CODEPOINT_ESCAPE) $ Chr(red) $ Chr(green) $ Chr(blue);
|
||||
}
|
||||
|
||||
// Helper function that converts `byte` with values between 0 and 15 into
|
||||
// a corresponding hex letter
|
||||
private final function string ByteToHexCharacter(byte component)
|
||||
{
|
||||
component = Clamp(component, 0, 15);
|
||||
if (component < 10) {
|
||||
return string(component);
|
||||
}
|
||||
return Chr(component - 10 + CODEPOINT_SMALL_A);
|
||||
}
|
||||
|
||||
// `byte` to `string` in hex
|
||||
private final function string ComponentToHex(byte component)
|
||||
{
|
||||
local byte high4Bits, low4Bits;
|
||||
low4Bits = component % 16;
|
||||
if (component >= 16) {
|
||||
high4Bits = (component - low4Bits) / 16;
|
||||
}
|
||||
else {
|
||||
high4Bits = 0;
|
||||
}
|
||||
return ByteToHexCharacter(high4Bits) $ ByteToHexCharacter(low4Bits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays given color as a string in a given style
|
||||
* (hex color representation by default).
|
||||
*
|
||||
* @param colorToConvert Color to display as a `string`.
|
||||
* @param displayType `enum` value, describing how should color
|
||||
* be displayed.
|
||||
* @return `string` representation of a given color in a given style.
|
||||
*/
|
||||
public final function string ToStringType(
|
||||
Color colorToConvert,
|
||||
optional ColorDisplayType displayType)
|
||||
{
|
||||
if (displayType == CLRDISPLAY_HEX) {
|
||||
return "#" $ ComponentToHex(colorToConvert.r)
|
||||
$ ComponentToHex(colorToConvert.g)
|
||||
$ ComponentToHex(colorToConvert.b);
|
||||
}
|
||||
else if (displayType == CLRDISPLAY_RGB)
|
||||
{
|
||||
return "rgb(" $ string(colorToConvert.r) $ ","
|
||||
$ string(colorToConvert.g) $ ","
|
||||
$ string(colorToConvert.b) $ ")";
|
||||
}
|
||||
else if (displayType == CLRDISPLAY_RGBA)
|
||||
{
|
||||
return "rgba(" $ string(colorToConvert.r) $ ","
|
||||
$ string(colorToConvert.g) $ ","
|
||||
$ string(colorToConvert.b) $ ","
|
||||
$ string(colorToConvert.a) $ ")";
|
||||
}
|
||||
else if (displayType == CLRDISPLAY_RGB_TAG)
|
||||
{
|
||||
return "rgb(r=" $ string(colorToConvert.r) $ ","
|
||||
$ "g=" $ string(colorToConvert.g) $ ","
|
||||
$ "b=" $ string(colorToConvert.b) $ ")";
|
||||
}
|
||||
//else if (displayType == CLRDISPLAY_RGBA_TAG)
|
||||
return "rgba(r=" $ string(colorToConvert.r) $ ","
|
||||
$ "g=" $ string(colorToConvert.g) $ ","
|
||||
$ "b=" $ string(colorToConvert.b) $ ","
|
||||
$ "a=" $ string(colorToConvert.a) $ ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays given color as a string in RGB or RGBA format, depending on
|
||||
* whether color is opaque.
|
||||
*
|
||||
* @param colorToConvert Color to display as a `string` in `CLRDISPLAY_RGB`
|
||||
* style if `colorToConvert.a == 255` and `CLRDISPLAY_RGBA` otherwise.
|
||||
* @return `string` representation of a given color in a given style.
|
||||
*/
|
||||
public final function string ToString(Color colorToConvert)
|
||||
{
|
||||
if (colorToConvert.a < 255) {
|
||||
return ToStringType(colorToConvert, CLRDISPLAY_RGBA);
|
||||
}
|
||||
return ToStringType(colorToConvert, CLRDISPLAY_RGB);
|
||||
}
|
||||
|
||||
// Parses color in `CLRDISPLAY_RGB` and `CLRDISPLAY_RGB_TAG` representations.
|
||||
private final function Color ParseRGB(Parser parser)
|
||||
{
|
||||
local int redComponent;
|
||||
local int greenComponent;
|
||||
local int blueComponent;
|
||||
local Parser.ParserState initialParserState;
|
||||
initialParserState = parser.GetCurrentState();
|
||||
parser.Match("rgb(", true)
|
||||
.MInteger(redComponent).Match(",")
|
||||
.MInteger(greenComponent).Match(",")
|
||||
.MInteger(blueComponent).Match(")");
|
||||
if (!parser.Ok())
|
||||
{
|
||||
parser.RestoreState(initialParserState).Match("rgb(", true)
|
||||
.Match("r=", true).MInteger(redComponent).Match(",")
|
||||
.Match("g=", true).MInteger(greenComponent).Match(",")
|
||||
.Match("b=", true).MInteger(blueComponent).Match(")");
|
||||
}
|
||||
return RGB(redComponent, greenComponent, blueComponent);
|
||||
}
|
||||
|
||||
// Parses color in `CLRDISPLAY_RGBA` and `CLRDISPLAY_RGBA_TAG` representations.
|
||||
private final function Color ParseRGBA(Parser parser)
|
||||
{
|
||||
local int redComponent;
|
||||
local int greenComponent;
|
||||
local int blueComponent;
|
||||
local int alphaComponent;
|
||||
local Parser.ParserState initialParserState;
|
||||
initialParserState = parser.GetCurrentState();
|
||||
parser.Match("rgba(", true)
|
||||
.MInteger(redComponent).Match(",")
|
||||
.MInteger(greenComponent).Match(",")
|
||||
.MInteger(blueComponent).Match(",")
|
||||
.MInteger(alphaComponent).Match(")");
|
||||
if (!parser.Ok())
|
||||
{
|
||||
parser.RestoreState(initialParserState).Match("rgba(", true)
|
||||
.Match("r=", true).MInteger(redComponent).Match(",")
|
||||
.Match("g=", true).MInteger(greenComponent).Match(",")
|
||||
.Match("b=", true).MInteger(blueComponent).Match(",")
|
||||
.Match("a=", true).MInteger(alphaComponent).Match(")");
|
||||
}
|
||||
return RGBA(redComponent, greenComponent, blueComponent, alphaComponent);
|
||||
}
|
||||
|
||||
// Parses color in `CLRDISPLAY_HEX` representation.
|
||||
private final function Color ParseHexColor(Parser parser)
|
||||
{
|
||||
local int redComponent;
|
||||
local int greenComponent;
|
||||
local int blueComponent;
|
||||
parser.Match("#")
|
||||
.MUnsignedInteger(redComponent, 16, 2)
|
||||
.MUnsignedInteger(greenComponent, 16, 2)
|
||||
.MUnsignedInteger(blueComponent, 16, 2);
|
||||
return RGB(redComponent, greenComponent, blueComponent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses given parser to try and parse a color in any of the
|
||||
* `ColorDisplayType` representations.
|
||||
*
|
||||
* @param parser Parser that method would use to parse color from
|
||||
* wherever it left. It's confirmed state will not be changed.
|
||||
* Do not treat `parser` bein in a non-failed state as a confirmation of
|
||||
* successful parsing: color parsing might fail regardless.
|
||||
* Check return value for that.
|
||||
* @param resultingColor Parsed color will be written here if parsing is
|
||||
* successful, otherwise value is undefined.
|
||||
* If parsed color did not specify alpha component - 255 will be used.
|
||||
* @return `true` if parsing was successful and false otherwise.
|
||||
*/
|
||||
public final function bool ParseWith(Parser parser, out Color resultingColor)
|
||||
{
|
||||
local bool successfullyParsed;
|
||||
local string colorAlias;
|
||||
local Parser colorParser;
|
||||
local Parser.ParserState initialParserState;
|
||||
if (parser == none) return false;
|
||||
resultingColor.a = 0xff;
|
||||
colorParser = parser;
|
||||
initialParserState = parser.GetCurrentState();
|
||||
if (parser.Match("$").MUntil(colorAlias,, true).Ok())
|
||||
{
|
||||
colorParser = _.text.ParseString(_.alias.TryColor(colorAlias));
|
||||
initialParserState = colorParser.GetCurrentState();
|
||||
}
|
||||
else {
|
||||
parser.RestoreState(initialParserState);
|
||||
}
|
||||
resultingColor = ParseRGB(colorParser);
|
||||
if (!colorParser.Ok())
|
||||
{
|
||||
colorParser.RestoreState(initialParserState);
|
||||
resultingColor = ParseRGBA(colorParser);
|
||||
}
|
||||
if (!colorParser.Ok())
|
||||
{
|
||||
colorParser.RestoreState(initialParserState);
|
||||
resultingColor = ParseHexColor(colorParser);
|
||||
}
|
||||
successfullyParsed = colorParser.Ok();
|
||||
if (colorParser != parser) {
|
||||
_.memory.Free(colorParser);
|
||||
}
|
||||
return successfullyParsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a color in any of the `ColorDisplayType` representations from the
|
||||
* beginning of a given `string`.
|
||||
*
|
||||
* @param stringWithColor String, that contains color definition at
|
||||
* the beginning. Anything after color definition is not used.
|
||||
* @param resultingColor Parsed color will be written here if parsing is
|
||||
* successful, otherwise value is undefined.
|
||||
* If parsed color did not specify alpha component - 255 will be used.
|
||||
* @param stringType How to treat given `string`,
|
||||
* see `StringType` for more details.
|
||||
* @return `true` if parsing was successful and false otherwise.
|
||||
*/
|
||||
public final function bool ParseString(
|
||||
string stringWithColor,
|
||||
out Color resultingColor,
|
||||
optional Text.StringType stringType)
|
||||
{
|
||||
local bool successfullyParsed;
|
||||
local Parser colorParser;
|
||||
colorParser = _.text.ParseString(stringWithColor, stringType);
|
||||
successfullyParsed = ParseWith(colorParser, resultingColor);
|
||||
_.memory.Free(colorParser);
|
||||
return successfullyParsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a color in any of the `ColorDisplayType` representations from the
|
||||
* beginning of a given `Text`.
|
||||
*
|
||||
* @param textWithColor `Text`, that contains color definition at
|
||||
* the beginning. Anything after color definition is not used.
|
||||
* @param resultingColor Parsed color will be written here if parsing is
|
||||
* successful, otherwise value is undefined.
|
||||
* If parsed color did not specify alpha component - 255 will be used.
|
||||
* @return `true` if parsing was successful and false otherwise.
|
||||
*/
|
||||
public final function bool ParseText(
|
||||
Text textWithColor,
|
||||
out Color resultingColor)
|
||||
{
|
||||
local bool successfullyParsed;
|
||||
local Parser colorParser;
|
||||
colorParser = _.text.Parse(textWithColor);
|
||||
successfullyParsed = ParseWith(colorParser, resultingColor);
|
||||
_.memory.Free(colorParser);
|
||||
return successfullyParsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a color in any of the `ColorDisplayType` representations from the
|
||||
* beginning of a given raw data.
|
||||
*
|
||||
* @param rawDataWithColor Raw data, that contains color definition at
|
||||
* the beginning. Anything after color definition is not used.
|
||||
* @param resultingColor Parsed color will be written here if parsing is
|
||||
* successful, otherwise value is undefined.
|
||||
* If parsed color did not specify alpha component - 255 will be used.
|
||||
* @return `true` if parsing was successful and false otherwise.
|
||||
*/
|
||||
public final function bool ParseRaw(
|
||||
array<Text.Character> rawDataWithColor,
|
||||
out Color resultingColor)
|
||||
{
|
||||
local bool successfullyParsed;
|
||||
local Parser colorParser;
|
||||
colorParser = _.text.ParseRaw(rawDataWithColor);
|
||||
successfullyParsed = ParseWith(colorParser, resultingColor);
|
||||
_.memory.Free(colorParser);
|
||||
return successfullyParsed;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
Pink=(R=255,G=192,B=203,A=255)
|
||||
LightPink=(R=255,G=182,B=193,A=255)
|
||||
HotPink=(R=255,G=105,B=180,A=255)
|
||||
DeepPink=(R=255,G=20,B=147,A=255)
|
||||
PaleVioletRed=(R=219,G=112,B=147,A=255)
|
||||
MediumVioletRed=(R=199,G=21,B=133,A=255)
|
||||
LightSalmon=(R=255,G=160,B=122,A=255)
|
||||
Salmon=(R=250,G=128,B=114,A=255)
|
||||
DarkSalmon=(R=233,G=150,B=122,A=255)
|
||||
LightCoral=(R=240,G=128,B=128,A=255)
|
||||
IndianRed=(R=205,G=92,B=92,A=255)
|
||||
Crimson=(R=220,G=20,B=60,A=255)
|
||||
Firebrick=(R=178,G=34,B=34,A=255)
|
||||
DarkRed=(R=139,G=0,B=0,A=255)
|
||||
Red=(R=255,G=0,B=0,A=255)
|
||||
OrangeRed=(R=255,G=69,B=0,A=255)
|
||||
Tomato=(R=255,G=99,B=71,A=255)
|
||||
Coral=(R=255,G=127,B=80,A=255)
|
||||
DarkOrange=(R=255,G=140,B=0,A=255)
|
||||
Orange=(R=255,G=165,B=0,A=255)
|
||||
Yellow=(R=255,G=255,B=0,A=255)
|
||||
LightYellow=(R=255,G=255,B=224,A=255)
|
||||
LemonChiffon=(R=255,G=250,B=205,A=255)
|
||||
LightGoldenrodYellow=(R=250,G=250,B=210,A=255)
|
||||
PapayaWhip=(R=255,G=239,B=213,A=255)
|
||||
Moccasin=(R=255,G=228,B=181,A=255)
|
||||
PeachPuff=(R=255,G=218,B=185,A=255)
|
||||
PaleGoldenrod=(R=238,G=232,B=170,A=255)
|
||||
Khaki=(R=240,G=230,B=140,A=255)
|
||||
DarkKhaki=(R=189,G=183,B=107,A=255)
|
||||
Gold=(R=255,G=215,B=0,A=255)
|
||||
Cornsilk=(R=255,G=248,B=220,A=255)
|
||||
BlanchedAlmond=(R=255,G=235,B=205,A=255)
|
||||
Bisque=(R=255,G=228,B=196,A=255)
|
||||
NavajoWhite=(R=255,G=222,B=173,A=255)
|
||||
Wheat=(R=245,G=222,B=179,A=255)
|
||||
Burlywood=(R=222,G=184,B=135,A=255)
|
||||
TanColor=(R=210,G=180,B=140,A=255)
|
||||
RosyBrown=(R=188,G=143,B=143,A=255)
|
||||
SandyBrown=(R=244,G=164,B=96,A=255)
|
||||
Goldenrod=(R=218,G=165,B=32,A=255)
|
||||
DarkGoldenrod=(R=184,G=134,B=11,A=255)
|
||||
Peru=(R=205,G=133,B=63,A=255)
|
||||
Chocolate=(R=210,G=105,B=30,A=255)
|
||||
SaddleBrown=(R=139,G=69,B=19,A=255)
|
||||
Sienna=(R=160,G=82,B=45,A=255)
|
||||
Brown=(R=165,G=42,B=42,A=255)
|
||||
Maroon=(R=128,G=0,B=0,A=255)
|
||||
DarkOliveGreen=(R=85,G=107,B=47,A=255)
|
||||
Olive=(R=128,G=128,B=0,A=255)
|
||||
OliveDrab=(R=107,G=142,B=35,A=255)
|
||||
YellowGreen=(R=154,G=205,B=50,A=255)
|
||||
LimeGreen=(R=50,G=205,B=50,A=255)
|
||||
Lime=(R=0,G=255,B=0,A=255)
|
||||
LawnGreen=(R=124,G=252,B=0,A=255)
|
||||
Chartreuse=(R=127,G=255,B=0,A=255)
|
||||
GreenYellow=(R=173,G=255,B=47,A=255)
|
||||
SpringGreen=(R=0,G=255,B=127,A=255)
|
||||
MediumSpringGreen=(R=0,G=250,B=154,A=255)
|
||||
LightGreen=(R=144,G=238,B=144,A=255)
|
||||
PaleGreen=(R=152,G=251,B=152,A=255)
|
||||
DarkSeaGreen=(R=143,G=188,B=143,A=255)
|
||||
MediumAquamarine=(R=102,G=205,B=170,A=255)
|
||||
MediumSeaGreen=(R=60,G=179,B=113,A=255)
|
||||
SeaGreen=(R=46,G=139,B=87,A=255)
|
||||
ForestGreen=(R=34,G=139,B=34,A=255)
|
||||
Green=(R=0,G=128,B=0,A=255)
|
||||
DarkGreen=(R=0,G=100,B=0,A=255)
|
||||
Aqua=(R=0,G=255,B=255,A=255)
|
||||
Cyan=(R=0,G=255,B=255,A=255)
|
||||
LightCyan=(R=224,G=255,B=255,A=255)
|
||||
PaleTurquoise=(R=175,G=238,B=238,A=255)
|
||||
Aquamarine=(R=127,G=255,B=212,A=255)
|
||||
Turquoise=(R=64,G=224,B=208,A=255)
|
||||
MediumTurquoise=(R=72,G=209,B=204,A=255)
|
||||
DarkTurquoise=(R=0,G=206,B=209,A=255)
|
||||
LightSeaGreen=(R=32,G=178,B=170,A=255)
|
||||
CadetBlue=(R=95,G=158,B=160,A=255)
|
||||
DarkCyan=(R=0,G=139,B=139,A=255)
|
||||
Teal=(R=0,G=128,B=128,A=255)
|
||||
LightSteelBlue=(R=176,G=196,B=222,A=255)
|
||||
PowderBlue=(R=176,G=224,B=230,A=255)
|
||||
LightBlue=(R=173,G=216,B=230,A=255)
|
||||
SkyBlue=(R=135,G=206,B=235,A=255)
|
||||
LightSkyBlue=(R=135,G=206,B=250,A=255)
|
||||
DeepSkyBlue=(R=0,G=191,B=255,A=255)
|
||||
DodgerBlue=(R=30,G=144,B=255,A=255)
|
||||
CornflowerBlue=(R=100,G=149,B=237,A=255)
|
||||
SteelBlue=(R=70,G=130,B=180,A=255)
|
||||
RoyalBlue=(R=65,G=105,B=225,A=255)
|
||||
Blue=(R=0,G=0,B=255,A=255)
|
||||
MediumBlue=(R=0,G=0,B=205,A=255)
|
||||
DarkBlue=(R=0,G=0,B=139,A=255)
|
||||
Navy=(R=0,G=0,B=128,A=255)
|
||||
MidnightBlue=(R=25,G=25,B=112,A=255)
|
||||
Lavender=(R=230,G=230,B=250,A=255)
|
||||
Thistle=(R=216,G=191,B=216,A=255)
|
||||
Plum=(R=221,G=160,B=221,A=255)
|
||||
Violet=(R=238,G=130,B=238,A=255)
|
||||
Orchid=(R=218,G=112,B=214,A=255)
|
||||
Fuchsia=(R=255,G=0,B=255,A=255)
|
||||
Magenta=(R=255,G=0,B=255,A=255)
|
||||
MediumOrchid=(R=186,G=85,B=211,A=255)
|
||||
MediumPurple=(R=147,G=112,B=219,A=255)
|
||||
BlueViolet=(R=138,G=43,B=226,A=255)
|
||||
DarkViolet=(R=148,G=0,B=211,A=255)
|
||||
DarkOrchid=(R=153,G=50,B=204,A=255)
|
||||
DarkMagenta=(R=139,G=0,B=139,A=255)
|
||||
Purple=(R=128,G=0,B=128,A=255)
|
||||
Indigo=(R=75,G=0,B=130,A=255)
|
||||
DarkSlateBlue=(R=72,G=61,B=139,A=255)
|
||||
SlateBlue=(R=106,G=90,B=205,A=255)
|
||||
MediumSlateBlue=(R=123,G=104,B=238,A=255)
|
||||
White=(R=255,G=255,B=255,A=255)
|
||||
Snow=(R=255,G=250,B=250,A=255)
|
||||
Honeydew=(R=240,G=255,B=240,A=255)
|
||||
MintCream=(R=245,G=255,B=250,A=255)
|
||||
Azure=(R=240,G=255,B=255,A=255)
|
||||
AliceBlue=(R=240,G=248,B=255,A=255)
|
||||
GhostWhite=(R=248,G=248,B=255,A=255)
|
||||
WhiteSmoke=(R=245,G=245,B=245,A=255)
|
||||
Seashell=(R=255,G=245,B=238,A=255)
|
||||
Beige=(R=245,G=245,B=220,A=255)
|
||||
OldLace=(R=253,G=245,B=230,A=255)
|
||||
FloralWhite=(R=255,G=250,B=240,A=255)
|
||||
Ivory=(R=255,G=255,B=240,A=255)
|
||||
AntiqueWhite=(R=250,G=235,B=215,A=255)
|
||||
Linen=(R=250,G=240,B=230,A=255)
|
||||
LavenderBlush=(R=255,G=240,B=245,A=255)
|
||||
MistyRose=(R=255,G=228,B=225,A=255)
|
||||
Gainsboro=(R=220,G=220,B=220,A=255)
|
||||
LightGray=(R=211,G=211,B=211,A=255)
|
||||
Silver=(R=192,G=192,B=192,A=255)
|
||||
Gray=(R=169,G=169,B=169,A=255)
|
||||
DimGray=(R=128,G=128,B=128,A=255)
|
||||
DarkGray=(R=105,G=105,B=105,A=255)
|
||||
LightSlateGray=(R=119,G=136,B=153,A=255)
|
||||
SlateGray=(R=112,G=128,B=144,A=255)
|
||||
DarkSlateGray=(R=47,G=79,B=79,A=255)
|
||||
Eigengrau=(R=22,G=22,B=29,A=255)
|
||||
Black=(R=0,G=0,B=0,A=255)
|
||||
CODEPOINT_SMALL_A = 97
|
||||
CODEPOINT_ESCAPE = 27
|
||||
}
|
||||
|
|
@ -1,507 +0,0 @@
|
|||
/**
|
||||
* Set of tests for Color API.
|
||||
* Copyright 2020 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_ColorAPI extends TestCase
|
||||
abstract;
|
||||
|
||||
protected static function TESTS()
|
||||
{
|
||||
Test_ColorCreation();
|
||||
Test_EqualityCheck();
|
||||
Test_ColorFixing();
|
||||
Test_ToString();
|
||||
Test_Parse();
|
||||
Test_GetTag();
|
||||
}
|
||||
|
||||
protected static function Test_ColorCreation()
|
||||
{
|
||||
Context("Testing `ColorAPI`'s functions for creating color structures.");
|
||||
SubTest_ColorCreationRGB();
|
||||
SubTest_ColorCreationRGBA();
|
||||
}
|
||||
|
||||
protected static function SubTest_ColorCreationRGB()
|
||||
{
|
||||
local Color createdColor;
|
||||
Issue("`RGB() function does not set red, green and blue components"
|
||||
@ "correctly.");
|
||||
createdColor = _().color.RGB(145, 67, 237);
|
||||
TEST_ExpectTrue(createdColor.r == 145);
|
||||
TEST_ExpectTrue(createdColor.g == 67);
|
||||
TEST_ExpectTrue(createdColor.b == 237);
|
||||
|
||||
Issue("`RGB() function does not set alpha component to 255.");
|
||||
TEST_ExpectTrue(createdColor.a == 255);
|
||||
|
||||
Issue("`RGB() function does not set special values (border values"
|
||||
@ "`0`, `255` and value `10`, incorrect for coloring a `string`) for"
|
||||
@"red, green and blue components correctly.");
|
||||
createdColor = _().color.RGB(0, 10, 255);
|
||||
TEST_ExpectTrue(createdColor.r == 0);
|
||||
TEST_ExpectTrue(createdColor.g == 10);
|
||||
TEST_ExpectTrue(createdColor.b == 255);
|
||||
|
||||
Issue("`RGB() function does not set alpha value to 255.");
|
||||
TEST_ExpectTrue(createdColor.a == 255);
|
||||
}
|
||||
|
||||
protected static function SubTest_ColorCreationRGBA()
|
||||
{
|
||||
local Color createdColor;
|
||||
Issue("`RGBA() function does not set red, green, blue, alpha"
|
||||
@ "components correctly.");
|
||||
createdColor = _().color.RGBA(93, 245, 1, 67);
|
||||
TEST_ExpectTrue(createdColor.r == 93);
|
||||
TEST_ExpectTrue(createdColor.g == 245);
|
||||
TEST_ExpectTrue(createdColor.b == 1);
|
||||
TEST_ExpectTrue(createdColor.a == 67);
|
||||
|
||||
Issue("`RGBA() function does not set special values (border values"
|
||||
@ "`0`, `255` and value `10`, incorrect for coloring a `string`) for"
|
||||
@"red, green, blue components correctly.");
|
||||
createdColor = _().color.RGBA(0, 10, 10, 255);
|
||||
TEST_ExpectTrue(createdColor.r == 0);
|
||||
TEST_ExpectTrue(createdColor.g == 10);
|
||||
TEST_ExpectTrue(createdColor.b == 10);
|
||||
TEST_ExpectTrue(createdColor.a == 255);
|
||||
}
|
||||
|
||||
protected static function Test_EqualityCheck()
|
||||
{
|
||||
Context("Testing `ColorAPI`'s functions for color equality check.");
|
||||
SubTest_EqualityCheckNotFixed();
|
||||
SubTest_EqualityCheckFixed();
|
||||
}
|
||||
|
||||
protected static function SubTest_EqualityCheckNotFixed()
|
||||
{
|
||||
local Color color1, color2, color3;
|
||||
color1 = _().color.RGB(45, 10, 19);
|
||||
color2 = _().color.RGB(45, 11, 19);
|
||||
color3 = _().color.RGBA(45, 10, 19, 178);
|
||||
Issue("`AreEqual()` does not recognized equal colors as such.");
|
||||
TEST_ExpectTrue(_().color.AreEqual(color1, color1));
|
||||
|
||||
Issue("`AreEqual()` does not recognized colors that differ only in alpha"
|
||||
@ "channel as equal.");
|
||||
TEST_ExpectTrue(_().color.AreEqual(color1, color3));
|
||||
|
||||
Issue("`AreEqual()` does not recognized different colors as such.");
|
||||
TEST_ExpectFalse(_().color.AreEqual(color1, color2));
|
||||
|
||||
Issue("`AreEqualWithAlpha()` does not recognized equal colors as such.");
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(color1, color1));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(color3, color3));
|
||||
|
||||
Issue("`AreEqualWithAlpha()` does not recognized different colors"
|
||||
@ "as such.");
|
||||
TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color2));
|
||||
TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color3));
|
||||
}
|
||||
|
||||
protected static function SubTest_EqualityCheckFixed()
|
||||
{
|
||||
local Color color1, color2, color3;
|
||||
color1 = _().color.RGB(45, 10, 0);
|
||||
color2 = _().color.RGB(45, 239, 19);
|
||||
color3 = _().color.RGBA(45, 11, 1, 178);
|
||||
Issue("`AreEqual()` does not recognized equal colors as such (with color"
|
||||
@ "auto-fix).");
|
||||
TEST_ExpectTrue(_().color.AreEqual(color1, color1, true));
|
||||
|
||||
Issue("`AreEqual()` does not recognized colors that differ only in alpha"
|
||||
@ "channel as equal (with color auto-fix).");
|
||||
TEST_ExpectTrue(_().color.AreEqual(color1, color3, true));
|
||||
|
||||
Issue("`AreEqual()` does not recognized different colors as such"
|
||||
@ "(with color auto-fix).");
|
||||
TEST_ExpectFalse(_().color.AreEqual(color1, color2, true));
|
||||
|
||||
Issue("`AreEqualWithAlpha()` does not recognized equal colors as such"
|
||||
@ "(with color auto-fix).");
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(color1, color1, true));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(color3, color3, true));
|
||||
|
||||
Issue("`AreEqualWithAlpha()` does not recognized different colors as such"
|
||||
@ "(with color auto-fix).");
|
||||
TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color2, true));
|
||||
TEST_ExpectFalse(_().color.AreEqualWithAlpha(color1, color3, true));
|
||||
}
|
||||
|
||||
protected static function Test_ColorFixing()
|
||||
{
|
||||
local Color validColor, brokenColor;
|
||||
validColor = _().color.RGB(23, 179, 244);
|
||||
brokenColor = _().color.RGB(10, 35, 0);
|
||||
Context("Testing `ColorAPI`'s functions for fixing color components for"
|
||||
@ "game's native render functions.");
|
||||
Issue("`FixColorComponent()` does not \"fix\" values it is expected to,"
|
||||
@ "the way it is expected to.");
|
||||
TEST_ExpectTrue(_().color.FixColorComponent(0) == 1);
|
||||
TEST_ExpectTrue(_().color.FixColorComponent(10) == 11);
|
||||
|
||||
Issue("`FixColorComponent()` changes values it should not.");
|
||||
TEST_ExpectTrue(_().color.FixColorComponent(9) == 9);
|
||||
TEST_ExpectTrue(_().color.FixColorComponent(255) == 255);
|
||||
TEST_ExpectTrue(_().color.FixColorComponent(87) == 87);
|
||||
|
||||
Issue("`FixColor()` changes colors it should not.");
|
||||
TEST_ExpectTrue(
|
||||
_().color.AreEqualWithAlpha(validColor,
|
||||
_().color.FixColor(validColor)));
|
||||
|
||||
Issue("`FixColor()` doesn't fix color it should fix in an expected way.");
|
||||
TEST_ExpectTrue(
|
||||
_().color.AreEqualWithAlpha(_().color.RGB(11, 35, 1),
|
||||
_().color.FixColor(brokenColor)));
|
||||
|
||||
Issue("`FixColor()` affects alpha channel.");
|
||||
TEST_ExpectTrue(_().color.FixColor(validColor).a == 255);
|
||||
validColor.a = 0;
|
||||
TEST_ExpectTrue(_().color.FixColor(validColor).a == 0);
|
||||
validColor.a = 10;
|
||||
TEST_ExpectTrue(_().color.FixColor(validColor).a == 10);
|
||||
}
|
||||
|
||||
protected static function Test_ToString()
|
||||
{
|
||||
Context("Testing `ColorAPI`'s `ToString()` function.");
|
||||
SubTest_ToStringType();
|
||||
SubTest_ToString();
|
||||
}
|
||||
|
||||
protected static function SubTest_ToStringType()
|
||||
{
|
||||
local Color normalColor, borderValueColor;
|
||||
normalColor = _().color.RGBA(24, 232, 187, 34);
|
||||
borderValueColor = _().color.RGBA(0, 255, 255, 0);
|
||||
Issue("`ToStringType()` improperly works with `CLRDISPLAY_HEX` option.");
|
||||
TEST_ExpectTrue(_().color.ToStringType(normalColor) ~= "#18e8bb");
|
||||
TEST_ExpectTrue(_().color.ToStringType(borderValueColor) ~= "#00ffff");
|
||||
|
||||
Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGB` option.");
|
||||
TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGB)
|
||||
~= "rgb(24,232,187)");
|
||||
TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGB)
|
||||
~= "rgb(0,255,255)");
|
||||
|
||||
Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGBA` option.");
|
||||
TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGBA)
|
||||
~= "rgba(24,232,187,34)");
|
||||
TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGBA)
|
||||
~= "rgba(0,255,255,0)");
|
||||
|
||||
Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGB_TAG`"
|
||||
@ "option.");
|
||||
TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGB_TAG)
|
||||
~= "rgb(r=24,g=232,b=187)");
|
||||
TEST_ExpectTrue(_().color.ToStringType(borderValueColor, CLRDISPLAY_RGB_TAG)
|
||||
~= "rgb(r=0,g=255,b=255)");
|
||||
|
||||
Issue("`ToStringType()` improperly works with `CLRDISPLAY_RGBA_TAG`"
|
||||
@ "option.");
|
||||
TEST_ExpectTrue(_().color.ToStringType(normalColor, CLRDISPLAY_RGBA_TAG)
|
||||
~= "rgba(r=24,g=232,b=187,a=34)");
|
||||
TEST_ExpectTrue(
|
||||
_().color.ToStringType(borderValueColor, CLRDISPLAY_RGBA_TAG)
|
||||
~= "rgba(r=0,g=255,b=255,a=0)");
|
||||
}
|
||||
|
||||
protected static function SubTest_ToString()
|
||||
{
|
||||
local Color opaqueColor, transparentColor;
|
||||
opaqueColor = _().color.RGBA(143, 211, 43, 255);
|
||||
transparentColor = _().color.RGBA(234, 32, 145, 13);
|
||||
Issue("`ToString()` improperly converts color with opaque color.");
|
||||
TEST_ExpectTrue(_().color.ToString(opaqueColor) ~= "rgb(143,211,43)");
|
||||
Issue("`ToString()` improperly converts color with transparent color.");
|
||||
TEST_ExpectTrue(_().color.ToString(transparentColor)
|
||||
~= "rgba(234,32,145,13)");
|
||||
}
|
||||
|
||||
protected static function Test_GetTag()
|
||||
{
|
||||
Context("Testing `ColorAPI`'s functionality of creating 4-byte color"
|
||||
@ "change sequences.");
|
||||
SubTest_GetTagColor();
|
||||
SubTest_GetTagRGB();
|
||||
}
|
||||
|
||||
protected static function SubTest_GetTagColor()
|
||||
{
|
||||
local Color normalColor, borderColor;
|
||||
normalColor = _().color.RGB(143, 211, 43);
|
||||
borderColor = _().color.RGB(10, 0, 255);
|
||||
Issue("`GetColorTag()` does not properly convert colors.");
|
||||
TEST_ExpectTrue(_().color.GetColorTag(normalColor)
|
||||
== (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43)));
|
||||
TEST_ExpectTrue(_().color.GetColorTag(borderColor)
|
||||
== (Chr(27) $ Chr(11) $ Chr(1) $ Chr(255)));
|
||||
|
||||
Issue("`GetColorTag()` does not properly convert colors when asked not to"
|
||||
@ "fix components.");
|
||||
TEST_ExpectTrue(_().color.GetColorTag(normalColor, true)
|
||||
== (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43)));
|
||||
TEST_ExpectTrue(_().color.GetColorTag(borderColor, true)
|
||||
== (Chr(27) $ Chr(10) $ Chr(1) $ Chr(255)));
|
||||
}
|
||||
|
||||
protected static function SubTest_GetTagRGB()
|
||||
{
|
||||
Issue("`GetColorTagRGB()` does not properly convert colors.");
|
||||
TEST_ExpectTrue(_().color.GetColorTagRGB(143, 211, 43)
|
||||
== (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43)));
|
||||
TEST_ExpectTrue(_().color.GetColorTagRGB(10, 0, 255)
|
||||
== (Chr(27) $ Chr(11) $ Chr(1) $ Chr(255)));
|
||||
|
||||
Issue("`GetColorTagRGB()` does not properly convert colors when asked"
|
||||
@ "not to fix components.");
|
||||
TEST_ExpectTrue(_().color.GetColorTagRGB(143, 211, 43, true)
|
||||
== (Chr(27) $ Chr(143) $ Chr(211) $ Chr(43)));
|
||||
TEST_ExpectTrue(_().color.GetColorTagRGB(10, 0, 255, true)
|
||||
== (Chr(27) $ Chr(10) $ Chr(1) $ Chr(255)));
|
||||
}
|
||||
|
||||
protected static function Test_Parse()
|
||||
{
|
||||
Context("Testing `ColorAPI`'s parsing functionality.");
|
||||
SubTest_ParseWithParser();
|
||||
SubTest_ParseStringPlain();
|
||||
SubTest_ParseStringColored();
|
||||
SubTest_ParseStringFormatted();
|
||||
SubTest_ParseText();
|
||||
SubTest_ParseRaw();
|
||||
}
|
||||
|
||||
protected static function SubTest_ParseWithParser()
|
||||
{
|
||||
local Color expectedColor, resultColor;
|
||||
expectedColor = _().color.RGBA(154, 255, 0, 187);
|
||||
Issue("`ParseWith()` cannot parse hex colors.");
|
||||
TEST_ExpectTrue(_().color.ParseWith(_().text.ParseString("#9aff00"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseWith()` cannot parse rgb colors.");
|
||||
TEST_ExpectTrue(_().color.ParseWith(_().text.ParseString("rgb(154,255,0)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseWith()` cannot parse rgba colors.");
|
||||
TEST_ExpectTrue(_().color.ParseWith(
|
||||
_().text.ParseString("rgba(154,255,0,187)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseWith()` cannot parse rgb colors with tags.");
|
||||
TEST_ExpectTrue(_().color.ParseWith(
|
||||
_().text.ParseString("rgb(r=154,g=255,b=0)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseWith()` cannot parse rgba colors with tags.");
|
||||
TEST_ExpectTrue(_().color.ParseWith(
|
||||
_().text.ParseString("rgba(r=154,g=255,b=0,a=187)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseWith()` reports success when parsing invalid color string.");
|
||||
TEST_ExpectFalse(_().color.ParseWith( _().text.ParseString("#9aff0g"),
|
||||
resultColor));
|
||||
}
|
||||
|
||||
protected static function SubTest_ParseStringPlain()
|
||||
{
|
||||
local Color expectedColor, resultColor;
|
||||
expectedColor = _().color.RGBA(154, 255, 0, 187);
|
||||
Issue("`ParseString()` cannot parse hex colors.");
|
||||
TEST_ExpectTrue(_().color.ParseString("#9aff00", resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString()` cannot parse rgb colors.");
|
||||
TEST_ExpectTrue(_().color.ParseString("rgb(154,255,0)", resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString()` cannot parse rgba colors.");
|
||||
TEST_ExpectTrue(_().color.ParseString("rgba(154,255,0,187)", resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString()` cannot parse rgb colors with tags.");
|
||||
TEST_ExpectTrue(_().color.ParseString("rgb(r=154,g=255,b=0)", resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString()` cannot parse rgba colors with tags.");
|
||||
TEST_ExpectTrue(_().color.ParseString( "rgba(r=154,g=255,b=0,a=187)",
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString()` reports success when parsing invalid color string.");
|
||||
TEST_ExpectFalse(_().color.ParseString("#9aff0g", resultColor));
|
||||
}
|
||||
|
||||
protected static function SubTest_ParseStringColored()
|
||||
{
|
||||
local Color expectedColor, resultColor;
|
||||
expectedColor = _().color.RGBA(154, 255, 0, 187);
|
||||
Issue("`ParseString(STRING_Colored)` cannot parse hex colors.");
|
||||
TEST_ExpectTrue(_().color.ParseString(
|
||||
"#9af" $ Chr(27) $ Chr(45) $ Chr(234) $ Chr(24) $ "f00",
|
||||
resultColor, STRING_Colored));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString(STRING_Colored)` cannot parse rgb colors.");
|
||||
TEST_ExpectTrue(_().color.ParseString(
|
||||
"rgb(154,2" $ Chr(27) $ Chr(23) $ Chr(32) $ Chr(53) $ "55,0)",
|
||||
resultColor, STRING_Colored));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString(STRING_Colored)` cannot parse rgba colors.");
|
||||
TEST_ExpectTrue(_().color.ParseString(
|
||||
"rgba(154,255,0,187" $ Chr(27) $ Chr(133) $ Chr(234) $ Chr(10) $ ")",
|
||||
resultColor, STRING_Colored));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString(STRING_Colored)` cannot parse rgb colors with tags.");
|
||||
TEST_ExpectTrue(_().color.ParseString(
|
||||
"rg" $ Chr(27) $ Chr(26) $ Chr(234) $ Chr(125) $ "b(r=154,g=255,b=0)",
|
||||
resultColor, STRING_Colored));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString(STRING_Colored)` cannot parse rgba colors with tags.");
|
||||
TEST_ExpectTrue(_().color.ParseString(
|
||||
"rgba(r=154,g=255,b" $ Chr(27) $ Chr(1) $ Chr(4) $ Chr(7) $ "=0,a=187)",
|
||||
resultColor, STRING_Colored));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
}
|
||||
|
||||
protected static function SubTest_ParseStringFormatted()
|
||||
{
|
||||
local Color expectedColor, resultColor;
|
||||
expectedColor = _().color.RGBA(154, 255, 0, 187);
|
||||
Issue("`ParseString(STRING_Formatted)` cannot parse hex colors.");
|
||||
TEST_ExpectTrue(_().color.ParseString(
|
||||
"#9a{#4753d5 ff0}0",
|
||||
resultColor, STRING_Formatted));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString(STRING_Formatted)` cannot parse rgb colors.");
|
||||
TEST_ExpectTrue(_().color.ParseString(
|
||||
"rg{rgb(45,67,123) b(154,25}5,0)",
|
||||
resultColor, STRING_Formatted));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString(STRING_Formatted)` cannot parse rgba colors.");
|
||||
TEST_ExpectTrue(_().color.ParseString(
|
||||
"rgba(154,2{#34d1a7 }55,0,187)",
|
||||
resultColor, STRING_Formatted));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString(STRING_Formatted)` cannot parse rgb colors with tags.");
|
||||
TEST_ExpectTrue(_().color.ParseString(
|
||||
"rgb(r{#34d1a7 }=154,g=255,b=0)",
|
||||
resultColor, STRING_Formatted));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseString(STRING_Formatted)` cannot parse rgba colors with"
|
||||
@ "tags.");
|
||||
TEST_ExpectTrue(_().color.ParseString(
|
||||
"r{rgb(12,12,253) gba(r=154,g=255,b=0,a=187)}",
|
||||
resultColor, STRING_Formatted));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
}
|
||||
|
||||
protected static function SubTest_ParseText()
|
||||
{
|
||||
local Color expectedColor, resultColor;
|
||||
expectedColor = _().color.RGBA(154, 255, 0, 187);
|
||||
Issue("`ParseText()` cannot parse hex colors.");
|
||||
TEST_ExpectTrue(_().color.ParseText(_().text.FromString("#9aff00"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseText()` cannot parse rgb colors.");
|
||||
TEST_ExpectTrue(_().color.ParseText(_().text.FromString("rgb(154,255,0)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseText()` cannot parse rgba colors.");
|
||||
TEST_ExpectTrue(_().color.ParseText(
|
||||
_().text.FromString("rgba(154,255,0,187)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseText()` cannot parse rgb colors with tags.");
|
||||
TEST_ExpectTrue(_().color.ParseText(
|
||||
_().text.FromString("rgb(r=154,g=255,b=0)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseText()` cannot parse rgba colors with tags.");
|
||||
TEST_ExpectTrue(_().color.ParseText(
|
||||
_().text.FromString("rgba(r=154,g=255,b=0,a=187)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseText()` reports success when parsing invalid color string.");
|
||||
TEST_ExpectFalse(_().color.ParseText( _().text.FromString("#9aff0g"),
|
||||
resultColor));
|
||||
}
|
||||
|
||||
protected static function SubTest_ParseRaw()
|
||||
{
|
||||
local Color expectedColor, resultColor;
|
||||
expectedColor = _().color.RGBA(154, 255, 0, 187);
|
||||
Issue("`ParseRaw()` cannot parse hex colors.");
|
||||
TEST_ExpectTrue(_().color.ParseRaw( _().text.StringToRaw("#9aff00"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseRaw()` cannot parse rgb colors.");
|
||||
TEST_ExpectTrue(_().color.ParseRaw( _().text.StringToRaw("rgb(154,255,0)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseRaw()` cannot parse rgba colors.");
|
||||
TEST_ExpectTrue(_().color.ParseRaw(
|
||||
_().text.StringToRaw("rgba(154,255,0,187)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseRaw()` cannot parse rgb colors with tags.");
|
||||
TEST_ExpectTrue(_().color.ParseRaw(
|
||||
_().text.StringToRaw("rgb(r=154,g=255,b=0)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqual(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseRaw()` cannot parse rgba colors with tags.");
|
||||
TEST_ExpectTrue(_().color.ParseRaw(
|
||||
_().text.StringToRaw("rgba(r=154,g=255,b=0,a=187)"),
|
||||
resultColor));
|
||||
TEST_ExpectTrue(_().color.AreEqualWithAlpha(resultColor, expectedColor));
|
||||
|
||||
Issue("`ParseRaw()` reports success when parsing invalid color string.");
|
||||
TEST_ExpectFalse(_().color.ParseRaw(_().text.StringToRaw("#9aff0g"),
|
||||
resultColor));
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
caseName = "Colors"
|
||||
}
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
/**
|
||||
* API that provides functions for outputting text in
|
||||
* Killing Floor's console. It takes care of coloring output and breaking up
|
||||
* long lines (since allowing game to handle line breaking completely
|
||||
* messes up console output).
|
||||
*
|
||||
* Actual output is taken care of by `ConsoleWriter` objects that this
|
||||
* API generates.
|
||||
* Copyright 2020 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 ConsoleAPI extends Singleton
|
||||
config(AcediaSystem);
|
||||
|
||||
/**
|
||||
* Main issue with console output in Killing Floor is
|
||||
* automatic line breaking of long enough messages:
|
||||
* it breaks formatting and can lead to an ugly text overlapping.
|
||||
* To fix this we will try to break up user's output into lines ourselves,
|
||||
* before game does it for us.
|
||||
*
|
||||
* We are not 100% sure how Killing Floor decides when to break the line,
|
||||
* but it seems to calculate how much text can actually fit in a certain
|
||||
* area on screen.
|
||||
* There are two issues:
|
||||
* 1. We do not know for sure what this limit value is.
|
||||
* Even if we knew how to compute it, we cannot do that in server mode,
|
||||
* since it depends on a screen resolution and font, which
|
||||
* can vary for different players.
|
||||
* 2. Even invisible characters, such as color change sequences,
|
||||
* that do not take any space on the screen, contribute towards
|
||||
* that limit. So for a heavily colored text we will have to
|
||||
* break line much sooner than for the plain text.
|
||||
* Both issues are solved by introducing two limits that users themselves
|
||||
* are allowed to change: visible character limit and total character limit.
|
||||
* ~ Total character limit will be a hard limit on a character amount in
|
||||
* a line (including hidden ones used for color change sequences) that
|
||||
* will be used to prevent Killing Floor's native line breaks.
|
||||
* ~ Visible character limit will be a lower limit on amount of actually
|
||||
* visible character. It introduction basically reserves some space that can be
|
||||
* used only for color change sequences. Without this limit lines with
|
||||
* colored lines will appear to be shorter that mono-colored ones.
|
||||
* Visible limit will help to alleviate this problem.
|
||||
*
|
||||
* For example, if we set total limit to `120` and visible limit to `80`:
|
||||
* 1. Line not formatted with color will all break at
|
||||
* around length of `80`.
|
||||
* 2. Since color change sequence consists of 4 characters:
|
||||
* we can fit up to `(120 - 80) / 4 = 10` color swaps into each line,
|
||||
* while still breaking them at a around the same length of `80`.
|
||||
* ~ To differentiate our line breaks from line breaks intended by
|
||||
* the user, we will also add 2 symbols worth of padding in front of all our
|
||||
* output:
|
||||
* 1. Before intended new line they will be just two spaces.
|
||||
* 2. After our line break we will replace first space with "|" to indicate
|
||||
* that we had to break a long line.
|
||||
*
|
||||
* Described measures are not perfect:
|
||||
* 1. Since Killing Floor's console doe not use monospaced font,
|
||||
* the same amount of characters on the line does not mean lines of
|
||||
* visually the same length;
|
||||
* 2. Heavily enough colored lines are still going to be shorter;
|
||||
* 3. Depending on a resolution, default limits may appear to either use
|
||||
* too little space (for high resolutions) or, on the contrary,
|
||||
* not prevent native line breaks (low resolutions).
|
||||
* In these cases user might be required to manually set limits;
|
||||
* 4. There are probably more.
|
||||
* But if seems to provide good enough results for the average use case.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Configures how text will be rendered in target console(s).
|
||||
*/
|
||||
struct ConsoleDisplaySettings
|
||||
{
|
||||
// What color to use for text by default
|
||||
var Color defaultColor;
|
||||
// How many visible characters in be displayed in a line?
|
||||
var int maxVisibleLineWidth;
|
||||
// How many total characters can be output at once?
|
||||
var int maxTotalLineWidth;
|
||||
};
|
||||
// We will store data for `ConsoleDisplaySettings` separately for the ease of
|
||||
// configuration.
|
||||
var private config Color defaultColor;
|
||||
var private config int maxVisibleLineWidth;
|
||||
var private config int maxTotalLineWidth;
|
||||
|
||||
/**
|
||||
* Return current global visible limit that describes how many (at most)
|
||||
* visible characters can be output in the console line.
|
||||
*
|
||||
* Instances of `ConsoleWriter` are initialized with this value,
|
||||
* but can later change this value independently.
|
||||
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||
*
|
||||
* @return Current global visible limit.
|
||||
*/
|
||||
public final function int GetVisibleLineLength()
|
||||
{
|
||||
return maxVisibleLineWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current global visible limit that describes how many (at most) visible
|
||||
* characters can be output in the console line.
|
||||
*
|
||||
* Instances of `ConsoleWriter` are initialized with this value,
|
||||
* but can later change this value independently.
|
||||
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||
*
|
||||
* @param newMaxVisibleLineWidth New global visible character limit.
|
||||
*/
|
||||
public final function SetVisibleLineLength(int newMaxVisibleLineWidth)
|
||||
{
|
||||
maxVisibleLineWidth = newMaxVisibleLineWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current global total limit that describes how many (at most)
|
||||
* characters can be output in the console line.
|
||||
*
|
||||
* Instances of `ConsoleWriter` are initialized with this value,
|
||||
* but can later change this value independently.
|
||||
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||
*
|
||||
* @return Current global total limit.
|
||||
*/
|
||||
public final function int GetTotalLineLength()
|
||||
{
|
||||
return maxTotalLineWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current global total limit that describes how many (at most)
|
||||
* characters can be output in the console line, counting both visible symbols
|
||||
* and color change sequences.
|
||||
*
|
||||
* Instances of `ConsoleWriter` are initialized with this value,
|
||||
* but can later change this value independently.
|
||||
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||
*
|
||||
* @param newMaxTotalLineWidth New global total character limit.
|
||||
*/
|
||||
public final function SetTotalLineLength(int newMaxTotalLineWidth)
|
||||
{
|
||||
maxTotalLineWidth = newMaxTotalLineWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current global total limit that describes how many (at most)
|
||||
* characters can be output in the console line.
|
||||
*
|
||||
* Instances of `ConsoleWriter` are initialized with this value,
|
||||
* but can later change this value independently.
|
||||
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||
*
|
||||
* @return Current default output color.
|
||||
*/
|
||||
public final function Color GetDefaultColor(int newMaxTotalLineWidth)
|
||||
{
|
||||
return defaultColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current global default color for console output.,
|
||||
*
|
||||
* Instances of `ConsoleWriter` are initialized with this value,
|
||||
* but can later change this value independently.
|
||||
* Changes to global values do not affect already created `ConsoleWriters`.
|
||||
*
|
||||
* @param newMaxTotalLineWidth New global default output color.
|
||||
*/
|
||||
public final function SetDefaultColor(Color newDefaultColor)
|
||||
{
|
||||
defaultColor = newDefaultColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns borrowed `ConsoleWriter` instance that will write into
|
||||
* consoles of all players.
|
||||
*
|
||||
* @return ConsoleWriter Borrowed `ConsoleWriter` instance, configured to
|
||||
* write into consoles of all players.
|
||||
* Never `none`.
|
||||
*/
|
||||
public final function ConsoleWriter ForAll()
|
||||
{
|
||||
local ConsoleDisplaySettings globalSettings;
|
||||
globalSettings.defaultColor = defaultColor;
|
||||
globalSettings.maxTotalLineWidth = maxTotalLineWidth;
|
||||
globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
|
||||
return ConsoleWriter(_.memory.Claim(class'ConsoleWriter'))
|
||||
.Initialize(globalSettings).ForAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns borrowed `ConsoleWriter` instance that will write into
|
||||
* console of the player with a given controller.
|
||||
*
|
||||
* @param targetController Player, to whom console we want to write.
|
||||
* If `none` - returned `ConsoleWriter` would be configured to
|
||||
* throw messages away.
|
||||
* @return Borrowed `ConsoleWriter` instance, configured to
|
||||
* write into consoles of all players.
|
||||
* Never `none`.
|
||||
*/
|
||||
public final function ConsoleWriter For(PlayerController targetController)
|
||||
{
|
||||
local ConsoleDisplaySettings globalSettings;
|
||||
globalSettings.defaultColor = defaultColor;
|
||||
globalSettings.maxTotalLineWidth = maxTotalLineWidth;
|
||||
globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
|
||||
return ConsoleWriter(_.memory.Claim(class'ConsoleWriter'))
|
||||
.Initialize(globalSettings).ForController(targetController);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new `ConsoleWriter` instance that will write into
|
||||
* consoles of all players.
|
||||
* Should be freed after use.
|
||||
*
|
||||
* @return ConsoleWriter New `ConsoleWriter` instance, configured to
|
||||
* write into consoles of all players.
|
||||
* Never `none`.
|
||||
*/
|
||||
public final function ConsoleWriter MakeForAll()
|
||||
{
|
||||
local ConsoleDisplaySettings globalSettings;
|
||||
globalSettings.defaultColor = defaultColor;
|
||||
globalSettings.maxTotalLineWidth = maxTotalLineWidth;
|
||||
globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
|
||||
return ConsoleWriter(_.memory.Allocate(class'ConsoleWriter'))
|
||||
.Initialize(globalSettings).ForAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns new `ConsoleWriter` instance that will write into
|
||||
* console of the player with a given controller.
|
||||
* Should be freed after use.
|
||||
*
|
||||
* @param targetController Player, to whom console we want to write.
|
||||
* If `none` - returned `ConsoleWriter` would be configured to
|
||||
* throw messages away.
|
||||
* @return New `ConsoleWriter` instance, configured to
|
||||
* write into consoles of all players.
|
||||
* Never `none`.
|
||||
*/
|
||||
public final function ConsoleWriter MakeFor(PlayerController targetController)
|
||||
{
|
||||
local ConsoleDisplaySettings globalSettings;
|
||||
globalSettings.defaultColor = defaultColor;
|
||||
globalSettings.maxTotalLineWidth = maxTotalLineWidth;
|
||||
globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
|
||||
return ConsoleWriter(_.memory.Allocate(class'ConsoleWriter'))
|
||||
.Initialize(globalSettings).ForController(targetController);
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
defaultColor = (R=255,G=255,B=255,A=255)
|
||||
// These should guarantee decent text output even at
|
||||
// 640x480 shit resolution
|
||||
maxVisibleLineWidth = 80
|
||||
maxTotalLineWidth = 108
|
||||
}
|
||||
|
|
@ -1,393 +0,0 @@
|
|||
/**
|
||||
* Object that provides a buffer functionality for Killing Floor's (in-game)
|
||||
* console output: it accepts content that user want to output and breaks it
|
||||
* into lines that will be well-rendered according to the given
|
||||
* `ConsoleDisplaySettings`.
|
||||
* Copyright 2020 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 ConsoleBuffer extends AcediaObject
|
||||
dependson(Text)
|
||||
dependson(ConsoleAPI);
|
||||
|
||||
/**
|
||||
* `ConsoleBuffer` works by breaking it's input into words, counting how much
|
||||
* space they take up and only then deciding to which line to append them
|
||||
* (new or the next, new one).
|
||||
*/
|
||||
|
||||
var private int CODEPOINT_ESCAPE;
|
||||
var private int CODEPOINT_NEWLINE;
|
||||
var private int COLOR_SEQUENCE_LENGTH;
|
||||
|
||||
// Display settings according to which to format our output
|
||||
var private ConsoleAPI.ConsoleDisplaySettings displaySettings;
|
||||
|
||||
/**
|
||||
* This structure is used to both share results of our work and for tracking
|
||||
* information about the line we are currently filling.
|
||||
*/
|
||||
struct LineRecord
|
||||
{
|
||||
// Contents of the line, in `STRING_Colored` format
|
||||
var string contents;
|
||||
// Is this a wrapped line?
|
||||
// `true` means that this line was supposed to be part part of another,
|
||||
// singular line of text, that had to be broken into smaller pieces.
|
||||
// Such lines will start with "|" in front of them in Acedia's
|
||||
// `ConsoleWriter`.
|
||||
var bool wrappedLine;
|
||||
// Information variables that describe how many visible and total symbols
|
||||
// (visible + color change sequences) are stored int the `line`
|
||||
var int visibleSymbolsStored;
|
||||
var int totalSymbolsStored;
|
||||
// Does `contents` contain a color change sequence?
|
||||
// Non-empty line can have no such sequence if they consist of whitespaces.
|
||||
var private bool colorInserted;
|
||||
// If `colorInserted == true`, stores the last inserted color.
|
||||
var private Color endColor;
|
||||
};
|
||||
// Lines that are ready to be output to the console
|
||||
var private array<LineRecord> completedLines;
|
||||
|
||||
// Line we are currently building
|
||||
var private LineRecord currentLine;
|
||||
// Word we are currently building, colors of it's characters will be
|
||||
// automatically converted into `STRCOLOR_Struct`, according to the default
|
||||
// color setting at the time of their addition.
|
||||
var private array<Text.Character> wordBuffer;
|
||||
// Amount of color swaps inside `wordBuffer`
|
||||
var private int colorSwapsInWordBuffer;
|
||||
|
||||
/**
|
||||
* Returns current setting used by this buffer to break up it's input into
|
||||
* lines fit to be output in console.
|
||||
*
|
||||
* @return Currently used `ConsoleDisplaySettings`.
|
||||
*/
|
||||
public final function ConsoleAPI.ConsoleDisplaySettings GetSettings()
|
||||
{
|
||||
return displaySettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets new setting to be used by this buffer to break up it's input into
|
||||
* lines fit to be output in console.
|
||||
*
|
||||
* It is recommended (although not required) to call `Flush()` before
|
||||
* changing settings. Not doing so would not lead to any errors or warnings,
|
||||
* but can lead to some wonky results and is considered an undefined behavior.
|
||||
*
|
||||
* @param newSettings New `ConsoleDisplaySettings` to be used.
|
||||
* @return Returns caller `ConsoleBuffer` to allow for method chaining.
|
||||
*/
|
||||
public final function ConsoleBuffer SetSettings(
|
||||
ConsoleAPI.ConsoleDisplaySettings newSettings)
|
||||
{
|
||||
displaySettings = newSettings;
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does caller `ConsoleBuffer` has any completed lines that can be output?
|
||||
*
|
||||
* "Completed line" means that nothing else will be added to it.
|
||||
* So negative (`false`) response does not mean that the buffer is empty, -
|
||||
* it can still contain an uncompleted and non-empty line that can still be
|
||||
* expanded with `InsertString()`. If you want to completely empty the buffer -
|
||||
* call the `Flush()` method.
|
||||
* Also see `IsEmpty()`.
|
||||
*
|
||||
* @return `true` if caller `ConsoleBuffer` has no completed lines and
|
||||
* `false` otherwise.
|
||||
*/
|
||||
public final function bool HasCompletedLines()
|
||||
{
|
||||
return (completedLines.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Does caller `ConsoleBuffer` has any unprocessed input?
|
||||
*
|
||||
* Note that `ConsoleBuffer` can be non-empty, but no completed line if it
|
||||
* currently builds one.
|
||||
* See `Flush()` and `HasCompletedLines()` methods.
|
||||
*
|
||||
* @return `true` if `ConsoleBuffer` is completely empty
|
||||
* (either did not receive or already returned all processed input) and
|
||||
* `false` otherwise.
|
||||
*/
|
||||
public final function bool IsEmpty()
|
||||
{
|
||||
if (HasCompletedLines()) return false;
|
||||
if (currentLine.totalSymbolsStored > 0) return false;
|
||||
if (wordBuffer.length > 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the buffer of all data, but leaving current settings intact.
|
||||
* After this calling method `IsEmpty()` should return `true`.
|
||||
*
|
||||
* @return Returns caller `ConsoleBuffer` to allow method chaining.
|
||||
*/
|
||||
public final function ConsoleBuffer Clear()
|
||||
{
|
||||
local LineRecord newLineRecord;
|
||||
currentLine = newLineRecord;
|
||||
completedLines.length = 0;
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a string into the buffer. This method does not automatically break
|
||||
* the line after the `input`, call `Flush()` or add line feed symbol "\n"
|
||||
* at the end of the `input` if you want that.
|
||||
*
|
||||
* @param input `string` to be added to the current line in caller
|
||||
* `ConsoleBuffer`.
|
||||
* @param inputType How to treat given `string` regarding coloring.
|
||||
* @return Returns caller `ConsoleBuffer` to allow method chaining.
|
||||
*/
|
||||
public final function ConsoleBuffer InsertString(
|
||||
string input,
|
||||
Text.StringType inputType)
|
||||
{
|
||||
local int inputConsumed;
|
||||
local array<Text.Character> rawInput;
|
||||
rawInput = _().text.StringToRaw(input, inputType);
|
||||
while (rawInput.length > 0)
|
||||
{
|
||||
// Fill word buffer, remove consumed input from `rawInput`
|
||||
inputConsumed = 0;
|
||||
while (inputConsumed < rawInput.length)
|
||||
{
|
||||
if (_().text.IsWhitespace(rawInput[inputConsumed])) break;
|
||||
InsertIntoWordBuffer(rawInput[inputConsumed]);
|
||||
inputConsumed += 1;
|
||||
}
|
||||
rawInput.Remove(0, inputConsumed);
|
||||
// If we didn't encounter any whitespace symbols - bail
|
||||
if (rawInput.length <= 0) {
|
||||
return self;
|
||||
}
|
||||
FlushWordBuffer();
|
||||
// Dump whitespaces into lines
|
||||
inputConsumed = 0;
|
||||
while (inputConsumed < rawInput.length)
|
||||
{
|
||||
if (!_().text.IsWhitespace(rawInput[inputConsumed])) break;
|
||||
AppendWhitespaceToCurrentLine(rawInput[inputConsumed]);
|
||||
inputConsumed += 1;
|
||||
}
|
||||
rawInput.Remove(0, inputConsumed);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns (and makes caller `ConsoleBuffer` forget) next completed line that
|
||||
* can be output to console in `STRING_Colored` format.
|
||||
*
|
||||
* If there are no completed line to return - returns an empty one.
|
||||
*
|
||||
* @return Next completed line that can be output, in `STRING_Colored` format.
|
||||
*/
|
||||
public final function LineRecord PopNextLine()
|
||||
{
|
||||
local LineRecord result;
|
||||
if (completedLines.length <= 0) return result;
|
||||
result = completedLines[0];
|
||||
completedLines.Remove(0, 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces all buffered data into "completed line" array, making it retrievable
|
||||
* by `PopNextLine()`.
|
||||
*
|
||||
* @return Next completed line that can be output, in `STRING_Colored` format.
|
||||
*/
|
||||
public final function ConsoleBuffer Flush()
|
||||
{
|
||||
FlushWordBuffer();
|
||||
BreakLine(false);
|
||||
return self;
|
||||
}
|
||||
|
||||
// It is assumed that passed characters are not whitespace, -
|
||||
// responsibility to check is on the one calling this method.
|
||||
private final function InsertIntoWordBuffer(Text.Character newCharacter)
|
||||
{
|
||||
local int newCharacterIndex;
|
||||
local Color oldColor, newColor;
|
||||
newCharacterIndex = wordBuffer.length;
|
||||
// Fix text color in the buffer to remember default color, if we use it.
|
||||
newCharacter.color =
|
||||
_().text.GetCharacterColor(newCharacter, displaySettings.defaultColor);
|
||||
newCharacter.colorType = STRCOLOR_Struct;
|
||||
wordBuffer[newCharacterIndex] = newCharacter;
|
||||
if (newCharacterIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
oldColor = wordBuffer[newCharacterIndex].color;
|
||||
newColor = wordBuffer[newCharacterIndex - 1].color;
|
||||
if (!_().color.AreEqual(oldColor, newColor, true)) {
|
||||
colorSwapsInWordBuffer += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Pushes whole `wordBuffer` into lines
|
||||
private final function FlushWordBuffer()
|
||||
{
|
||||
local int i;
|
||||
local Color newColor;
|
||||
if (!WordCanFitInCurrentLine() && WordCanFitInNewLine()) {
|
||||
BreakLine(true);
|
||||
}
|
||||
for (i = 0; i < wordBuffer.length; i += 1)
|
||||
{
|
||||
if (!CanAppendNonWhitespaceIntoLine(wordBuffer[i])) {
|
||||
BreakLine(true);
|
||||
}
|
||||
newColor = wordBuffer[i].color;
|
||||
if (MustSwapColorsFor(newColor))
|
||||
{
|
||||
currentLine.contents $= _().color.GetColorTag(newColor);
|
||||
currentLine.totalSymbolsStored += COLOR_SEQUENCE_LENGTH;
|
||||
currentLine.colorInserted = true;
|
||||
currentLine.endColor = newColor;
|
||||
}
|
||||
currentLine.contents $= Chr(wordBuffer[i].codePoint);
|
||||
currentLine.totalSymbolsStored += 1;
|
||||
currentLine.visibleSymbolsStored += 1;
|
||||
}
|
||||
wordBuffer.length = 0;
|
||||
colorSwapsInWordBuffer = 0;
|
||||
}
|
||||
|
||||
private final function BreakLine(bool makeWrapped)
|
||||
{
|
||||
local LineRecord newLineRecord;
|
||||
if (currentLine.visibleSymbolsStored > 0) {
|
||||
completedLines[completedLines.length] = currentLine;
|
||||
}
|
||||
currentLine = newLineRecord;
|
||||
currentLine.wrappedLine = makeWrapped;
|
||||
}
|
||||
|
||||
private final function bool MustSwapColorsFor(Color newColor)
|
||||
{
|
||||
if (!currentLine.colorInserted) return true;
|
||||
return !_().color.AreEqual(currentLine.endColor, newColor, true);
|
||||
}
|
||||
|
||||
private final function bool CanAppendWhitespaceIntoLine()
|
||||
{
|
||||
// We always allow to append at least something into empty line,
|
||||
// otherwise we can never insert it anywhere
|
||||
if (currentLine.totalSymbolsStored <= 0) return true;
|
||||
if (currentLine.totalSymbolsStored >= displaySettings.maxTotalLineWidth)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (currentLine.visibleSymbolsStored >= displaySettings.maxVisibleLineWidth)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private final function bool CanAppendNonWhitespaceIntoLine(
|
||||
Text.Character nextCharacter)
|
||||
{
|
||||
// We always allow to insert at least something into empty line,
|
||||
// otherwise we can never insert it anywhere
|
||||
if (currentLine.totalSymbolsStored <= 0) {
|
||||
return true;
|
||||
}
|
||||
// Check if we can fit a single character by fitting a whitespace symbol.
|
||||
if (!CanAppendWhitespaceIntoLine()) {
|
||||
return false;
|
||||
}
|
||||
if (!MustSwapColorsFor(nextCharacter.color)) {
|
||||
return true;
|
||||
}
|
||||
// Can we fit character + color swap sequence?
|
||||
return ( currentLine.totalSymbolsStored + COLOR_SEQUENCE_LENGTH + 1
|
||||
<= displaySettings.maxTotalLineWidth);
|
||||
}
|
||||
|
||||
// For performance reasons assumes that passed character is a whitespace,
|
||||
// the burden of checking is on the caller.
|
||||
private final function AppendWhitespaceToCurrentLine(Text.Character whitespace)
|
||||
{
|
||||
if (_().text.IsCodePoint(whitespace, CODEPOINT_NEWLINE)) {
|
||||
BreakLine(true);
|
||||
return;
|
||||
}
|
||||
if (!CanAppendWhitespaceIntoLine()) {
|
||||
BreakLine(true);
|
||||
}
|
||||
currentLine.contents $= Chr(whitespace.codePoint);
|
||||
currentLine.totalSymbolsStored += 1;
|
||||
currentLine.visibleSymbolsStored += 1;
|
||||
}
|
||||
|
||||
private final function bool WordCanFitInNewLine()
|
||||
{
|
||||
local int totalCharactersInWord;
|
||||
if (wordBuffer.length <= 0) return true;
|
||||
if (wordBuffer.length > displaySettings.maxVisibleLineWidth) {
|
||||
return false;
|
||||
}
|
||||
// `(colorSwapsInWordBuffer + 1)` counts how many times we must
|
||||
// switch color inside a word + 1 for setting initial color
|
||||
totalCharactersInWord = wordBuffer.length
|
||||
+ (colorSwapsInWordBuffer + 1) * COLOR_SEQUENCE_LENGTH;
|
||||
return (totalCharactersInWord <= displaySettings.maxTotalLineWidth);
|
||||
}
|
||||
|
||||
private final function bool WordCanFitInCurrentLine()
|
||||
{
|
||||
local int totalLimit, visibleLimit;
|
||||
local int totalCharactersInWord;
|
||||
if (wordBuffer.length <= 0) return true;
|
||||
totalLimit =
|
||||
displaySettings.maxTotalLineWidth - currentLine.totalSymbolsStored;
|
||||
visibleLimit =
|
||||
displaySettings.maxVisibleLineWidth - currentLine.visibleSymbolsStored;
|
||||
// Visible symbols check
|
||||
if (wordBuffer.length > visibleLimit) {
|
||||
return false;
|
||||
}
|
||||
// Total symbols check
|
||||
totalCharactersInWord = wordBuffer.length
|
||||
+ colorSwapsInWordBuffer * COLOR_SEQUENCE_LENGTH;
|
||||
if (MustSwapColorsFor(wordBuffer[0].color)) {
|
||||
totalCharactersInWord += COLOR_SEQUENCE_LENGTH;
|
||||
}
|
||||
return (totalCharactersInWord <= totalLimit);
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
CODEPOINT_ESCAPE = 27
|
||||
CODEPOINT_NEWLINE = 10
|
||||
// CODEPOINT_ESCAPE + <redByte> + <greenByte> + <blueByte>
|
||||
COLOR_SEQUENCE_LENGTH = 4
|
||||
}
|
||||
|
|
@ -1,373 +0,0 @@
|
|||
/**
|
||||
* Object that provides simple access to console output.
|
||||
* Can either write to a certain player's console or to all consoles at once.
|
||||
* Supports "fancy" and "raw" output (for more details @see `ConsoleAPI`).
|
||||
* Copyright 2020 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 ConsoleWriter extends AcediaObject
|
||||
dependson(ConsoleAPI)
|
||||
dependson(ConnectionService);
|
||||
|
||||
// Prefixes we output before every line to signify whether they were broken
|
||||
// or not
|
||||
var private string NEWLINE_PREFIX;
|
||||
var private string BROKENLINE_PREFIX;
|
||||
|
||||
/**
|
||||
* Describes current output target of the `ConsoleWriter`.
|
||||
*/
|
||||
enum ConsoleWriterTarget
|
||||
{
|
||||
// No one. Can happed if our target disconnects.
|
||||
CWTARGET_None,
|
||||
// A certain player.
|
||||
CWTARGET_Player,
|
||||
// All players.
|
||||
CWTARGET_All
|
||||
};
|
||||
var private ConsoleWriterTarget targetType;
|
||||
// Controller of the player that will receive output passed
|
||||
// to this `ConsoleWriter`.
|
||||
// Only used when `targetType == CWTARGET_Player`
|
||||
var private PlayerController outputTarget;
|
||||
var private ConsoleBuffer outputBuffer;
|
||||
|
||||
var private ConsoleAPI.ConsoleDisplaySettings displaySettings;
|
||||
|
||||
public final function ConsoleWriter Initialize(
|
||||
ConsoleAPI.ConsoleDisplaySettings newDisplaySettings)
|
||||
{
|
||||
displaySettings = newDisplaySettings;
|
||||
if (outputBuffer == none) {
|
||||
outputBuffer = ConsoleBuffer(_().memory.Allocate(class'ConsoleBuffer'));
|
||||
}
|
||||
else {
|
||||
outputBuffer.Clear();
|
||||
}
|
||||
outputBuffer.SetSettings(displaySettings);
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current default color for caller `ConsoleWriter`.
|
||||
*
|
||||
* This method returns default color, i.e. color that will be used if no other
|
||||
* is specified by text you're outputting.
|
||||
* If color is specified, this value will be ignored.
|
||||
*
|
||||
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||
* (or such value from any other `ConsoleWriter`) and affects only
|
||||
* output produced by this `ConsoleWriter`.
|
||||
*
|
||||
* @return Current default color.
|
||||
*/
|
||||
public final function Color GetColor()
|
||||
{
|
||||
return displaySettings.defaultColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets default color for caller 'ConsoleWriter`'s output.
|
||||
*
|
||||
* This only changes default color, i.e. color that will be used if no other is
|
||||
* specified by text you're outputting.
|
||||
* If color is specified, this value will be ignored.
|
||||
*
|
||||
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||
* (or such value from any other `ConsoleWriter`) and affects only
|
||||
* output produced by this `ConsoleWriter`.
|
||||
*
|
||||
* @param newDefaultColor New color to use when none specified by text itself.
|
||||
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||
*/
|
||||
public final function ConsoleWriter SetColor(Color newDefaultColor)
|
||||
{
|
||||
displaySettings.defaultColor = newDefaultColor;
|
||||
if (outputBuffer != none) {
|
||||
outputBuffer.SetSettings(displaySettings);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current visible limit that describes how many (at most)
|
||||
* visible characters can be output in the console line.
|
||||
*
|
||||
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||
* (or such value from any other `ConsoleWriter`) and affects only
|
||||
* output produced by this `ConsoleWriter`.
|
||||
*
|
||||
* @return Current global visible limit.
|
||||
*/
|
||||
public final function int GetVisibleLineLength()
|
||||
{
|
||||
return displaySettings.maxVisibleLineWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current visible limit that describes how many (at most) visible
|
||||
* characters can be output in the console line.
|
||||
*
|
||||
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||
* (or such value from any other `ConsoleWriter`) and affects only
|
||||
* output produced by this `ConsoleWriter`.
|
||||
*
|
||||
* @param newVisibleLimit New global visible limit.
|
||||
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||
*/
|
||||
public final function ConsoleWriter SetVisibleLineLength(
|
||||
int newMaxVisibleLineWidth
|
||||
)
|
||||
{
|
||||
displaySettings.maxVisibleLineWidth = newMaxVisibleLineWidth;
|
||||
if (outputBuffer != none) {
|
||||
outputBuffer.SetSettings(displaySettings);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return current total limit that describes how many (at most)
|
||||
* characters can be output in the console line.
|
||||
*
|
||||
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||
* (or such value from any other `ConsoleWriter`) and affects only
|
||||
* output produced by this `ConsoleWriter`.
|
||||
*
|
||||
* @return Current global total limit.
|
||||
*/
|
||||
public final function int GetTotalLineLength()
|
||||
{
|
||||
return displaySettings.maxTotalLineWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets current total limit that describes how many (at most)
|
||||
* characters can be output in the console line.
|
||||
*
|
||||
* This value is not synchronized with the global value from `ConsoleAPI`
|
||||
* (or such value from any other `ConsoleWriter`) and affects only
|
||||
* output produced by this `ConsoleWriter`.
|
||||
*
|
||||
* @param newTotalLimit New global total limit.
|
||||
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||
*/
|
||||
public final function ConsoleWriter SetTotalLineLength(int newMaxTotalLineWidth)
|
||||
{
|
||||
displaySettings.maxTotalLineWidth = newMaxTotalLineWidth;
|
||||
if (outputBuffer != none) {
|
||||
outputBuffer.SetSettings(displaySettings);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures caller `ConsoleWriter` to output to all players.
|
||||
* `Flush()` will be automatically called between target change.
|
||||
*
|
||||
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||
*/
|
||||
public final function ConsoleWriter ForAll()
|
||||
{
|
||||
Flush();
|
||||
targetType = CWTARGET_All;
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures caller `ConsoleWriter` to output only to a player,
|
||||
* given by a passed `PlayerController`.
|
||||
* `Flush()` will be automatically called between target change.
|
||||
*
|
||||
* @param targetController Player, to whom console we want to write.
|
||||
* If `none` - caller `ConsoleWriter` would be configured to
|
||||
* throw messages away.
|
||||
* @return ConsoleWriter Returns caller `ConsoleWriter` to allow for
|
||||
* method chaining.
|
||||
*/
|
||||
public final function ConsoleWriter ForController(
|
||||
PlayerController targetController
|
||||
)
|
||||
{
|
||||
Flush();
|
||||
if (targetController != none)
|
||||
{
|
||||
targetType = CWTARGET_Player;
|
||||
outputTarget = targetController;
|
||||
}
|
||||
else {
|
||||
targetType = CWTARGET_None;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns type of current target for the caller `ConsoleWriter`.
|
||||
*
|
||||
* @return `ConsoleWriterTarget` value, describing current target of
|
||||
* the caller `ConsoleWriter`.
|
||||
*/
|
||||
public final function ConsoleWriterTarget CurrentTarget()
|
||||
{
|
||||
if (targetType == CWTARGET_Player && outputTarget == none) {
|
||||
targetType = CWTARGET_None;
|
||||
}
|
||||
return targetType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `PlayerController` of the player to whom console caller
|
||||
* `ConsoleWriter` is outputting messages.
|
||||
*
|
||||
* @return `PlayerController` of the player to whom console caller
|
||||
* `ConsoleWriter` is outputting messages.
|
||||
* Returns `none` iff it currently outputs to every player or to no one.
|
||||
*/
|
||||
public final function PlayerController GetTargetPlayerController()
|
||||
{
|
||||
if (targetType == CWTARGET_All) return none;
|
||||
return outputTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs all buffered input and moves further output onto a new line.
|
||||
*
|
||||
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||
*/
|
||||
public final function ConsoleWriter Flush()
|
||||
{
|
||||
outputBuffer.Flush();
|
||||
SendBuffer();
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a formatted string into console.
|
||||
*
|
||||
* Does not trigger console output, for that use `WriteLine()` or `Flush()`.
|
||||
*
|
||||
* To output a different type of string into a console, use `WriteT()`.
|
||||
*
|
||||
* @param message Formatted string to output.
|
||||
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||
*/
|
||||
public final function ConsoleWriter Write(string message)
|
||||
{
|
||||
outputBuffer.InsertString(message, STRING_Formatted);
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a formatted string into console.
|
||||
* Result will be output immediately, starts a new line.
|
||||
*
|
||||
* To output a different type of string into a console, use `WriteLineT()`.
|
||||
*
|
||||
* @param message Formatted string to output.
|
||||
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||
*/
|
||||
public final function ConsoleWriter WriteLine(string message)
|
||||
{
|
||||
outputBuffer.InsertString(message, STRING_Formatted);
|
||||
Flush();
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a `string` of specified type into console.
|
||||
*
|
||||
* Does not trigger console output, for that use `WriteLineT()` or `Flush()`.
|
||||
*
|
||||
* To output a formatted string you might want to simply use `Write()`.
|
||||
*
|
||||
* @param message String of a given type to output.
|
||||
* @param inputType Type of the string method should output.
|
||||
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||
*/
|
||||
public final function ConsoleWriter WriteT(
|
||||
string message,
|
||||
Text.StringType inputType)
|
||||
{
|
||||
outputBuffer.InsertString(message, inputType);
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a `string` of specified type into console.
|
||||
* Result will be output immediately, starts a new line.
|
||||
*
|
||||
* To output a formatted string you might want to simply use `WriteLine()`.
|
||||
*
|
||||
* @param message String of a given type to output.
|
||||
* @param inputType Type of the string method should output.
|
||||
* @return Returns caller `ConsoleWriter` to allow for method chaining.
|
||||
*/
|
||||
public final function ConsoleWriter WriteLineT(
|
||||
string message,
|
||||
Text.StringType inputType)
|
||||
{
|
||||
outputBuffer.InsertString(message, inputType);
|
||||
Flush();
|
||||
return self;
|
||||
}
|
||||
|
||||
// Send all completed lines from an `outputBuffer`
|
||||
private final function SendBuffer()
|
||||
{
|
||||
local string prefix;
|
||||
local ConnectionService service;
|
||||
local ConsoleBuffer.LineRecord nextLineRecord;
|
||||
while (outputBuffer.HasCompletedLines())
|
||||
{
|
||||
nextLineRecord = outputBuffer.PopNextLine();
|
||||
if (nextLineRecord.wrappedLine) {
|
||||
prefix = NEWLINE_PREFIX;
|
||||
}
|
||||
else {
|
||||
prefix = BROKENLINE_PREFIX;
|
||||
}
|
||||
service = ConnectionService(class'ConnectionService'.static.Require());
|
||||
SendConsoleMessage(service, prefix $ nextLineRecord.contents);
|
||||
}
|
||||
}
|
||||
|
||||
// Assumes `service != none`, caller function must ensure that.
|
||||
private final function SendConsoleMessage(
|
||||
ConnectionService service,
|
||||
string message)
|
||||
{
|
||||
local int i;
|
||||
local array<ConnectionService.Connection> connections;
|
||||
if (targetType != CWTARGET_All)
|
||||
{
|
||||
if (outputTarget != none) {
|
||||
outputTarget.ClientMessage(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
connections = service.GetActiveConnections();
|
||||
for (i = 0; i < connections.length; i += 1) {
|
||||
connections[i].controllerReference.ClientMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
NEWLINE_PREFIX = "| "
|
||||
BROKENLINE_PREFIX = " "
|
||||
}
|
||||
|
|
@ -1,351 +0,0 @@
|
|||
/**
|
||||
* This class implements JSON array storage capabilities.
|
||||
* Array stores ordered JSON values that can be referred by their index.
|
||||
* It can contain any mix of JSON value types and cannot have any gaps,
|
||||
* i.e. in array of length N, there must be a valid value for all indices
|
||||
* from 0 to N-1.
|
||||
* Copyright 2020 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 JArray extends JSON;
|
||||
|
||||
// Data will simply be stored as an array of JSON values
|
||||
var private array<JStorageAtom> data;
|
||||
|
||||
// Return type of value stored at a given index.
|
||||
// Returns `JSON_Undefined` if and only if given index is out of bounds.
|
||||
public final function JType GetTypeOf(int index)
|
||||
{
|
||||
if (index < 0) return JSON_Undefined;
|
||||
if (index >= data.length) return JSON_Undefined;
|
||||
|
||||
return data[index].type;
|
||||
}
|
||||
|
||||
// Returns current length of this array.
|
||||
public final function int GetLength()
|
||||
{
|
||||
return data.length;
|
||||
}
|
||||
|
||||
// Changes length of this array.
|
||||
// In case of the increase - fills new indices with `null` values.
|
||||
public final function SetLength(int newLength)
|
||||
{
|
||||
local int i;
|
||||
local int oldLength;
|
||||
oldLength = data.length;
|
||||
data.length = newLength;
|
||||
if (oldLength >= newLength)
|
||||
{
|
||||
return;
|
||||
}
|
||||
i = oldLength;
|
||||
while (i < newLength)
|
||||
{
|
||||
SetNull(i);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Following functions are getters for various types of variables.
|
||||
// Getter for null value simply checks if it's null
|
||||
// and returns true/false as a result.
|
||||
// Getters for simple types (number, string, boolean) can have optional
|
||||
// default value specified, that will be returned if requested variable
|
||||
// doesn't exist or has a different type.
|
||||
// Getters for object and array types don't take default values and
|
||||
// will simply return `none`.
|
||||
public final function float GetNumber(int index, optional float defaultValue)
|
||||
{
|
||||
if (index < 0) return defaultValue;
|
||||
if (index >= data.length) return defaultValue;
|
||||
if (data[index].type != JSON_Number) return defaultValue;
|
||||
|
||||
return data[index].numberValue;
|
||||
}
|
||||
|
||||
public final function string GetString(int index, optional string defaultValue)
|
||||
{
|
||||
if (index < 0) return defaultValue;
|
||||
if (index >= data.length) return defaultValue;
|
||||
if (data[index].type != JSON_String) return defaultValue;
|
||||
|
||||
return data[index].stringValue;
|
||||
}
|
||||
|
||||
public final function bool GetBoolean(int index, optional bool defaultValue)
|
||||
{
|
||||
if (index < 0) return defaultValue;
|
||||
if (index >= data.length) return defaultValue;
|
||||
if (data[index].type != JSON_Boolean) return defaultValue;
|
||||
|
||||
return data[index].booleanValue;
|
||||
}
|
||||
|
||||
public final function bool IsNull(int index)
|
||||
{
|
||||
if (index < 0) return false;
|
||||
if (index >= data.length) return false;
|
||||
|
||||
return (data[index].type == JSON_Null);
|
||||
}
|
||||
|
||||
public final function JArray GetArray(int index)
|
||||
{
|
||||
if (index < 0) return none;
|
||||
if (index >= data.length) return none;
|
||||
if (data[index].type != JSON_Array) return none;
|
||||
|
||||
return JArray(data[index].complexValue);
|
||||
}
|
||||
|
||||
public final function JObject GetObject(int index)
|
||||
{
|
||||
if (index < 0) return none;
|
||||
if (index >= data.length) return none;
|
||||
if (data[index].type != JSON_Object) return none;
|
||||
|
||||
return JObject(data[index].complexValue);
|
||||
}
|
||||
|
||||
// Following functions provide simple setters for boolean, string, number
|
||||
// and null values.
|
||||
// If passed index is negative - does nothing.
|
||||
// If index lies beyond array length (`>= GetLength()`), -
|
||||
// these functions will expand array in the same way as `GetLength()` function.
|
||||
// This can be prevented by setting optional parameter `preventExpansion` to
|
||||
// `false` (nothing will be done in this case).
|
||||
// They return object itself, allowing user to chain calls like this:
|
||||
// `array.SetNumber("num1", 1).SetNumber("num2", 2);`.
|
||||
public final function JArray SetNumber
|
||||
(
|
||||
int index,
|
||||
float value,
|
||||
optional bool preventExpansion
|
||||
)
|
||||
{
|
||||
local JStorageAtom newStorageValue;
|
||||
if (index < 0) return self;
|
||||
|
||||
if (index >= data.length)
|
||||
{
|
||||
if (preventExpansion)
|
||||
{
|
||||
return self;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLength(index + 1);
|
||||
}
|
||||
}
|
||||
newStorageValue.type = JSON_Number;
|
||||
newStorageValue.numberValue = value;
|
||||
data[index] = newStorageValue;
|
||||
return self;
|
||||
}
|
||||
|
||||
public final function JArray SetString
|
||||
(
|
||||
int index,
|
||||
string value,
|
||||
optional bool preventExpansion
|
||||
)
|
||||
{
|
||||
local JStorageAtom newStorageValue;
|
||||
if (index < 0) return self;
|
||||
|
||||
if (index >= data.length)
|
||||
{
|
||||
if (preventExpansion)
|
||||
{
|
||||
return self;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLength(index + 1);
|
||||
}
|
||||
}
|
||||
newStorageValue.type = JSON_String;
|
||||
newStorageValue.stringValue = value;
|
||||
data[index] = newStorageValue;
|
||||
return self;
|
||||
}
|
||||
|
||||
public final function JArray SetBoolean
|
||||
(
|
||||
int index,
|
||||
bool value,
|
||||
optional bool preventExpansion
|
||||
)
|
||||
{
|
||||
local JStorageAtom newStorageValue;
|
||||
if (index < 0) return self;
|
||||
|
||||
if (index >= data.length)
|
||||
{
|
||||
if (preventExpansion)
|
||||
{
|
||||
return self;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLength(index + 1);
|
||||
}
|
||||
}
|
||||
newStorageValue.type = JSON_Boolean;
|
||||
newStorageValue.booleanValue = value;
|
||||
data[index] = newStorageValue;
|
||||
return self;
|
||||
}
|
||||
|
||||
public final function JArray SetNull
|
||||
(
|
||||
int index,
|
||||
optional bool preventExpansion
|
||||
)
|
||||
{
|
||||
local JStorageAtom newStorageValue;
|
||||
if (index < 0) return self;
|
||||
|
||||
if (index >= data.length)
|
||||
{
|
||||
if (preventExpansion)
|
||||
{
|
||||
return self;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLength(index + 1);
|
||||
}
|
||||
}
|
||||
newStorageValue.type = JSON_Null;
|
||||
data[index] = newStorageValue;
|
||||
return self;
|
||||
}
|
||||
|
||||
// JSON array and object types don't have setters, but instead have
|
||||
// functions to create a new, empty array/object under a certain name.
|
||||
// If passed index is negative - does nothing.
|
||||
// If index lies beyond array length (`>= GetLength()`), -
|
||||
// these functions will expand array in the same way as `GetLength()` function.
|
||||
// This can be prevented by setting optional parameter `preventExpansion` to
|
||||
// `false` (nothing will be done in this case).
|
||||
// They return object itself, allowing user to chain calls like this:
|
||||
// `array.CreateObject("sub object").CreateArray("sub array");`.
|
||||
public final function JArray CreateArray
|
||||
(
|
||||
int index,
|
||||
optional bool preventExpansion
|
||||
)
|
||||
{
|
||||
local JStorageAtom newStorageValue;
|
||||
if (index < 0) return self;
|
||||
|
||||
if (index >= data.length)
|
||||
{
|
||||
if (preventExpansion)
|
||||
{
|
||||
return self;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLength(index + 1);
|
||||
}
|
||||
}
|
||||
newStorageValue.type = JSON_Array;
|
||||
newStorageValue.complexValue = _.json.newArray();
|
||||
data[index] = newStorageValue;
|
||||
return self;
|
||||
}
|
||||
|
||||
public final function JArray CreateObject
|
||||
(
|
||||
int index,
|
||||
optional bool preventExpansion
|
||||
)
|
||||
{
|
||||
local JStorageAtom newStorageValue;
|
||||
if (index < 0) return self;
|
||||
|
||||
if (index >= data.length)
|
||||
{
|
||||
if (preventExpansion)
|
||||
{
|
||||
return self;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetLength(index + 1);
|
||||
}
|
||||
}
|
||||
newStorageValue.type = JSON_Object;
|
||||
newStorageValue.complexValue = _.json.newObject();
|
||||
data[index] = newStorageValue;
|
||||
return self;
|
||||
}
|
||||
|
||||
// Wrappers for setter functions that don't take index or
|
||||
// `preventExpansion` parameters and add/create value at the end of the array.
|
||||
public final function JArray AddNumber(float value)
|
||||
{
|
||||
return SetNumber(data.length, value);
|
||||
}
|
||||
|
||||
public final function JArray AddString(string value)
|
||||
{
|
||||
return SetString(data.length, value);
|
||||
}
|
||||
|
||||
public final function JArray AddBoolean(bool value)
|
||||
{
|
||||
return SetBoolean(data.length, value);
|
||||
}
|
||||
|
||||
public final function JArray AddNull()
|
||||
{
|
||||
return SetNull(data.length);
|
||||
}
|
||||
|
||||
public final function JArray AddArray()
|
||||
{
|
||||
return CreateArray(data.length);
|
||||
}
|
||||
|
||||
public final function JArray AddObject()
|
||||
{
|
||||
return CreateObject(data.length);
|
||||
}
|
||||
|
||||
// Removes up to `amount` (minimum of `1`) of values, starting from
|
||||
// a given index.
|
||||
// If `index` falls outside array boundaries - nothing will be done.
|
||||
// Returns `true` if value was actually removed and `false` if it didn't exist.
|
||||
public final function bool RemoveValue(int index, optional int amount)
|
||||
{
|
||||
if (index < 0) return false;
|
||||
if (index >= data.length) return false;
|
||||
|
||||
amount = Max(amount, 1);
|
||||
amount = Min(amount, data.length - index);
|
||||
data.Remove(index, amount);
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
/**
|
||||
* This class implements JSON object storage capabilities.
|
||||
* Whenever one wants to store JSON data, they need to define such object.
|
||||
* It stores name-value pairs, where names are strings and values can be:
|
||||
* ~ Boolean, string, null or number (float in this implementation) data;
|
||||
* ~ Other JSON objects;
|
||||
* ~ JSON Arrays (see `JArray` class).
|
||||
*
|
||||
* This implementation provides getters and setters for boolean, string,
|
||||
* null or number types that allow to freely set and fetch their values
|
||||
* by name.
|
||||
* JSON objects and arrays can be fetched by getters, but you cannot
|
||||
* add existing object or array to another object. Instead one has to create
|
||||
* a new, empty object with a certain name and then fill it with data.
|
||||
* This allows to avoid loop situations, where object is contained in itself.
|
||||
* Functions to remove existing values are also provided and are applicable
|
||||
* to all variable types.
|
||||
* Setters can also be used to overwrite any value by a different value,
|
||||
* even of a different type.
|
||||
* Copyright 2020 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 JObject extends JSON;
|
||||
|
||||
// We will store all our properties as a simple array of name-value pairs.
|
||||
struct JProperty
|
||||
{
|
||||
var string name;
|
||||
var JStorageAtom value;
|
||||
};
|
||||
var private array<JProperty> properties;
|
||||
|
||||
// Returns index of name-value pair in `properties` for a given name.
|
||||
// Returns `-1` if such a pair does not exist.
|
||||
private final function int GetPropertyIndex(string name)
|
||||
{
|
||||
local int i;
|
||||
for (i = 0; i < properties.length; i += 1)
|
||||
{
|
||||
if (name == properties[i].name)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Returns `JType` of a variable with a given name in our properties.
|
||||
// This function can be used to check if certain variable exists
|
||||
// in this object, since if such variable does not exist -
|
||||
// function will return `JSON_Undefined`.
|
||||
public final function JType GetTypeOf(string name)
|
||||
{
|
||||
local int index;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0) return JSON_Undefined;
|
||||
|
||||
return properties[index].value.type;
|
||||
}
|
||||
|
||||
// Following functions are getters for various types of variables.
|
||||
// Getter for null value simply checks if it's null
|
||||
// and returns true/false as a result.
|
||||
// Getters for simple types (number, string, boolean) can have optional
|
||||
// default value specified, that will be returned if requested variable
|
||||
// doesn't exist or has a different type.
|
||||
// Getters for object and array types don't take default values and
|
||||
// will simply return `none`.
|
||||
public final function float GetNumber(string name, optional float defaultValue)
|
||||
{
|
||||
local int index;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0) return defaultValue;
|
||||
if (properties[index].value.type != JSON_Number) return defaultValue;
|
||||
|
||||
return properties[index].value.numberValue;
|
||||
}
|
||||
|
||||
public final function string GetString
|
||||
(
|
||||
string name,
|
||||
optional string defaultValue
|
||||
)
|
||||
{
|
||||
local int index;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0) return defaultValue;
|
||||
if (properties[index].value.type != JSON_String) return defaultValue;
|
||||
|
||||
return properties[index].value.stringValue;
|
||||
}
|
||||
|
||||
public final function bool GetBoolean(string name, optional bool defaultValue)
|
||||
{
|
||||
local int index;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0) return defaultValue;
|
||||
if (properties[index].value.type != JSON_Boolean) return defaultValue;
|
||||
|
||||
return properties[index].value.booleanValue;
|
||||
}
|
||||
|
||||
public final function bool IsNull(string name)
|
||||
{
|
||||
local int index;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0) return false;
|
||||
if (properties[index].value.type != JSON_Null) return false;
|
||||
|
||||
return (properties[index].value.type == JSON_Null);
|
||||
}
|
||||
|
||||
public final function JArray GetArray(string name)
|
||||
{
|
||||
local int index;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0) return none;
|
||||
if (properties[index].value.type != JSON_Array) return none;
|
||||
|
||||
return JArray(properties[index].value.complexValue);
|
||||
}
|
||||
|
||||
public final function JObject GetObject(string name)
|
||||
{
|
||||
local int index;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0) return none;
|
||||
if (properties[index].value.type != JSON_Object) return none;
|
||||
|
||||
return JObject(properties[index].value.complexValue);
|
||||
}
|
||||
|
||||
// Following functions provide simple setters for boolean, string, number
|
||||
// and null values.
|
||||
// They return object itself, allowing user to chain calls like this:
|
||||
// `object.SetNumber("num1", 1).SetNumber("num2", 2);`.
|
||||
public final function JObject SetNumber(string name, float value)
|
||||
{
|
||||
local int index;
|
||||
local JProperty newProperty;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0)
|
||||
{
|
||||
index = properties.length;
|
||||
}
|
||||
|
||||
newProperty.name = name;
|
||||
newProperty.value.type = JSON_Number;
|
||||
newProperty.value.numberValue = value;
|
||||
properties[index] = newProperty;
|
||||
return self;
|
||||
}
|
||||
|
||||
public final function JObject SetString(string name, string value)
|
||||
{
|
||||
local int index;
|
||||
local JProperty newProperty;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0)
|
||||
{
|
||||
index = properties.length;
|
||||
}
|
||||
newProperty.name = name;
|
||||
newProperty.value.type = JSON_String;
|
||||
newProperty.value.stringValue = value;
|
||||
properties[index] = newProperty;
|
||||
return self;
|
||||
}
|
||||
|
||||
public final function JObject SetBoolean(string name, bool value)
|
||||
{
|
||||
local int index;
|
||||
local JProperty newProperty;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0)
|
||||
{
|
||||
index = properties.length;
|
||||
}
|
||||
newProperty.name = name;
|
||||
newProperty.value.type = JSON_Boolean;
|
||||
newProperty.value.booleanValue = value;
|
||||
properties[index] = newProperty;
|
||||
return self;
|
||||
}
|
||||
|
||||
public final function JObject SetNull(string name)
|
||||
{
|
||||
local int index;
|
||||
local JProperty newProperty;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0)
|
||||
{
|
||||
index = properties.length;
|
||||
}
|
||||
newProperty.name = name;
|
||||
newProperty.value.type = JSON_Null;
|
||||
properties[index] = newProperty;
|
||||
return self;
|
||||
}
|
||||
|
||||
// JSON array and object types don't have setters, but instead have
|
||||
// functions to create a new, empty array/object under a certain name.
|
||||
// They return object itself, allowing user to chain calls like this:
|
||||
// `object.CreateObject("folded object").CreateArray("names list");`.
|
||||
public final function JObject CreateArray(string name)
|
||||
{
|
||||
local int index;
|
||||
local JProperty newProperty;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0)
|
||||
{
|
||||
index = properties.length;
|
||||
}
|
||||
newProperty.name = name;
|
||||
newProperty.value.type = JSON_Array;
|
||||
newProperty.value.complexValue = _.json.newArray();
|
||||
properties[index] = newProperty;
|
||||
return self;
|
||||
}
|
||||
|
||||
public final function JObject CreateObject(string name)
|
||||
{
|
||||
local int index;
|
||||
local JProperty newProperty;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0)
|
||||
{
|
||||
index = properties.length;
|
||||
}
|
||||
newProperty.name = name;
|
||||
newProperty.value.type = JSON_Object;
|
||||
newProperty.value.complexValue = _.json.newObject();
|
||||
properties[index] = newProperty;
|
||||
return self;
|
||||
}
|
||||
|
||||
// Removes values with a given name.
|
||||
// Returns `true` if value was actually removed and `false` if it didn't exist.
|
||||
public final function bool RemoveValue(string name)
|
||||
{
|
||||
local int index;
|
||||
index = GetPropertyIndex(name);
|
||||
if (index < 0) return false;
|
||||
|
||||
properties.Remove(index, 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
/**
|
||||
* JSON is an open standard file format, and data interchange format,
|
||||
* that uses human-readable text to store and transmit data objects
|
||||
* consisting of name–value pairs and array data types.
|
||||
* For more information refer to https://en.wikipedia.org/wiki/JSON
|
||||
* This is a base class for implementation of JSON data storage for Acedia.
|
||||
* It does not implement parsing and printing from/into human-readable
|
||||
* text representation, just provides means to store such information.
|
||||
*
|
||||
* JSON data is stored as an object (represented via `JSONObject`) that
|
||||
* contains a set of name-value pairs, where value can be
|
||||
* a number, string, boolean value, another object or
|
||||
* an array (represented by `JSONArray`).
|
||||
* Copyright 2020 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 JSON extends AcediaActor
|
||||
abstract;
|
||||
|
||||
// Enumeration for possible types of JSON values.
|
||||
enum JType
|
||||
{
|
||||
// Technical type, used to indicate that requested value is missing.
|
||||
// Undefined values are not part of JSON format.
|
||||
JSON_Undefined,
|
||||
// An empty value, in teste representation defined by a single word "null".
|
||||
JSON_Null,
|
||||
// A number, recorded as a float.
|
||||
// JSON itself doesn't specify whether number is an integer or float.
|
||||
JSON_Number,
|
||||
// A string.
|
||||
JSON_String,
|
||||
// A bool value.
|
||||
JSON_Boolean,
|
||||
// Array of other JSON values, stored without names;
|
||||
// Single array can contain any mix of value types.
|
||||
JSON_Array,
|
||||
// Another JSON object, i.e. associative array of name-value pairs
|
||||
JSON_Object
|
||||
};
|
||||
|
||||
// Stores a single JSON value
|
||||
struct JStorageAtom
|
||||
{
|
||||
// What type is stored exactly?
|
||||
// Depending on that, uses one of the other fields as a storage.
|
||||
var protected JType type;
|
||||
var protected float numberValue;
|
||||
var protected string stringValue;
|
||||
var protected bool booleanValue;
|
||||
// Used for storing both JSON objects and arrays.
|
||||
var protected JSON complexValue;
|
||||
};
|
||||
|
||||
// TODO: Rewrite JSON object to use more efficient storage data structures
|
||||
// that will support subtypes:
|
||||
// ~ Number: byte, int, float
|
||||
// ~ String: string, class
|
||||
// (maybe move to auto generated code?).
|
||||
// TODO: Add cleanup queue to efficiently and without crashes clean up
|
||||
// removed objects.
|
||||
// TODO: Add `JValue` - a reference type for number / string / boolean / null
|
||||
// TODO: Add accessors for last values.
|
||||
// TODO: Add path-getters.
|
||||
// TODO: Add iterators.
|
||||
// TODO: Add parsing/printing.
|
||||
// TODO: Add functions for deep copy.
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
/**
|
||||
* Provides convenient access to JSON-related functions.
|
||||
* Copyright 2019 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 JSONAPI extends Singleton;
|
||||
|
||||
public function JObject newObject()
|
||||
{
|
||||
local JObject newObject;
|
||||
newObject = Spawn(class'JObject');
|
||||
return newObject;
|
||||
}
|
||||
|
||||
public function JArray newArray()
|
||||
{
|
||||
local JArray newArray;
|
||||
newArray = Spawn(class'JArray');
|
||||
return newArray;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,711 +0,0 @@
|
|||
/**
|
||||
* Set of tests for JSON data storage, implemented via
|
||||
* `JObject` and `JArray`.
|
||||
* Copyright 2020 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_JSON extends TestCase
|
||||
abstract;
|
||||
|
||||
protected static function TESTS()
|
||||
{
|
||||
local JObject jsonData;
|
||||
jsonData = _().json.newObject();
|
||||
Test_ObjectGetSetRemove();
|
||||
Test_ArrayGetSetRemove();
|
||||
}
|
||||
|
||||
protected static function Test_ObjectGetSetRemove()
|
||||
{
|
||||
SubTest_Undefined();
|
||||
SubTest_StringGetSetRemove();
|
||||
SubTest_BooleanGetSetRemove();
|
||||
SubTest_NumberGetSetRemove();
|
||||
SubTest_NullGetSetRemove();
|
||||
SubTest_MultipleVariablesGetSet();
|
||||
SubTest_Object();
|
||||
}
|
||||
|
||||
protected static function Test_ArrayGetSetRemove()
|
||||
{
|
||||
Context("Testing get/set/remove functions for JSON arrays");
|
||||
SubTest_ArrayUndefined();
|
||||
SubTest_ArrayStringGetSetRemove();
|
||||
SubTest_ArrayBooleanGetSetRemove();
|
||||
SubTest_ArrayNumberGetSetRemove();
|
||||
SubTest_ArrayNullGetSetRemove();
|
||||
SubTest_ArrayMultipleVariablesStorage();
|
||||
SubTest_ArrayMultipleVariablesRemoval();
|
||||
SubTest_ArrayRemovingMultipleVariablesAtOnce();
|
||||
SubTest_ArrayExpansions();
|
||||
}
|
||||
|
||||
protected static function SubTest_Undefined()
|
||||
{
|
||||
local JObject testJSON;
|
||||
testJSON = _().json.newObject();
|
||||
|
||||
Context("Testing how `JObject` handles undefined values");
|
||||
Issue("Undefined variable doesn't have proper type.");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Undefined);
|
||||
|
||||
Issue("There is a variable in an empty object after `GetTypeOf` call.");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Undefined);
|
||||
|
||||
Issue("Getters don't return default values for undefined variables.");
|
||||
TEST_ExpectTrue(testJSON.GetNumber("some_var", 0) == 0);
|
||||
TEST_ExpectTrue(testJSON.GetString("some_var", "") == "");
|
||||
TEST_ExpectTrue(testJSON.GetBoolean("some_var", false) == false);
|
||||
TEST_ExpectNone(testJSON.GetObject("some_var"));
|
||||
TEST_ExpectNone(testJSON.GetArray("some_var"));
|
||||
}
|
||||
|
||||
protected static function SubTest_BooleanGetSetRemove()
|
||||
{
|
||||
local JObject testJSON;
|
||||
testJSON = _().json.newObject();
|
||||
testJSON.SetBoolean("some_boolean", true);
|
||||
|
||||
Context("Testing `JObject`'s get/set/remove functions for" @
|
||||
"boolean variables");
|
||||
Issue("Boolean type isn't properly set by `SetBoolean`");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf("some_boolean") == JSON_Boolean);
|
||||
|
||||
Issue("Variable value is incorrectly assigned by `SetBoolean`");
|
||||
TEST_ExpectTrue(testJSON.GetBoolean("some_boolean") == true);
|
||||
|
||||
Issue("Variable value isn't correctly reassigned by `SetBoolean`");
|
||||
testJSON.SetBoolean("some_boolean", false);
|
||||
TEST_ExpectTrue(testJSON.GetBoolean("some_boolean") == false);
|
||||
|
||||
Issue( "Getting boolean variable as a wrong type" @
|
||||
"doesn't yield default value");
|
||||
TEST_ExpectTrue(testJSON.GetNumber("some_boolean", 7) == 7);
|
||||
|
||||
Issue("Boolean variable isn't being properly removed");
|
||||
testJSON.RemoveValue("some_boolean");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf("some_boolean") == JSON_Undefined);
|
||||
|
||||
Issue( "Getters don't return default value for missing key that" @
|
||||
"previously stored boolean value, that got removed");
|
||||
TEST_ExpectTrue(testJSON.GetBoolean("some_boolean", true) == true);
|
||||
}
|
||||
|
||||
protected static function SubTest_StringGetSetRemove()
|
||||
{
|
||||
local JObject testJSON;
|
||||
testJSON = _().json.newObject();
|
||||
testJSON.SetString("some_string", "first string");
|
||||
|
||||
Context("Testing `JObject`'s get/set/remove functions for" @
|
||||
"string variables");
|
||||
Issue("String type isn't properly set by `SetString`");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf("some_string") == JSON_String);
|
||||
|
||||
Issue("Value is incorrectly assigned by `SetString`");
|
||||
TEST_ExpectTrue(testJSON.GetString("some_string") == "first string");
|
||||
|
||||
Issue( "Providing default variable value makes 'GetString'" @
|
||||
"return wrong value");
|
||||
TEST_ExpectTrue( testJSON.GetString("some_string", "alternative")
|
||||
== "first string");
|
||||
|
||||
Issue("Variable value isn't correctly reassigned by `SetString`");
|
||||
testJSON.SetString("some_string", "new string!~");
|
||||
TEST_ExpectTrue(testJSON.GetString("some_string") == "new string!~");
|
||||
|
||||
Issue( "Getting string variable as a wrong type" @
|
||||
"doesn't yield default value");
|
||||
TEST_ExpectTrue(testJSON.GetBoolean("some_string", true) == true);
|
||||
|
||||
Issue("String variable isn't being properly removed");
|
||||
testJSON.RemoveValue("some_string");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf("some_string") == JSON_Undefined);
|
||||
|
||||
Issue( "Getters don't return default value for missing key that" @
|
||||
"previously stored string value, but got removed");
|
||||
TEST_ExpectTrue(testJSON.GetString("some_string", "other") == "other");
|
||||
}
|
||||
|
||||
protected static function SubTest_NumberGetSetRemove()
|
||||
{
|
||||
local JObject testJSON;
|
||||
testJSON = _().json.newObject();
|
||||
testJSON.SetNumber("some_number", 3.5);
|
||||
|
||||
Context("Testing `JObject`'s get/set/remove functions for" @
|
||||
"number variables");
|
||||
Issue("Number type isn't properly set by `SetNumber`");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf("some_number") == JSON_Number);
|
||||
|
||||
Issue("Value is incorrectly assigned by `SetNumber`");
|
||||
TEST_ExpectTrue(testJSON.GetNumber("some_number") == 3.5);
|
||||
|
||||
Issue( "Providing default variable value makes 'GetNumber'" @
|
||||
"return wrong value");
|
||||
TEST_ExpectTrue(testJSON.GetNumber("some_number", 5) == 3.5);
|
||||
|
||||
Issue("Variable value isn't correctly reassigned by `SetNumber`");
|
||||
testJSON.SetNumber("some_number", 7);
|
||||
TEST_ExpectTrue(testJSON.GetNumber("some_number") == 7);
|
||||
|
||||
Issue( "Getting number variable as a wrong type" @
|
||||
"doesn't yield default value.");
|
||||
TEST_ExpectTrue(testJSON.GetString("some_number", "default") == "default");
|
||||
|
||||
Issue("Number type isn't being properly removed");
|
||||
testJSON.RemoveValue("some_number");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf("some_number") == JSON_Undefined);
|
||||
|
||||
Issue( "Getters don't return default value for missing key that" @
|
||||
"previously stored number value, that got removed");
|
||||
TEST_ExpectTrue(testJSON.GetNumber("some_number", 13) == 13);
|
||||
}
|
||||
|
||||
protected static function SubTest_NullGetSetRemove()
|
||||
{
|
||||
local JObject testJSON;
|
||||
testJSON = _().json.newObject();
|
||||
|
||||
Context("Testing `JObject`'s get/set/remove functions for" @
|
||||
"null values");
|
||||
Issue("Undefined variable is incorrectly considered `null`");
|
||||
TEST_ExpectFalse(testJSON.IsNull("some_var"));
|
||||
|
||||
Issue("Number variable is incorrectly considered `null`");
|
||||
testJSON.SetNumber("some_var", 4);
|
||||
TEST_ExpectFalse(testJSON.IsNull("some_var"));
|
||||
|
||||
Issue("Boolean variable is incorrectly considered `null`");
|
||||
testJSON.SetBoolean("some_var", true);
|
||||
TEST_ExpectFalse(testJSON.IsNull("some_var"));
|
||||
|
||||
Issue("String variable is incorrectly considered `null`");
|
||||
testJSON.SetString("some_var", "string");
|
||||
TEST_ExpectFalse(testJSON.IsNull("some_var"));
|
||||
|
||||
Issue("Null value is incorrectly assigned");
|
||||
testJSON.SetNull("some_var");
|
||||
TEST_ExpectTrue(testJSON.IsNull("some_var"));
|
||||
|
||||
Issue("Null type isn't properly set by `SetNumber`");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Null);
|
||||
|
||||
Issue("Null value isn't being properly removed.");
|
||||
testJSON.RemoveValue("some_var");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf("some_var") == JSON_Undefined);
|
||||
}
|
||||
|
||||
protected static function SubTest_MultipleVariablesGetSet()
|
||||
{
|
||||
local int i;
|
||||
local bool correctValue, allValuesCorrect;
|
||||
local JObject testJSON;
|
||||
testJSON = _().json.newObject();
|
||||
Context("Testing how `JObject` handles addition, change and removal" @
|
||||
"of relatively large (hundreds) number of variables");
|
||||
for (i = 0; i < 2000; i += 1)
|
||||
{
|
||||
testJSON.SetNumber("num" $ string(i), 4 * i*i - 2.6 * i + 0.75);
|
||||
}
|
||||
for (i = 0; i < 500; i += 1)
|
||||
{
|
||||
testJSON.SetString("num" $ string(i), "str" $ string(Sin(i)));
|
||||
}
|
||||
for (i = 1500; i < 2000; i += 1)
|
||||
{
|
||||
testJSON.RemoveValue("num" $ string(i));
|
||||
}
|
||||
allValuesCorrect = true;
|
||||
for (i = 0; i < 200; i += 1)
|
||||
{
|
||||
if (i < 500)
|
||||
{
|
||||
correctValue = ( testJSON.GetString("num" $ string(i))
|
||||
== ("str" $ string(Sin(i))) );
|
||||
Issue("Variables are incorrectly overwritten");
|
||||
}
|
||||
else if(i < 1500)
|
||||
{
|
||||
correctValue = ( testJSON.GetNumber("num" $ string(i))
|
||||
== 4 * i*i - 2.6 * i + 0.75);
|
||||
Issue("Variables are lost");
|
||||
}
|
||||
else
|
||||
{
|
||||
correctValue = ( testJSON.GetTypeOf("num" $ string(i))
|
||||
== JSON_Undefined);
|
||||
Issue("Variables aren't removed");
|
||||
}
|
||||
if (!correctValue)
|
||||
{
|
||||
allValuesCorrect = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
TEST_ExpectTrue(allValuesCorrect);
|
||||
}
|
||||
|
||||
protected static function SubTest_Object()
|
||||
{
|
||||
local JObject testObject;
|
||||
Context("Testing setters and getters for folded objects");
|
||||
testObject = _().json.newObject();
|
||||
testObject.CreateObject("folded");
|
||||
testObject.GetObject("folded").CreateObject("folded");
|
||||
testObject.SetString("out", "string outside");
|
||||
testObject.GetObject("folded").SetNumber("mid", 8);
|
||||
testObject.GetObject("folded")
|
||||
.GetObject("folded")
|
||||
.SetString("in", "string inside");
|
||||
|
||||
Issue("Addressing variables in root object doesn't work");
|
||||
TEST_ExpectTrue(testObject.GetString("out", "default") == "string outside");
|
||||
|
||||
Issue("Addressing variables in folded object doesn't work");
|
||||
TEST_ExpectTrue(testObject.GetObject("folded").GetNumber("mid", 1) == 8);
|
||||
|
||||
Issue("Addressing plain variables in folded (twice) object doesn't work");
|
||||
TEST_ExpectTrue(testObject.GetObject("folded").GetObject("folded")
|
||||
.GetString("in", "default") == "string inside");
|
||||
}
|
||||
|
||||
protected static function SubTest_ArrayUndefined()
|
||||
{
|
||||
local JArray testJSON;
|
||||
testJSON = _().json.newArray();
|
||||
Context("Testing how `JArray` handles undefined values");
|
||||
Issue("Undefined variable doesn't have `JSON_Undefined` type");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined);
|
||||
|
||||
Issue("There is a variable in an empty object after `GetTypeOf` call");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined);
|
||||
|
||||
Issue("Negative index refers to a defined value");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf(-1) == JSON_Undefined);
|
||||
|
||||
Issue("Getters don't return default values for undefined variables");
|
||||
TEST_ExpectTrue(testJSON.GetNumber(0, 0) == 0);
|
||||
TEST_ExpectTrue(testJSON.GetString(0, "") == "");
|
||||
TEST_ExpectTrue(testJSON.GetBoolean(0, false) == false);
|
||||
TEST_ExpectNone(testJSON.GetObject(0));
|
||||
TEST_ExpectNone(testJSON.GetArray(0));
|
||||
|
||||
Issue( "Getters don't return user-defined default values for" @
|
||||
"undefined variables");
|
||||
TEST_ExpectTrue(testJSON.GetNumber(0, 10) == 10);
|
||||
TEST_ExpectTrue(testJSON.GetString(0, "test") == "test");
|
||||
TEST_ExpectTrue(testJSON.GetBoolean(0, true) == true);
|
||||
}
|
||||
|
||||
protected static function SubTest_ArrayBooleanGetSetRemove()
|
||||
{
|
||||
local JArray testJSON;
|
||||
testJSON = _().json.newArray();
|
||||
testJSON.SetBoolean(0, true);
|
||||
|
||||
Context("Testing `JArray`'s get/set/remove functions for" @
|
||||
"boolean variables");
|
||||
Issue("Boolean type isn't properly set by `SetBoolean`");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Boolean);
|
||||
|
||||
Issue("Value is incorrectly assigned by `SetBoolean`");
|
||||
TEST_ExpectTrue(testJSON.GetBoolean(0) == true);
|
||||
testJSON.SetBoolean(0, false);
|
||||
|
||||
Issue("Variable value isn't correctly reassigned by `SetBoolean`");
|
||||
TEST_ExpectTrue(testJSON.GetBoolean(0) == false);
|
||||
|
||||
Issue( "Getting boolean variable as a wrong type" @
|
||||
"doesn't yield default value");
|
||||
TEST_ExpectTrue(testJSON.GetNumber(0, 7) == 7);
|
||||
|
||||
Issue("Boolean variable isn't being properly removed");
|
||||
testJSON.RemoveValue(0);
|
||||
TEST_ExpectTrue( testJSON.GetTypeOf(0) == JSON_Undefined);
|
||||
|
||||
Issue( "Getters don't return default value for missing key that" @
|
||||
"previously stored boolean value, but got removed");
|
||||
TEST_ExpectTrue(testJSON.GetBoolean(0, true) == true);
|
||||
}
|
||||
|
||||
protected static function SubTest_ArrayStringGetSetRemove()
|
||||
{
|
||||
local JArray testJSON;
|
||||
testJSON = _().json.newArray();
|
||||
testJSON.SetString(0, "first string");
|
||||
|
||||
Context("Testing `JArray`'s get/set/remove functions for" @
|
||||
"string variables");
|
||||
Issue("String type isn't properly set by `SetString`");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_String);
|
||||
|
||||
Issue("Value is incorrectly assigned by `SetString`");
|
||||
TEST_ExpectTrue(testJSON.GetString(0) == "first string");
|
||||
|
||||
Issue( "Providing default variable value makes 'GetString'" @
|
||||
"return incorrect value");
|
||||
TEST_ExpectTrue(testJSON.GetString(0, "alternative") == "first string");
|
||||
|
||||
Issue("Variable value isn't correctly reassigned by `SetString`");
|
||||
testJSON.SetString(0, "new string!~");
|
||||
TEST_ExpectTrue(testJSON.GetString(0) == "new string!~");
|
||||
|
||||
Issue( "Getting string variable as a wrong type" @
|
||||
"doesn't yield default value");
|
||||
TEST_ExpectTrue(testJSON.GetBoolean(0, true) == true);
|
||||
|
||||
Issue("Boolean variable isn't being properly removed");
|
||||
testJSON.RemoveValue(0);
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined);
|
||||
|
||||
Issue( "Getters don't return default value for missing key that" @
|
||||
"previously stored string value, but got removed");
|
||||
TEST_ExpectTrue(testJSON.GetString(0, "other") == "other");
|
||||
}
|
||||
|
||||
protected static function SubTest_ArrayNumberGetSetRemove()
|
||||
{
|
||||
local JArray testJSON;
|
||||
testJSON = _().json.newArray();
|
||||
testJSON.SetNumber(0, 3.5);
|
||||
|
||||
Context("Testing `JArray`'s get/set/remove functions for" @
|
||||
"number variables");
|
||||
Issue("Number type isn't properly set by `SetNumber`");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Number);
|
||||
|
||||
Issue("Value is incorrectly assigned by `SetNumber`");
|
||||
TEST_ExpectTrue(testJSON.GetNumber(0) == 3.5);
|
||||
|
||||
Issue( "Providing default variable value makes 'GetNumber'" @
|
||||
"return incorrect value");
|
||||
TEST_ExpectTrue(testJSON.GetNumber(0, 5) == 3.5);
|
||||
|
||||
Issue("Variable value isn't correctly reassigned by `SetNumber`");
|
||||
testJSON.SetNumber(0, 7);
|
||||
TEST_ExpectTrue(testJSON.GetNumber(0) == 7);
|
||||
|
||||
Issue( "Getting number variable as a wrong type" @
|
||||
"doesn't yield default value");
|
||||
TEST_ExpectTrue(testJSON.GetString(0, "default") == "default");
|
||||
|
||||
Issue("Number type isn't being properly removed");
|
||||
testJSON.RemoveValue(0);
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined);
|
||||
|
||||
Issue( "Getters don't return default value for missing key that" @
|
||||
"previously stored number value, but got removed");
|
||||
TEST_ExpectTrue(testJSON.GetNumber(0, 13) == 13);
|
||||
}
|
||||
|
||||
protected static function SubTest_ArrayNullGetSetRemove()
|
||||
{
|
||||
local JArray testJSON;
|
||||
testJSON = _().json.newArray();
|
||||
|
||||
Context("Testing `JArray`'s get/set/remove functions for" @
|
||||
"null values");
|
||||
|
||||
Issue("Undefined variable is incorrectly considered `null`");
|
||||
TEST_ExpectFalse(testJSON.IsNull(0));
|
||||
TEST_ExpectFalse(testJSON.IsNull(2));
|
||||
TEST_ExpectFalse(testJSON.IsNull(-1));
|
||||
|
||||
Issue("Number variable is incorrectly considered `null`");
|
||||
testJSON.SetNumber(0, 4);
|
||||
TEST_ExpectFalse(testJSON.IsNull(0));
|
||||
|
||||
Issue("Boolean variable is incorrectly considered `null`");
|
||||
testJSON.SetBoolean(0, true);
|
||||
TEST_ExpectFalse(testJSON.IsNull(0));
|
||||
|
||||
Issue("String variable is incorrectly considered `null`");
|
||||
testJSON.SetString(0, "string");
|
||||
TEST_ExpectFalse(testJSON.IsNull(0));
|
||||
|
||||
Issue("Null value is incorrectly assigned");
|
||||
testJSON.SetNull(0);
|
||||
TEST_ExpectTrue(testJSON.IsNull(0));
|
||||
|
||||
Issue("Null type isn't properly set by `SetNumber`");
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Null);
|
||||
|
||||
Issue("Null value isn't being properly removed");
|
||||
testJSON.RemoveValue(0);
|
||||
TEST_ExpectTrue(testJSON.GetTypeOf(0) == JSON_Undefined);
|
||||
}
|
||||
|
||||
// Returns following array:
|
||||
// [10.0, "test string", "another string", true, 0.0, {"var": 7.0}]
|
||||
protected static function JArray Prepare_Array()
|
||||
{
|
||||
local JArray testArray;
|
||||
testArray = _().json.newArray();
|
||||
testArray.AddNumber(10.0f)
|
||||
.AddString("test string")
|
||||
.AddString("another string")
|
||||
.AddBoolean(true)
|
||||
.AddNumber(0.0f)
|
||||
.AddObject();
|
||||
testArray.GetObject(5).SetNumber("var", 7);
|
||||
return testArray;
|
||||
}
|
||||
|
||||
protected static function SubTest_ArrayMultipleVariablesStorage()
|
||||
{
|
||||
local JArray testArray;
|
||||
testArray = Prepare_Array();
|
||||
|
||||
Context("Testing how `JArray` handles adding and" @
|
||||
"changing several variables");
|
||||
Issue("Stored values are compromised.");
|
||||
TEST_ExpectTrue(testArray.GetNumber(0) == 10.0f);
|
||||
TEST_ExpectTrue(testArray.GetString(1) == "test string");
|
||||
TEST_ExpectTrue(testArray.GetString(2) == "another string");
|
||||
TEST_ExpectTrue(testArray.GetBoolean(3) == true);
|
||||
TEST_ExpectTrue(testArray.GetNumber(4) == 0.0f);
|
||||
TEST_ExpectTrue(testArray.GetObject(5).GetNumber("var") == 7);
|
||||
|
||||
Issue("Values incorrectly change their values.");
|
||||
testArray.SetString(3, "new string");
|
||||
TEST_ExpectTrue(testArray.GetString(3) == "new string");
|
||||
|
||||
Issue( "After overwriting boolean value with a different type," @
|
||||
"attempting go get it as a boolean gives old value," @
|
||||
"instead of default");
|
||||
TEST_ExpectTrue(testArray.GetBoolean(3, false) == false);
|
||||
|
||||
Issue("Type of the variable is incorrectly changed.");
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_String);
|
||||
}
|
||||
|
||||
protected static function SubTest_ArrayMultipleVariablesRemoval()
|
||||
{
|
||||
local JArray testArray;
|
||||
testArray = Prepare_Array();
|
||||
// Test removing variables
|
||||
// After `Prepare_Array`, our array should be:
|
||||
// [10.0, "test string", "another string", true, 0.0, {"var": 7.0}]
|
||||
|
||||
Context("Testing how `JArray` handles adding and" @
|
||||
"removing several variables");
|
||||
Issue("Values are incorrectly removed");
|
||||
testArray.RemoveValue(2);
|
||||
// [10.0, "test string", true, 0.0, {"var": 7.0}]
|
||||
Issue("Values are incorrectly removed");
|
||||
TEST_ExpectTrue(testArray.GetNumber(0) == 10.0);
|
||||
TEST_ExpectTrue(testArray.GetString(1) == "test string");
|
||||
TEST_ExpectTrue(testArray.GetBoolean(2) == true);
|
||||
TEST_ExpectTrue(testArray.GetNumber(3) == 0.0f);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(4) == JSON_Object);
|
||||
|
||||
Issue("First element incorrectly removed");
|
||||
testArray.RemoveValue(0);
|
||||
// ["test string", true, 0.0, {"var": 7.0}]
|
||||
TEST_ExpectTrue(testArray.GetString(0) == "test string");
|
||||
TEST_ExpectTrue(testArray.GetBoolean(1) == true);
|
||||
TEST_ExpectTrue(testArray.GetNumber(2) == 0.0f);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Object);
|
||||
TEST_ExpectTrue(testArray.GetObject(3).GetNumber("var") == 7.0);
|
||||
|
||||
Issue("Last element incorrectly removed");
|
||||
testArray.RemoveValue(3);
|
||||
// ["test string", true, 0.0]
|
||||
TEST_ExpectTrue(testArray.GetLength() == 3);
|
||||
TEST_ExpectTrue(testArray.GetString(0) == "test string");
|
||||
TEST_ExpectTrue(testArray.GetBoolean(1) == true);
|
||||
TEST_ExpectTrue(testArray.GetNumber(2) == 0.0f);
|
||||
|
||||
Issue("Removing all elements is handled incorrectly");
|
||||
testArray.RemoveValue(0);
|
||||
testArray.RemoveValue(0);
|
||||
testArray.RemoveValue(0);
|
||||
TEST_ExpectTrue(testArray.Getlength() == 0);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Undefined);
|
||||
}
|
||||
|
||||
protected static function SubTest_ArrayRemovingMultipleVariablesAtOnce()
|
||||
{
|
||||
local JArray testArray;
|
||||
testArray = _().json.newArray();
|
||||
testArray.AddNumber(10.0f)
|
||||
.AddString("test string")
|
||||
.AddString("another string")
|
||||
.AddNumber(7.0);
|
||||
|
||||
Context("Testing how `JArray`' handles removing" @
|
||||
"multiple elements at once");
|
||||
Issue("Multiple values are incorrectly removed");
|
||||
testArray.RemoveValue(1, 2);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 2);
|
||||
TEST_ExpectTrue(testArray.GetNumber(1) == 7.0);
|
||||
|
||||
testArray.AddNumber(4.0f)
|
||||
.AddString("test string")
|
||||
.AddString("another string")
|
||||
.AddNumber(8.0);
|
||||
|
||||
// Current array:
|
||||
// [10.0, 7.0, 4.0, "test string", "another string", 8.0]
|
||||
Issue("Last value is incorrectly removed");
|
||||
testArray.RemoveValue(5, 1);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 5);
|
||||
TEST_ExpectTrue(testArray.GetString(4) == "another string");
|
||||
|
||||
// Current array:
|
||||
// [10.0, 7.0, 4.0, "test string", "another string"]
|
||||
Issue("Tail elements are incorrectly removed");
|
||||
testArray.RemoveValue(3, 4);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 3);
|
||||
TEST_ExpectTrue(testArray.GetNumber(0) == 10.0);
|
||||
TEST_ExpectTrue(testArray.GetNumber(2) == 4.0);
|
||||
|
||||
Issue("Array empties incorrectly");
|
||||
testArray.RemoveValue(0, testArray.GetLength());
|
||||
TEST_ExpectTrue(testArray.GetLength() == 0);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Undefined);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Undefined);
|
||||
}
|
||||
|
||||
protected static function SubTest_ArrayExpansions()
|
||||
{
|
||||
local JArray testArray;
|
||||
testArray = _().json.newArray();
|
||||
|
||||
Context("Testing how `JArray`' handles expansions/shrinking " @
|
||||
"via `SetLength()`");
|
||||
Issue("`SetLength()` doesn't properly expand empty array");
|
||||
testArray.SetLength(2);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 2);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null);
|
||||
|
||||
Issue("`SetLength()` doesn't properly expand non-empty array");
|
||||
testArray.AddNumber(1);
|
||||
testArray.SetLength(4);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 4);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(0) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Number);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetNumber(2) == 1);
|
||||
SubSubTest_ArraySetNumberExpansions();
|
||||
SubSubTest_ArraySetStringExpansions();
|
||||
SubSubTest_ArraySetBooleanExpansions();
|
||||
}
|
||||
|
||||
protected static function SubSubTest_ArraySetNumberExpansions()
|
||||
{
|
||||
local JArray testArray;
|
||||
testArray = _().json.newArray();
|
||||
|
||||
Context("Testing how `JArray`' handles expansions via" @
|
||||
"`SetNumber()` function");
|
||||
Issue("Setters don't create correct first element");
|
||||
testArray.SetNumber(0, 1);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 1);
|
||||
TEST_ExpectTrue(testArray.GetNumber(0) == 1);
|
||||
|
||||
Issue( "`SetNumber()` doesn't properly define array when setting" @
|
||||
"value out-of-bounds");
|
||||
testArray = _().json.newArray();
|
||||
testArray.AddNumber(1);
|
||||
testArray.SetNumber(4, 2);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 5);
|
||||
TEST_ExpectTrue(testArray.GetNumber(0) == 1);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetNumber(4) == 2);
|
||||
|
||||
Issue("`SetNumber()` expands array even when it told not to");
|
||||
testArray.SetNumber(6, 7, true);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 5);
|
||||
TEST_ExpectTrue(testArray.GetNumber(6) == 0);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(5) == JSON_Undefined);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(6) == JSON_Undefined);
|
||||
}
|
||||
|
||||
protected static function SubSubTest_ArraySetStringExpansions()
|
||||
{
|
||||
local JArray testArray;
|
||||
testArray = _().json.newArray();
|
||||
|
||||
Context("Testing how `JArray`' handles expansions via" @
|
||||
"`SetString()` function");
|
||||
Issue("Setters don't create correct first element");
|
||||
testArray.SetString(0, "str");
|
||||
TEST_ExpectTrue(testArray.GetLength() == 1);
|
||||
TEST_ExpectTrue(testArray.GetString(0) == "str");
|
||||
|
||||
Issue( "`SetString()` doesn't properly define array when setting" @
|
||||
"value out-of-bounds");
|
||||
testArray = _().json.newArray();
|
||||
testArray.AddString("str");
|
||||
testArray.SetString(4, "str2");
|
||||
TEST_ExpectTrue(testArray.GetLength() == 5);
|
||||
TEST_ExpectTrue(testArray.GetString(0) == "str");
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetString(4) == "str2");
|
||||
|
||||
Issue("`SetString()` expands array even when it told not to");
|
||||
testArray.SetString(6, "new string", true);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 5);
|
||||
TEST_ExpectTrue(testArray.GetString(6) == "");
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(5) == JSON_Undefined);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(6) == JSON_Undefined);
|
||||
}
|
||||
|
||||
protected static function SubSubTest_ArraySetBooleanExpansions()
|
||||
{
|
||||
local JArray testArray;
|
||||
testArray = _().json.newArray();
|
||||
|
||||
Context("Testing how `JArray`' handles expansions via" @
|
||||
"`SetBoolean()` function");
|
||||
Issue("Setters don't create correct first element");
|
||||
testArray.SetBoolean(0, false);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 1);
|
||||
TEST_ExpectTrue(testArray.GetBoolean(0) == false);
|
||||
|
||||
Issue( "`SetBoolean()` doesn't properly define array when setting" @
|
||||
"value out-of-bounds");
|
||||
testArray = _().json.newArray();
|
||||
testArray.AddBoolean(true);
|
||||
testArray.SetBoolean(4, true);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 5);
|
||||
TEST_ExpectTrue(testArray.GetBoolean(0) == true);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(1) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(2) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(3) == JSON_Null);
|
||||
TEST_ExpectTrue(testArray.GetBoolean(4) == true);
|
||||
|
||||
Issue("`SetBoolean()` expands array even when it told not to");
|
||||
testArray.SetBoolean(6, true, true);
|
||||
TEST_ExpectTrue(testArray.GetLength() == 5);
|
||||
TEST_ExpectTrue(testArray.GetBoolean(6) == false);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(5) == JSON_Undefined);
|
||||
TEST_ExpectTrue(testArray.GetTypeOf(6) == JSON_Undefined);
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
caseName = "JSON"
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
/**
|
||||
* Event generator for events, related to broadcasting messages
|
||||
* through standard Unreal Script means:
|
||||
* 1. text messages, typed by a player;
|
||||
* 2. localized messages, identified by a LocalMessage class and id.
|
||||
* Allows to make decisions whether or not to propagate certain messages.
|
||||
* Copyright 2020 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 BroadcastEvents extends Events
|
||||
abstract;
|
||||
|
||||
struct LocalizedMessage
|
||||
{
|
||||
// Every localized message is described by a class and id.
|
||||
// For example, consider 'KFMod.WaitingMessage':
|
||||
// if passed 'id' is '1',
|
||||
// then it's supposed to be a message about new wave,
|
||||
// but if passed 'id' is '2',
|
||||
// then it's about completing the wave.
|
||||
var class<LocalMessage> class;
|
||||
var int id;
|
||||
// Localized messages in unreal script can be passed along with
|
||||
// optional arguments, described by variables below.
|
||||
var PlayerReplicationInfo relatedPRI1;
|
||||
var PlayerReplicationInfo relatedPRI2;
|
||||
var Object relatedObject;
|
||||
};
|
||||
|
||||
static function bool CallCanBroadcast(Actor broadcaster, int recentSentTextSize)
|
||||
{
|
||||
local int i;
|
||||
local bool result;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0;i < listeners.length;i += 1)
|
||||
{
|
||||
result = class<BroadcastListenerBase>(listeners[i])
|
||||
.static.CanBroadcast(broadcaster, recentSentTextSize);
|
||||
if (!result) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static function bool CallHandleText
|
||||
(
|
||||
Actor sender,
|
||||
out string message,
|
||||
name messageType
|
||||
)
|
||||
{
|
||||
local int i;
|
||||
local bool result;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0;i < listeners.length;i += 1)
|
||||
{
|
||||
result = class<BroadcastListenerBase>(listeners[i])
|
||||
.static.HandleText(sender, message, messageType);
|
||||
if (!result) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static function bool CallHandleTextFor
|
||||
(
|
||||
PlayerController receiver,
|
||||
Actor sender,
|
||||
out string message,
|
||||
name messageType
|
||||
)
|
||||
{
|
||||
local int i;
|
||||
local bool result;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0;i < listeners.length;i += 1)
|
||||
{
|
||||
result = class<BroadcastListenerBase>(listeners[i])
|
||||
.static.HandleTextFor(receiver, sender, message, messageType);
|
||||
if (!result) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static function bool CallHandleLocalized
|
||||
(
|
||||
Actor sender,
|
||||
LocalizedMessage message
|
||||
)
|
||||
{
|
||||
local int i;
|
||||
local bool result;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0;i < listeners.length;i += 1)
|
||||
{
|
||||
result = class<BroadcastListenerBase>(listeners[i])
|
||||
.static.HandleLocalized(sender, message);
|
||||
if (!result) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static function bool CallHandleLocalizedFor
|
||||
(
|
||||
PlayerController receiver,
|
||||
Actor sender,
|
||||
LocalizedMessage message
|
||||
)
|
||||
{
|
||||
local int i;
|
||||
local bool result;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0;i < listeners.length;i += 1)
|
||||
{
|
||||
result = class<BroadcastListenerBase>(listeners[i])
|
||||
.static.HandleLocalizedFor(receiver, sender, message);
|
||||
if (!result) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedListener = class'BroadcastListenerBase'
|
||||
}
|
||||
|
|
@ -1,197 +0,0 @@
|
|||
/**
|
||||
* 'BroadcastHandler' class that used by Acedia to catch
|
||||
* broadcasting events. For Acedia to work properly it needs to be added to
|
||||
* the very beginning of the broadcast handlers' chain.
|
||||
* Copyright 2020 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/>.
|
||||
*/
|
||||
// TODO: make it work from any place in the chain.
|
||||
class BroadcastHandler extends Engine.BroadcastHandler
|
||||
dependson(BroadcastEvents);
|
||||
|
||||
// The way vanilla 'BroadcastHandler' works - it can check if broadcast is
|
||||
// possible for any actor, but for actually sending the text messages it will
|
||||
// try to extract player's data from it
|
||||
// and will simply pass 'none' if it can't.
|
||||
// We remember senders in this array in order to pass real ones to our events.
|
||||
// Array instead of variable is to account for folded calls
|
||||
// (when handling of broadcast events leads to another message generation).
|
||||
var private array<Actor> storedSenders;
|
||||
|
||||
// We want to insert our code in some of the functions between
|
||||
// 'AllowsBroadcast' check and actual broadcasting,
|
||||
// so we can't just use a 'super.AllowsBroadcast()' call.
|
||||
// Instead we first manually do this check, then perform our logic and then
|
||||
// make a super call, but with 'blockAllowsBroadcast' flag set to 'true',
|
||||
// which causes overloaded 'AllowsBroadcast()' to omit actual checks.
|
||||
var private bool blockAllowsBroadcast;
|
||||
|
||||
// Functions below simply reroute vanilla's broadcast events to
|
||||
// Acedia's 'BroadcastEvents', while keeping original senders
|
||||
// and blocking 'AllowsBroadcast()' as described in comments for
|
||||
// 'storedSenders' and 'blockAllowsBroadcast'.
|
||||
|
||||
public function bool HandlerAllowsBroadcast(Actor broadcaster, int sentTextNum)
|
||||
{
|
||||
local bool canBroadcast;
|
||||
// Check listeners
|
||||
canBroadcast = class'BroadcastEvents'.static
|
||||
.CallCanBroadcast(broadcaster, sentTextNum);
|
||||
// Check other broadcast handlers (if present)
|
||||
if (canBroadcast && nextBroadcastHandler != none)
|
||||
{
|
||||
canBroadcast = nextBroadcastHandler
|
||||
.HandlerAllowsBroadcast(broadcaster, sentTextNum);
|
||||
}
|
||||
return canBroadcast;
|
||||
}
|
||||
|
||||
function Broadcast(Actor sender, coerce string message, optional name type)
|
||||
{
|
||||
local bool canTryToBroadcast;
|
||||
if (!AllowsBroadcast(sender, Len(message)))
|
||||
return;
|
||||
canTryToBroadcast = class'BroadcastEvents'.static
|
||||
.CallHandleText(sender, message, type);
|
||||
if (canTryToBroadcast)
|
||||
{
|
||||
storedSenders[storedSenders.length] = sender;
|
||||
blockAllowsBroadcast = true;
|
||||
super.Broadcast(sender, message, type);
|
||||
blockAllowsBroadcast = false;
|
||||
storedSenders.length = storedSenders.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function BroadcastTeam
|
||||
(
|
||||
Controller sender,
|
||||
coerce string message,
|
||||
optional name type
|
||||
)
|
||||
{
|
||||
local bool canTryToBroadcast;
|
||||
if (!AllowsBroadcast(sender, Len(message)))
|
||||
return;
|
||||
canTryToBroadcast = class'BroadcastEvents'.static
|
||||
.CallHandleText(sender, message, type);
|
||||
if (canTryToBroadcast)
|
||||
{
|
||||
storedSenders[storedSenders.length] = sender;
|
||||
blockAllowsBroadcast = true;
|
||||
super.BroadcastTeam(sender, message, type);
|
||||
blockAllowsBroadcast = false;
|
||||
storedSenders.length = storedSenders.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
event AllowBroadcastLocalized
|
||||
(
|
||||
Actor sender,
|
||||
class<LocalMessage> message,
|
||||
optional int switch,
|
||||
optional PlayerReplicationInfo relatedPRI1,
|
||||
optional PlayerReplicationInfo relatedPRI2,
|
||||
optional Object optionalObject
|
||||
)
|
||||
{
|
||||
local bool canTryToBroadcast;
|
||||
local BroadcastEvents.LocalizedMessage packedMessage;
|
||||
if (!AllowsBroadcast(sender, Len(message)))
|
||||
return;
|
||||
packedMessage.class = message;
|
||||
packedMessage.id = switch;
|
||||
packedMessage.relatedPRI1 = relatedPRI1;
|
||||
packedMessage.relatedPRI2 = relatedPRI2;
|
||||
packedMessage.relatedObject = optionalObject;
|
||||
canTryToBroadcast = class'BroadcastEvents'.static
|
||||
.CallHandleLocalized(sender, packedMessage);
|
||||
if (canTryToBroadcast)
|
||||
{
|
||||
super.AllowBroadcastLocalized( sender, message, switch,
|
||||
relatedPRI1, relatedPRI2,
|
||||
optionalObject);
|
||||
}
|
||||
}
|
||||
|
||||
function bool AllowsBroadcast(actor broadcaster, int len)
|
||||
{
|
||||
if (blockAllowsBroadcast)
|
||||
return true;
|
||||
return super.AllowsBroadcast(broadcaster, len);
|
||||
}
|
||||
|
||||
function bool AcceptBroadcastText
|
||||
(
|
||||
PlayerController receiver,
|
||||
PlayerReplicationInfo senderPRI,
|
||||
out string message,
|
||||
optional name type
|
||||
)
|
||||
{
|
||||
local bool canBroadcast;
|
||||
local Actor sender;
|
||||
if (senderPRI != none)
|
||||
{
|
||||
sender = PlayerController(senderPRI.owner);
|
||||
}
|
||||
if (sender == none && storedSenders.length > 0)
|
||||
{
|
||||
sender = storedSenders[storedSenders.length - 1];
|
||||
}
|
||||
canBroadcast = class'BroadcastEvents'.static
|
||||
.CallHandleTextFor(receiver, sender, message, type);
|
||||
if (!canBroadcast)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return super.AcceptBroadcastText(receiver, senderPRI, message, type);
|
||||
}
|
||||
|
||||
|
||||
function bool AcceptBroadcastLocalized
|
||||
(
|
||||
PlayerController receiver,
|
||||
Actor sender,
|
||||
class<LocalMessage> message,
|
||||
optional int switch,
|
||||
optional PlayerReplicationInfo relatedPRI1,
|
||||
optional PlayerReplicationInfo relatedPRI2,
|
||||
optional Object obj
|
||||
)
|
||||
{
|
||||
local bool canBroadcast;
|
||||
local BroadcastEvents.LocalizedMessage packedMessage;
|
||||
packedMessage.class = message;
|
||||
packedMessage.id = switch;
|
||||
packedMessage.relatedPRI1 = relatedPRI1;
|
||||
packedMessage.relatedPRI2 = relatedPRI2;
|
||||
packedMessage.relatedObject = obj;
|
||||
canBroadcast = class'BroadcastEvents'.static
|
||||
.CallHandleLocalizedFor(receiver, sender, packedMessage);
|
||||
if (!canBroadcast)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return super.AcceptBroadcastLocalized( receiver, sender, message, switch,
|
||||
relatedPRI1, relatedPRI2, obj);
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
blockAllowsBroadcast = false
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
/**
|
||||
* Listener for events, related to broadcasting messages
|
||||
* through standard Unreal Script means:
|
||||
* 1. text messages, typed by a player;
|
||||
* 2. localized messages, identified by a LocalMessage class and id.
|
||||
* Allows to make decisions whether or not to propagate certain messages.
|
||||
* Copyright 2020 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 BroadcastListenerBase extends Listener
|
||||
abstract;
|
||||
|
||||
static final function PlayerController GetController(Actor sender)
|
||||
{
|
||||
local Pawn senderPawn;
|
||||
senderPawn = Pawn(sender);
|
||||
if (senderPawn != none) return PlayerController(senderPawn.controller);
|
||||
return PlayerController(sender);
|
||||
}
|
||||
|
||||
// This event is called whenever registered broadcast handlers are asked if
|
||||
// they'd allow given actor ('broadcaster') to broadcast a text message,
|
||||
// given that none so far rejected it and he recently already broadcasted
|
||||
// or tried to broadcast 'recentSentTextSize' symbols of text
|
||||
// (that value is periodically reset in 'GameInfo',
|
||||
// by default should be each second).
|
||||
// NOTE: this function is ONLY called when someone tries to
|
||||
// broadcast TEXT messages.
|
||||
// If one of the listeners returns 'false', -
|
||||
// it will be treated just like one of broadcasters returning 'false'
|
||||
// in 'AllowsBroadcast' and this method won't be called for remaining
|
||||
// active listeners.
|
||||
static function bool CanBroadcast(Actor broadcaster, int recentSentTextSize)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// This event is called whenever a someone is trying to broadcast
|
||||
// a text message (typically the typed by a player).
|
||||
// This function is called once per message and allows you to change it
|
||||
// (by changing 'message' argument) before any of the players receive it.
|
||||
// Return 'true' to allow the message through.
|
||||
// If one of the listeners returns 'false', -
|
||||
// it will be treated just like one of broadcasters returning 'false'
|
||||
// in 'AcceptBroadcastText' and this method won't be called for remaining
|
||||
// active listeners.
|
||||
static function bool HandleText
|
||||
(
|
||||
Actor sender,
|
||||
out string message,
|
||||
optional name messageType
|
||||
)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// This event is similar to 'HandleText', but is called for every player
|
||||
// the message is sent to.
|
||||
// If allows you to alter the message, but the changes are accumulated
|
||||
// as events go through the players.
|
||||
static function bool HandleTextFor
|
||||
(
|
||||
PlayerController receiver,
|
||||
Actor sender,
|
||||
out string message,
|
||||
optional name messageType
|
||||
)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// This event is called whenever a localized message is trying to
|
||||
// get broadcasted to a certain player ('receiver').
|
||||
// Return 'true' to allow the message through.
|
||||
// If one of the listeners returns 'false', -
|
||||
// it will be treated just like one of broadcasters returning 'false'
|
||||
// in 'AcceptBroadcastText' and this method won't be called for remaining
|
||||
// active listeners.
|
||||
static function bool HandleLocalized
|
||||
(
|
||||
Actor sender,
|
||||
BroadcastEvents.LocalizedMessage message
|
||||
)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// This event is similar to 'HandleLocalized', but is called for
|
||||
// every player the message is sent to.
|
||||
static function bool HandleLocalizedFor
|
||||
(
|
||||
PlayerController receiver,
|
||||
Actor sender,
|
||||
BroadcastEvents.LocalizedMessage message
|
||||
)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedEvents = class'BroadcastEvents'
|
||||
}
|
||||
|
||||
// Text messages can (optionally) have their type specified.
|
||||
// Examples of it are names 'Say' and 'CriticalEvent'.
|
||||
|
|
@ -1,159 +0,0 @@
|
|||
/**
|
||||
* One of the two classes that make up a core of event system in Acedia.
|
||||
*
|
||||
* 'Events' (or it's child) class shouldn't be instantiated.
|
||||
* Usually module would provide '...Events' class that defines
|
||||
* certain set of static functions that can generate event calls to
|
||||
* all it's active listeners.
|
||||
* If you're simply using modules someone made, -
|
||||
* you don't need to bother yourself with further specifics.
|
||||
* If you wish to create your own event generator,
|
||||
* then first create a '...ListenerBase' object
|
||||
* (more about it in the description of 'Listener' class)
|
||||
* and set 'relatedListener' variable to point to it's class.
|
||||
* Then for each event create a caller function in your 'Event' class,
|
||||
* following this template:
|
||||
* ____________________________________________________________________________
|
||||
* | static function CallEVENT_NAME(<ARGUMENTS>)
|
||||
* | {
|
||||
* | local int i;
|
||||
* | local array< class<Listener> > listeners;
|
||||
* | listeners = GetListeners();
|
||||
* | for (i = 0; i < listeners.length; i += 1)
|
||||
* | {
|
||||
* | class<...ListenerBase>(listeners[i])
|
||||
* | .static.EVENT_NAME(<ARGUMENTS>);
|
||||
* | }
|
||||
* | }
|
||||
* |___________________________________________________________________________
|
||||
* If each listener must indicate whether it gives it's permission for
|
||||
* something to happen, then use this template:
|
||||
* ____________________________________________________________________________
|
||||
* | static function CallEVENT_NAME(<ARGUMENTS>)
|
||||
* | {
|
||||
* | local int i;
|
||||
* | local bool result;
|
||||
* | local array< class<Listener> > listeners;
|
||||
* | listeners = GetListeners();
|
||||
* | for (i = 0; i < listeners.length; i += 1)
|
||||
* | {
|
||||
* | result = class<...ListenerBase>(listeners[i])
|
||||
* | .static.EVENT_NAME(<ARGUMENTS>);
|
||||
* | if (!result) return false;
|
||||
* | }
|
||||
* | return true;
|
||||
* | }
|
||||
* |___________________________________________________________________________
|
||||
* For concrete example look at
|
||||
* 'MutatorEvents' and 'MutatorListenerBase'.
|
||||
* Copyright 2020 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 Events extends Object
|
||||
abstract;
|
||||
|
||||
var private array< class<Listener> > listeners;
|
||||
|
||||
var public const class<Listener> relatedListener;
|
||||
|
||||
// Even class can also auto-spawn a `Service`,
|
||||
// in case it's require to generate events
|
||||
var public const class<Service> connectedServiceClass;
|
||||
// Set this to `true`if you want `connectedServiceClass` service to also
|
||||
// auto-shutdown whenever no-one listens to the events.
|
||||
var public const bool shutDownServiceWithoutListeners;
|
||||
|
||||
static public final function array< class<Listener> > GetListeners()
|
||||
{
|
||||
return default.listeners;
|
||||
}
|
||||
|
||||
// Make given listener active.
|
||||
// If listener was already activated also returns 'false'.
|
||||
static public final function bool ActivateListener(class<Listener> newListener)
|
||||
{
|
||||
local int i;
|
||||
if (newListener == none) return false;
|
||||
if (!ClassIsChildOf(newListener, default.relatedListener)) return false;
|
||||
|
||||
// Spawn service, if absent
|
||||
if ( default.listeners.length == 0
|
||||
&& default.connectedServiceClass != none) {
|
||||
default.connectedServiceClass.static.Require();
|
||||
}
|
||||
// Add listener
|
||||
for (i = 0;i < default.listeners.length;i += 1)
|
||||
{
|
||||
if (default.listeners[i] == newListener) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
default.listeners[default.listeners.length] = newListener;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Make given listener inactive.
|
||||
// If listener wasn't active returns 'false'.
|
||||
static public final function bool DeactivateListener(class<Listener> listener)
|
||||
{
|
||||
local int i;
|
||||
local bool removedListener;
|
||||
local Service service;
|
||||
if (listener == none) return false;
|
||||
|
||||
// Remove listener
|
||||
for (i = 0; i < default.listeners.length; i += 1)
|
||||
{
|
||||
if (default.listeners[i] == listener)
|
||||
{
|
||||
default.listeners.Remove(i, 1);
|
||||
removedListener = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Remove unneeded service
|
||||
if ( default.shutDownServiceWithoutListeners
|
||||
&& default.listeners.length == 0
|
||||
&& default.connectedServiceClass != none)
|
||||
{
|
||||
service = Service(default.connectedServiceClass.static.GetInstance());
|
||||
if (service != none) {
|
||||
service.Destroy();
|
||||
}
|
||||
}
|
||||
return removedListener;
|
||||
}
|
||||
|
||||
static public final function bool IsActiveListener(class<Listener> listener)
|
||||
{
|
||||
local int i;
|
||||
if (listener == none) return false;
|
||||
|
||||
for (i = 0; i < default.listeners.length; i += 1)
|
||||
{
|
||||
if (default.listeners[i] == listener)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedListener = class'Listener'
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
/**
|
||||
* One of the two classes that make up a core of event system in Acedia.
|
||||
*
|
||||
* 'Listener' (or it's child) class shouldn't be instantiated.
|
||||
* Usually module would provide '...ListenerBase' class that defines
|
||||
* certain set of static functions, corresponding to events it can listen to.
|
||||
* In order to handle those events you must create it's child class and
|
||||
* override said functions. But they will only be called if
|
||||
* 'SetActive(true)' is called for that child class.
|
||||
* To create you own '...ListenerBase' class you need to define
|
||||
* a static function for each event you wish it to catch and
|
||||
* set 'relatedEvents' variable to point at the 'Events' class
|
||||
* that will generate your events.
|
||||
* For concrete example look at
|
||||
* 'ConnectionEvents' and 'ConnectionListenerBase'.
|
||||
* Copyright 2019 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 Listener extends Object
|
||||
abstract;
|
||||
|
||||
var public const class<Events> relatedEvents;
|
||||
|
||||
|
||||
static public final function SetActive(bool active)
|
||||
{
|
||||
if (active)
|
||||
{
|
||||
default.relatedEvents.static.ActivateListener(default.class);
|
||||
}
|
||||
else
|
||||
{
|
||||
default.relatedEvents.static.DeactivateListener(default.class);
|
||||
}
|
||||
}
|
||||
|
||||
static public final function IsActive(bool active)
|
||||
{
|
||||
default.relatedEvents.static.IsActiveListener(default.class);
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedEvents = class'Events'
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
/**
|
||||
* Event generator that repeats events of a mutator.
|
||||
* Copyright 2019 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 MutatorEvents extends Events
|
||||
abstract;
|
||||
|
||||
static function bool CallCheckReplacement(Actor other, out byte isSuperRelevant)
|
||||
{
|
||||
local int i;
|
||||
local bool result;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0; i < listeners.length; i += 1)
|
||||
{
|
||||
result = class<MutatorListenerBase>(listeners[i])
|
||||
.static.CheckReplacement(other, isSuperRelevant);
|
||||
if (!result) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static function bool CallMutate(string command, PlayerController sendingPlayer)
|
||||
{
|
||||
local int i;
|
||||
local bool result;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0; i < listeners.length;i += 1)
|
||||
{
|
||||
result = class<MutatorListenerBase>(listeners[i])
|
||||
.static.Mutate(command, sendingPlayer);
|
||||
if (!result) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedListener = class'MutatorListenerBase'
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
/**
|
||||
* Listener for events, normally propagated by mutators.
|
||||
* Copyright 2019 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 MutatorListenerBase extends Listener
|
||||
abstract;
|
||||
|
||||
// This event is called whenever 'CheckReplacement'
|
||||
// check is propagated through mutators.
|
||||
// If one of the listeners returns 'false', -
|
||||
// it will be treated just like a mutator returning 'false'
|
||||
// in 'CheckReplacement' and
|
||||
// this method won't be called for remaining active listeners.
|
||||
static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// This event is called whenever 'Mutate' is propagated through mutators.
|
||||
// If one of the listeners returns 'false', -
|
||||
// this method won't be called for remaining active listeners or mutators.
|
||||
// If all listeners return 'true', -
|
||||
// mutate command will be further propagated to the rest of the mutators.
|
||||
static function bool Mutate(string command, PlayerController sendingPlayer)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedEvents = class'MutatorEvents'
|
||||
}
|
||||
|
|
@ -1,117 +0,0 @@
|
|||
/**
|
||||
* Feature represents a certain subset of Acedia's functionality that
|
||||
* can be enabled or disabled, according to server owner's wishes.
|
||||
* In the current version of Acedia enabling or disabling a feature requires
|
||||
* manually editing configuration file and restarting a server.
|
||||
* Factually feature is just a collection of settings with one universal
|
||||
* 'isActive' setting that tells Acedia whether or not to load a feature.
|
||||
* Copyright 2019 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 Feature extends Singleton
|
||||
abstract
|
||||
config(Acedia);
|
||||
|
||||
// Setting that tells Acedia whether or not to enable this feature
|
||||
// during initialization.
|
||||
// Only it's default value is ever used.
|
||||
var private config bool autoEnable;
|
||||
|
||||
// Listeners listed here will be automatically activated.
|
||||
var public const array< class<Listener> > requiredListeners;
|
||||
|
||||
// Sets whether to enable this feature by default.
|
||||
public static final function SetAutoEnable(bool doEnable)
|
||||
{
|
||||
default.autoEnable = doEnable;
|
||||
StaticSaveConfig();
|
||||
}
|
||||
|
||||
public static final function bool IsAutoEnabled()
|
||||
{
|
||||
return default.autoEnable;
|
||||
}
|
||||
|
||||
// Whether feature is enabled is determined by
|
||||
public static final function bool IsEnabled()
|
||||
{
|
||||
return (GetInstance() != none);
|
||||
}
|
||||
|
||||
// Enables feature of given class.
|
||||
public static final function Feature EnableMe()
|
||||
{
|
||||
local Feature newInstance;
|
||||
if (IsEnabled())
|
||||
{
|
||||
return Feature(GetInstance());
|
||||
}
|
||||
default.blockSpawning = false;
|
||||
newInstance = class'Acedia'.static.GetInstance().Spawn(default.class);
|
||||
default.blockSpawning = true;
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
public static final function bool DisableMe()
|
||||
{
|
||||
local Feature myself;
|
||||
myself = Feature(GetInstance());
|
||||
if (myself != none)
|
||||
{
|
||||
myself.Destroy();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Event functions that are called when
|
||||
protected function OnEnabled(){}
|
||||
protected function OnDisabled(){}
|
||||
|
||||
// Set listeners' status
|
||||
private static function SetListenersActiveSatus(bool newStatus)
|
||||
{
|
||||
local int i;
|
||||
for (i = 0; i < default.requiredListeners.length; i += 1)
|
||||
{
|
||||
if (default.requiredListeners[i] == none) continue;
|
||||
default.requiredListeners[i].static.SetActive(newStatus);
|
||||
}
|
||||
}
|
||||
|
||||
protected function OnCreated()
|
||||
{
|
||||
default.blockSpawning = true;
|
||||
SetListenersActiveSatus(true);
|
||||
OnEnabled();
|
||||
}
|
||||
|
||||
protected function OnDestroyed()
|
||||
{
|
||||
SetListenersActiveSatus(false);
|
||||
OnDisabled();
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
autoEnable = false
|
||||
DrawType = DT_None
|
||||
// Prevent spawning this feature by any other means than 'EnableMe()'.
|
||||
blockSpawning = true
|
||||
// Features are server-only actors
|
||||
remoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
/**
|
||||
* API that provides functions quick access to Acedia's
|
||||
* logging functionality.
|
||||
* Copyright 2020 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 LoggerAPI extends Singleton;
|
||||
|
||||
var private LoggerService logService;
|
||||
|
||||
protected function OnCreated()
|
||||
{
|
||||
logService = LoggerService(class'LoggerService'.static.Require());
|
||||
}
|
||||
|
||||
public final function Track(string message)
|
||||
{
|
||||
if (logService == none)
|
||||
{
|
||||
class'LoggerService'.static.LogMessageToKFLog(LOG_Track, message);
|
||||
return;
|
||||
}
|
||||
logService.LogMessage(LOG_Track, message);
|
||||
}
|
||||
|
||||
public final function Debug(string message)
|
||||
{
|
||||
if (logService == none)
|
||||
{
|
||||
class'LoggerService'.static.LogMessageToKFLog(LOG_Debug, message);
|
||||
return;
|
||||
}
|
||||
logService.LogMessage(LOG_Debug, message);
|
||||
}
|
||||
|
||||
public final function Info(string message)
|
||||
{
|
||||
if (logService == none)
|
||||
{
|
||||
class'LoggerService'.static.LogMessageToKFLog(LOG_Info, message);
|
||||
return;
|
||||
}
|
||||
logService.LogMessage(LOG_Info, message);
|
||||
}
|
||||
|
||||
public final function Warning(string message)
|
||||
{
|
||||
if (logService == none)
|
||||
{
|
||||
class'LoggerService'.static.LogMessageToKFLog(LOG_Warning, message);
|
||||
return;
|
||||
}
|
||||
logService.LogMessage(LOG_Warning, message);
|
||||
}
|
||||
|
||||
public final function Failure(string message)
|
||||
{
|
||||
if (logService == none)
|
||||
{
|
||||
class'LoggerService'.static.LogMessageToKFLog(LOG_Failure, message);
|
||||
return;
|
||||
}
|
||||
logService.LogMessage(LOG_Failure, message);
|
||||
}
|
||||
|
||||
public final function Fatal(string message)
|
||||
{
|
||||
if (logService == none)
|
||||
{
|
||||
class'LoggerService'.static.LogMessageToKFLog(LOG_Fatal, message);
|
||||
return;
|
||||
}
|
||||
logService.LogMessage(LOG_Fatal, message);
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
/**
|
||||
* Logger that allows to separate log messages into several levels of
|
||||
* significance and lets users and admins to access only the ones they want
|
||||
* and/or receive notifications when they happen.
|
||||
* Copyright 2020 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 LoggerService extends Service
|
||||
config(AcediaLogger);
|
||||
|
||||
// Log levels, available in Acedia.
|
||||
enum LogLevel
|
||||
{
|
||||
// For the purposes of "tracing" the code, when trying to figure out
|
||||
// where exactly problems occurred.
|
||||
// Should not be used in any released version of
|
||||
// your packages/mutators.
|
||||
LOG_Track,
|
||||
// Information that can be used to track down errors that occur on
|
||||
// other people's systems, that developer cannot otherwise pinpoint.
|
||||
// Should be used with purpose of tracking a certain issue and
|
||||
// not "just in case".
|
||||
LOG_Debug,
|
||||
// Information about important events that should be occurring under
|
||||
// normal conditions, such as initializations/shutdowns,
|
||||
// successful completion of significant events, configuration assumptions.
|
||||
// Should not occur too often.
|
||||
LOG_Info,
|
||||
// For recoverable issues, anything that might cause errors or
|
||||
// oddities in behavior.
|
||||
// Should be used sparingly, i.e. player disconnecting might cause
|
||||
// interruption in some logic, but should not cause a warning,
|
||||
// since it is something expected to happen normally.
|
||||
LOG_Warning,
|
||||
// Use this for errors, - events that some operation cannot recover from,
|
||||
// but still does not require your module to shut down.
|
||||
LOG_Failure,
|
||||
// Anything that does not allow your module or game to function,
|
||||
// completely irrecoverable failure state.
|
||||
LOG_Fatal
|
||||
};
|
||||
|
||||
var private const string kfLogPrefix;
|
||||
var private const string traceLevelName;
|
||||
var private const string DebugLevelName;
|
||||
var private const string infoLevelName;
|
||||
var private const string warningLevelName;
|
||||
var private const string errorLevelName;
|
||||
var private const string fatalLevelName;
|
||||
|
||||
var private config array< class<Manifest> > registeredManifests;
|
||||
var private config bool logTraceInKFLog;
|
||||
var private config bool logDebugInKFLog;
|
||||
var private config bool logInfoInKFLog;
|
||||
var private config bool logWarningInKFLog;
|
||||
var private config bool logErrorInKFLog;
|
||||
var private config bool logFatalInKFLog;
|
||||
|
||||
var private array<string> traceMessages;
|
||||
var private array<string> debugMessages;
|
||||
var private array<string> infoMessages;
|
||||
var private array<string> warningMessages;
|
||||
var private array<string> errorMessages;
|
||||
var private array<string> fatalMessages;
|
||||
|
||||
public final function bool ShouldAddToKFLog(LogLevel messageLevel)
|
||||
{
|
||||
if (messageLevel == LOG_Trace && logTraceInKFLog) return true;
|
||||
if (messageLevel == LOG_Debug && logDebugInKFLog) return true;
|
||||
if (messageLevel == LOG_Info && logInfoInKFLog) return true;
|
||||
if (messageLevel == LOG_Warning && logWarningInKFLog) return true;
|
||||
if (messageLevel == LOG_Error && logErrorInKFLog) return true;
|
||||
if (messageLevel == LOG_Fatal && logFatalInKFLog) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public final static function LogMessageToKFLog
|
||||
(
|
||||
LogLevel messageLevel,
|
||||
string message
|
||||
)
|
||||
{
|
||||
local string levelPrefix;
|
||||
levelPrefix = default.kfLogPrefix;
|
||||
switch (messageLevel)
|
||||
{
|
||||
case LOG_Trace:
|
||||
levelPrefix = levelPrefix $ default.traceLevelName;
|
||||
break;
|
||||
case LOG_Debug:
|
||||
levelPrefix = levelPrefix $ default.debugLevelName;
|
||||
break;
|
||||
case LOG_Info:
|
||||
levelPrefix = levelPrefix $ default.infoLevelName;
|
||||
break;
|
||||
case LOG_Warning:
|
||||
levelPrefix = levelPrefix $ default.warningLevelName;
|
||||
break;
|
||||
case LOG_Error:
|
||||
levelPrefix = levelPrefix $ default.errorLevelName;
|
||||
break;
|
||||
case LOG_Fatal:
|
||||
levelPrefix = levelPrefix $ default.fatalLevelName;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
Log(levelPrefix @ message);
|
||||
}
|
||||
|
||||
public final function LogMessage(LogLevel messageLevel, string message)
|
||||
{
|
||||
switch (messageLevel)
|
||||
{
|
||||
case LOG_Trace:
|
||||
traceMessages[traceMessages.length] = message;
|
||||
case LOG_Debug:
|
||||
debugMessages[debugMessages.length] = message;
|
||||
case LOG_Info:
|
||||
infoMessages[infoMessages.length] = message;
|
||||
case LOG_Warning:
|
||||
warningMessages[warningMessages.length] = message;
|
||||
case LOG_Error:
|
||||
errorMessages[errorMessages.length] = message;
|
||||
case LOG_Fatal:
|
||||
fatalMessages[fatalMessages.length] = message;
|
||||
default:
|
||||
}
|
||||
if (ShouldAddToKFLog(messageLevel))
|
||||
{
|
||||
LogMessageToKFLog(messageLevel, message);
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
// Log everything by default, if someone does not like it -
|
||||
// he/she can disable it themselves.
|
||||
logTraceInKFLog = true
|
||||
logDebugInKFLog = true
|
||||
logInfoInKFLog = true
|
||||
logWarningInKFLog = true
|
||||
logErrorInKFLog = true
|
||||
logFatalInKFLog = true
|
||||
// Parts of the prefix for our log messages, redirected into kf log file.
|
||||
kfLogPrefix = "Acedia:"
|
||||
traceLevelName = "Trace"
|
||||
debugLevelName = "Debug"
|
||||
infoLevelName = "Info"
|
||||
warningLevelName = "Warning"
|
||||
errorLevelName = "Error"
|
||||
fatalLevelName = "Fatal"
|
||||
}
|
||||
|
|
@ -1,290 +0,0 @@
|
|||
/**
|
||||
* API that provides functions for managing objects and actors by providing
|
||||
* easy and general means to create and destroy them, that allow to make use of
|
||||
* temporary `Object`s in a more efficient way.
|
||||
* This is a low-level API that most users of Acedia, most likely,
|
||||
* would not have to use, since creation of most objects would use their own
|
||||
* wrapper functions around this API.
|
||||
* Copyright 2020 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 MemoryAPI extends Singleton;
|
||||
|
||||
// This variable counts ticks and should be different each new tick.
|
||||
var private int currentTick;
|
||||
|
||||
// Stores instance of an `Object` that can be borrowed from the pool.
|
||||
struct BorrowableRecord
|
||||
{
|
||||
// Borrowable instance
|
||||
var Object instance;
|
||||
// Was this object borrowed?
|
||||
// This flag will persist unless object was explicitly freed,
|
||||
// even if borrowed reference timed out.
|
||||
var bool borrowed;
|
||||
// When was this object borrowed?
|
||||
// Used to automatically free borrowed objects after the tick has passed.
|
||||
var int borrowTick;
|
||||
};
|
||||
|
||||
// Available object pools
|
||||
var private array<BorrowableRecord> borrowPool;
|
||||
|
||||
// Checks if instance in the given `record` is borrowed.
|
||||
private final function bool IsBorrowed(BorrowableRecord record)
|
||||
{
|
||||
// `record.borrowed` means instance was borrowed,
|
||||
// but not explicitly freed;
|
||||
// `record.borrowTick >= currentTick` means that rights to the borrowed
|
||||
// instance hasn't yet ran out.
|
||||
return (record.borrowed && record.borrowTick >= currentTick);
|
||||
}
|
||||
|
||||
// Loads a reference to class instance from it's string representation.
|
||||
private final function class<Object> LoadClass(string classReference)
|
||||
{
|
||||
return class<Object>(DynamicLoadObject(classReference, class'Class', true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `Object` / `Actor` of a given class.
|
||||
*
|
||||
* If uses a proper spawning mechanism for both objects (`new`)
|
||||
* and actors (`Spawn`).
|
||||
*
|
||||
* @param classToAllocate Class of the `Object` / `Actor` that this method
|
||||
* must create.
|
||||
* @return Newly created object, might be `none` if creation has failed.
|
||||
*/
|
||||
public final function Object Allocate(class<Object> classToAllocate)
|
||||
{
|
||||
local class<Actor> actorClassToSpawn;
|
||||
if (classToAllocate == none) return none;
|
||||
|
||||
actorClassToSpawn = class<Actor>(classToAllocate);
|
||||
if (actorClassToSpawn != none)
|
||||
{
|
||||
return Spawn(actorClassToSpawn);
|
||||
}
|
||||
return (new classToAllocate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new `Object` / `Actor` of a given class.
|
||||
*
|
||||
* If uses a proper spawning mechanism for both objects (`new`)
|
||||
* and actors (`Spawn`).
|
||||
*
|
||||
* @param classToAllocate Text representation (name) of the class of the
|
||||
* `Object` / `Actor` that this method must create.
|
||||
* Should contain full package-path.
|
||||
* @return Newly created object, might be `none` if creation has failed.
|
||||
*/
|
||||
public final function Object AllocateByReference(string refToClassToAllocate)
|
||||
{
|
||||
return Allocate(LoadClass(refToClassToAllocate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Borrows an instance of an `Object` / `Actor` of the given class
|
||||
* from the pool.
|
||||
* Borrowed instance will be auto-freed during next tick.
|
||||
*
|
||||
* @param classToBorrow Class of an `Object` / `Actor` we want to borrow.
|
||||
* @return Borrowed object, might be `none` if borrow pool is empty and
|
||||
* creation of a new `Object` / `Actor` has failed.
|
||||
*/
|
||||
public final function Object Borrow(class<Object> classToBorrow)
|
||||
{
|
||||
local int i;
|
||||
local BorrowableRecord newRecord;
|
||||
for (i = 0; i < borrowPool.length; i += 1)
|
||||
{
|
||||
if (IsBorrowed(borrowPool[i])) continue;
|
||||
if (borrowPool[i].instance == none) continue;
|
||||
if (borrowPool[i].instance.class != classToBorrow) continue;
|
||||
|
||||
borrowPool[i].borrowed = true;
|
||||
borrowPool[i].borrowTick = currentTick;
|
||||
return borrowPool[i].instance;
|
||||
}
|
||||
// Create a new instance to borrow, if there isn't any available for
|
||||
// the given class.
|
||||
newRecord.borrowed = false;
|
||||
newRecord.instance = Allocate(classToBorrow);
|
||||
if (newRecord.instance != none)
|
||||
{
|
||||
borrowPool[borrowPool.length] = newRecord;
|
||||
}
|
||||
return newRecord.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Borrows an instance of an `Object` / `Actor` of the given class
|
||||
* from the pool.
|
||||
* Borrowed instance will be auto-freed during next tick.
|
||||
*
|
||||
* @param classToBorrow Text representation (name) of the class of
|
||||
* an `Object` / `Actor` we want to borrow.
|
||||
* @return Borrowed object, might be `none` if borrow pool is empty and
|
||||
* creation of a new `Object` / `Actor` has failed.
|
||||
*/
|
||||
public final function Object BorrowByReference(string refToClassToBorrow)
|
||||
{
|
||||
return Borrow(LoadClass(refToClassToBorrow));
|
||||
}
|
||||
|
||||
/**
|
||||
* Claims an instance of an `Object` / `Actor` of the given class
|
||||
* from the pool.
|
||||
* Claimed instances are removed from the borrow pool and
|
||||
* will not be automatically freed.
|
||||
*
|
||||
* @param classToClaim Class of an `Object` / `Actor` we wish to borrow.
|
||||
* @return Borrowed object, might be `none` if borrow pool is empty and
|
||||
* creation of a new `Object` / `Actor` has failed.
|
||||
*/
|
||||
public final function Object Claim(class<Object> classToClaim)
|
||||
{
|
||||
local int i;
|
||||
local Object instance;
|
||||
for (i = 0; i < borrowPool.length; i += 1)
|
||||
{
|
||||
if (IsBorrowed(borrowPool[i])) continue;
|
||||
if (borrowPool[i].instance == none) continue;
|
||||
if (borrowPool[i].instance.class != classToClaim) continue;
|
||||
|
||||
instance = borrowPool[i].instance;
|
||||
borrowPool.Remove(i, 1);
|
||||
return instance;
|
||||
}
|
||||
// Create a new instance to borrow, if there isn't any available for
|
||||
// the given class.
|
||||
return Allocate(classToClaim);
|
||||
}
|
||||
|
||||
/**
|
||||
* Claims an instance of an `Object` / `Actor` of the given class
|
||||
* from the pool.
|
||||
* Claimed instances are removed from the borrow pool and
|
||||
* will not be automatically freed.
|
||||
*
|
||||
* @param classToClaim Text representation (name) of the class of
|
||||
* an `Object` / `Actor` we wish to claim.
|
||||
* @return Borrowed object, might be `none` if borrow pool is empty and
|
||||
* creation of a new `Object` / `Actor` has failed.
|
||||
*/
|
||||
public final function Object ClaimByReference(string refToClassToClaim)
|
||||
{
|
||||
return Claim(LoadClass(refToClassToClaim));
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees given `Object` / `Actor` resource.
|
||||
*
|
||||
* By default `Actor`s are destroyed.
|
||||
* Due to limitations of the engine objects cannot be outright destroyed.
|
||||
* Instead, they are put into a "borrow pool", from where they can later be
|
||||
* taken for a reuse.
|
||||
*
|
||||
* @param objectToDelete `Object` / `Actor` that must be freed.
|
||||
* @param forceMakeBorrowable Only has an effect if `objectToDelete`
|
||||
* is an `Actor`, in which case it forces it to be added
|
||||
* to the borrow pool, instead of being destroyed.
|
||||
*/
|
||||
public final function Free
|
||||
(
|
||||
Object objectToDelete,
|
||||
optional bool forceMakeBorrowable
|
||||
)
|
||||
{
|
||||
local int i;
|
||||
local Actor actorToDelete;
|
||||
local BorrowableRecord newRecord;
|
||||
if (objectToDelete == none) return;
|
||||
|
||||
actorToDelete = Actor(objectToDelete);
|
||||
if (actorToDelete != none && !forceMakeBorrowable)
|
||||
{
|
||||
actorToDelete.Destroy();
|
||||
return;
|
||||
}
|
||||
// Check if `objectToDelete` is already in our records.
|
||||
for (i = 0; i < borrowPool.length; i += 1)
|
||||
{
|
||||
if (borrowPool[i].instance == objectToDelete)
|
||||
{
|
||||
borrowPool[i].borrowed = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If not - add it
|
||||
newRecord.instance = objectToDelete;
|
||||
newRecord.borrowed = false;
|
||||
borrowPool[borrowPool.length] = newRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces Unreal Engine to do garbage collection.
|
||||
* By default also cleans up all the objects in the borrow object pool.
|
||||
*
|
||||
* Process of garbage collection causes significant lag spike during the game
|
||||
* and should be used carefully.
|
||||
*
|
||||
* NOTE: method does not guarantee that borrow pool will be empty after
|
||||
* this call (even with `keepBorrowedObjectPool = true`),
|
||||
* since some of the borrowable objects might be currently in use and,
|
||||
* therefore, cannot be garbage collected.
|
||||
*
|
||||
* @param keepBorrowedObjectPool Set this to `true` to NOT garbage collect
|
||||
* objects in a borrow pool. Otherwise keep it `false`.
|
||||
*/
|
||||
public final function CollectGarbage(optional bool keepBorrowedObjectPool)
|
||||
{
|
||||
local int i;
|
||||
if (!keepBorrowedObjectPool)
|
||||
{
|
||||
// Dereference all non-borrowed objects from borrow pool,
|
||||
// so that they can be garbage collected.
|
||||
i = 0;
|
||||
while (i < borrowPool.length)
|
||||
{
|
||||
if ( borrowPool[i].instance == none
|
||||
|| !IsBorrowed(borrowPool[i]) )
|
||||
{
|
||||
borrowPool.Remove(i, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// This makes Unreal Engine do garbage collection
|
||||
ConsoleCommand("obj garbage");
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
currentTick += 1;
|
||||
}
|
||||
|
||||
// TODO: add cleaning on cooldown
|
||||
defaultproperties
|
||||
{
|
||||
currentTick = 0
|
||||
}
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
/**
|
||||
* Parent class for all services used in Acedia.
|
||||
* Currently simply makes itself server-only.
|
||||
* Copyright 2020 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 Service extends Singleton
|
||||
abstract;
|
||||
|
||||
// Listeners listed here will be automatically activated.
|
||||
var public const array< class<Listener> > requiredListeners;
|
||||
|
||||
// Enables feature of given class.
|
||||
public static final function Service Require()
|
||||
{
|
||||
local Service newInstance;
|
||||
if (IsRunning())
|
||||
{
|
||||
return Service(GetInstance());
|
||||
}
|
||||
default.blockSpawning = false;
|
||||
newInstance = class'Acedia'.static.GetInstance().Spawn(default.class);
|
||||
default.blockSpawning = true;
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
// Whether service is currently running is determined by
|
||||
public static final function bool IsRunning()
|
||||
{
|
||||
return (GetInstance() != none);
|
||||
}
|
||||
|
||||
protected function OnLaunch(){}
|
||||
protected function OnShutdown(){}
|
||||
|
||||
protected function OnCreated()
|
||||
{
|
||||
default.blockSpawning = true;
|
||||
SetListenersActiveSatus(true);
|
||||
OnLaunch();
|
||||
}
|
||||
|
||||
protected function OnDestroyed()
|
||||
{
|
||||
SetListenersActiveSatus(false);
|
||||
OnShutdown();
|
||||
}
|
||||
|
||||
// Set listeners' status
|
||||
private static function SetListenersActiveSatus(bool newStatus)
|
||||
{
|
||||
local int i;
|
||||
for (i = 0; i < default.requiredListeners.length; i += 1)
|
||||
{
|
||||
if (default.requiredListeners[i] == none) continue;
|
||||
default.requiredListeners[i].static.SetActive(newStatus);
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
DrawType = DT_None
|
||||
// Prevent spawning this feature by any other means than 'Launch()'.
|
||||
blockSpawning = true
|
||||
// Features are server-only actors
|
||||
remoteRole = ROLE_None
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
/**
|
||||
* Singleton is an auxiliary class, meant to be used as a base for others,
|
||||
* that allows for only one instance of it to exist.
|
||||
* To make sure your child class properly works, either don't overload
|
||||
* 'PreBeginPlay' or make sure to call it's parent's version.
|
||||
* Copyright 2019 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 Singleton extends AcediaActor
|
||||
abstract;
|
||||
|
||||
// Default value of this variable will store one and only existing version
|
||||
// of actor of this class.
|
||||
var private Singleton activeInstance;
|
||||
|
||||
// Setting default value of this variable to 'true' prevents creation of
|
||||
// a singleton, even if no instances of it exist.
|
||||
// Only a default value is ever used.
|
||||
var protected bool blockSpawning;
|
||||
|
||||
public final static function Singleton GetInstance(optional bool spawnIfMissing)
|
||||
{
|
||||
local bool instanceExists;
|
||||
instanceExists = default.activeInstance != none
|
||||
&& !default.activeInstance.bPendingDelete;
|
||||
if (instanceExists) {
|
||||
return default.activeInstance;
|
||||
}
|
||||
if (spawnIfMissing) {
|
||||
return class'Acedia'.static.GetInstance().Spawn(default.class);
|
||||
}
|
||||
return none;
|
||||
}
|
||||
|
||||
public final static function bool IsSingletonCreationBlocked()
|
||||
{
|
||||
return default.blockSpawning;
|
||||
}
|
||||
|
||||
protected function OnCreated(){}
|
||||
protected function OnDestroyed(){}
|
||||
|
||||
// Make sure only one instance of 'Singleton' exists at any point in time.
|
||||
// Instead of overloading this function we suggest you overload a special
|
||||
// event function `OnCreated()` that is called whenever a valid `Singleton`
|
||||
// instance is spawned.
|
||||
// If you absolutely must overload this function in any child class -
|
||||
// first call this version of the method and then check if
|
||||
// you are about to be deleted 'bDeleteMe == true':
|
||||
// ____________________________________________________________________________
|
||||
// | super.PreBeginPlay();
|
||||
// | // ^^^ If singleton wasn't already created, - only after that call
|
||||
// | // will instance, returned by 'GetInstance()', be set.
|
||||
// | if (bDeleteMe)
|
||||
// | return;
|
||||
// |___________________________________________________________________________
|
||||
event PreBeginPlay()
|
||||
{
|
||||
super.PreBeginPlay();
|
||||
if (default.blockSpawning || GetInstance() != none)
|
||||
{
|
||||
Destroy();
|
||||
}
|
||||
else
|
||||
{
|
||||
default.activeInstance = self;
|
||||
OnCreated();
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure only one instance of 'Singleton' exists at any point in time.
|
||||
// Instead of overloading this function we suggest you overload a special
|
||||
// event function `OnDestroyed()` that is called whenever a valid `Singleton`
|
||||
// instance is destroyed.
|
||||
// If you absolutely must overload this function in any child class -
|
||||
// first call this version of the method.
|
||||
event Destroyed()
|
||||
{
|
||||
super.Destroyed();
|
||||
if (self == default.activeInstance)
|
||||
{
|
||||
OnDestroyed();
|
||||
default.activeInstance = none;
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
blockSpawning = false
|
||||
}
|
||||
|
|
@ -1,294 +0,0 @@
|
|||
/**
|
||||
* Class for storing and processing the information about how well testing
|
||||
* against a certain issue went.
|
||||
* Copyright 2020 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 IssueSummary extends AcediaObject;
|
||||
|
||||
// Each issue is uniquely identified by these values.
|
||||
var private class<TestCase> ownerCase;
|
||||
var private string context;
|
||||
var private string description;
|
||||
|
||||
// Records, in chronological order, results of the tests that were
|
||||
// run to test this issue.
|
||||
var private array<byte> successRecords;
|
||||
|
||||
private final function byte BoolToByte(bool boolToConvert)
|
||||
{
|
||||
if (boolToConvert) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets `TestCase`, context and description for the issue,
|
||||
* tracked in this summary.
|
||||
*
|
||||
* Can only be successfully called once, but will fail if passed a `none`
|
||||
* class reference to `TestCase`.
|
||||
*
|
||||
* @param targetCase `TestCase`, in which issue,
|
||||
* relevant to this summary, is defined.
|
||||
* @param targetContext Context, in which this issue,
|
||||
* relevant to this summary, is defined.
|
||||
* @param targetDescription Description of the issue relevant to
|
||||
* this summary.
|
||||
* @return `true` if `TestCase`, context and description were successfully set,
|
||||
* `false` otherwise.
|
||||
*/
|
||||
public final function bool SetIssue(
|
||||
class<TestCase> targetCase,
|
||||
string targetContext,
|
||||
string targetDescription
|
||||
)
|
||||
{
|
||||
if (ownerCase != none) return false;
|
||||
if (initCase == none) return false;
|
||||
ownerCase = targetCase;
|
||||
context = targetContext;
|
||||
description = targetDescription;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns context for the issue in question.
|
||||
*
|
||||
* `TestCase` can be important for both displaying information about testing to
|
||||
* the user and distinguishing between two different issues with the same
|
||||
* description and context.
|
||||
* @see `TestCase` for more information.
|
||||
*
|
||||
* @return Test case that tested for relevant issue.
|
||||
*/
|
||||
public final function class<TestCase> GetTestCase()
|
||||
{
|
||||
return ownerCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns context for the issue in question.
|
||||
*
|
||||
* Context can be important for both displaying information about testing to
|
||||
* the user and distinguishing between two different issues with
|
||||
* the same description and in the same `TestCase`.
|
||||
* @see `TestCase` for more information.
|
||||
*
|
||||
* @return Context for relevant issue.
|
||||
*/
|
||||
public final function string GetContext()
|
||||
{
|
||||
if (ownerCase == none) return "";
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns description for the issue in question.
|
||||
*
|
||||
* Description of an issue is the main way to distinguish between
|
||||
* different possibly arising problems.
|
||||
* Two different issues can have the same description if they are defined
|
||||
* in different `TestCase`s and/or in different context.
|
||||
* @see `TestCase` for more information.
|
||||
*
|
||||
* @return Description for the issue in question.
|
||||
*/
|
||||
public final function string GetDescription()
|
||||
{
|
||||
if (ownerCase == none) return "";
|
||||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds result of another test (success or not) to the records of this summary.
|
||||
*
|
||||
* @param success `true` if test was successful and had passed,
|
||||
* `false` otherwise.
|
||||
*/
|
||||
public final function AddTestResult(bool success)
|
||||
{
|
||||
successRecords[successRecords.length] = BoolToByte(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns total amount of test results recorded in caller summary.
|
||||
* Never a negative value.
|
||||
*
|
||||
* @return Amount of tests that were run.
|
||||
*/
|
||||
public final function int GetTotalTestsAmount()
|
||||
{
|
||||
return successRecords.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns total amount of recorded successful test results in caller summary.
|
||||
* Never a negative value.
|
||||
*
|
||||
* @return Amount of recorded successfully performed tests for
|
||||
* the relevant issue.
|
||||
*/
|
||||
public final function int GetSuccessfulTestsAmount()
|
||||
{
|
||||
local int i;
|
||||
local int counter;
|
||||
counter = 0;
|
||||
for (i = 0; i < successRecords.length; i += 1)
|
||||
{
|
||||
if (successRecords[i] > 0) {
|
||||
counter += 1;
|
||||
}
|
||||
}
|
||||
return counter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns total amount of recorded failed test results in caller summary.
|
||||
* Never a negative value.
|
||||
*
|
||||
* @return Amount of recorded failed tests for the relevant issue.
|
||||
*/
|
||||
public final function int GetFailedTestsAmount()
|
||||
{
|
||||
return GetTotalTestsAmount() - GetSuccessfulTestsAmount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns total success rate ("amount of successes" / "total amount of tests")
|
||||
* of recorded test results for relevant issue
|
||||
* (value between 0 and 1, including boundaries).
|
||||
*
|
||||
* If there are no test results recorded - returns `-1`.
|
||||
*
|
||||
* @return Success rate of recorded test results for the relevant issue
|
||||
* Returns values outside [0; 1] segment (specifically, negative values)
|
||||
* iff no test results at all were recorded.
|
||||
*/
|
||||
public final function float GetSuccessRate()
|
||||
{
|
||||
local int totalTestsAmount;
|
||||
totalTestsAmount = GetTotalTestsAmount();
|
||||
if (totalTestsAmount <= 0) {
|
||||
return -1;
|
||||
}
|
||||
return GetSuccessfulTestsAmount() / totalTestsAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether all tests recorded in this summary have passed.
|
||||
*
|
||||
* @return `true` if all tests for relevant issue have passed,
|
||||
* `false` otherwise.
|
||||
*/
|
||||
public final function bool HasPassedAllTests()
|
||||
{
|
||||
return (GetFailedTestsAmount() <= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns boolean array of test results: each element recording whether test
|
||||
* was a success (`>0`) or a failure (`0`).
|
||||
*
|
||||
* All results in the array are in a chronological order of arrival.
|
||||
*
|
||||
* @return Returns copy of boolean array of recorded test results.
|
||||
*/
|
||||
public final function array<byte> GetTestRecords()
|
||||
{
|
||||
return successRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns index numbers (starting from 1, not 0) of tests that ended in
|
||||
* a success, while performed for the same test case, context and issue.
|
||||
* So if tests went: [success, success, failure, success, failure],
|
||||
* method will return: [1, 2, 4].
|
||||
*
|
||||
* All results in the array are in a chronological order of arrival.
|
||||
*
|
||||
* @return index numbers of successful tests.
|
||||
*/
|
||||
public final function array<int> GetSuccessfulTests()
|
||||
{
|
||||
local int i;
|
||||
local array<int> result;
|
||||
for (i = 0; i < successRecords.length; i += 1)
|
||||
{
|
||||
if (successRecords[i] > 0) {
|
||||
result[result.length] = i + 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns index numbers (starting from 1, not 0) of tests that ended in
|
||||
* a failure, while performed for the same test case, context and issue.
|
||||
* So if tests went: [success, success, failure, success, failure],
|
||||
* method will return: [3, 5].
|
||||
*
|
||||
* All results in the array are in a chronological order of arrival.
|
||||
*
|
||||
* @return index numbers of successful tests.
|
||||
*/
|
||||
public final function array<int> GetFailedTests()
|
||||
{
|
||||
local int i;
|
||||
local array<int> result;
|
||||
for (i = 0; i < successRecords.length; i += 1)
|
||||
{
|
||||
if (successRecords[i] == 0) {
|
||||
result[result.length] = i + 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a formatted text representation of the caller `IssueSummary`
|
||||
* in a following format:
|
||||
* "{$text_default <issue_description>} {$text_subtle [<failed_test_numbers>]}"
|
||||
*
|
||||
* @return Formatted string with text representation of the
|
||||
* caller `IssueSummary`.
|
||||
*/
|
||||
public final function string ToString()
|
||||
{
|
||||
local int i;
|
||||
local string result;
|
||||
local array<int> failedTests;
|
||||
result = "{$text_default" @ GetDescription() $ "}";
|
||||
if (GetFailedTestsAmount() <= 0) {
|
||||
return result;
|
||||
}
|
||||
result @= "{$text_subtle [";
|
||||
failedTests = GetFailedTests();
|
||||
for (i = 0; i < failedTests.length; i += 1)
|
||||
{
|
||||
if (i < failedTests.length - 1) {
|
||||
result $= string(failedTests[i]) $ ", ";
|
||||
}
|
||||
else {
|
||||
result $= string(failedTests[i]);
|
||||
}
|
||||
}
|
||||
return (result $ "]");
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
/**
|
||||
* Event generator for events related to testing.
|
||||
* Copyright 2020 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 TestingEvents extends Events
|
||||
abstract;
|
||||
|
||||
static function CallTestingBegan(array< class<TestCase> > testQueue)
|
||||
{
|
||||
local int i;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0; i < listeners.length; i += 1)
|
||||
{
|
||||
class<TestingListenerBase>(listeners[i])
|
||||
.static.TestingBegan(testQueue);
|
||||
}
|
||||
}
|
||||
|
||||
static function CallCaseTested(
|
||||
class<TestCase> testedCase,
|
||||
TestCaseSummary result)
|
||||
{
|
||||
local int i;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0; i < listeners.length; i += 1)
|
||||
{
|
||||
class<TestingListenerBase>(listeners[i])
|
||||
.static.CaseTested(testedCase, result);
|
||||
}
|
||||
}
|
||||
|
||||
static function CallTestingEnded(
|
||||
array< class<TestCase> > testQueue,
|
||||
array<TestCaseSummary> results)
|
||||
{
|
||||
local int i;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0; i < listeners.length; i += 1)
|
||||
{
|
||||
class<TestingListenerBase>(listeners[i])
|
||||
.static.TestingEnded(testQueue, results);
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedListener = class'TestingListenerBase'
|
||||
}
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
/**
|
||||
* This service allows to separate running separate `TestCase`s in separate
|
||||
* ticks, which helps to avoid hang ups or false infinite loop detection.
|
||||
* Copyright 2020 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 TestingService extends Service
|
||||
config(AcediaSystem);
|
||||
|
||||
// All test cases, loaded from all available packages.
|
||||
// Always use `default` copy of this array.
|
||||
var private array< class<TestCase> > registeredTestCases;
|
||||
|
||||
// Will be `true` if we have yet more tests to run
|
||||
// (either during current or following ticks)
|
||||
var private bool runningTests;
|
||||
// Queue with all test cases for the current/next testing
|
||||
var private array< class<TestCase> > testCasesToRun;
|
||||
// Track which test case we need to execute during next tick
|
||||
var private int nextTestCase;
|
||||
|
||||
// Record test results during the last test run here.
|
||||
// After testing has finished - copy them into it's default value
|
||||
// `default.summarizedResults` to be available even after `TestingService`
|
||||
// shuts down.
|
||||
var private array<TestCaseSummary> summarizedResults;
|
||||
|
||||
// Configuration variables that tell Acedia what tests to run
|
||||
// (and whether to run any at all) on start up.
|
||||
var public config const bool runTestsOnStartUp;
|
||||
var public config const bool filterTestsByName;
|
||||
var public config const bool filterTestsByGroup;
|
||||
var public config const string requiredName;
|
||||
var public config const string requiredGroup;
|
||||
|
||||
// Shortcut to `TestingEvents`, so that we don't have to write
|
||||
// class'TestingEvents' every time.
|
||||
var const class<TestingEvents> events;
|
||||
|
||||
/**
|
||||
* Registers another `TestCase` class for later testing.
|
||||
*
|
||||
* @return `true` if registration was successful.
|
||||
*/
|
||||
public final static function bool RegisterTestCase(class<TestCase> newTestCase)
|
||||
{
|
||||
local int i;
|
||||
if (newTestCase == none) return false;
|
||||
|
||||
for (i = 0; i < default.registeredTestCases.length; i += 1)
|
||||
{
|
||||
if (default.registeredTestCases[i] == newTestCase) {
|
||||
return false;
|
||||
}
|
||||
// Warn if there are test cases with the same name and group
|
||||
if ( !(default.registeredTestCases[i].static.GetGroup()
|
||||
~= newTestCase.static.GetGroup())) {
|
||||
continue;
|
||||
}
|
||||
if ( !(default.registeredTestCases[i].static.GetName()
|
||||
~= newTestCase.static.GetName())) {
|
||||
continue;
|
||||
}
|
||||
default._.logger.Warning("Two different test cases with name \""
|
||||
$ newTestCase.static.GetName() $ "\" in the same group \""
|
||||
$ newTestCase.static.GetGroup() $ "\"have been registered:"
|
||||
@ "\"" $ string(newTestCase) $ "\" and \""
|
||||
$ string(default.registeredTestCases[i])
|
||||
$ "\". This can lead to issues and it is not something you can fix,"
|
||||
@ "- contact developers of the relevant packages.");
|
||||
}
|
||||
default.registeredTestCases[default.registeredTestCases.length] =
|
||||
newTestCase;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether service is still in the process of running tests.
|
||||
*
|
||||
* @return `true` if there are still some tests that are scheduled, but
|
||||
* were not yet ran and `false` otherwise.
|
||||
*/
|
||||
public final static function bool IsRunningTests()
|
||||
{
|
||||
local TestingService myInstance;
|
||||
myInstance = TestingService(class'TestingService'.static.GetInstance());
|
||||
if (myInstance == none) return false;
|
||||
|
||||
return myInstance.runningTests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the results of the last tests run.
|
||||
*
|
||||
* If no tests were run - returns an empty array.
|
||||
*
|
||||
* @return Results of the last tests run.
|
||||
*/
|
||||
public final static function array<TestCaseSummary> GetLastResults()
|
||||
{
|
||||
return default.summarizedResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds all tests to the testing queue.
|
||||
*
|
||||
* To actually run them use `Run()`.
|
||||
* To only run certain tests, - filter them by `FilterByName()`
|
||||
* and `FilterByGroup()`
|
||||
*
|
||||
* Will do nothing if service is already in the process of testing
|
||||
* (`IsRunningTests() == true`).
|
||||
*
|
||||
* @return Caller `TestService` to allow for method chaining.
|
||||
*/
|
||||
public final function TestingService PrepareTests()
|
||||
{
|
||||
if (runningTests) {
|
||||
return self;
|
||||
}
|
||||
testCasesToRun = default.registeredTestCases;
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters tests in current queue to only those that have a specific name.
|
||||
* Should be used after `PrepareTests()` call, but before `Run()`.
|
||||
*
|
||||
* Will do nothing if service is already in the process of testing
|
||||
* (`IsRunningTests() == true`).
|
||||
*
|
||||
* @return Caller `TestService` to allow for method chaining.
|
||||
*/
|
||||
public final function TestingService FilterByName(string caseName)
|
||||
{
|
||||
local int i;
|
||||
local array< class<TestCase> > preFiltered;
|
||||
if (runningTests) {
|
||||
return self;
|
||||
}
|
||||
preFiltered = testCasesToRun;
|
||||
testCasesToRun.length = 0;
|
||||
for (i = 0; i < preFiltered.length; i += 1)
|
||||
{
|
||||
if (preFiltered[i].static.GetName() ~= caseName) {
|
||||
testCasesToRun[testCasesToRun.length] = preFiltered[i];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters tests in current queue to only those that belong to
|
||||
* a specific group. Should be used after `PrepareTests()` call,
|
||||
* but before `Run()`.
|
||||
*
|
||||
* Will do nothing if service is already in the process of testing
|
||||
* (`IsRunningTests() == true`).
|
||||
*
|
||||
* @return Caller `TestService` to allow for method chaining.
|
||||
*/
|
||||
public final function TestingService FilterByGroup(string caseGroup)
|
||||
{
|
||||
local int i;
|
||||
local array< class<TestCase> > preFiltered;
|
||||
if (runningTests) {
|
||||
return self;
|
||||
}
|
||||
preFiltered = testCasesToRun;
|
||||
testCasesToRun.length = 0;
|
||||
for (i = 0; i < preFiltered.length; i += 1)
|
||||
{
|
||||
if (preFiltered[i].static.GetGroup() ~= caseGroup) {
|
||||
testCasesToRun[testCasesToRun.length] = preFiltered[i];
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes `TestingService` run all tests in a current queue.
|
||||
*
|
||||
* Queue musty be build before hand: start with `PrepareTests()` call and
|
||||
* optionally use `FilterByName()` / `FilterByGroup()` before
|
||||
* `Run()` method call.
|
||||
*
|
||||
* @return `false` if service is already performing the testing
|
||||
* and `true` otherwise. Note that `TestingService` might be inactive even
|
||||
* after `Run()` call that returns `true`, if the testing queue was empty.
|
||||
*/
|
||||
public final function bool Run()
|
||||
{
|
||||
if (runningTests) {
|
||||
return false;
|
||||
}
|
||||
nextTestCase = 0;
|
||||
runningTests = true;
|
||||
summarizedResults.length = 0;
|
||||
events.static.CallTestingBegan(testCasesToRun);
|
||||
if (testCasesToRun.length <= 0) {
|
||||
runningTests = false;
|
||||
events.static.CallTestingEnded(testCasesToRun, summarizedResults);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private final function DoTestingStep()
|
||||
{
|
||||
local TestCaseSummary newResult;
|
||||
if (nextTestCase >= testCasesToRun.length)
|
||||
{
|
||||
runningTests = false;
|
||||
default.summarizedResults = summarizedResults;
|
||||
events.static.CallTestingEnded(testCasesToRun, summarizedResults);
|
||||
return;
|
||||
}
|
||||
testCasesToRun[nextTestCase].static.PerformTests();
|
||||
newResult = testCasesToRun[nextTestCase].static.GetSummary();
|
||||
events.static.CallCaseTested(testCasesToRun[nextTestCase], newResult);
|
||||
summarizedResults[summarizedResults.length] = newResult;
|
||||
nextTestCase += 1;
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
// This will destroy us on the next tick after we were
|
||||
// either created or finished performing tests
|
||||
if (!runningTests) {
|
||||
Destroy();
|
||||
return;
|
||||
}
|
||||
DoTestingStep();
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
runTestsOnStartUp = false
|
||||
events = class'TestingEvents'
|
||||
}
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
/**
|
||||
* Base class aimed to contain sets of tests for various components of
|
||||
* Acedia and it's features.
|
||||
* Neither this class, nor it's children aren't supposed to
|
||||
* be instantiated.
|
||||
* Copyright 2020 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 TestCase extends AcediaObject
|
||||
abstract;
|
||||
|
||||
// Name by which this set of unit tests can be referred to.
|
||||
var protected const string caseName;
|
||||
// Name of group to which this set of unit tests belong.
|
||||
var protected const string caseGroup;
|
||||
|
||||
// Were all tests performed?
|
||||
var private bool finishedTests;
|
||||
// Context under which we are currently performing our tests.
|
||||
var private string currentContext;
|
||||
// Error message that will be generated if some test will fail now.
|
||||
var private string currentIssue;
|
||||
|
||||
// Summary where we are recording results of all our tests.
|
||||
var private TestCaseSummary currentSummary;
|
||||
|
||||
/**
|
||||
* Sets context for any tests that will follow this call (but before the next
|
||||
* `Context()` call).
|
||||
*
|
||||
* Context is supposed to be a short description about what
|
||||
* exactly you are testing. When reporting failed tests, - failures will be
|
||||
* grouped up by a context.
|
||||
*
|
||||
* Changing current context will also reset current issue, to set it up
|
||||
* use `Issue()` method.
|
||||
*
|
||||
* @param context Context for the following tests.
|
||||
*/
|
||||
public final static function Context(string context)
|
||||
{
|
||||
default.currentContext = context;
|
||||
default.currentIssue = ""; // Reset issue.
|
||||
}
|
||||
|
||||
// Call this function to define an error message for tests that
|
||||
// would fail after it.
|
||||
// Message is reset by another call of `Issue()` or
|
||||
// by changing the context via `Context()`.
|
||||
/**
|
||||
* Changes an issue that any following tests (but before the next `Issue()` or
|
||||
* `Context()` call) will test for.
|
||||
*
|
||||
* Issue is the message that will be displayed to the user if any relevant
|
||||
* tests have failed.
|
||||
*
|
||||
* NOTE: Current issue will be reset by any `Context()` call.
|
||||
*
|
||||
* @param issue Issue that following tests will test for.
|
||||
*/
|
||||
public final static function Issue(string issue)
|
||||
{
|
||||
default.currentIssue = issue;
|
||||
}
|
||||
|
||||
// Following functions provide simple test primitives
|
||||
|
||||
/**
|
||||
* This call will record either one success or one failure for the caller
|
||||
* `TestCase` class, depending on passed `bool` argument.
|
||||
*
|
||||
* @param result Your test's result as a `bool` value: `true` will record a
|
||||
* success and `false` a failure.
|
||||
*/
|
||||
public final static function TEST_ExpectTrue(bool result)
|
||||
{
|
||||
RecordTestResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* This call will record either one success or one failure for the caller
|
||||
* `TestCase` class, depending on passed `bool` argument.
|
||||
*
|
||||
* @param result Your test's result as a `bool` value: `false` will result in
|
||||
* recording a success and `true` in a failure.
|
||||
*/
|
||||
public final static function TEST_ExpectFalse(bool result)
|
||||
{
|
||||
RecordTestResult(!result);
|
||||
}
|
||||
|
||||
/**
|
||||
* This call will record either one success or one failure for the caller
|
||||
* `TestCase` class, depending on passed `Object` argument.
|
||||
*
|
||||
* @param result Your test's result as an `Object` value: `none` will result
|
||||
* in recording success and any non-`none` value in failure.
|
||||
*/
|
||||
public final static function TEST_ExpectNone(Object object)
|
||||
{
|
||||
RecordTestResult(object == none);
|
||||
}
|
||||
|
||||
/**
|
||||
* This call will record either one success or one failure for the caller
|
||||
* `TestCase` class, depending on passed `Object` argument.
|
||||
*
|
||||
* @param result Your test's result as an `Object` value: any non-`none`
|
||||
* value will result in recording success and `none` in failure.
|
||||
*/
|
||||
public final static function TEST_ExpectNotNone(Object object)
|
||||
{
|
||||
RecordTestResult(object != none);
|
||||
}
|
||||
|
||||
// Records (in current context summary) that another test was performed and
|
||||
// succeeded/failed, along with given error message.
|
||||
private final static function RecordTestResult(bool isSuccessful)
|
||||
{
|
||||
if (default.finishedTests) return;
|
||||
if (default.currentSummary == none) return;
|
||||
default.currentSummary.AddTestResult( default.currentContext,
|
||||
default.currentIssue,
|
||||
isSuccessful);
|
||||
}
|
||||
|
||||
/**
|
||||
* Once testing has finished returns compiled results as a
|
||||
* `TestCaseSummary` object.
|
||||
*
|
||||
* @return `TestCaseSummary` with compiled results if the testing has finished
|
||||
* and `none` otherwise.
|
||||
*/
|
||||
public final static function TestCaseSummary GetSummary()
|
||||
{
|
||||
if (!default.finishedTests) {
|
||||
return none;
|
||||
}
|
||||
return default.currentSummary;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this `TestCase` has already finished running all it's tests.
|
||||
* Finished testing means a prepared `TestCaseSummary` is available
|
||||
* (by `GetSummary()` method).
|
||||
*
|
||||
* @return `true` if this test case already did the testing
|
||||
* and `false` otherwise.
|
||||
*/
|
||||
public final static function bool HasFinishedTesting()
|
||||
{
|
||||
return default.finishedTests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns name of this `TestCase`.
|
||||
*
|
||||
* @return Name of this `TestCase`.
|
||||
*/
|
||||
public final static function string GetName()
|
||||
{
|
||||
return default.caseName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns group name of this `TestCase`.
|
||||
*
|
||||
* @return Group name of this `TestCase`.
|
||||
*/
|
||||
public final static function string GetGroup()
|
||||
{
|
||||
return default.caseGroup;
|
||||
}
|
||||
|
||||
// Calling this function will perform unit tests defined in `TESTS()`
|
||||
// function of this test case and will prepare the summary,
|
||||
// obtainable through `GetSummary()` function.
|
||||
// Returns `true` if all tests have successfully passed
|
||||
// and `false` otherwise.
|
||||
/**
|
||||
* Performs all tests for this `TestCase`.
|
||||
* Guaranteed to be done after this finishes.
|
||||
*
|
||||
* @return `true` if all tests have finished successfully
|
||||
* and `false` otherwise.
|
||||
*/
|
||||
public final static function bool PerformTests()
|
||||
{
|
||||
default.finishedTests = false;
|
||||
_().memory.Free(default.currentSummary);
|
||||
default.currentSummary = new class'TestCaseSummary';
|
||||
default.currentSummary.Initialize(default.class);
|
||||
TESTS();
|
||||
default.finishedTests = true;
|
||||
return default.currentSummary.HasPassedAllTests();
|
||||
}
|
||||
|
||||
/**
|
||||
* Any tests that your `TestCase` class needs to perform should be put in
|
||||
* this function.
|
||||
* To separate tests into groups it's recommended (as a style
|
||||
* consideration) to put them in separate function calls and give these
|
||||
* functions names starting with "Test_". They can have further folded
|
||||
* functions with prefix "SubTest_", which can contain "SubSubTest_", etc..
|
||||
*/
|
||||
protected static function TESTS(){}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
caseName = ""
|
||||
caseGroup = ""
|
||||
}
|
||||
|
|
@ -1,540 +0,0 @@
|
|||
/**
|
||||
* Class for storing and processing the information about how well testing
|
||||
* for a certain `TestCase` went. That information is stored as
|
||||
* a collection of `IssueSummary`s, that can be accessed all at once
|
||||
* or by their context.
|
||||
* `TestCaseSummary` must be initialized for some `TestCase` before it can
|
||||
* be used for anything (unlike `IssueSummary`).
|
||||
* Copyright 2020 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 TestCaseSummary extends AcediaObject;
|
||||
|
||||
// Case for which this summary was initialized.
|
||||
// `none` if it was not.
|
||||
var private class<TestCase> ownerCase;
|
||||
|
||||
/**
|
||||
*
|
||||
* We will store issue summaries for different contexts separately.
|
||||
* INVARIANT: any function that adds records to `contextRecords`
|
||||
* must guarantee that:
|
||||
* 1. No two distinct records will have the same `context`;
|
||||
* 2. All the `IssueSummary`s in `issueSummaries` array have different
|
||||
* issue descriptions.
|
||||
* Comparisons of `string`s for two above conditions are case-insensitive.
|
||||
*/
|
||||
struct ContextRecord
|
||||
{
|
||||
var string context;
|
||||
var array<IssueSummary> issueSummaries;
|
||||
};
|
||||
var private array<ContextRecord> contextRecords;
|
||||
|
||||
// String literals used for displaying array of test case summaries
|
||||
var private const string indent;
|
||||
var private const string reportHeader;
|
||||
var private const string reportSuccessfulEnding;
|
||||
var private const string reportUnsuccessfulEnding;
|
||||
|
||||
/**
|
||||
* Initializes caller summary for given `TestCase` class.
|
||||
* Can only be successfully done once, but will fail if
|
||||
* passed a `none` reference.
|
||||
*
|
||||
* @param targetCase `TestCase` class for which this summary will be
|
||||
* recording test results.
|
||||
* @return `true` if initialization was successful and `false otherwise
|
||||
* (either summary already initialized or passed reference is `none`).
|
||||
*/
|
||||
public final function bool Initialize(class<TestCase> targetCase)
|
||||
{
|
||||
if (ownerCase != none) return false;
|
||||
if (targetCase == none) return false;
|
||||
ownerCase = targetCase;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns index of a context record with a given description
|
||||
* (`context`) in `contextRecords`.
|
||||
* Creates one if missing. Never fails.
|
||||
*
|
||||
* @param context Context that desired record must match.
|
||||
* @return Index of the context record that matches `context`.
|
||||
* Returned index is always valid.
|
||||
*/
|
||||
private final function int TouchContext(string context)
|
||||
{
|
||||
local int i;
|
||||
local ContextRecord newRecord;
|
||||
// Try to find existing record with given context description
|
||||
for (i = 0; i < contextRecords.length; i += 1)
|
||||
{
|
||||
if (context ~= contextRecords[i].context) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// If there is none - make a new one
|
||||
newRecord.context = context;
|
||||
contextRecords[contextRecords.length] = newRecord;
|
||||
return (contextRecords.length - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds indices of a context record and an `IssueSummary` in
|
||||
* a nested array that have matching `context`
|
||||
* and `issueDescription`.
|
||||
* Creates records and/or `IssueSummary` if missing. Never fails.
|
||||
*
|
||||
* @param context Context description that
|
||||
* desired record must match.
|
||||
* @param issueDescription Issue description that
|
||||
* desired `IssueSummary`must match.
|
||||
* @param recordIndex Index of the context record that matches
|
||||
* `context` description will be recorded here.
|
||||
* Returned value is always valid. Passed value is discarded.
|
||||
* @param recordIndex Index of the `IssueSummary` that matches
|
||||
* `issueDescription` description will be recorded here.
|
||||
* Returned value is always valid. Passed value is discarded.
|
||||
*/
|
||||
private final function TouchIssue(
|
||||
string context,
|
||||
string issueDescription,
|
||||
out int recordIndex,
|
||||
out int issueIndex
|
||||
)
|
||||
{
|
||||
local int i;
|
||||
local array<IssueSummary> issueSummaries;
|
||||
recordIndex = TouchContext(context);
|
||||
issueSummaries = contextRecords[recordIndex].issueSummaries;
|
||||
// Try to find existing issue summary with a given description
|
||||
for (i = 0; i < issueSummaries.length; i += 1)
|
||||
{
|
||||
if (issueSummaries[i] == none) continue;
|
||||
if (issueDescription ~= issueSummaries[i].GetDescription())
|
||||
{
|
||||
issueIndex = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If there is none - add a new one
|
||||
issueIndex = issueSummaries.length;
|
||||
issueSummaries[issueIndex] = new class'IssueSummary';
|
||||
issueSummaries[issueIndex].SetIssue(ownerCase, context, issueDescription);
|
||||
contextRecords[recordIndex].issueSummaries = issueSummaries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if caller summary was correctly initialized.
|
||||
*
|
||||
* @return `true` if summary was correctly initialized and `false` otherwise.
|
||||
*/
|
||||
public final function bool IsInitialized()
|
||||
{
|
||||
return (ownerCase != none);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds result of another test (success or not) to the records of this summary.
|
||||
*
|
||||
* @param context Context under which test was performed.
|
||||
* @param issueDescription Description of issue,
|
||||
* for which test was performed.
|
||||
* @param success `true` if test was successful and had passed,
|
||||
* `false` otherwise.
|
||||
*/
|
||||
public final function AddTestResult(
|
||||
string context,
|
||||
string issueDescription,
|
||||
bool success
|
||||
)
|
||||
{
|
||||
local int recordIndex, issueIndex;
|
||||
TouchIssue(context, issueDescription, recordIndex, issueIndex);
|
||||
contextRecords[recordIndex]
|
||||
.issueSummaries[issueIndex]
|
||||
.AddTestResult(success);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all contexts, for which caller summary has any records of tests
|
||||
* being performed.
|
||||
*
|
||||
* To check if particular context exists you can use `DoesContextExists()`.
|
||||
*
|
||||
* @return Array of `string`s, each representing one of the contexts,
|
||||
* used in tests.
|
||||
* Guarantees no duplicates (equality without accounting for case).
|
||||
*/
|
||||
public final function array<string> GetContexts()
|
||||
{
|
||||
local int i;
|
||||
local array<string> result;
|
||||
for (i = 0; i < contextRecords.length; i += 1) {
|
||||
result[result.length] = contextRecords[i].context;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if given context has any records about performing tests
|
||||
* (whether they ended in success or a failure) under it.
|
||||
*
|
||||
* To get an array of all existing contexts use `GetContexts()`.
|
||||
*
|
||||
* @param context A context to check for existing in records.
|
||||
* @return `true` if there was a record about a test being performed under
|
||||
* a given context and `false` otherwise.
|
||||
*/
|
||||
public final function bool DoesContextExists(string context)
|
||||
{
|
||||
local int i;
|
||||
for (i = 0; i < contextRecords.length; i += 1)
|
||||
{
|
||||
if (contextRecords[i].context ~= context) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* `IssueSummary`s for every issue that was tested and recorded in
|
||||
* the caller `TestCaseSummary`.
|
||||
*
|
||||
* @return Array of `IssueSummary`s for every tested and recorded issue.
|
||||
*/
|
||||
public final function array<IssueSummary> GetIssueSummaries()
|
||||
{
|
||||
local int i, j;
|
||||
local array<IssueSummary> recordedSummaries;
|
||||
local array<IssueSummary> result;
|
||||
for (i = 0; i < contextRecords.length; i += 1)
|
||||
{
|
||||
recordedSummaries = contextRecords[i].issueSummaries;
|
||||
for (j = 0; j < recordedSummaries.length; j += 1) {
|
||||
result[result.length] = recordedSummaries[j];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `IssueSummary`s for every issue that was tested under
|
||||
* a given context and recorded in caller `TestCaseSummary`.
|
||||
*
|
||||
* @param context Context under which issues of interest were tested.
|
||||
* @return Array of `IssueSummary`s for every issue that was tested under
|
||||
* given context.
|
||||
*/
|
||||
public final function array<IssueSummary> GetIssueSummariesForContext(
|
||||
string context
|
||||
)
|
||||
{
|
||||
local int i;
|
||||
local array<IssueSummary> emptyResult;
|
||||
for (i = 0; i < contextRecords.length; i += 1)
|
||||
{
|
||||
if (contextRecords[i].context ~= context) {
|
||||
return contextRecords[i].issueSummaries;
|
||||
}
|
||||
}
|
||||
return emptyResult;
|
||||
}
|
||||
|
||||
// Counts total amount of tests performed under the contexts
|
||||
// corresponding to `contextRecords[recordIndex]` record.
|
||||
private final function int GetTotalTestsAmountForRecord(int recordIndex)
|
||||
{
|
||||
local int i;
|
||||
local int result;
|
||||
local array<IssueSummary> issueSummaries;
|
||||
issueSummaries = contextRecords[recordIndex].issueSummaries;
|
||||
result = 0;
|
||||
for (i = 0; i < issueSummaries.length; i += 1)
|
||||
{
|
||||
if (issueSummaries[i] == none) continue;
|
||||
result += issueSummaries[i].GetTotalTestsAmount();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total amount of performed tests, recorded in caller `TestCaseSummary`.
|
||||
*
|
||||
* If you are interested in amount of test under a specific context, -
|
||||
* use `GetTotalTestsAmountForContext()` instead.
|
||||
*
|
||||
* @return Total amount of performed tests.
|
||||
*/
|
||||
public final function int GetTotalTestsAmount()
|
||||
{
|
||||
local int i;
|
||||
local int result;
|
||||
for (i = 0; i < contextRecords.length; i += 1)
|
||||
{
|
||||
result += GetTotalTestsAmountForRecord(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total amount of tests, performed under a context `context` and
|
||||
* recorded in caller `TestCaseSummary`.
|
||||
*
|
||||
* If you are interested in total amount of test under all contexts, -
|
||||
* use `GetTotalTestsAmount()` instead.
|
||||
*
|
||||
* @param context Context for which method must count amount of
|
||||
* performed tests.
|
||||
* @return Total amount of tests, performed under given context.
|
||||
* If given context does not exist in records, - returns `-1`.
|
||||
*/
|
||||
public final function int GetTotalTestsAmountForContext(string context)
|
||||
{
|
||||
local int i;
|
||||
for (i = 0; i < contextRecords.length; i += 1)
|
||||
{
|
||||
if (context ~= contextRecords[i].context) {
|
||||
return GetTotalTestsAmountForRecord(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Counts total amount of successful tests performed under the contexts
|
||||
// corresponding to `contextRecords[recordIndex]` record.
|
||||
private final function int GetSuccessfulTestsAmountForRecord(int recordIndex)
|
||||
{
|
||||
local int i;
|
||||
local int result;
|
||||
local array<IssueSummary> issueSummaries;
|
||||
issueSummaries = contextRecords[recordIndex].issueSummaries;
|
||||
result = 0;
|
||||
for (i = 0; i < issueSummaries.length; i += 1)
|
||||
{
|
||||
if (issueSummaries[i] == none) continue;
|
||||
result += issueSummaries[i].GetSuccessfulTestsAmount();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total amount of successfully performed tests,
|
||||
* recorded in caller `TestCaseSummary`.
|
||||
*
|
||||
* If you are interested in amount of successful test under a specific context,
|
||||
* - use `GetSuccessfulTestsAmountForContext()` instead.
|
||||
*
|
||||
* @return Total amount of successfully performed tests.
|
||||
*/
|
||||
public final function int GetSuccessfulTestsAmount()
|
||||
{
|
||||
local int i;
|
||||
local int result;
|
||||
for (i = 0; i < contextRecords.length; i += 1)
|
||||
{
|
||||
result += GetSuccessfulTestsAmountForRecord(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total amount of tests, performed under a context `context` and
|
||||
* recorded in caller `TestCaseSummary`.
|
||||
*
|
||||
* If you are interested in total amount of successful test under all contexts,
|
||||
* - use `GetSuccessfulTestsAmount()` instead.
|
||||
*
|
||||
* @param context Context for which we method must count amount of
|
||||
* successful tests.
|
||||
* @return Total amount of successful tests, performed under given context.
|
||||
* If given context does not exist in records, - returns `-1`.
|
||||
*/
|
||||
public final function int GetSuccessfulTestsAmountForContext(string context)
|
||||
{
|
||||
local int i;
|
||||
for (i = 0; i < contextRecords.length; i += 1)
|
||||
{
|
||||
if (context ~= contextRecords[i].context) {
|
||||
return GetSuccessfulTestsAmountForRecord(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Counts total amount of tests, failed under the contexts
|
||||
// corresponding to `contextRecords[recordIndex]` record.
|
||||
private final function int GetFailedTestsAmountForRecord(int recordIndex)
|
||||
{
|
||||
local int i;
|
||||
local int result;
|
||||
local array<IssueSummary> issueSummaries;
|
||||
issueSummaries = contextRecords[recordIndex].issueSummaries;
|
||||
result = 0;
|
||||
for (i = 0; i < issueSummaries.length; i += 1)
|
||||
{
|
||||
if (issueSummaries[i] == none) continue;
|
||||
result += issueSummaries[i].GetFailedTestsAmount();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total amount of failed tests, recorded in caller `TestCaseSummary`.
|
||||
*
|
||||
* If you are interested in amount of failed test under a specific context, -
|
||||
* use `GetFailedTestsAmountForContext()` instead.
|
||||
*
|
||||
* @return Total amount of failed tests.
|
||||
*/
|
||||
public final function int GetFailedTestsAmount()
|
||||
{
|
||||
local int i;
|
||||
local int result;
|
||||
for (i = 0; i < contextRecords.length; i += 1)
|
||||
{
|
||||
result += GetFailedTestsAmountForRecord(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total amount of failed tests, performed under a context `context` and
|
||||
* recorded in caller `TestCaseSummary`.
|
||||
*
|
||||
* If you are interested in total amount of failed test under all contexts, -
|
||||
* use `GetFailedTestsAmount()` instead.
|
||||
*
|
||||
* @param context Context for which method must count amount of
|
||||
* failed tests.
|
||||
* @return Total amount of failed tests, performed under given context.
|
||||
* If given context does not exist in records, - returns `-1`.
|
||||
*/
|
||||
public final function int GetFailedTestsAmountForContext(string context)
|
||||
{
|
||||
local int i;
|
||||
for (i = 0; i < contextRecords.length; i += 1)
|
||||
{
|
||||
if (context ~= contextRecords[i].context) {
|
||||
return GetFailedTestsAmountForRecord(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether all tests recorded in this summary have passed.
|
||||
*
|
||||
* @return `true` if all tests have passed, `false` otherwise.
|
||||
*/
|
||||
public final function bool HasPassedAllTests()
|
||||
{
|
||||
return (GetFailedTestsAmount() <= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether all tests, performed under given context and
|
||||
* recorded in this summary, have passed.
|
||||
*
|
||||
* @return `true` if all tests under given context have passed,
|
||||
* `false` otherwise.
|
||||
* If given context does not exists - it did not fail any tests.
|
||||
*/
|
||||
public final function bool HasPassedAllTestsForContext(string context)
|
||||
{
|
||||
return (GetFailedTestsAmountForContext(context) <= 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a text summary for a set of results, given as array of
|
||||
* `TestCaseSummary`s (exactly how results are returned by `TestingService`).
|
||||
*
|
||||
* @param summaries `TestCase` summaries (obtained as a result of testing)
|
||||
* that we want to display.
|
||||
* @return Test representation of `summaries` as an array of
|
||||
* formatted strings, where each string corresponds to it's own line.
|
||||
*/
|
||||
public final static function array<string> GenerateStringSummary(
|
||||
array<TestCaseSummary> summaries)
|
||||
{
|
||||
local int i;
|
||||
local bool allTestsPassed;
|
||||
local array<string> result;
|
||||
allTestsPassed = true;
|
||||
result[0] = default.reportHeader;
|
||||
for (i = 0; i < summaries.length; i += 1)
|
||||
{
|
||||
if (summaries[i] == none) continue;
|
||||
summaries[i].AppendCaseSummary(result);
|
||||
allTestsPassed = allTestsPassed && summaries[i].HasPassedAllTests();
|
||||
}
|
||||
if (allTestsPassed) {
|
||||
result[result.length] = default.reportSuccessfulEnding;
|
||||
}
|
||||
else {
|
||||
result[result.length] = default.reportUnsuccessfulEnding;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Add text representation of caller `TestCase` to the existing array `result`.
|
||||
private final function AppendCaseSummary(out array<string> result)
|
||||
{
|
||||
local int i, j;
|
||||
local array<string> contexts;
|
||||
local string testCaseAnnouncement;
|
||||
local array<IssueSummary> issues;
|
||||
if (ownerCase == none) return;
|
||||
// Announce case
|
||||
testCaseAnnouncement = "{$text_default Test case {$text_emphasis";
|
||||
if (ownerCase.static.GetGroup() != "") {
|
||||
testCaseAnnouncement @= "[" $ ownerCase.static.GetGroup() $ "]";
|
||||
}
|
||||
testCaseAnnouncement @= ownerCase.static.GetName() $ "}:}";
|
||||
if (GetFailedTestsAmount() > 0) {
|
||||
testCaseAnnouncement @= "{$text_failure failed}!";
|
||||
}
|
||||
else {
|
||||
testCaseAnnouncement @= "{$text_ok passed}!";
|
||||
}
|
||||
result[result.length] = testCaseAnnouncement;
|
||||
// Report failed tests
|
||||
contexts = GetContexts();
|
||||
for (i = 0;i < contexts.length; i += 1)
|
||||
{
|
||||
if (GetFailedTestsAmountForContext(contexts[i]) <= 0) continue;
|
||||
result[result.length] = "{$text_warning " $ contexts[i] $ "}";
|
||||
issues = GetIssueSummariesForContext(contexts[i]);
|
||||
for (j = 0; j < issues.length; j += 1)
|
||||
{
|
||||
if (issues[j] == none) continue;
|
||||
if (issues[j].GetFailedTestsAmount() <= 0) continue;
|
||||
result[result.length] = indent $ issues[j].ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
indent = " "
|
||||
reportHeader = "{$text_default ############################## {$text_emphasis Test summary} ###############################}"
|
||||
reportSuccessfulEnding = "{$text_default ########################### {$text_ok All tests have passed!} ############################}"
|
||||
reportUnsuccessfulEnding = "{$text_default ########################## {$text_failure Some tests have failed :(} ###########################}"
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,290 +0,0 @@
|
|||
/**
|
||||
* Text object, meant as Acedia's replacement for a `string` type,
|
||||
* that is supposed to provide a better (although by no means full)
|
||||
* Unicode support than what is available from built-in unrealscript functions.
|
||||
* Main differences with `string` are:
|
||||
* 1. Text is a reference type, that doesn't copy it's contents with each
|
||||
* assignment.
|
||||
* 2. It's functions such as `ToUpper()` work with larger sets of
|
||||
* symbols than native functions such as `Caps()` that only work with
|
||||
* ASCII Latin;
|
||||
* 3. Can store a wider range of characters than `string`, although
|
||||
* the only way to actually add them to `Text` is via directly
|
||||
* inputting Unicode code points.
|
||||
* 4. Since it's functionality implemented in unrealscript,
|
||||
* Text is slower that a string;
|
||||
* 5. Once created, Text object won't disappear until garbage collection
|
||||
* is performed, even if it is not referenced anywhere.
|
||||
|
||||
* API that provides extended text handling with extended Cyrillic (Russian)
|
||||
* support (native functions like `Caps` only work with Latin letters).
|
||||
* Copyright 2020 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 Text extends AcediaObject;
|
||||
|
||||
// Used to store a result of a `ParseSign()` function.
|
||||
enum StringType
|
||||
{
|
||||
STRING_Plain,
|
||||
STRING_Colored,
|
||||
STRING_Formatted
|
||||
};
|
||||
|
||||
enum LetterCase
|
||||
{
|
||||
LCASE_Lower,
|
||||
LCASE_Upper
|
||||
};
|
||||
|
||||
enum StringColorType
|
||||
{
|
||||
STRCOLOR_Default,
|
||||
STRCOLOR_Struct,
|
||||
STRCOLOR_Alias
|
||||
};
|
||||
|
||||
struct Character
|
||||
{
|
||||
var int codePoint;
|
||||
// `false` if relevant character has a particular color,
|
||||
// `true` if it does not (use context-dependent default color).
|
||||
var StringColorType colorType;
|
||||
// Color of the relevant character if `isDefaultColor == false`.
|
||||
var Color color;
|
||||
var string colorAlias;
|
||||
};
|
||||
// We will store our string data in two different ways at once to make getters
|
||||
// faster at the cost of doing more work in functions that change the string.
|
||||
var private array<Character> contents;
|
||||
|
||||
/**
|
||||
* Sets new value of the `Text` object, that has called this method,
|
||||
* to be equal to the given `Text`. Does not change given `Text`.
|
||||
*
|
||||
* @param source After this function caller `Text` will have exactly
|
||||
* the same contents as given parameter.
|
||||
* @return Returns the calling `Text` object, to allow for function chaining.
|
||||
*/
|
||||
public final function Text Copy(Text otherText)
|
||||
{
|
||||
contents = otherText.contents;
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces data of caller `Text` object with data given by the array of
|
||||
* Unicode code points, preserving the order of characters where it matters
|
||||
* (some modifier code points are allowed arbitrary order in Unicode standard).
|
||||
*
|
||||
* `Text` isn't a simple wrapper around array of Unicode code points, so
|
||||
* this function call should be assumed to be more expensive than
|
||||
* a simple copy.
|
||||
*
|
||||
* @param source New contents of the `Text`.
|
||||
* @return Returns the calling object, to allow for function chaining.
|
||||
*/
|
||||
public final function Text CopyRaw(array<Character> rawSource)
|
||||
{
|
||||
contents = rawSource;
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies contents of the given string into caller `Text`.
|
||||
*
|
||||
* `Text` isn't a simple wrapper around unrealscript's `string`, so
|
||||
* this function call should be assumed to be more expensive than simple
|
||||
* `string` copy.
|
||||
*
|
||||
* @param source New contents of the caller `Text`.
|
||||
* @return Returns the calling `Text` object, to allow for function chaining.
|
||||
*/
|
||||
public final function Text CopyString(string source)
|
||||
{
|
||||
CopyRaw(_().text.StringToRaw(source));
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns data in the caller `Text` object in form of an array of
|
||||
* Unicode code points, preserving the order of characters where it matters
|
||||
* (some modifier code points are allowed arbitrary order in Unicode standard).
|
||||
*/
|
||||
public final function array<Character> ToRaw()
|
||||
{
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the `string` representation of contents of the caller `Text`.
|
||||
*
|
||||
* Unreal Engine doesn't seem to store code points higher than 2^16 in
|
||||
* `string`, so some data might be lost in the process.
|
||||
* (To check if it concerns you, refer to the Unicode symbol table,
|
||||
* but it is not a problem for most people).
|
||||
*/
|
||||
public final function string ToString(optional StringType resultType)
|
||||
{
|
||||
return _().text.RawToString(contents, resultType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the caller `Text` and a given `Text` have contain equal text
|
||||
* content, according to Unicode standard. By default case-sensitive.
|
||||
*/
|
||||
public final function bool IsEqual
|
||||
(
|
||||
Text otherText,
|
||||
optional bool caseInsensitive
|
||||
)
|
||||
{
|
||||
local int i;
|
||||
local array<Character> otherContentsCopy;
|
||||
local TextAPI api;
|
||||
if (contents.length != otherText.contents.length) return false;
|
||||
|
||||
api = _().text;
|
||||
// There's some evidence that UnrealEngine might copy the whole
|
||||
// `otherText.contents` each time we access any element,
|
||||
// so just copy it once.
|
||||
otherContentsCopy = otherText.contents;
|
||||
for (i = 0; i < contents.length; i += 1)
|
||||
{
|
||||
if (!api.AreEqual(contents[i], otherContentsCopy[i], caseInsensitive))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the caller `Text` contains the same text content as the given
|
||||
* `string`. By default case-sensitive.
|
||||
*
|
||||
* If text contains Unicode code points that can't be stored in
|
||||
* a given `string`, equality should be considered impossible.
|
||||
*/
|
||||
public final function bool IsEqualToString
|
||||
(
|
||||
string source,
|
||||
optional bool caseInsensitive,
|
||||
optional StringType sourceType
|
||||
)
|
||||
{
|
||||
local int i;
|
||||
local array<Character> rawSource;
|
||||
local TextAPI api;
|
||||
api = _().text;
|
||||
rawSource = api.StringToRaw(source, sourceType);
|
||||
if (contents.length != rawSource.length) return false;
|
||||
|
||||
for (i = 0; i < contents.length; i += 1)
|
||||
{
|
||||
if (!api.AreEqual(contents[i], rawSource[i], caseInsensitive))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the string has no characters, otherwise returns `false`.
|
||||
*/
|
||||
public final function bool IsEmpty()
|
||||
{
|
||||
return (contents.length == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to returns Unicode code point, stored in caller `Text` at the
|
||||
* given `index`.
|
||||
*
|
||||
* Doesn't properly work if `Text` contains characters consisting of
|
||||
* multiple code points.
|
||||
*
|
||||
* @return For a valid index (non-negative, not exceeding the length,
|
||||
* given by `GetLength()` of the `Text`) returns Unicode code point,
|
||||
* stored in caller `Text` at the given `index`; otherwise - returns `-1`.
|
||||
*/
|
||||
public final function Character GetCharacter(optional int index)
|
||||
{
|
||||
if (index < 0) return _().text.GetInvalidCharacter();
|
||||
if (index >= contents.length) return _().text.GetInvalidCharacter();
|
||||
|
||||
return contents[index];
|
||||
}
|
||||
|
||||
/*
|
||||
* Converts caller `Text` to lower case.
|
||||
*
|
||||
* Changes every symbol contained in caller `Text` to it's lower case folding
|
||||
* (according to Unicode standard). Symbols without lower case folding
|
||||
* (like "&" or "!") are left unchanged.
|
||||
*
|
||||
* @return Returns the calling object, to allow for function chaining.
|
||||
*/
|
||||
public final function Text ToLower()
|
||||
{
|
||||
local int i;
|
||||
local TextAPI api;
|
||||
api = _().text;
|
||||
for (i = 0; i < contents.length; i += 1)
|
||||
{
|
||||
contents[i] = api.ToLower(contents[i]);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/*
|
||||
* Converts caller `Text` to upper case.
|
||||
*
|
||||
* Changes every symbol contained in caller `Text` to it's upper case folding
|
||||
* (according to Unicode standard). Symbols without upper case folding
|
||||
* (like "&" or "!") are left unchanged.
|
||||
*
|
||||
* @return Returns the calling object, to allow for function chaining.
|
||||
*/
|
||||
public final function Text ToUpper()
|
||||
{
|
||||
local int i;
|
||||
local TextAPI api;
|
||||
api = _().text;
|
||||
for (i = 0; i < contents.length; i += 1)
|
||||
{
|
||||
contents[i] = api.ToUpper(contents[i]);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
public final function int GetHash() {
|
||||
return _().text.GetHashRaw(contents);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns amount of symbols in the caller `Text`.
|
||||
*/
|
||||
public final function int GetLength()
|
||||
{
|
||||
return contents.length;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1,85 +0,0 @@
|
|||
/**
|
||||
* This actor attaches itself to the ammo boxes
|
||||
* and imitates their collision to let us detect when they're picked up.
|
||||
* Copyright 2020 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 AmmoPickupStalker extends Actor;
|
||||
|
||||
// Ammo box this stalker is attached to.
|
||||
// If it is destroyed (not just picked up) - stalker must die too.
|
||||
var private KFAmmoPickup target;
|
||||
|
||||
// This variable is used to record if our 'target' ammo box was in
|
||||
// active state ('Pickup') last time we've checked.
|
||||
// We need this because ammo box's 'Touch' event can fire off first and
|
||||
// force the box to sleep before stalker could catch same event.
|
||||
// Without this variable we would have no way to know if player
|
||||
// simply walked near the place of a sleeping box or actually grabbed it.
|
||||
var private bool wasActive;
|
||||
|
||||
// Static function that spawns a new stalker for the given box.
|
||||
// Careful, as there's no checks for whether a stalker is
|
||||
// already attached to it.
|
||||
// Ensuring that is on the user of the function.
|
||||
public final static function StalkAmmoPickup(KFAmmoPickup newTarget)
|
||||
{
|
||||
local AmmoPickupStalker newStalker;
|
||||
if (newTarget == none) return;
|
||||
|
||||
newStalker = newTarget.Spawn(class'AmmoPickupStalker');
|
||||
newStalker.target = newTarget;
|
||||
newStalker.SetBase(newTarget);
|
||||
newStalker.SetCollision(true);
|
||||
newStalker.SetCollisionSize(newTarget.collisionRadius,
|
||||
newTarget.collisionHeight);
|
||||
}
|
||||
|
||||
event Touch(Actor other)
|
||||
{
|
||||
local FixAmmoSelling ammoSellingFix;
|
||||
if (target == none) return;
|
||||
// If our box was sleeping for while (more than a tick), -
|
||||
// player couldn't have gotten any ammo.
|
||||
if (!wasActive && !target.IsInState('Pickup')) return;
|
||||
|
||||
ammoSellingFix = FixAmmoSelling(class'FixAmmoSelling'.static.GetInstance());
|
||||
if (ammoSellingFix != none)
|
||||
{
|
||||
ammoSellingFix.RecordAmmoPickup(Pawn(other), target);
|
||||
}
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
if (target != none)
|
||||
{
|
||||
wasActive = target.IsInState('Pickup');
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
// Server-only, hidden
|
||||
remoteRole = ROLE_None
|
||||
bAlwaysRelevant = true
|
||||
drawType = DT_None
|
||||
}
|
||||
|
|
@ -1,395 +0,0 @@
|
|||
/**
|
||||
* This feature addressed an oversight in vanilla code that
|
||||
* allows clients to sell weapon's ammunition.
|
||||
* Moreover, when being sold, ammunition cost is always multiplied by 0.75,
|
||||
* without taking into an account possible discount a player might have.
|
||||
* This allows cheaters to "print money" by buying and selling ammo over and
|
||||
* over again ammunition for some weapons,
|
||||
* notably pipe bombs (74% discount for lvl6 demolition)
|
||||
* and crossbow (42% discount for lvl6 sharpshooter).
|
||||
*
|
||||
* This feature fixes this problem by setting 'pickupClass' variable in
|
||||
* potentially abusable weapons to our own value that won't receive a discount.
|
||||
* Luckily for us, it seems that pickup spawn and discount checks are the only
|
||||
* two place where variable is directly checked in a vanilla game's code
|
||||
* ('default.pickupClass' is used everywhere else),
|
||||
* so we can easily deal with the side effects of such change.
|
||||
* Copyright 2020 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 FixAmmoSelling extends Feature;
|
||||
|
||||
/**
|
||||
* We will replace 'pickupClass' variable for all instances of potentially
|
||||
* abusable weapons. That is weapons, that have a discount for their ammunition
|
||||
* (via 'GetAmmoCostScaling' function in a corresponding perk class).
|
||||
* They are defined (along with our pickup replacements) in 'rules' array.
|
||||
* That array isn't configurable, since the abusable status is hardcoded into
|
||||
* perk classes and the main mod that allows to change those (ServerPerks),
|
||||
* also solves ammo selling by a more direct method
|
||||
* (only available for the mods that replace player pawn class).
|
||||
* This change already completely fixes ammo printing.
|
||||
* Possible concern with changing the value of 'pickupClass' is that
|
||||
* it might affect gameplay in too many ways.
|
||||
* But, luckily for us, that value is only used when spawning a new pickup and
|
||||
* in 'ServerBuyAmmo' function of 'KFPawn'
|
||||
* (all the other places use it's default value instead).
|
||||
* This means that the only two side-effects of our change are:
|
||||
* 1. That wrong pickup class will be spawned. This problem is easily
|
||||
* solved by replacing spawned actor in 'CheckReplacement'.
|
||||
* 2. That ammo will be sold at a different (lower for us) price,
|
||||
* while trader would still display and require the original price.
|
||||
* This problem is solved by manually taking from player the difference
|
||||
* between what he should have had to pay and what he actually paid.
|
||||
* This brings us to the second issue -
|
||||
* detecting when player bought the ammo.
|
||||
* Unfortunately, it doesn't seem possible to detect with 100% certainty
|
||||
* without replacing pawn or shop classes,
|
||||
* so we have to eliminate other possibilities.
|
||||
* There are seem to be three ways for players to get more ammo:
|
||||
* 1. For some mod to give it;
|
||||
* 2. Found it an ammo box;
|
||||
* 3. To buy ammo (can only happen in trader).
|
||||
* We don't want to provide mods with low-level API for bug fixes,
|
||||
* so to ensure the compatibility, mods that want to increase ammo values
|
||||
* will have to solve compatibility issue by themselves:
|
||||
* either by reimplementing this fix (possibly the best option)
|
||||
* or by giving players appropriate money along with the ammo.
|
||||
* The only other case we have to eliminate is ammo boxes.
|
||||
* First, all cases of ammo boxes outside the trader are easy to detect,
|
||||
* since in this case we can be sure that player didn't buy ammo
|
||||
* (and mods that can allow it can just get rid of
|
||||
* 'ServerSellAmmo' function directly, similarly to how ServerPerks does it).
|
||||
* We'll detect all the other boxes by attaching an auxiliary actor
|
||||
* ('AmmoPickupStalker') to them, that will fire off 'Touch' event
|
||||
* at the same time as ammo boxes.
|
||||
* The only possible problem is that part of the ammo cost is
|
||||
* taken with a slight delay, which leaves cheaters a window of opportunity
|
||||
* to buy more than they can afford.
|
||||
* This issue is addressed by each ammo type costing as little as possible
|
||||
* (its' cost for corresponding perk at lvl6)
|
||||
* and a flag that does allow players to go into negative dosh values
|
||||
* (the cost is potential bugs in this fix itself, that
|
||||
* can somewhat affect regular players).
|
||||
*/
|
||||
|
||||
// Due to how this fix works, players with level below 6 get charged less
|
||||
// than necessary by the shop and this fix must take the rest of
|
||||
// the cost by itself.
|
||||
// The problem is, due to how ammo purchase is coded, low-level (<6 lvl)
|
||||
// players can actually buy more ammo for "fixed" weapons than they can afford
|
||||
// by filling ammo for one or all weapons.
|
||||
// Setting this flag to 'true' will allow us to still take full cost
|
||||
// from them, putting them in "debt" (having negative dosh amount).
|
||||
// If you don't want to have players with negative dosh values on your server
|
||||
// as a side-effect of this fix, then leave this flag as 'false',
|
||||
// letting low level players buy ammo cheaper
|
||||
// (but not cheaper than lvl6 could).
|
||||
// NOTE: this issue doesn't affect level 6 players.
|
||||
// NOTE #2: this fix does give players below level 6 some
|
||||
// technical advantage compared to vanilla game, but this advantage
|
||||
// cannot exceed benefits of having level 6.
|
||||
var private config const bool allowNegativeDosh;
|
||||
|
||||
// This structure records what classes of weapons can be abused
|
||||
// and what pickup class we should use to fix the exploit.
|
||||
struct ReplacementRule
|
||||
{
|
||||
var class<KFWeapon> abusableWeapon;
|
||||
var class<KFWeaponPickup> pickupReplacement;
|
||||
};
|
||||
|
||||
// Actual list of abusable weapons.
|
||||
var private const array<ReplacementRule> rules;
|
||||
|
||||
// We create one such record for any
|
||||
// abusable weapon instance in the game to store:
|
||||
struct WeaponRecord
|
||||
{
|
||||
// The instance itself.
|
||||
var KFWeapon weapon;
|
||||
// Corresponding ammo instance
|
||||
// (all abusable weapons only have one ammo type).
|
||||
var KFAmmunition ammo;
|
||||
// Last ammo amount we've seen, used to detect players gaining ammo
|
||||
// (from either ammo boxes or buying it).
|
||||
var int lastAmmoAmount;
|
||||
};
|
||||
|
||||
// All weapons we've detected so far.
|
||||
var private array<WeaponRecord> registeredWeapons;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
local KFWeapon nextWeapon;
|
||||
local KFAmmoPickup nextPickup;
|
||||
// Find all abusable weapons
|
||||
foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon)
|
||||
{
|
||||
FixWeapon(nextWeapon);
|
||||
}
|
||||
// Start tracking all ammo boxes
|
||||
foreach level.DynamicActors(class'KFMod.KFAmmoPickup', nextPickup)
|
||||
{
|
||||
class'AmmoPickupStalker'.static.StalkAmmoPickup(nextPickup);
|
||||
}
|
||||
}
|
||||
|
||||
protected function OnDisabled()
|
||||
{
|
||||
local int i;
|
||||
local AmmoPickupStalker nextStalker;
|
||||
local array<AmmoPickupStalker> stalkers;
|
||||
// Restore all the 'pickupClass' variables we've changed.
|
||||
for (i = 0; i < registeredWeapons.length; i += 1)
|
||||
{
|
||||
if (registeredWeapons[i].weapon != none)
|
||||
{
|
||||
registeredWeapons[i].weapon.pickupClass =
|
||||
registeredWeapons[i].weapon.default.pickupClass;
|
||||
}
|
||||
}
|
||||
registeredWeapons.length = 0;
|
||||
// Kill all the stalkers;
|
||||
// to be safe, avoid destroying them directly in the iterator.
|
||||
foreach level.DynamicActors(class'AmmoPickupStalker', nextStalker)
|
||||
{
|
||||
stalkers[stalkers.length] = nextStalker;
|
||||
}
|
||||
for (i = 0; i < stalkers.length; i += 1)
|
||||
{
|
||||
if (stalkers[i] != none)
|
||||
{
|
||||
stalkers[i].Destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if given class is a one of our pickup replacer classes.
|
||||
public static final function bool IsReplacer(class<Actor> pickupClass)
|
||||
{
|
||||
local int i;
|
||||
if (pickupClass == none) return false;
|
||||
for (i = 0; i < default.rules.length; i += 1)
|
||||
{
|
||||
if (pickupClass == default.rules[i].pickupReplacement)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 1. Checks if weapon can be abused and if it can, - fixes the problem.
|
||||
// 2. Starts tracking abusable weapon to detect when player buys ammo for it.
|
||||
public final function FixWeapon(KFWeapon potentialAbuser)
|
||||
{
|
||||
local int i;
|
||||
local WeaponRecord newRecord;
|
||||
if (potentialAbuser == none) return;
|
||||
|
||||
for (i = 0; i < registeredWeapons.length; i += 1)
|
||||
{
|
||||
if (registeredWeapons[i].weapon == potentialAbuser)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < rules.length; i += 1)
|
||||
{
|
||||
if (potentialAbuser.class == rules[i].abusableWeapon)
|
||||
{
|
||||
potentialAbuser.pickupClass = rules[i].pickupReplacement;
|
||||
newRecord.weapon = potentialAbuser;
|
||||
registeredWeapons[registeredWeapons.length] = newRecord;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Finds ammo instance for recorded weapon in it's owner's inventory.
|
||||
private final function WeaponRecord FindAmmoInstance(WeaponRecord record)
|
||||
{
|
||||
local Inventory invIter;
|
||||
local KFAmmunition ammo;
|
||||
if (record.weapon == none) return record;
|
||||
if (record.weapon.instigator == none) return record;
|
||||
|
||||
// Find instances anew
|
||||
invIter = record.weapon.instigator.inventory;
|
||||
while (invIter != none)
|
||||
{
|
||||
if (record.weapon.ammoClass[0] == invIter.class)
|
||||
{
|
||||
ammo = KFAmmunition(invIter);
|
||||
}
|
||||
invIter = invIter.inventory;
|
||||
}
|
||||
// Add missing instances
|
||||
if (ammo != none)
|
||||
{
|
||||
record.ammo = ammo;
|
||||
record.lastAmmoAmount = ammo.ammoAmount;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
// Calculates how much more player should have paid for 'ammoAmount'
|
||||
// amount of ammo, compared to how much trader took after our fix.
|
||||
private final function float GetPriceCorrection
|
||||
(
|
||||
KFWeapon kfWeapon,
|
||||
int ammoAmount
|
||||
)
|
||||
{
|
||||
local float boughtMagFraction;
|
||||
// 'vanillaPrice' - price that would be calculated
|
||||
// without our interference
|
||||
// 'fixPrice' - price that will be calculated after
|
||||
// we've replaced pickup class
|
||||
local float vanillaPrice, fixPrice;
|
||||
local KFPlayerReplicationInfo kfRI;
|
||||
local class<KFWeaponPickup> vanillaPickupClass, fixPickupClass;
|
||||
if (kfWeapon == none || kfWeapon.instigator == none) return 0.0;
|
||||
fixPickupClass = class<KFWeaponPickup>(kfWeapon.pickupClass);
|
||||
vanillaPickupClass = class<KFWeaponPickup>(kfWeapon.default.pickupClass);
|
||||
if (fixPickupClass == none || vanillaPickupClass == none) return 0.0;
|
||||
|
||||
// Calculate base prices
|
||||
boughtMagFraction = (float(ammoAmount) / kfWeapon.default.magCapacity);
|
||||
fixPrice = boughtMagFraction * fixPickupClass.default.AmmoCost;
|
||||
vanillaPrice = boughtMagFraction * vanillaPickupClass.default.AmmoCost;
|
||||
// Apply perk discount for vanilla price
|
||||
// (we don't need to consider secondary ammo or husk gun special cases,
|
||||
// since such weapons can't be abused via ammo dosh-printing)
|
||||
kfRI = KFPlayerReplicationInfo(kfWeapon.instigator.playerReplicationInfo);
|
||||
if (kfRI != none && kfRI.clientVeteranSkill != none)
|
||||
{
|
||||
vanillaPrice *= kfRI.clientVeteranSkill.static.
|
||||
GetAmmoCostScaling(kfRI, vanillaPickupClass);
|
||||
}
|
||||
// TWI's code rounds up ammo cost
|
||||
// to the integer value whenever ammo is bought,
|
||||
// so to calculate exactly how much we need to correct the cost,
|
||||
// we must find difference between the final, rounded cost values.
|
||||
return float(Max(0, int(vanillaPrice) - int(fixPrice)));
|
||||
}
|
||||
|
||||
// Takes current ammo and last recorded in 'record' value to calculate
|
||||
// how much money to take from the player
|
||||
// (calculations are done via 'GetPriceCorrection').
|
||||
private final function WeaponRecord TaxAmmoChange(WeaponRecord record)
|
||||
{
|
||||
local int ammoDiff;
|
||||
local KFPawn taxPayer;
|
||||
local PlayerReplicationInfo replicationInfo;
|
||||
taxPayer = KFPawn(record.weapon.instigator);
|
||||
if (record.weapon == none || taxPayer == none) return record;
|
||||
// No need to charge money if player couldn't have
|
||||
// possibly bought the ammo.
|
||||
if (!taxPayer.CanBuyNow()) return record;
|
||||
// Find ammo difference with recorded value.
|
||||
if (record.ammo != none)
|
||||
{
|
||||
ammoDiff = Max(0, record.ammo.ammoAmount - record.lastAmmoAmount);
|
||||
record.lastAmmoAmount = record.ammo.ammoAmount;
|
||||
}
|
||||
// Make player pay dosh
|
||||
replicationInfo = taxPayer.playerReplicationInfo;
|
||||
if (replicationInfo != none)
|
||||
{
|
||||
replicationInfo.score -= GetPriceCorrection(record.weapon, ammoDiff);
|
||||
// This shouldn't happen, since shop is supposed to make sure
|
||||
// player has enough dosh to buy ammo at full price
|
||||
// (actual price + our correction).
|
||||
// But if user is extra concerned about it, -
|
||||
// we can additionally for force the score above 0.
|
||||
if (!allowNegativeDosh)
|
||||
{
|
||||
replicationInfo.score = FMax(0, replicationInfo.score);
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
// Changes our records to account for player picking up the ammo box,
|
||||
// to avoid charging his for it.
|
||||
public final function RecordAmmoPickup(Pawn pawnWithAmmo, KFAmmoPickup pickup)
|
||||
{
|
||||
local int i;
|
||||
local int newAmount;
|
||||
// Check conditions from 'KFAmmoPickup' code ('Touch' function)
|
||||
if (pickup == none) return;
|
||||
if (pawnWithAmmo == none) return;
|
||||
if (pawnWithAmmo.controller == none) return;
|
||||
if (!pawnWithAmmo.bCanPickupInventory) return;
|
||||
if (!FastTrace(pawnWithAmmo.location, pickup.location)) return;
|
||||
|
||||
// Add relevant amount of ammo to our records
|
||||
for (i = 0; i < registeredWeapons.length; i += 1)
|
||||
{
|
||||
if (registeredWeapons[i].weapon == none) continue;
|
||||
if (registeredWeapons[i].weapon.instigator == pawnWithAmmo)
|
||||
{
|
||||
newAmount = registeredWeapons[i].lastAmmoAmount
|
||||
+ registeredWeapons[i].ammo.ammoPickupAmount;
|
||||
newAmount = Min(registeredWeapons[i].ammo.maxAmmo, newAmount);
|
||||
registeredWeapons[i].lastAmmoAmount = newAmount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
local int i;
|
||||
// For all the weapon records...
|
||||
i = 0;
|
||||
while (i < registeredWeapons.length)
|
||||
{
|
||||
// ...remove dead records
|
||||
if (registeredWeapons[i].weapon == none)
|
||||
{
|
||||
registeredWeapons.Remove(i, 1);
|
||||
continue;
|
||||
}
|
||||
// ...find ammo if it's missing
|
||||
if (registeredWeapons[i].ammo == none)
|
||||
{
|
||||
registeredWeapons[i] = FindAmmoInstance(registeredWeapons[i]);
|
||||
}
|
||||
// ...tax for ammo, if we can
|
||||
registeredWeapons[i] = TaxAmmoChange(registeredWeapons[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
allowNegativeDosh = false
|
||||
rules(0)=(abusableWeapon=class'KFMod.Crossbow',pickupReplacement=class'FixAmmoSellingClass_CrossbowPickup')
|
||||
rules(1)=(abusableWeapon=class'KFMod.PipeBombExplosive',pickupReplacement=class'FixAmmoSellingClass_PipeBombPickup')
|
||||
rules(2)=(abusableWeapon=class'KFMod.M79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M79Pickup')
|
||||
rules(3)=(abusableWeapon=class'KFMod.GoldenM79GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_GoldenM79Pickup')
|
||||
rules(4)=(abusableWeapon=class'KFMod.M32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_M32Pickup')
|
||||
rules(5)=(abusableWeapon=class'KFMod.CamoM32GrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_CamoM32Pickup')
|
||||
rules(6)=(abusableWeapon=class'KFMod.LAW',pickupReplacement=class'FixAmmoSellingClass_LAWPickup')
|
||||
rules(7)=(abusableWeapon=class'KFMod.SPGrenadeLauncher',pickupReplacement=class'FixAmmoSellingClass_SPGrenadePickup')
|
||||
rules(8)=(abusableWeapon=class'KFMod.SealSquealHarpoonBomber',pickupReplacement=class'FixAmmoSellingClass_SealSquealPickup')
|
||||
rules(9)=(abusableWeapon=class'KFMod.SeekerSixRocketLauncher',pickupReplacement=class'FixAmmoSellingClass_SeekerSixPickup')
|
||||
// Listeners
|
||||
requiredListeners(0) = class'MutatorListener_FixAmmoSelling'
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for M32 to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* Copyright 2019 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 FixAmmoSellingClass_CamoM32Pickup extends CamoM32Pickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 42
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for xbow to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* Copyright 2019 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 FixAmmoSellingClass_CrossbowPickup extends CrossbowPickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 11.6
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for m79 to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* Copyright 2019 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 FixAmmoSellingClass_GoldenM79Pickup extends GoldenM79Pickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 7
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for LAW to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* Copyright 2019 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 FixAmmoSellingClass_LAWPickup extends LAWPickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 21
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for M32 to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* Copyright 2019 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 FixAmmoSellingClass_M32Pickup extends M32Pickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 42
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for M79 to that
|
||||
* of a level 6 player and doesn't allow for a perk discount.
|
||||
* Copyright 2019 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 FixAmmoSellingClass_M79Pickup extends M79Pickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 7
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for pipes
|
||||
* to that of a level 6 player and doesn't allow for a perk discount.
|
||||
* Copyright 2019 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 FixAmmoSellingClass_PipeBombPickup extends PipeBombPickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 195
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
/**
|
||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for
|
||||
* orca grnade launcher to that of a level 6 player
|
||||
* and doesn't allow for a perk discount.
|
||||
* Copyright 2019 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 FixAmmoSellingClass_SPGrenadePickup extends SPGrenadePickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 7
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for harpoon
|
||||
* to that of a level 6 player and doesn't allow for a perk discount.
|
||||
* Copyright 2019 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 FixAmmoSellingClass_SealSquealPickup extends SealSquealPickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 21
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* A helper class for 'FixAmmoSelling' that sets ammo cost for seeker
|
||||
* to that of a level 6 player and doesn't allow for a perk discount.
|
||||
* Copyright 2019 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 FixAmmoSellingClass_SeekerSixPickup extends SeekerSixPickup;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
AmmoCost = 10.5
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
/**
|
||||
* Overloaded mutator events listener to register every new
|
||||
* spawned weapon and ammo pickup.
|
||||
* Copyright 2020 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 MutatorListener_FixAmmoSelling extends MutatorListenerBase
|
||||
abstract;
|
||||
|
||||
static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
|
||||
{
|
||||
if (other == none) return true;
|
||||
|
||||
// We need to replace pickup classes back,
|
||||
// as they might not even exist on clients.
|
||||
if (class'FixAmmoSelling'.static.IsReplacer(other.class))
|
||||
{
|
||||
ReplacePickupWith(Pickup(other));
|
||||
return false;
|
||||
}
|
||||
CheckAbusableWeapon(KFWeapon(other));
|
||||
// If it's ammo pickup - we need to stalk it
|
||||
class'AmmoPickupStalker'.static.StalkAmmoPickup(KFAmmoPickup(other));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function CheckAbusableWeapon(KFWeapon newWeapon)
|
||||
{
|
||||
local FixAmmoSelling ammoSellingFix;
|
||||
if (newWeapon == none) return;
|
||||
ammoSellingFix = FixAmmoSelling(class'FixAmmoSelling'.static.GetInstance());
|
||||
if (ammoSellingFix == none) return;
|
||||
ammoSellingFix.FixWeapon(newWeapon);
|
||||
}
|
||||
|
||||
// This function recreates the logic of 'KFWeapon.DropFrom()',
|
||||
// since standard 'ReplaceWith' function produces bad results.
|
||||
private static function ReplacePickupWith(Pickup oldPickup)
|
||||
{
|
||||
local Pawn instigator;
|
||||
local Pickup newPickup;
|
||||
local KFWeapon relevantWeapon;
|
||||
if (oldPickup == none) return;
|
||||
instigator = oldPickup.instigator;
|
||||
if (instigator == none) return;
|
||||
relevantWeapon = GetWeaponOfClass(instigator, oldPickup.inventoryType);
|
||||
if (relevantWeapon == none) return;
|
||||
|
||||
newPickup = relevantWeapon.Spawn( relevantWeapon.default.pickupClass,,,
|
||||
relevantWeapon.location);
|
||||
newPickup.InitDroppedPickupFor(relevantWeapon);
|
||||
newPickup.velocity = relevantWeapon.velocity +
|
||||
Vector(instigator.rotation) * 100;
|
||||
if (instigator.health > 0)
|
||||
KFWeaponPickup(newPickup).bThrown = true;
|
||||
}
|
||||
|
||||
// TODO: this is code duplication, some sort of solution is needed
|
||||
static final function KFWeapon GetWeaponOfClass
|
||||
(
|
||||
Pawn playerPawn,
|
||||
class<Inventory> weaponClass
|
||||
)
|
||||
{
|
||||
local Inventory invIter;
|
||||
if (playerPawn == none) return none;
|
||||
|
||||
invIter = playerPawn.inventory;
|
||||
while (invIter != none)
|
||||
{
|
||||
if (invIter.class == weaponClass)
|
||||
{
|
||||
return KFWeapon(invIter);
|
||||
}
|
||||
invIter = invIter.inventory;
|
||||
}
|
||||
return none;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedEvents = class'MutatorEvents'
|
||||
}
|
||||
|
|
@ -1,252 +0,0 @@
|
|||
/**
|
||||
* This feature addressed two dosh-related issues:
|
||||
* 1. Crashing servers by spamming 'CashPickup' actors with 'TossCash';
|
||||
* 2. Breaking collision detection logic by stacking large amount of
|
||||
* 'CashPickup' actors in one place, which allows one to either
|
||||
* reach unintended locations or even instantly kill zeds.
|
||||
*
|
||||
* It fixes them by limiting speed, with which dosh can spawn, and
|
||||
* allowing this limit to decrease when there's already too much dosh
|
||||
* present on the map.
|
||||
* Copyright 2019 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 FixDoshSpam extends Feature;
|
||||
|
||||
/**
|
||||
* First, we limit amount of dosh that can be spawned simultaneously.
|
||||
* The simplest method is to place a cooldown on spawning 'CashPickup' actors,
|
||||
* i.e. after spawning one 'CashPickup' we'd completely prevent spawning
|
||||
* any other instances of it for a fixed amount of time.
|
||||
* However, that might allow a malicious spammer to block others from
|
||||
* throwing dosh, - all he needs to do is to spam dosh at right time intervals.
|
||||
* We'll resolve this issue by recording how many 'CashPickup' actors
|
||||
* each player has spawned as their "contribution" and decay
|
||||
* that value with time, only allowing to spawn new dosh after
|
||||
* contribution decayed to zero. Speed of decay is derived from current dosh
|
||||
* spawning speed limit and decreases with amount of players
|
||||
* with non-zero contributions (since it means that they're throwing dosh).
|
||||
* Second issue is player amassing a large amount of dosh in one point
|
||||
* that leads to skipping collision checks, which then allows players to pass
|
||||
* through level geometry or enter zeds' collisions, instantly killing them.
|
||||
* Since dosh disappears on it's own, the easiest method to prevent that is to
|
||||
* severely limit how much dosh players can throw per second,
|
||||
* so that there's never enough dosh laying around to affect collision logic.
|
||||
* The downside to such severe limitations is that game behaves less
|
||||
* vanilla-like, where you could throw away streams of dosh.
|
||||
* To solve that we'll first use a more generous limit on dosh players can
|
||||
* throw per second, but will track how much dosh is currently present
|
||||
* in a level and linearly decelerate speed, according to that amount.
|
||||
*/
|
||||
|
||||
// Highest and lowest speed with which players can throw dosh wads.
|
||||
// It'll be evenly spread between all players.
|
||||
// For example, if speed is set to 6 and only one player will be spamming dosh,
|
||||
// - he'll be able to throw 6 wads of dosh per second;
|
||||
// but if all 6 players are spamming it, - each will throw only 1 per second.
|
||||
// NOTE: these speed values can be exceeded, since a player is guaranteed
|
||||
// to be able to throw at least one wad of dosh, if he didn't do so in awhile.
|
||||
// NOTE #2: if maximum value is less than minimum one,
|
||||
// the lowest (maximum one) will be used.
|
||||
var private config const float doshPerSecondLimitMax;
|
||||
var private config const float doshPerSecondLimitMin;
|
||||
// Amount of dosh pickups on the map at which we must set dosh per second
|
||||
// to 'doshPerSecondLimitMin'.
|
||||
// We use 'doshPerSecondLimitMax' when there's no dosh on the map and
|
||||
// scale linearly between them as it's amount grows.
|
||||
var private config const int criticalDoshAmount;
|
||||
|
||||
// To limit dosh spawning speed we need some measure of
|
||||
// time passage between ticks.
|
||||
// This variable stores last value seen by us as a good approximation.
|
||||
// It's a real (not in-game) time.
|
||||
var private float lastTickDuration;
|
||||
|
||||
// This structure records how much a certain player has
|
||||
// contributed to an overall dosh creation.
|
||||
struct DoshStreamPerPlayer
|
||||
{
|
||||
var PlayerController player;
|
||||
// Amount of dosh we remember this player creating, decays with time.
|
||||
var float contribution;
|
||||
};
|
||||
var private array<DoshStreamPerPlayer> currentContributors;
|
||||
|
||||
// Wads of cash that are lying around on the map.
|
||||
var private array<CashPickup> wads;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
local CashPickup nextCash;
|
||||
// Find all wads of cash laying around on the map,
|
||||
// so that we could accordingly limit the cash spam.
|
||||
foreach level.DynamicActors(class'KFMod.CashPickup', nextCash)
|
||||
{
|
||||
wads[wads.length] = nextCash;
|
||||
}
|
||||
}
|
||||
|
||||
protected function OnDisabled()
|
||||
{
|
||||
wads.length = 0;
|
||||
currentContributors.length = 0;
|
||||
}
|
||||
|
||||
// Did player with this controller contribute to the latest dosh generation?
|
||||
public final function bool IsContributor(PlayerController player)
|
||||
{
|
||||
return (GetContributorIndex(player) >= 0);
|
||||
}
|
||||
|
||||
// Did we already reach allowed limit of dosh per second?
|
||||
public final function bool IsDoshStreamOverLimit()
|
||||
{
|
||||
local int i;
|
||||
local float overallContribution;
|
||||
overallContribution = 0.0;
|
||||
for (i = 0; i < currentContributors.length; i += 1)
|
||||
{
|
||||
overallContribution += currentContributors[i].contribution;
|
||||
}
|
||||
return (overallContribution > lastTickDuration * GetCurrentDPSLimit());
|
||||
}
|
||||
|
||||
// What is our current dosh per second limit?
|
||||
private final function float GetCurrentDPSLimit()
|
||||
{
|
||||
local float speedScale;
|
||||
if (doshPerSecondLimitMax < doshPerSecondLimitMin)
|
||||
{
|
||||
return doshPerSecondLimitMax;
|
||||
}
|
||||
speedScale = Float(wads.length) / Float(criticalDoshAmount);
|
||||
speedScale = FClamp(speedScale, 0.0, 1.0);
|
||||
// At 0.0 scale (no dosh on the map) - use max speed
|
||||
// At 1.0 scale (critical dosh on the map) - use min speed
|
||||
return Lerp(speedScale, doshPerSecondLimitMax, doshPerSecondLimitMin);
|
||||
}
|
||||
|
||||
// Returns index of the contributor corresponding to the given controller.
|
||||
// Returns '-1' if no connection correspond to the given controller.
|
||||
// Returns '-1' if given controller is equal to 'none'.
|
||||
private final function int GetContributorIndex(PlayerController player)
|
||||
{
|
||||
local int i;
|
||||
if (player == none) return -1;
|
||||
|
||||
for (i = 0; i < currentContributors.length; i += 1)
|
||||
{
|
||||
if (currentContributors[i].player == player)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Adds given cash to given player contribution record and
|
||||
// registers that cash in our wads array.
|
||||
// Does nothing if given cash was already registered.
|
||||
public final function AddContribution(PlayerController player, CashPickup cash)
|
||||
{
|
||||
local int i;
|
||||
local int playerIndex;
|
||||
local DoshStreamPerPlayer newStreamRecord;
|
||||
// Check if given dosh was already accounted for.
|
||||
for (i = 0; i < wads.length; i += 1)
|
||||
{
|
||||
if (cash == wads[i])
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
wads[wads.length] = cash;
|
||||
// Add contribution to player
|
||||
playerIndex = GetContributorIndex(player);
|
||||
if (playerIndex >= 0)
|
||||
{
|
||||
currentContributors[playerIndex].contribution += 1.0;
|
||||
return;
|
||||
}
|
||||
newStreamRecord.player = player;
|
||||
newStreamRecord.contribution = 1.0;
|
||||
currentContributors[currentContributors.length] = newStreamRecord;
|
||||
}
|
||||
|
||||
private final function ReducePlayerContributions(float trueTimePassed)
|
||||
{
|
||||
local int i;
|
||||
local float streamReduction;
|
||||
streamReduction = trueTimePassed *
|
||||
(GetCurrentDPSLimit() / currentContributors.length);
|
||||
for (i = 0; i < currentContributors.length; i += 1)
|
||||
{
|
||||
currentContributors[i].contribution -= streamReduction;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean out wads that disappeared or were picked up by players.
|
||||
private final function CleanWadsArray()
|
||||
{
|
||||
local int i;
|
||||
i = 0;
|
||||
while (i < wads.length)
|
||||
{
|
||||
if (wads[i] == none)
|
||||
{
|
||||
wads.Remove(i, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't track players that no longer contribute to dosh generation.
|
||||
private final function RemoveNonContributors()
|
||||
{
|
||||
local int i;
|
||||
local array<DoshStreamPerPlayer> updContributors;
|
||||
for (i = 0; i < currentContributors.length; i += 1)
|
||||
{
|
||||
// We want to keep on record even players that quit,
|
||||
// since their contribution still must be accounted for.
|
||||
if (currentContributors[i].contribution <= 0.0) continue;
|
||||
updContributors[updContributors.length] = currentContributors[i];
|
||||
}
|
||||
currentContributors = updContributors;
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
local float trueTimePassed;
|
||||
trueTimePassed = delta * (1.1 / level.timeDilation);
|
||||
CleanWadsArray();
|
||||
ReducePlayerContributions(trueTimePassed);
|
||||
RemoveNonContributors();
|
||||
lastTickDuration = trueTimePassed;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
doshPerSecondLimitMax = 50
|
||||
doshPerSecondLimitMin = 5
|
||||
criticalDoshAmount = 25
|
||||
// Listeners
|
||||
requiredListeners(0) = class'MutatorListener_FixDoshSpam'
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/**
|
||||
* Overloaded mutator events listener to catch and, possibly,
|
||||
* prevent spawning dosh actors.
|
||||
* Copyright 2019 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 MutatorListener_FixDoshSpam extends MutatorListenerBase
|
||||
abstract;
|
||||
|
||||
static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
|
||||
{
|
||||
local FixDoshSpam doshFix;
|
||||
local PlayerController player;
|
||||
if (other.class != class'CashPickup') return true;
|
||||
// This means this dosh wasn't spawned in 'TossCash' of 'KFPawn',
|
||||
// so it isn't related to the exploit we're trying to fix.
|
||||
if (other.instigator == none) return true;
|
||||
doshFix = FixDoshSpam(class'FixDoshSpam'.static.GetInstance());
|
||||
if (doshFix == none) return true;
|
||||
|
||||
// We only want to prevent spawning cash if we're already over
|
||||
// the limit and the one trying to throw this cash contributed to it.
|
||||
// We allow other players to throw at least one wad of cash.
|
||||
player = PlayerController(other.instigator.controller);
|
||||
if (doshFix.IsDoshStreamOverLimit() && doshFix.IsContributor(player))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// If we do spawn cash - record this contribution.
|
||||
doshFix.AddContribution(player, CashPickup(other));
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedEvents = class'MutatorEvents'
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/**
|
||||
* This rule detects any pickup events to allow us to
|
||||
* properly record and/or fix pistols' prices.
|
||||
* Copyright 2019 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 DualiesCostRule extends GameRules;
|
||||
|
||||
function bool OverridePickupQuery
|
||||
(
|
||||
Pawn other,
|
||||
Pickup item,
|
||||
out byte allowPickup
|
||||
)
|
||||
{
|
||||
local KFWeaponPickup weaponPickup;
|
||||
local FixDualiesCost dualiesCostFix;
|
||||
weaponPickup = KFWeaponPickup(item);
|
||||
dualiesCostFix = FixDualiesCost(class'FixDualiesCost'.static.GetInstance());
|
||||
if (weaponPickup != none && dualiesCostFix != none)
|
||||
{
|
||||
dualiesCostFix.ApplyPendingValues();
|
||||
dualiesCostFix.StoreSinglePistolValues();
|
||||
dualiesCostFix.SetNextSellValue(weaponPickup.sellValue);
|
||||
}
|
||||
return super.OverridePickupQuery(other, item, allowPickup);
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,454 +0,0 @@
|
|||
/**
|
||||
* This feature fixes several issues related to the selling price of both
|
||||
* single and dual pistols, all originating from the existence of dual weapons.
|
||||
* Most notable issue is the ability to "print" money by buying and
|
||||
* selling pistols in a certain way.
|
||||
*
|
||||
* It fixes all of the issues by manually setting pistols'
|
||||
* 'SellValue' variables to proper values.
|
||||
* Fix only works with vanilla pistols, as it's unpredictable what
|
||||
* custom ones can do and they can handle these issues on their own
|
||||
* in a better way.
|
||||
* Copyright 2020 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 FixDualiesCost extends Feature;
|
||||
|
||||
/**
|
||||
* Issues with pistols' cost may look varied and surface in
|
||||
* a plethora of ways, but all of them originate from the two main errors
|
||||
* in vanilla's code:
|
||||
* 1. If you have a pistol in your inventory at the time when you
|
||||
* buy/pickup another one - the sell value of resulting dualies is
|
||||
* incorrectly set to the sell value of the second pistol;
|
||||
* 2. When player has dual pistols and drops one on the floor, -
|
||||
* the sell value for the one left with the player isn't set.
|
||||
* All weapons in Killing Floor get sell value assigned to them
|
||||
* (appropriately, in a 'SellValue' variable). This is to ensure that the sell
|
||||
* price is set the moment players buys the gun. Otherwise, due to ridiculous
|
||||
* perked discounts, you'd be able to buy a pistol at 30% price
|
||||
* as sharpshooter, but sell at 75% of a price as any other perk,
|
||||
* resulting in 45% of pure profit.
|
||||
* Unfortunately, that's exactly what happens when 'SellValue' isn't set
|
||||
* (left as it's default value of '-1'): sell value of such weapons is
|
||||
* determined only at the moment of sale and depends on the perk of the seller,
|
||||
* allowing for possible exploits.
|
||||
*
|
||||
* These issues are fixed by directly assigning
|
||||
* proper values to 'SellValue'. To do that we need to detect when player
|
||||
* buys/sells/drops/picks up weapons, which we accomplish by catching
|
||||
* 'CheckReplacement' event for weapon instances. This approach has two issues.
|
||||
* One is that, if vanilla's code sets an incorrect sell value, -
|
||||
* it's doing it after weapon is spawned and, therefore,
|
||||
* after 'CheckReplacement' call, so we have, instead, to remember to do
|
||||
* it later, as early as possible
|
||||
* (either the next tick or before another operation with weapons).
|
||||
* Another issue is that when you have a pistol and pick up a pistol of
|
||||
* the same type, - at the moment dualies instance is spawned,
|
||||
* the original pistol in player's inventory is gone and we can't use
|
||||
* it's sell value to calculate new value of dual pistols.
|
||||
* This problem is solved by separately recording the value for every
|
||||
* single pistol every tick.
|
||||
* However, if pistol pickups are placed close enough together on the map,
|
||||
* player can start touching them (which triggers a pickup) at the same time,
|
||||
* picking them both in a single tick. This leaves us no room to record
|
||||
* the value of a single pistol players picks up first.
|
||||
* To get it we use game rules to catch 'OverridePickupQuery' event that's
|
||||
* called before the first one gets destroyed,
|
||||
* but after it's sell value was already set.
|
||||
* Last issue is that when player picks up a second pistol - we don't know
|
||||
* it's sell value and, therefore, can't calculate value of dual pistols.
|
||||
* This is resolved by recording that value directly from a pickup,
|
||||
* in abovementioned function 'OverridePickupQuery'.
|
||||
* NOTE: 9mm is an exception due to the fact that you always have at least
|
||||
* one and the last one can't be sold. We'll deal with it by setting
|
||||
* the following rule: sell value of the un-droppable pistol is always 0
|
||||
* and the value of a pair of 9mms is the value of the single droppable pistol.
|
||||
*/
|
||||
|
||||
// Some issues involve possible decrease in pistols' price and
|
||||
// don't lead to exploit, but are still bugs and require fixing.
|
||||
// If you have a Deagle in your inventory and then get another one
|
||||
// (by either buying or picking it off the ground) - the price of resulting
|
||||
// dual pistols will be set to the price of the last deagle,
|
||||
// like the first one wasn't worth anything at all.
|
||||
// In particular this means that (prices are off-perk for more clarity):
|
||||
// 1. If you buy dual deagles (-1000 do$h) and then sell them at 75% of
|
||||
// the cost (+750 do$h), you lose 250 do$h;
|
||||
// 2. If you first buy a deagle (-500 do$h), then buy
|
||||
// the second one (-500 do$h) and then sell them, you'll only get
|
||||
// 75% of the cost of 1 deagle (+375 do$h), now losing 625 do$h;
|
||||
// 3. So if you already have bought a deagle (-500 do$h),
|
||||
// you can get a more expensive weapon by doing a stupid thing
|
||||
// and first selling your Deagle (+375 do$h),
|
||||
// then buying dual deagles (-1000 do$h).
|
||||
// If you sell them after that, you'll gain 75% of the cost of
|
||||
// dual deagles (+750 do$h), leaving you with losing only 375 do$h.
|
||||
// Of course, situations described above are only relevant if you're planning
|
||||
// to sell your weapons at some point and most people won't even notice it.
|
||||
// But such an oversight still shouldn't exist in a game and we fix it by
|
||||
// setting sell value of dualies as a sum of values of each pistol.
|
||||
// Yet, fixing this issue leads to players having more expensive
|
||||
// (while fairly priced) weapons than on vanilla, technically making
|
||||
// the game easier. And some people might object to having that in
|
||||
// a whitelisted bug-fixing feature.
|
||||
// These people are, without a question, complete degenerates.
|
||||
// But making mods for only non-mentally challenged isn't inclusive.
|
||||
// So we add this option.
|
||||
// Set it to 'false' if you only want to fix ammo printing
|
||||
// and leave the rest of the bullshit as-is.
|
||||
var private config const bool allowSellValueIncrease;
|
||||
|
||||
// Describe all the possible pairs of dual pistols in a vanilla game.
|
||||
struct DualiesPair
|
||||
{
|
||||
var class<KFWeapon> single;
|
||||
var class<KFWeapon> dual;
|
||||
};
|
||||
var private const array<DualiesPair> dualiesClasses;
|
||||
|
||||
// Describe sell values that need to be applied at earliest later point.
|
||||
struct WeaponValuePair
|
||||
{
|
||||
var KFWeapon weapon;
|
||||
var float value;
|
||||
};
|
||||
var private const array<WeaponValuePair> pendingValues;
|
||||
|
||||
// Describe sell values of all currently existing single pistols.
|
||||
struct WeaponDataRecord
|
||||
{
|
||||
var KFWeapon reference;
|
||||
var class<KFWeapon> class;
|
||||
var float value;
|
||||
// The whole point of this structure is to remember value of a weapon
|
||||
// after it's destroyed. Since 'reference' will become 'none' by then,
|
||||
// we will use the 'owner' reference to identify the weapon.
|
||||
var Pawn owner;
|
||||
};
|
||||
var private const array<WeaponDataRecord> storedValues;
|
||||
|
||||
// Sell value of the last seen pickup in 'OverridePickupQuery'
|
||||
var private int nextSellValue;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
local KFWeapon nextWeapon;
|
||||
// Find all frags, that spawned when this fix wasn't running.
|
||||
foreach level.DynamicActors(class'KFMod.KFWeapon', nextWeapon)
|
||||
{
|
||||
RegisterSinglePistol(nextWeapon, false);
|
||||
}
|
||||
level.game.AddGameModifier(Spawn(class'DualiesCostRule'));
|
||||
}
|
||||
|
||||
protected function OnDisabled()
|
||||
{
|
||||
local GameRules rulesIter;
|
||||
local DualiesCostRule ruleToDestroy;
|
||||
// Check first rule
|
||||
if (level.game.gameRulesModifiers == none) return;
|
||||
|
||||
ruleToDestroy = DualiesCostRule(level.game.gameRulesModifiers);
|
||||
if (ruleToDestroy != none)
|
||||
{
|
||||
level.game.gameRulesModifiers = ruleToDestroy.nextGameRules;
|
||||
ruleToDestroy.Destroy();
|
||||
return;
|
||||
}
|
||||
// Check rest of the rules
|
||||
rulesIter = level.game.gameRulesModifiers;
|
||||
while (rulesIter != none)
|
||||
{
|
||||
ruleToDestroy = DualiesCostRule(rulesIter.nextGameRules);
|
||||
if (ruleToDestroy != none)
|
||||
{
|
||||
rulesIter.nextGameRules = ruleToDestroy.nextGameRules;
|
||||
ruleToDestroy.Destroy();
|
||||
}
|
||||
rulesIter = rulesIter.nextGameRules;
|
||||
}
|
||||
}
|
||||
|
||||
public final function SetNextSellValue(int newValue)
|
||||
{
|
||||
nextSellValue = newValue;
|
||||
}
|
||||
|
||||
// Finds a weapon of a given class in given 'Pawn' 's inventory.
|
||||
// Returns 'none' if weapon isn't there.
|
||||
private final function KFWeapon GetWeaponOfClass
|
||||
(
|
||||
Pawn playerPawn,
|
||||
class<KFWeapon> weaponClass
|
||||
)
|
||||
{
|
||||
local Inventory invIter;
|
||||
if (playerPawn == none) return none;
|
||||
|
||||
invIter = playerPawn.inventory;
|
||||
while (invIter != none)
|
||||
{
|
||||
if (invIter.class == weaponClass)
|
||||
{
|
||||
return KFWeapon(invIter);
|
||||
}
|
||||
invIter = invIter.inventory;
|
||||
}
|
||||
return none;
|
||||
}
|
||||
|
||||
// Gets weapon index in our record of dual pistol classes.
|
||||
// Second variable determines whether we're searching for single
|
||||
// or dual variant:
|
||||
// ~ 'true' - searching for single
|
||||
// ~ 'false' - for dual
|
||||
// Returns '-1' if weapon isn't found
|
||||
// (dual MK23 won't be found as a single weapon).
|
||||
private final function int GetIndexAs(KFWeapon weapon, bool asSingle)
|
||||
{
|
||||
local int i;
|
||||
if (weapon == none) return -1;
|
||||
|
||||
for (i = 0; i < dualiesClasses.length; i += 1)
|
||||
{
|
||||
if (asSingle && dualiesClasses[i].single == weapon.class)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
if (!asSingle && dualiesClasses[i].dual == weapon.class)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Calculates full cost of a weapon with a discount,
|
||||
// dependent on it's instigator's perk.
|
||||
private final function float GetFullCost(KFWeapon weapon)
|
||||
{
|
||||
local float cost;
|
||||
local class<KFWeaponPickup> pickupClass;
|
||||
local KFPlayerReplicationInfo instigatorRI;
|
||||
if (weapon == none) return 0.0;
|
||||
pickupClass = class<KFWeaponPickup>(weapon.default.pickupClass);
|
||||
if (pickupClass == none) return 0.0;
|
||||
|
||||
cost = pickupClass.default.cost;
|
||||
if (weapon.instigator != none)
|
||||
{
|
||||
instigatorRI =
|
||||
KFPlayerReplicationInfo(weapon.instigator.playerReplicationInfo);
|
||||
}
|
||||
if (instigatorRI != none && instigatorRI.clientVeteranSkill != none)
|
||||
{
|
||||
cost *= instigatorRI.clientVeteranSkill.static
|
||||
.GetCostScaling(instigatorRI, pickupClass);
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
// If passed weapon is a pistol - we start tracking it's value;
|
||||
// Otherwise - do nothing.
|
||||
public final function RegisterSinglePistol
|
||||
(
|
||||
KFWeapon singlePistol,
|
||||
bool justSpawned
|
||||
)
|
||||
{
|
||||
local WeaponDataRecord newRecord;
|
||||
if (singlePistol == none) return;
|
||||
if (GetIndexAs(singlePistol, true) < 0) return;
|
||||
|
||||
newRecord.reference = singlePistol;
|
||||
newRecord.class = singlePistol.class;
|
||||
newRecord.owner = singlePistol.instigator;
|
||||
if (justSpawned)
|
||||
{
|
||||
newRecord.value = nextSellValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
newRecord.value = singlePistol.sellValue;
|
||||
}
|
||||
storedValues[storedValues.length] = newRecord;
|
||||
}
|
||||
|
||||
// Fixes sell value after player throws one pistol out of a pair.
|
||||
public final function FixCostAfterThrow(KFWeapon singlePistol)
|
||||
{
|
||||
local int index;
|
||||
local KFWeapon dualPistols;
|
||||
if (singlePistol == none) return;
|
||||
index = GetIndexAs(singlePistol, true);
|
||||
if (index < 0) return;
|
||||
dualPistols = GetWeaponOfClass( singlePistol.instigator,
|
||||
dualiesClasses[index].dual);
|
||||
if (dualPistols == none) return;
|
||||
|
||||
// Sell value recorded into 'dualPistols' will end up as a value of
|
||||
// a dropped pickup.
|
||||
// Sell value of 'singlePistol' will be the value for the pistol,
|
||||
// left in player's hands.
|
||||
if (dualPistols.class == class'KFMod.Single')
|
||||
{
|
||||
// 9mm is an exception.
|
||||
// Remaining weapon costs nothing.
|
||||
singlePistol.sellValue = 0;
|
||||
// We don't change the sell value of the dropped weapon,
|
||||
// as it's default behavior to transfer full value of a pair to it.
|
||||
return;
|
||||
}
|
||||
// For other pistols - divide the value.
|
||||
singlePistol.sellValue = dualPistols.sellValue / 2;
|
||||
dualPistols.sellValue = singlePistol.sellValue;
|
||||
}
|
||||
|
||||
// Fixes sell value after buying a pair of dual pistols,
|
||||
// if player already had a single version.
|
||||
public final function FixCostAfterBuying(KFWeapon dualPistols)
|
||||
{
|
||||
local int index;
|
||||
local KFWeapon singlePistol;
|
||||
local WeaponValuePair newPendingValue;
|
||||
if (dualPistols == none) return;
|
||||
index = GetIndexAs(dualPistols, false);
|
||||
if (index < 0) return;
|
||||
singlePistol = GetWeaponOfClass(dualPistols.instigator,
|
||||
dualiesClasses[index].single);
|
||||
if (singlePistol == none) return;
|
||||
|
||||
// 'singlePistol' will get destroyed, so it's sell value is irrelevant.
|
||||
// 'dualPistols' will be the new pair of pistols, but it's value will
|
||||
// get overwritten by vanilla's code after this function.
|
||||
// So we must add it to pending values to be changed later.
|
||||
newPendingValue.weapon = dualPistols;
|
||||
if (dualPistols.class == class'KFMod.Dualies')
|
||||
{
|
||||
// 9mm is an exception.
|
||||
// The value of pair of 9mms is the price of additional pistol,
|
||||
// that defined as a price of a pair in game.
|
||||
newPendingValue.value = GetFullCost(dualPistols) * 0.75;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Otherwise price of a pair is the price of two pistols:
|
||||
// 'singlePistol.sellValue' - the one we had
|
||||
// '(FullCost / 2) * 0.75' - and the one we bought
|
||||
newPendingValue.value = singlePistol.sellValue
|
||||
+ (GetFullCost(dualPistols) / 2) * 0.75;
|
||||
}
|
||||
pendingValues[pendingValues.length] = newPendingValue;
|
||||
}
|
||||
|
||||
// Fixes sell value after player picks up a single pistol,
|
||||
// while already having one of the same time in his inventory.
|
||||
public final function FixCostAfterPickUp(KFWeapon dualPistols)
|
||||
{
|
||||
local int i;
|
||||
local int index;
|
||||
local KFWeapon singlePistol;
|
||||
local WeaponValuePair newPendingValue;
|
||||
if (dualPistols == none) return;
|
||||
// In both cases of:
|
||||
// 1. buying dualies, without having a single pistol of
|
||||
// corresponding type;
|
||||
// 2. picking up a second pistol, while having another one;
|
||||
// by the time of 'CheckReplacement' (and, therefore, this function)
|
||||
// is called, there's no longer any single pistol in player's inventory
|
||||
// (in first case it never was there, in second - it got destroyed).
|
||||
// To distinguish between those possibilities we can check the owner of
|
||||
// the spawned weapon, since it's only set to instigator at the time of
|
||||
// 'CheckReplacement' when player picks up a weapon.
|
||||
// So we require that owner exists.
|
||||
if (dualPistols.owner == none) return;
|
||||
index = GetIndexAs(dualPistols, false);
|
||||
if (index < 0) return;
|
||||
singlePistol = GetWeaponOfClass(dualPistols.instigator,
|
||||
dualiesClasses[index].single);
|
||||
if (singlePistol != none) return;
|
||||
|
||||
if (nextSellValue == -1)
|
||||
{
|
||||
nextSellValue = GetFullCost(dualPistols) * 0.75;
|
||||
}
|
||||
for (i = 0; i < storedValues.length; i += 1)
|
||||
{
|
||||
if (storedValues[i].reference != none) continue;
|
||||
if (storedValues[i].class != dualiesClasses[index].single) continue;
|
||||
if (storedValues[i].owner != dualPistols.instigator) continue;
|
||||
newPendingValue.weapon = dualPistols;
|
||||
newPendingValue.value = storedValues[i].value + nextSellValue;
|
||||
pendingValues[pendingValues.length] = newPendingValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public final function ApplyPendingValues()
|
||||
{
|
||||
local int i;
|
||||
for (i = 0; i < pendingValues.length; i += 1)
|
||||
{
|
||||
if (pendingValues[i].weapon == none) continue;
|
||||
// Our fixes can only increase the correct ('!= -1')
|
||||
// sell value of weapons, so if we only need to change sell value
|
||||
// if we're allowed to increase it or it's incorrect.
|
||||
if (allowSellValueIncrease || pendingValues[i].weapon.sellValue == -1)
|
||||
{
|
||||
pendingValues[i].weapon.sellValue = pendingValues[i].value;
|
||||
}
|
||||
}
|
||||
pendingValues.length = 0;
|
||||
}
|
||||
|
||||
public final function StoreSinglePistolValues()
|
||||
{
|
||||
local int i;
|
||||
i = 0;
|
||||
while (i < storedValues.length)
|
||||
{
|
||||
if (storedValues[i].reference == none)
|
||||
{
|
||||
storedValues.Remove(i, 1);
|
||||
continue;
|
||||
}
|
||||
storedValues[i].owner = storedValues[i].reference.instigator;
|
||||
storedValues[i].value = storedValues[i].reference.sellValue;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
ApplyPendingValues();
|
||||
StoreSinglePistolValues();
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
allowSellValueIncrease = true
|
||||
// Inner variables
|
||||
dualiesClasses(0)=(single=class'KFMod.Single',dual=class'KFMod.Dualies')
|
||||
dualiesClasses(1)=(single=class'KFMod.Magnum44Pistol',dual=class'KFMod.Dual44Magnum')
|
||||
dualiesClasses(2)=(single=class'KFMod.MK23Pistol',dual=class'KFMod.DualMK23Pistol')
|
||||
dualiesClasses(3)=(single=class'KFMod.Deagle',dual=class'KFMod.DualDeagle')
|
||||
dualiesClasses(4)=(single=class'KFMod.GoldenDeagle',dual=class'KFMod.GoldenDualDeagle')
|
||||
dualiesClasses(5)=(single=class'KFMod.FlareRevolver',dual=class'KFMod.DualFlareRevolver')
|
||||
// Listeners
|
||||
requiredListeners(0) = class'MutatorListener_FixDualiesCost'
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
/**
|
||||
* Overloaded mutator events listener to catch when pistol-type weapons
|
||||
* (single or dual) are spawned and to correct their price.
|
||||
* Copyright 2020 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 MutatorListener_FixDualiesCost extends MutatorListenerBase
|
||||
abstract;
|
||||
|
||||
static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
|
||||
{
|
||||
local KFWeapon weapon;
|
||||
local FixDualiesCost dualiesCostFix;
|
||||
weapon = KFWeapon(other);
|
||||
if (weapon == none) return true;
|
||||
dualiesCostFix = FixDualiesCost(class'FixDualiesCost'.static.GetInstance());
|
||||
if (dualiesCostFix == none) return true;
|
||||
|
||||
dualiesCostFix.RegisterSinglePistol(weapon, true);
|
||||
dualiesCostFix.FixCostAfterThrow(weapon);
|
||||
dualiesCostFix.FixCostAfterBuying(weapon);
|
||||
dualiesCostFix.FixCostAfterPickUp(weapon);
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedEvents = class'MutatorEvents'
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
/**
|
||||
* This rule detects suspicious attempts to deal damage and
|
||||
* applies friendly fire scaling according to 'FixFFHack's rules.
|
||||
* Copyright 2019 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 FFHackRule extends GameRules;
|
||||
|
||||
function int NetDamage
|
||||
(
|
||||
int originalDamage,
|
||||
int damage,
|
||||
Pawn injured,
|
||||
Pawn instigator,
|
||||
Vector hitLocation,
|
||||
out Vector momentum,
|
||||
class<DamageType> damageType
|
||||
)
|
||||
{
|
||||
local KFGameType gameType;
|
||||
local FixFFHack ffHackFix;
|
||||
gameType = KFGameType(level.game);
|
||||
// Something is very wrong and we can just bail on this damage
|
||||
if (damageType == none || gameType == none) return 0;
|
||||
|
||||
// We only check when suspicious instigators that aren't a world
|
||||
if (!damageType.default.bCausedByWorld && IsSuspicious(instigator))
|
||||
{
|
||||
ffHackFix = FixFFHack(class'FixFFHack'.static.GetInstance());
|
||||
if (ffHackFix != none && ffHackFix.ShouldScaleDamage(damageType))
|
||||
{
|
||||
// Remove pushback to avoid environmental kills
|
||||
momentum = Vect(0.0, 0.0, 0.0);
|
||||
damage *= gameType.friendlyFireScale;
|
||||
}
|
||||
}
|
||||
return super.NetDamage( originalDamage, damage, injured, instigator,
|
||||
hitLocation, momentum, damageType);
|
||||
}
|
||||
|
||||
private function bool IsSuspicious(Pawn instigator)
|
||||
{
|
||||
// Instigator vanished
|
||||
if (instigator == none) return true;
|
||||
|
||||
// Instigator already became spectator
|
||||
if (KFPawn(instigator) != none)
|
||||
{
|
||||
if (instigator.playerReplicationInfo != none)
|
||||
{
|
||||
return instigator.playerReplicationInfo.bOnlySpectator;
|
||||
}
|
||||
return true; // Replication info is gone => suspicious
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
/**
|
||||
* This feature fixes a bug that can allow players to bypass server's
|
||||
* friendly fire limitations and teamkill.
|
||||
* Usual fixes apply friendly fire scale to suspicious damage themselves, which
|
||||
* also disables some of the environmental damage.
|
||||
* In order to avoid that, this fix allows server owner to define precisely
|
||||
* to what damage types to apply the friendly fire scaling.
|
||||
* It should be all damage types related to projectiles.
|
||||
* Copyright 2019 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 FixFFHack extends Feature;
|
||||
|
||||
/**
|
||||
* It's possible to bypass friendly fire damage scaling and always deal
|
||||
* full damage to other players, if one were to either leave the server or
|
||||
* spectate right after shooting a projectile. We use game rules to catch
|
||||
* such occurrences and apply friendly fire scaling to weapons,
|
||||
* specified by server admins.
|
||||
* To specify required subset of weapons, one must first
|
||||
* chose a general rule (scale by default / don't scale by default) and then,
|
||||
* optionally, add exceptions to it.
|
||||
* Choosing 'scaleByDefault == true' as a general rule will make this fix
|
||||
* behave in the similar way to 'KFExplosiveFix' by mutant and will disable
|
||||
* some environmental sources of damage on some maps. One can then add relevant
|
||||
* damage classes as exceptions to fix that downside, but making an extensive
|
||||
* list of such sources might prove problematic.
|
||||
* On the other hand, setting 'scaleByDefault == false' will allow to get
|
||||
* rid of team-killing exploits by simply adding damage types of all
|
||||
* projectile weapons, used on a server. This fix comes with such filled-in
|
||||
* list of all vanilla projectile classes.
|
||||
*/
|
||||
|
||||
// Defines a general rule for choosing whether or not to apply
|
||||
// friendly fire scaling.
|
||||
// This can be overwritten by exceptions ('alwaysScale' or 'neverScale').
|
||||
// Enabling scaling by default without any exceptions in 'neverScale' will
|
||||
// make this fix behave almost identically to Mutant's 'Explosives Fix Mutator'.
|
||||
var private config const bool scaleByDefault;
|
||||
// Damage types, for which we should always reapply friendly fire scaling.
|
||||
var private config const array< class<DamageType> > alwaysScale;
|
||||
// Damage types, for which we should never reapply friendly fire scaling.
|
||||
var private config const array< class<DamageType> > neverScale;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
level.game.AddGameModifier(Spawn(class'FFHackRule'));
|
||||
}
|
||||
|
||||
protected function OnDisabled()
|
||||
{
|
||||
local GameRules rulesIter;
|
||||
local FFHackRule ruleToDestroy;
|
||||
// Check first rule
|
||||
if (level.game.gameRulesModifiers == none) return;
|
||||
|
||||
ruleToDestroy = FFHackRule(level.game.gameRulesModifiers);
|
||||
if (ruleToDestroy != none)
|
||||
{
|
||||
level.game.gameRulesModifiers = ruleToDestroy.nextGameRules;
|
||||
ruleToDestroy.Destroy();
|
||||
return;
|
||||
}
|
||||
// Check rest of the rules
|
||||
rulesIter = level.game.gameRulesModifiers;
|
||||
while (rulesIter != none)
|
||||
{
|
||||
ruleToDestroy = FFHackRule(rulesIter.nextGameRules);
|
||||
if (ruleToDestroy != none)
|
||||
{
|
||||
rulesIter.nextGameRules = ruleToDestroy.nextGameRules;
|
||||
ruleToDestroy.Destroy();
|
||||
}
|
||||
rulesIter = rulesIter.nextGameRules;
|
||||
}
|
||||
}
|
||||
|
||||
// Checks general rule and exception list
|
||||
public final function bool ShouldScaleDamage(class<DamageType> damageType)
|
||||
{
|
||||
local int i;
|
||||
local array< class<DamageType> > exceptions;
|
||||
if (damageType == none) return false;
|
||||
|
||||
if (scaleByDefault)
|
||||
exceptions = neverScale;
|
||||
else
|
||||
exceptions = alwaysScale;
|
||||
for (i = 0; i < exceptions.length; i += 1)
|
||||
{
|
||||
if (exceptions[i] == damageType)
|
||||
{
|
||||
return (!scaleByDefault);
|
||||
}
|
||||
}
|
||||
return scaleByDefault;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
scaleByDefault = false
|
||||
// Vanilla damage types for projectiles
|
||||
alwaysScale(0) = class'KFMod.DamTypeCrossbuzzsawHeadShot'
|
||||
alwaysScale(1) = class'KFMod.DamTypeCrossbuzzsaw'
|
||||
alwaysScale(2) = class'KFMod.DamTypeFrag'
|
||||
alwaysScale(3) = class'KFMod.DamTypePipeBomb'
|
||||
alwaysScale(4) = class'KFMod.DamTypeM203Grenade'
|
||||
alwaysScale(5) = class'KFMod.DamTypeM79Grenade'
|
||||
alwaysScale(6) = class'KFMod.DamTypeM79GrenadeImpact'
|
||||
alwaysScale(7) = class'KFMod.DamTypeM32Grenade'
|
||||
alwaysScale(8) = class'KFMod.DamTypeLAW'
|
||||
alwaysScale(9) = class'KFMod.DamTypeLawRocketImpact'
|
||||
alwaysScale(10) = class'KFMod.DamTypeFlameNade'
|
||||
alwaysScale(11) = class'KFMod.DamTypeFlareRevolver'
|
||||
alwaysScale(12) = class'KFMod.DamTypeFlareProjectileImpact'
|
||||
alwaysScale(13) = class'KFMod.DamTypeBurned'
|
||||
alwaysScale(14) = class'KFMod.DamTypeTrenchgun'
|
||||
alwaysScale(15) = class'KFMod.DamTypeHuskGun'
|
||||
alwaysScale(16) = class'KFMod.DamTypeCrossbow'
|
||||
alwaysScale(17) = class'KFMod.DamTypeCrossbowHeadShot'
|
||||
alwaysScale(18) = class'KFMod.DamTypeM99SniperRifle'
|
||||
alwaysScale(19) = class'KFMod.DamTypeM99HeadShot'
|
||||
alwaysScale(20) = class'KFMod.DamTypeShotgun'
|
||||
alwaysScale(21) = class'KFMod.DamTypeNailGun'
|
||||
alwaysScale(22) = class'KFMod.DamTypeDBShotgun'
|
||||
alwaysScale(23) = class'KFMod.DamTypeKSGShotgun'
|
||||
alwaysScale(24) = class'KFMod.DamTypeBenelli'
|
||||
alwaysScale(25) = class'KFMod.DamTypeSPGrenade'
|
||||
alwaysScale(26) = class'KFMod.DamTypeSPGrenadeImpact'
|
||||
alwaysScale(27) = class'KFMod.DamTypeSeekerSixRocket'
|
||||
alwaysScale(28) = class'KFMod.DamTypeSeekerRocketImpact'
|
||||
alwaysScale(29) = class'KFMod.DamTypeSealSquealExplosion'
|
||||
alwaysScale(30) = class'KFMod.DamTypeRocketImpact'
|
||||
alwaysScale(31) = class'KFMod.DamTypeBlowerThrower'
|
||||
alwaysScale(32) = class'KFMod.DamTypeSPShotgun'
|
||||
alwaysScale(33) = class'KFMod.DamTypeZEDGun'
|
||||
alwaysScale(34) = class'KFMod.DamTypeZEDGunMKII'
|
||||
}
|
||||
|
|
@ -1,233 +0,0 @@
|
|||
/**
|
||||
* This feature fixes a vulnerability in a code of 'Frag' that can allow
|
||||
* player to throw grenades even when he no longer has any.
|
||||
* There's also no cooldowns on the throw, which can lead to a server crash.
|
||||
* Copyright 2019 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 FixInfiniteNades extends Feature;
|
||||
|
||||
/**
|
||||
* It is possible to call 'ServerThrow' function from client,
|
||||
* forcing it to get executed on a server. This function consumes the grenade
|
||||
* ammo and spawns a nade, but it doesn't check if player had any grenade ammo
|
||||
* in the first place, allowing you him to throw however many grenades
|
||||
* he wants. Moreover, unlike a regular throwing method, calling this function
|
||||
* allows to spawn many grenades without any delay,
|
||||
* which can lead to a server crash.
|
||||
*
|
||||
* This fix tracks every instance of 'Frag' weapon that's responsible for
|
||||
* throwing grenades and records how much ammo they have have.
|
||||
* This is necessary, because whatever means we use, when we get a say in
|
||||
* preventing grenade from spawning the ammo was already reduced.
|
||||
* This means that we can't distinguished between a player abusing a bug by
|
||||
* throwing grenade when he doesn't have necessary ammo and player throwing
|
||||
* his last nade, as in both cases current ammo visible to us will be 0.
|
||||
* Then, before every nade throw, it checks if player has enough ammo and
|
||||
* blocks grenade from spawning if he doesn't.
|
||||
* We change a 'FireModeClass[0]' from 'FragFire' to 'FixedFragFire' and
|
||||
* only call 'super.DoFireEffect()' if we decide spawning grenade
|
||||
* should be allowed. The side effect is a change in server's 'FireModeClass'.
|
||||
*/
|
||||
|
||||
// Setting this flag to 'true' will allow to throw grenades by calling
|
||||
// 'ServerThrow' directly, as long as player has necessary ammo.
|
||||
// This can allow some players to throw grenades much quicker than intended,
|
||||
// therefore it's suggested to keep this flag set to 'false'.
|
||||
var private config const bool ignoreTossFlags;
|
||||
|
||||
// Records how much ammo given frag grenade ('Frag') has.
|
||||
struct FragAmmoRecord
|
||||
{
|
||||
var public Frag fragReference;
|
||||
var public int amount;
|
||||
};
|
||||
var private array<FragAmmoRecord> ammoRecords;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
local Frag nextFrag;
|
||||
// Find all frags, that spawned when this fix wasn't running.
|
||||
foreach level.DynamicActors(class'KFMod.Frag', nextFrag)
|
||||
{
|
||||
RegisterFrag(nextFrag);
|
||||
}
|
||||
RecreateFrags();
|
||||
}
|
||||
|
||||
protected function OnDisabled()
|
||||
{
|
||||
RecreateFrags();
|
||||
ammoRecords.length = 0;
|
||||
}
|
||||
|
||||
// Returns index of the connection corresponding to the given controller.
|
||||
// Returns '-1' if no connection correspond to the given controller.
|
||||
// Returns '-1' if given controller is equal to 'none'.
|
||||
private final function int GetAmmoIndex(Frag fragToCheck)
|
||||
{
|
||||
local int i;
|
||||
if (fragToCheck == none) return -1;
|
||||
|
||||
for (i = 0; i < ammoRecords.length; i += 1)
|
||||
{
|
||||
if (ammoRecords[i].fragReference == fragToCheck)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Recreates all the 'Frag' actors, to change their fire mode mid-game.
|
||||
private final function RecreateFrags()
|
||||
{
|
||||
local int i;
|
||||
local float maxAmmo, currentAmmo;
|
||||
local Frag newFrag;
|
||||
local Pawn fragOwner;
|
||||
local array<FragAmmoRecord> oldRecords;
|
||||
oldRecords = ammoRecords;
|
||||
for (i = 0; i < oldRecords.length; i += 1)
|
||||
{
|
||||
// Check if we even need to recreate that instance of 'Frag'
|
||||
if (oldRecords[i].fragReference == none) continue;
|
||||
fragOwner = oldRecords[i].fragReference.instigator;
|
||||
if (fragOwner == none) continue;
|
||||
// Recreate
|
||||
oldRecords[i].fragReference.Destroy();
|
||||
fragOwner.CreateInventory("KFMod.Frag");
|
||||
newFrag = GetPawnFrag(fragOwner);
|
||||
// Restore ammo amount
|
||||
if (newFrag != none)
|
||||
{
|
||||
newFrag.GetAmmoCount(maxAmmo, currentAmmo);
|
||||
newFrag.AddAmmo(oldRecords[i].amount - Int(currentAmmo), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility function to help find a 'Frag' instance in a given pawn's inventory.
|
||||
static private final function Frag GetPawnFrag(Pawn pawnWithFrag)
|
||||
{
|
||||
local Frag foundFrag;
|
||||
local Inventory invIter;
|
||||
if (pawnWithFrag == none) return none;
|
||||
invIter = pawnWithFrag.inventory;
|
||||
while (invIter != none)
|
||||
{
|
||||
foundFrag = Frag(invIter);
|
||||
if (foundFrag != none)
|
||||
{
|
||||
return foundFrag;
|
||||
}
|
||||
invIter = invIter.inventory;
|
||||
}
|
||||
return none;
|
||||
}
|
||||
|
||||
// Utility function for extracting current ammo amount from a frag class.
|
||||
private final function int GetFragAmmo(Frag fragReference)
|
||||
{
|
||||
local float maxAmmo;
|
||||
local float currentAmmo;
|
||||
if (fragReference == none) return 0;
|
||||
|
||||
fragReference.GetAmmoCount(maxAmmo, currentAmmo);
|
||||
return Int(currentAmmo);
|
||||
}
|
||||
|
||||
// Attempts to add new 'Frag' instance to our records.
|
||||
public final function RegisterFrag(Frag newFrag)
|
||||
{
|
||||
local int index;
|
||||
local FragAmmoRecord newRecord;
|
||||
index = GetAmmoIndex(newFrag);
|
||||
if (index >= 0) return;
|
||||
|
||||
newRecord.fragReference = newFrag;
|
||||
newRecord.amount = GetFragAmmo(newFrag);
|
||||
ammoRecords[ammoRecords.length] = newRecord;
|
||||
}
|
||||
|
||||
// This function tells our fix that there was a nade throw and we should
|
||||
// reduce current 'Frag' ammo in our records.
|
||||
// Returns 'true' if we had ammo for that, and 'false' if we didn't.
|
||||
public final function bool RegisterNadeThrow(Frag relevantFrag)
|
||||
{
|
||||
if (CanThrowGrenade(relevantFrag))
|
||||
{
|
||||
ReduceGrenades(relevantFrag);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Can we throw grenade according to our rules?
|
||||
// A throw can be prevented if:
|
||||
// - we think that player doesn't have necessary ammo;
|
||||
// - Player isn't currently 'tossing' a nade,
|
||||
// meaning it was a direct call of 'ServerThrow'.
|
||||
private final function bool CanThrowGrenade(Frag fragToCheck)
|
||||
{
|
||||
local int index;
|
||||
// Nothing to check
|
||||
if (fragToCheck == none) return false;
|
||||
// No ammo
|
||||
index = GetAmmoIndex(fragToCheck);
|
||||
if (index < 0) return false;
|
||||
if (ammoRecords[index].amount <= 0) return false;
|
||||
// Not tossing
|
||||
if (ignoreTossFlags) return true;
|
||||
if (!fragToCheck.bTossActive || fragToCheck.bTossSpawned) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reduces recorded amount of ammo in our records for the given nade.
|
||||
private final function ReduceGrenades(Frag relevantFrag)
|
||||
{
|
||||
local int index;
|
||||
index = GetAmmoIndex(relevantFrag);
|
||||
if (index < 0) return;
|
||||
ammoRecords[index].amount -= 1;
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
local int i;
|
||||
// Update our ammo records with current, correct data.
|
||||
i = 0;
|
||||
while (i < ammoRecords.length)
|
||||
{
|
||||
if (ammoRecords[i].fragReference != none)
|
||||
{
|
||||
ammoRecords[i].amount = GetFragAmmo(ammoRecords[i].fragReference);
|
||||
i += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
ammoRecords.Remove(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
ignoreTossFlags = false
|
||||
// Listeners
|
||||
requiredListeners(0) = class'MutatorListener_FixInfiniteNades'
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
/**
|
||||
* A replacement for vanilla 'FragFire' fire class for 'Frag' weapon that
|
||||
* adds additional ammo check in accordance to ammo records
|
||||
* of 'FixInfiniteNades'.
|
||||
* Copyright 2019 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 FixedFragFire extends KFMod.FragFire;
|
||||
|
||||
function DoFireEffect()
|
||||
{
|
||||
local FixInfiniteNades nadeFix;
|
||||
nadeFix = FixInfiniteNades(class'FixInfiniteNades'.static.GetInstance());
|
||||
if (nadeFix == none || nadeFix.RegisterNadeThrow(Frag(weapon)))
|
||||
{
|
||||
super.DoFireEffect();
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
/**
|
||||
* Overloaded mutator events listener to catch
|
||||
* new 'Frag' weapons and 'Nade' projectiles.
|
||||
* Copyright 2019 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 MutatorListener_FixInfiniteNades extends MutatorListenerBase
|
||||
abstract;
|
||||
|
||||
static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
|
||||
{
|
||||
local Frag relevantFrag;
|
||||
local FixInfiniteNades nadeFix;
|
||||
nadeFix = FixInfiniteNades(class'FixInfiniteNades'.static.GetInstance());
|
||||
if (nadeFix == none) return true;
|
||||
|
||||
// Handle detecting new frag (weapons that allows to throw nades)
|
||||
relevantFrag = Frag(other);
|
||||
if (relevantFrag != none)
|
||||
{
|
||||
nadeFix.RegisterFrag(relevantFrag);
|
||||
relevantFrag.FireModeClass[0] = class'FixedFragFire';
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
|
|
@ -1,225 +0,0 @@
|
|||
/**
|
||||
* This feature addressed two inventory issues:
|
||||
* 1. Players carrying amount of weapons that shouldn't be allowed by the
|
||||
* weight limit.
|
||||
* 2. Players carrying two variants of the same gun.
|
||||
* For example carrying both M32 and camo M32.
|
||||
* Single and dual version of the same weapon are also considered
|
||||
* the same gun, so you can't carry both MK23 and dual MK23 or
|
||||
* dual handcannons and golden handcannon.
|
||||
*
|
||||
* It fixes them by doing repeated checks to find violations of those rules
|
||||
* and destroys all droppable weapons of people that use this exploit.
|
||||
* Copyright 2020 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 FixInventoryAbuse extends Feature;
|
||||
|
||||
// How often (in seconds) should we do our inventory validations?
|
||||
// We shouldn't really worry about performance, but there's also no need to
|
||||
// do this check too often.
|
||||
var private config const float checkInterval;
|
||||
|
||||
struct DualiesPair
|
||||
{
|
||||
var class<KFWeaponPickup> single;
|
||||
var class<KFWeaponPickup> dual;
|
||||
};
|
||||
// For this fix to properly work, this array must contain an entry for
|
||||
// every dual weapon in the game (like pistols, with single and dual versions).
|
||||
// It's made configurable in case of custom dual weapons.
|
||||
var private config const array<DualiesPair> dualiesClasses;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
local float actualInterval;
|
||||
actualInterval = checkInterval;
|
||||
if (actualInterval <= 0)
|
||||
{
|
||||
actualInterval = 0.25;
|
||||
}
|
||||
SetTimer(actualInterval, true);
|
||||
}
|
||||
|
||||
protected function OnDisabled()
|
||||
{
|
||||
SetTimer(0.0f, false);
|
||||
}
|
||||
|
||||
// Did player with this controller contribute to the latest dosh generation?
|
||||
private final function bool IsWeightLimitViolated(KFHumanPawn playerPawn)
|
||||
{
|
||||
if (playerPawn == none) return false;
|
||||
return (playerPawn.currentWeight > playerPawn.maxCarryWeight);
|
||||
}
|
||||
|
||||
// Returns a root pickup class.
|
||||
// For non-dual weapons, root class is defined as either:
|
||||
// 1. the first variant (reskin), if there are variants for that weapon;
|
||||
// 2. and as the class itself, if there are no variants.
|
||||
// For dual weapons (all dual pistols) root class is defined as
|
||||
// a root of their single version.
|
||||
// This definition is useful because:
|
||||
// ~ Vanilla game rules are such that player can only have two weapons
|
||||
// in the inventory if they have different roots;
|
||||
// ~ Root is easy to find.
|
||||
private final function class<KFWeaponPickup> GetRootPickupClass(KFWeapon weapon)
|
||||
{
|
||||
local int i;
|
||||
local class<KFWeaponPickup> root;
|
||||
if (weapon == none) return none;
|
||||
// Start with a pickup of the given weapons
|
||||
root = class<KFWeaponPickup>(weapon.default.pickupClass);
|
||||
if (root == none) return none;
|
||||
|
||||
// In case it's a dual version - find corresponding single pickup class
|
||||
// (it's root would be the same).
|
||||
for (i = 0; i < dualiesClasses.length; i += 1)
|
||||
{
|
||||
if (dualiesClasses[i].dual == root)
|
||||
{
|
||||
root = dualiesClasses[i].single;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Take either first variant class or the class itself, -
|
||||
// it's going to be root by definition.
|
||||
if (root.default.variantClasses.length > 0)
|
||||
{
|
||||
root = class<KFWeaponPickup>(root.default.variantClasses[0]);
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
// Returns 'true' if passed pawn has two weapons that are just variants of
|
||||
// each other (they have the same root, see 'GetRootPickupClass').
|
||||
private final function bool HasDuplicateGuns(KFHumanPawn playerPawn)
|
||||
{
|
||||
local int i, j;
|
||||
local Inventory inv;
|
||||
local KFWeapon nextWeapon;
|
||||
local class<KFWeaponPickup> rootClass;
|
||||
local array< class<Pickup> > rootList;
|
||||
if (playerPawn == none) return false;
|
||||
|
||||
// First find a root for every weapon in the pawn's inventory.
|
||||
for (inv = playerPawn.inventory; inv != none; inv = inv.inventory)
|
||||
{
|
||||
nextWeapon = KFWeapon(inv);
|
||||
if (nextWeapon == none) continue;
|
||||
if (nextWeapon.bKFNeverThrow) continue;
|
||||
rootClass = GetRootPickupClass(nextWeapon);
|
||||
if (rootClass != none)
|
||||
{
|
||||
rootList[rootList.length] = rootClass;
|
||||
}
|
||||
}
|
||||
// Then just check obtained roots for duplicates.
|
||||
for (i = 0; i < rootList.length; i += 1)
|
||||
{
|
||||
for (j = i + 1; j < rootList.length; j += 1)
|
||||
{
|
||||
if (rootList[i] == rootList[j])
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private final function Vector DropWeapon(KFWeapon weaponToDrop)
|
||||
{
|
||||
local Vector x, y, z;
|
||||
local Vector weaponVelocity;
|
||||
local Vector dropLocation;
|
||||
local KFHumanPawn playerPawn;
|
||||
if (weaponToDrop == none) return Vect(0, 0, 0);
|
||||
playerPawn = KFHumanPawn(weaponToDrop.instigator);
|
||||
if (playerPawn == none) return Vect(0, 0, 0);
|
||||
|
||||
// Calculations from 'PlayerController.ServerThrowWeapon'
|
||||
weaponVelocity = Vector(playerPawn.GetViewRotation());
|
||||
weaponVelocity *= (playerPawn.velocity dot weaponVelocity) + 150;
|
||||
weaponVelocity += Vect(0, 0, 100);
|
||||
// Calculations from 'Pawn.TossWeapon'
|
||||
GetAxes(playerPawn.rotation, x, y, z);
|
||||
dropLocation = playerPawn.location + 0.8 * playerPawn.collisionRadius * x -
|
||||
0.5 * playerPawn.collisionRadius * y;
|
||||
// Do the drop
|
||||
weaponToDrop.velocity = weaponVelocity;
|
||||
weaponToDrop.DropFrom(dropLocation);
|
||||
}
|
||||
|
||||
// Kill the gun devil!
|
||||
private final function DropEverything(KFHumanPawn playerPawn)
|
||||
{
|
||||
local int i;
|
||||
local Inventory inv;
|
||||
local KFWeapon nextWeapon;
|
||||
local array<KFWeapon> weaponList;
|
||||
if (playerPawn == none) return;
|
||||
// Going through the linked list while removing items can be tricky,
|
||||
// so just find all weapons first.
|
||||
for (inv = playerPawn.inventory; inv != none; inv = inv.inventory)
|
||||
{
|
||||
nextWeapon = KFWeapon(inv);
|
||||
if (nextWeapon == none) continue;
|
||||
if (nextWeapon.bKFNeverThrow) continue;
|
||||
weaponList[weaponList.length] = nextWeapon;
|
||||
}
|
||||
// And destroy them later.
|
||||
for(i = 0; i < weaponList.length; i += 1)
|
||||
{
|
||||
DropWeapon(weaponList[i]);
|
||||
}
|
||||
}
|
||||
|
||||
event Timer()
|
||||
{
|
||||
local int i;
|
||||
local KFHumanPawn nextPawn;
|
||||
local ConnectionService service;
|
||||
local array<ConnectionService.Connection> connections;
|
||||
service = ConnectionService(class'ConnectionService'.static.GetInstance());
|
||||
if (service == none) return;
|
||||
|
||||
connections = service.GetActiveConnections();
|
||||
for (i = 0; i < connections.length; i += 1)
|
||||
{
|
||||
nextPawn = none;
|
||||
if (connections[i].controllerReference != none)
|
||||
{
|
||||
nextPawn = KFHumanPawn(connections[i].controllerReference.pawn);
|
||||
}
|
||||
if (IsWeightLimitViolated(nextPawn) || HasDuplicateGuns(nextPawn))
|
||||
{
|
||||
DropEverything(nextPawn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
checkInterval = 0.25
|
||||
dualiesClasses(0)=(single=class'KFMod.SinglePickup',dual=class'KFMod.DualiesPickup')
|
||||
dualiesClasses(1)=(single=class'KFMod.Magnum44Pickup',dual=class'KFMod.Dual44MagnumPickup')
|
||||
dualiesClasses(2)=(single=class'KFMod.MK23Pickup',dual=class'KFMod.DualMK23Pickup')
|
||||
dualiesClasses(3)=(single=class'KFMod.DeaglePickup',dual=class'KFMod.DualDeaglePickup')
|
||||
dualiesClasses(4)=(single=class'KFMod.GoldenDeaglePickup',dual=class'KFMod.GoldenDualDeaglePickup')
|
||||
dualiesClasses(5)=(single=class'KFMod.FlareRevolverPickup',dual=class'KFMod.DualFlareRevolverPickup')
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/**
|
||||
* Overloaded broadcast events listener to catch the moment
|
||||
* someone becomes alive player / spectator.
|
||||
* Copyright 2020 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 BroadcastListener_FixSpectatorCrash extends BroadcastListenerBase
|
||||
abstract;
|
||||
|
||||
var private const int becomeAlivePlayerID;
|
||||
var private const int becomeSpectatorID;
|
||||
|
||||
static function bool HandleLocalized
|
||||
(
|
||||
Actor sender,
|
||||
BroadcastEvents.LocalizedMessage message
|
||||
)
|
||||
{
|
||||
local FixSpectatorCrash specFix;
|
||||
local PlayerController senderController;
|
||||
if (sender == none) return true;
|
||||
if (sender.level == none || sender.level.game == none) return true;
|
||||
if (message.class != sender.level.game.gameMessageClass) return true;
|
||||
if ( message.id != default.becomeAlivePlayerID
|
||||
&& message.id != default.becomeSpectatorID) return true;
|
||||
|
||||
specFix = FixSpectatorCrash(class'FixSpectatorCrash'.static.GetInstance());
|
||||
senderController = GetController(sender);
|
||||
specFix.NotifyStatusChange(senderController);
|
||||
return (!specFix.IsViolator(senderController));
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
becomeAlivePlayerID = 1
|
||||
becomeSpectatorID = 14
|
||||
}
|
||||
|
|
@ -1,291 +0,0 @@
|
|||
/**
|
||||
* This feature attempts to prevent server crashes caused by someone
|
||||
* quickly switching between being spectator and an active player.
|
||||
*
|
||||
* We do so by disconnecting players who start switching way too fast
|
||||
* (more than twice in a short amount of time) and temporarily faking a large
|
||||
* amount of players on the server, to prevent such spam from affecting the server.
|
||||
* Copyright 2020 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 FixSpectatorCrash extends Feature
|
||||
dependson(ConnectionService);
|
||||
|
||||
/**
|
||||
* We use broadcast events to track when someone is switching
|
||||
* to active player or spectator and remember such people
|
||||
* for a short time (cooldown), defined by ('spectatorChangeTimeout').
|
||||
* If one of the player we've remembered tries to switch again,
|
||||
* before the defined cooldown ran out, - we kick him
|
||||
* by destroying his controller.
|
||||
* One possible problem arises from the fact that controllers aren't
|
||||
* immediately destroyed and instead initiate player disconnection, -
|
||||
* exploiter might have enough time to cause a lag or even crash the server.
|
||||
* We address this issue by temporarily blocking anyone from
|
||||
* becoming active player (we do this by setting 'numPlayers' variable in
|
||||
* killing floor's game info to a large value).
|
||||
* After all malicious players have successfully disconnected, -
|
||||
* we remove the block.
|
||||
*/
|
||||
|
||||
// This fix will try to kick any player that switches between active player
|
||||
// and cooldown faster than time (in seconds) in this value.
|
||||
// NOTE: raising this value past default value of '0.25'
|
||||
// won't actually improve crash prevention.
|
||||
var private config const float spectatorChangeTimeout;
|
||||
|
||||
// [ADVANCED] Don't change this setting unless you know what you're doing.
|
||||
// Allows you to turn off server blocking.
|
||||
// Players that don't respect timeout will still be kicked.
|
||||
// This might be needed if this fix conflicts with another mutator
|
||||
// that also changes 'numPlayers'.
|
||||
// However, it is necessary to block aggressive enough server crash attempts,
|
||||
// but can cause compatibility issues with some mutators.
|
||||
// It's highly preferred to rewrite such a mutator to be compatible.
|
||||
// NOTE: it should be compatible with most faked players-type mutators,
|
||||
// since this fix remembers the difference between amount of
|
||||
// real players and 'numPlayers'.
|
||||
// After unblocking, it sets 'numPlayers' to
|
||||
// the current amount of real players + that difference.
|
||||
// So 4 players + 3 (=7 numPlayers) after kicking 1 player becomes
|
||||
// 3 players + 3 (=6 numPlayers).
|
||||
var private config const bool allowServerBlock;
|
||||
|
||||
// Stores remaining cooldown value before the next allowed
|
||||
// spectator change per player.
|
||||
struct CooldownRecord
|
||||
{
|
||||
var PlayerController player;
|
||||
var float cooldown;
|
||||
};
|
||||
|
||||
// Currently active cooldowns
|
||||
var private array<CooldownRecord> currentCooldowns;
|
||||
|
||||
// Players who were decided to be violators and
|
||||
// were marked for disconnecting.
|
||||
// We'll be maintaining server block as long as even one
|
||||
// of them hasn't yet disconnected.
|
||||
var private array<PlayerController> violators;
|
||||
|
||||
// Is server currently blocked?
|
||||
var private bool becomingActiveBlocked;
|
||||
// This value introduced to accommodate mods such as faked player that can
|
||||
// change 'numPlayers' to a value that isn't directly tied to the
|
||||
// current number of active players.
|
||||
// We remember the difference between active players and 'numPlayers'
|
||||
/// variable in game type before server block and add it after block is over.
|
||||
// If some mod introduces a more complicated relation between amount of
|
||||
// active players and 'numPlayers', then it must take care of
|
||||
// compatibility on it's own.
|
||||
var private int recordedNumPlayersMod;
|
||||
|
||||
// If given 'PlayerController' is registered in our cooldown records, -
|
||||
// returns it's index.
|
||||
// If it doesn't exists (or 'none' value was passes), - returns '-1'.
|
||||
private final function int GetCooldownIndex(PlayerController player)
|
||||
{
|
||||
local int i;
|
||||
if (player == none) return -1;
|
||||
|
||||
for (i = 0; i < currentCooldowns.length; i += 1)
|
||||
{
|
||||
if (currentCooldowns[i].player == player)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Checks if given 'PlayerController' is registered as a violator.
|
||||
// 'none' value isn't a violator.
|
||||
public final function bool IsViolator(PlayerController player)
|
||||
{
|
||||
local int i;
|
||||
if (player == none) return false;
|
||||
|
||||
for (i = 0; i < violators.length; i += 1)
|
||||
{
|
||||
if (violators[i] == player)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// This function is to notify our fix that some player just changed status
|
||||
// of active player / spectator.
|
||||
// If passes value isn't 'none', it puts given player on cooldown or kicks him.
|
||||
public final function NotifyStatusChange(PlayerController player)
|
||||
{
|
||||
local int index;
|
||||
local CooldownRecord newRecord;
|
||||
if (player == none) return;
|
||||
|
||||
index = GetCooldownIndex(player);
|
||||
// Players already on cool down must be kicked and marked as violators
|
||||
if (index >= 0)
|
||||
{
|
||||
player.Destroy();
|
||||
currentCooldowns.Remove(index, 1);
|
||||
violators[violators.length] = player;
|
||||
if (allowServerBlock)
|
||||
{
|
||||
SetBlock(true);
|
||||
}
|
||||
}
|
||||
// Players that aren't on cooldown are
|
||||
// either violators (do nothing, just wait for their disconnect)
|
||||
// or didn't recently change their status (put them on cooldown).
|
||||
else if (!IsViolator(player))
|
||||
{
|
||||
newRecord.player = player;
|
||||
newRecord.cooldown = spectatorChangeTimeout;
|
||||
currentCooldowns[currentCooldowns.length] = newRecord;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 'true' to block server, 'false' to unblock.
|
||||
// Only works if 'allowServerBlock' is set to 'true'.
|
||||
private final function SetBlock(bool activateBlock)
|
||||
{
|
||||
local KFGameType kfGameType;
|
||||
// Do we even need to do anything?
|
||||
if (!allowServerBlock) return;
|
||||
if (activateBlock == becomingActiveBlocked) return;
|
||||
// Only works with 'KFGameType' and it's children.
|
||||
if (level != none) kfGameType = KFGameType(level.game);
|
||||
if (kfGameType == none) return;
|
||||
|
||||
// Actually block/unblock
|
||||
becomingActiveBlocked = activateBlock;
|
||||
if (activateBlock)
|
||||
{
|
||||
recordedNumPlayersMod = GetNumPlayersMod();
|
||||
// This value both can't realistically fall below
|
||||
// 'kfGameType.maxPlayer' and won't overflow from random increase
|
||||
// in vanilla code.
|
||||
kfGameType.numPlayers = maxInt / 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Adding 'recordedNumPlayersMod' should prevent
|
||||
// faked players from breaking.
|
||||
kfGameType.numPlayers = GetRealPlayers() + recordedNumPlayersMod;
|
||||
}
|
||||
}
|
||||
|
||||
// Performs server blocking if violators have disconnected.
|
||||
private final function TryUnblocking()
|
||||
{
|
||||
local int i;
|
||||
if (!allowServerBlock) return;
|
||||
if (!becomingActiveBlocked) return;
|
||||
|
||||
for (i = 0; i < violators.length; i += 1)
|
||||
{
|
||||
if (violators[i] != none)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
SetBlock(false);
|
||||
}
|
||||
|
||||
// Counts current amount of "real" active players
|
||||
// (connected to the server and not spectators).
|
||||
// Need 'ConnectionService' to be running, otherwise return '-1'.
|
||||
private final function int GetRealPlayers()
|
||||
{
|
||||
// Auxiliary variables
|
||||
local int i;
|
||||
local int realPlayersAmount;
|
||||
local PlayerController player;
|
||||
// Information extraction
|
||||
local ConnectionService service;
|
||||
local array<ConnectionService.Connection> connections;
|
||||
service = ConnectionService(class'ConnectionService'.static.GetInstance());
|
||||
if (service == none) return -1;
|
||||
|
||||
// Count non-spectators
|
||||
connections = service.GetActiveConnections();
|
||||
realPlayersAmount = 0;
|
||||
for (i = 0; i < connections.length; i += 1)
|
||||
{
|
||||
player = connections[i].controllerReference;
|
||||
if (player == none) continue;
|
||||
if (player.playerReplicationInfo == none) continue;
|
||||
if (!player.playerReplicationInfo.bOnlySpectator)
|
||||
{
|
||||
realPlayersAmount += 1;
|
||||
}
|
||||
}
|
||||
return realPlayersAmount;
|
||||
}
|
||||
|
||||
// Calculates difference between current amount of "real" active players
|
||||
// and 'numPlayers' from 'KFGameType'.
|
||||
// Most typically this difference will be non-zero when using
|
||||
// faked players-type mutators
|
||||
// (difference will be equal to the amount of faked players).
|
||||
private final function int GetNumPlayersMod()
|
||||
{
|
||||
local KFGameType kfGameType;
|
||||
if (level != none) kfGameType = KFGameType(level.game);
|
||||
if (kfGameType == none) return 0;
|
||||
return kfGameType.numPlayers - GetRealPlayers();
|
||||
}
|
||||
|
||||
private final function ReduceCooldowns(float timePassed)
|
||||
{
|
||||
local int i;
|
||||
i = 0;
|
||||
while (i < currentCooldowns.length)
|
||||
{
|
||||
currentCooldowns[i].cooldown -= timePassed;
|
||||
if ( currentCooldowns[i].player != none
|
||||
&& currentCooldowns[i].cooldown > 0.0)
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentCooldowns.Remove(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
local float trueTimePassed;
|
||||
trueTimePassed = delta * (1.1 / level.timeDilation);
|
||||
TryUnblocking();
|
||||
ReduceCooldowns(trueTimePassed);
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
// Configurable variables
|
||||
spectatorChangeTimeout = 0.25
|
||||
allowServerBlock = true
|
||||
// Inner variables
|
||||
becomingActiveBlocked = false
|
||||
// Listeners
|
||||
requiredListeners(0) = class'BroadcastListener_FixSpectatorCrash'
|
||||
}
|
||||
|
|
@ -1,188 +0,0 @@
|
|||
/**
|
||||
* This feature fixes lags caused by a zed time that can occur
|
||||
* on some maps when a lot of zeds are present at once.
|
||||
* As a side effect it also fixes an issue where during zed time speed up
|
||||
* 'zedTimeSlomoScale' was assumed to be default value of '0.2'.
|
||||
* Now zed time will behave correctly with mods that
|
||||
* change 'zedTimeSlomoScale'.
|
||||
* Copyright 2020 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 FixZedTimeLags extends Feature
|
||||
dependson(ConnectionService);
|
||||
|
||||
/**
|
||||
* When zed time activates, game speed is immediately set to
|
||||
* 'zedTimeSlomoScale' (0.2 by default), defined, like all other variables,
|
||||
* in 'KFGameType'. Zed time lasts 'zedTimeDuration' seconds (3.0 by default),
|
||||
* but during last 'zedTimeDuration * 0.166' seconds (by default 0.498)
|
||||
* it starts to speed back up, causing game speed to update every tick.
|
||||
* This makes animations look more smooth when exiting zed-time;
|
||||
* however, updating speed every tick for that purpose seems like
|
||||
* an overkill and, combined with things like
|
||||
* increased tick rate, certain maps and raised zed limit,
|
||||
* it can lead to noticeable lags at the end of zed time.
|
||||
* To fix this issue we disable 'Tick' event in
|
||||
* 'KFGameType' and then repeat that functionality in our own 'Tick' event,
|
||||
* but only perform game speed updates occasionally,
|
||||
* to make sure that overall amount of updates won't go over a limit,
|
||||
* that can be configured via 'maxGameSpeedUpdatesAmount'
|
||||
* Author's test (looking really hard on clots' animations)
|
||||
* seem to suggest that there shouldn't be much visible difference if
|
||||
* we limit game speed updates to about 2 or 3.
|
||||
*/
|
||||
|
||||
// Max amount of game speed updates during speed up phase
|
||||
// (actual amount of updates can't be larger than amount of ticks).
|
||||
// On servers with default 30 tick rate there's usually
|
||||
// about 13 updates total on vanilla game.
|
||||
// Values lower than 1 are treated like 1.
|
||||
var private config const int maxGameSpeedUpdatesAmount;
|
||||
// [ADVANCED] Don't change this setting unless you know what you're doing.
|
||||
// Compatibility setting that allows to keep 'GameInfo' 's 'Tick' event
|
||||
// from being disabled.
|
||||
// Useful when running Acedia along with custom 'GameInfo'
|
||||
// (that isn't 'KFGameType') that relies on 'Tick' event.
|
||||
// Note, however, that in order to keep this fix working properly,
|
||||
// it's on you to make sure 'KFGameType.Tick()' logic isn't executed.
|
||||
var private config const bool disableTick;
|
||||
// Counts how much time is left until next update
|
||||
var private float updateCooldown;
|
||||
// Recorded game type, to avoid constant conversions every tick
|
||||
var private KFGameType gameType;
|
||||
|
||||
protected function OnEnabled()
|
||||
{
|
||||
gameType = KFGameType(level.game);
|
||||
if (gameType == none)
|
||||
{
|
||||
Destroy();
|
||||
}
|
||||
else if (disableTick)
|
||||
{
|
||||
gameType.Disable('Tick');
|
||||
}
|
||||
}
|
||||
|
||||
protected function OnDisabled()
|
||||
{
|
||||
gameType = KFGameType(level.game);
|
||||
if (gameType != none && disableTick)
|
||||
{
|
||||
gameType.Enable('Tick');
|
||||
}
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
local float trueTimePassed;
|
||||
if (gameType == none) return;
|
||||
if (!gameType.bZEDTimeActive) return;
|
||||
// Unfortunately we need to keep disabling 'Tick' probe function,
|
||||
// because it constantly gets enabled back and I don't know where
|
||||
// (maybe native code?); only really matters during zed time.
|
||||
if (disableTick)
|
||||
{
|
||||
gameType.Disable('Tick');
|
||||
}
|
||||
// How much real (not in-game) time has passed
|
||||
trueTimePassed = delta * (1.1 / level.timeDilation);
|
||||
gameType.currentZEDTimeDuration -= trueTimePassed;
|
||||
|
||||
// Handle speeding up phase
|
||||
if (gameType.bSpeedingBackUp)
|
||||
{
|
||||
DoSpeedBackUp(trueTimePassed);
|
||||
}
|
||||
else if (gameType.currentZEDTimeDuration < GetSpeedupDuration())
|
||||
{
|
||||
gameType.bSpeedingBackUp = true;
|
||||
updateCooldown = GetFullUpdateCooldown();
|
||||
TellClientsZedTimeEnds();
|
||||
DoSpeedBackUp(trueTimePassed);
|
||||
}
|
||||
// End zed time once it's duration has passed
|
||||
if (gameType.currentZEDTimeDuration <= 0)
|
||||
{
|
||||
gameType.bZEDTimeActive = false;
|
||||
gameType.bSpeedingBackUp = false;
|
||||
gameType.zedTimeExtensionsUsed = 0;
|
||||
gameType.SetGameSpeed(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
private final function TellClientsZedTimeEnds()
|
||||
{
|
||||
local int i;
|
||||
local KFPlayerController player;
|
||||
local ConnectionService service;
|
||||
local array<ConnectionService.Connection> connections;
|
||||
service = ConnectionService(class'ConnectionService'.static.GetInstance());
|
||||
if (service == none) return;
|
||||
connections = service.GetActiveConnections();
|
||||
for (i = 0; i < connections.length; i += 1)
|
||||
{
|
||||
player = KFPlayerController(connections[i].controllerReference);
|
||||
if (player != none)
|
||||
{
|
||||
// Play sound of leaving zed time
|
||||
player.ClientExitZedTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This function is called every tick during speed up phase and manages
|
||||
// gradual game speed increase.
|
||||
private final function DoSpeedBackUp(float trueTimePassed)
|
||||
{
|
||||
// Game speed will always be updated in our 'Tick' event
|
||||
// at the very end of the zed time.
|
||||
// The rest of the updates will be uniformly distributed
|
||||
// over the speed up duration.
|
||||
|
||||
local float newGameSpeed;
|
||||
local float slowdownScale;
|
||||
if (maxGameSpeedUpdatesAmount <= 1) return;
|
||||
if (updateCooldown > 0.0)
|
||||
{
|
||||
updateCooldown -= trueTimePassed;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
updateCooldown = GetFullUpdateCooldown();
|
||||
}
|
||||
slowdownScale = gameType.currentZEDTimeDuration / GetSpeedupDuration();
|
||||
newGameSpeed = Lerp(slowdownScale, 1.0, gameType.zedTimeSlomoScale);
|
||||
gameType.SetGameSpeed(newGameSpeed);
|
||||
}
|
||||
|
||||
private final function float GetSpeedupDuration()
|
||||
{
|
||||
return gameType.zedTimeDuration * 0.166;
|
||||
}
|
||||
|
||||
private final function float GetFullUpdateCooldown()
|
||||
{
|
||||
return GetSpeedupDuration() / maxGameSpeedUpdatesAmount;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
maxGameSpeedUpdatesAmount = 3
|
||||
disableTick = true
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
/**
|
||||
* Class for an object that will provide an access to a Acedia's functionality
|
||||
* by giving a reference to this actor to all Acedia's objects and actors,
|
||||
* emulating a global API namespace.
|
||||
* Copyright 2020 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 Global extends Singleton;
|
||||
|
||||
var public Acedia acedia;
|
||||
var public LoggerAPI logger;
|
||||
var public JSONAPI json;
|
||||
var public AliasesAPI alias;
|
||||
var public TextAPI text;
|
||||
var public MemoryAPI memory;
|
||||
var public ConsoleAPI console;
|
||||
var public ColorAPI color;
|
||||
|
||||
// TODO: APIs must be `remoteRole = ROLE_None`
|
||||
protected function OnCreated()
|
||||
{
|
||||
acedia = class'Acedia'.static.GetInstance();
|
||||
Spawn(class'LoggerAPI');
|
||||
logger = LoggerAPI(class'LoggerAPI'.static.GetInstance());
|
||||
Spawn(class'JSONAPI');
|
||||
json = JSONAPI(class'JSONAPI'.static.GetInstance());
|
||||
Spawn(class'AliasesAPI');
|
||||
alias = AliasesAPI(class'AliasesAPI'.static.GetInstance());
|
||||
Spawn(class'TextAPI');
|
||||
text = TextAPI(class'TextAPI'.static.GetInstance());
|
||||
Spawn(class'MemoryAPI');
|
||||
memory = MemoryAPI(class'MemoryAPI'.static.GetInstance());
|
||||
Spawn(class'ConsoleAPI');
|
||||
console = ConsoleAPI(class'ConsoleAPI'.static.GetInstance());
|
||||
Spawn(class'ColorAPI');
|
||||
color = ColorAPI(class'ColorAPI'.static.GetInstance());
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
/**
|
||||
* Manifest is meant to describe contents of the package (mutator file)
|
||||
* as well as what actors/objects should be automatically created when package
|
||||
* is loaded and what event listeners should be activated.
|
||||
* Currently only implements automatic listener activation.
|
||||
* Copyright 2019 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 Manifest extends Object
|
||||
abstract;
|
||||
|
||||
// List of features in this manifest's package.
|
||||
var public const array< class<AliasSource> > aliasSources;
|
||||
|
||||
// List of features in this manifest's package.
|
||||
var public const array< class<Feature> > features;
|
||||
|
||||
// List of features in this manifest's package.
|
||||
var public const array< class<TestCase> > testCases;
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
aliasSources(0) = class'AliasSource'
|
||||
aliasSources(1) = class'WeaponAliasSource'
|
||||
aliasSources(2) = class'ColorAliasSource'
|
||||
features(0) = class'FixZedTimeLags'
|
||||
features(1) = class'FixDoshSpam'
|
||||
features(2) = class'FixFFHack'
|
||||
features(3) = class'FixInfiniteNades'
|
||||
features(4) = class'FixAmmoSelling'
|
||||
features(5) = class'FixSpectatorCrash'
|
||||
features(6) = class'FixDualiesCost'
|
||||
features(7) = class'FixInventoryAbuse'
|
||||
// Unit tests
|
||||
testCases(0) = class'TEST_Aliases'
|
||||
testCases(1) = class'TEST_ColorAPI'
|
||||
testCases(2) = class'TEST_JSON'
|
||||
testCases(3) = class'TEST_Text'
|
||||
testCases(4) = class'TEST_TextAPI'
|
||||
testCases(5) = class'TEST_Parser'
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* Main and only Acedia mutator used for loading Acedia packages
|
||||
* Main and only Acedia mutator used for loading Acedia packages
|
||||
* and providing access to mutator events' calls.
|
||||
* Name is chosen to make config files more readable.
|
||||
* Copyright 2020 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
|
|
@ -18,7 +19,7 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class Acedia extends Mutator
|
||||
class Packages extends Mutator
|
||||
config(Acedia);
|
||||
|
||||
// Default value of this variable will be used to store
|
||||
|
|
@ -26,15 +27,22 @@ class Acedia extends Mutator
|
|||
// as well as to ensure there's only one copy of it.
|
||||
// We can't use 'Singleton' class for that,
|
||||
// as we have to derive from 'Mutator'.
|
||||
var private Acedia selfReference;
|
||||
var private Packages selfReference;
|
||||
|
||||
// Acedia's reference to a `Global` object.
|
||||
var private Global _;
|
||||
|
||||
// Package's manifest is supposed to always have a name of
|
||||
// "<package_name>.Manifest", this variable stores the ".Manifest" part
|
||||
var private const string manifestSuffix;
|
||||
|
||||
// Array of predefined services that must be started along with Acedia mutator.
|
||||
var private config array< class<Manifest> > registeredManifests;
|
||||
var private config array<string> package;
|
||||
|
||||
// Array of predefined services that must be started along with Acedia mutator.
|
||||
var private array< class<Service> > systemServices;
|
||||
// AcediaCore package that this launcher is build for
|
||||
var private config const string corePackage;
|
||||
|
||||
static public final function Acedia GetInstance()
|
||||
static public final function Packages GetInstance()
|
||||
{
|
||||
return default.selfReference;
|
||||
}
|
||||
|
|
@ -56,11 +64,33 @@ event PreBeginPlay()
|
|||
|
||||
private final function BootUp()
|
||||
{
|
||||
local int i;
|
||||
Spawn(class'Global');
|
||||
for (i = 0; i < registeredManifests.length; i += 1) {
|
||||
LoadManifest(registeredManifests[i]);
|
||||
local int i;
|
||||
local class<_manifest> nextManifest;
|
||||
_ = Spawn(class'Global');
|
||||
// Load core
|
||||
nextManifest = LoadManifestClass(corePackage);
|
||||
if (nextManifest == none)
|
||||
{
|
||||
_.logger.Fatal("Cannot load required AcediaCore package \""
|
||||
$ corePackage $ "\". Acedia will shut down.");
|
||||
Destroy();
|
||||
return;
|
||||
}
|
||||
LoadManifest(nextManifest);
|
||||
// Load packages
|
||||
for (i = 0; i < package.length; i += 1)
|
||||
{
|
||||
nextManifest = LoadManifestClass(package[i]);
|
||||
if (nextManifest == none)
|
||||
{
|
||||
_.logger.Failure("Cannot load `Manifest` for package \""
|
||||
$ package[i] $ "\". Check if it's missing or"
|
||||
@ "if it's name is spelled incorrectly.");
|
||||
continue;
|
||||
}
|
||||
LoadManifest(nextManifest);
|
||||
}
|
||||
// Inject broadcast handler
|
||||
InjectBroadcastHandler(); // TODO: move this to 'SideEffect' mechanic
|
||||
}
|
||||
|
||||
|
|
@ -75,10 +105,24 @@ private final function RunStartUpTests()
|
|||
if (testService.filterTestsByGroup) {
|
||||
testService.FilterByName(testService.requiredGroup);
|
||||
}
|
||||
testService.Run();
|
||||
if (testService.Run())
|
||||
{
|
||||
// This listener will output test results into server's console
|
||||
class'TestingListener_AcediaLauncher'.static.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
_.logger.Failure("Could not launch Acedia's start up testing process.");
|
||||
}
|
||||
}
|
||||
|
||||
private final function LoadManifest(class<Manifest> manifestClass)
|
||||
private final function class<_manifest> LoadManifestClass(string packageName)
|
||||
{
|
||||
return class<_manifest>(DynamicLoadObject( packageName $ manifestSuffix,
|
||||
class'Class', true));
|
||||
}
|
||||
|
||||
private final function LoadManifest(class<_manifest> manifestClass)
|
||||
{
|
||||
local int i;
|
||||
// Load alias sources
|
||||
|
|
@ -140,13 +184,13 @@ function Mutate(string command, PlayerController sendingController)
|
|||
|
||||
defaultproperties
|
||||
{
|
||||
// Add Acedia's own manifest
|
||||
registeredManifests(0) = class'Manifest'
|
||||
corePackage = "AcediaCore_0_2"
|
||||
manifestSuffix = ".Manifest"
|
||||
// This is a server-only mutator
|
||||
remoteRole = ROLE_None
|
||||
bAlwaysRelevant = true
|
||||
// Mutator description
|
||||
GroupName = "Core mutator"
|
||||
FriendlyName = "Acedia"
|
||||
Description = "Launcher for Acedia modules"
|
||||
GroupName = "Package loader"
|
||||
FriendlyName = "Acedia loader"
|
||||
Description = "Launcher for Acedia packages"
|
||||
}
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
/**
|
||||
* Event generator for 'ConnectionService'.
|
||||
* Copyright 2019 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 ConnectionEvents extends Events
|
||||
dependson(ConnectionService)
|
||||
abstract;
|
||||
|
||||
static function CallPlayerConnected(ConnectionService.Connection connection)
|
||||
{
|
||||
local int i;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0; i < listeners.length; i += 1)
|
||||
{
|
||||
class<ConnectionListenerBase>(listeners[i])
|
||||
.static.PlayerConnected(connection);
|
||||
}
|
||||
}
|
||||
|
||||
static function CallPlayerDisconnected(ConnectionService.Connection connection)
|
||||
{
|
||||
local int i;
|
||||
local array< class<Listener> > listeners;
|
||||
listeners = GetListeners();
|
||||
for (i = 0; i < listeners.length; i += 1)
|
||||
{
|
||||
class<ConnectionListenerBase>(listeners[i])
|
||||
.static.PlayerDisconnected(connection);
|
||||
}
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedListener = class'ConnectionListenerBase'
|
||||
connectedServiceClass = class'ConnectionService'
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
/**
|
||||
* Listener for events generated by 'ConnectionService'.
|
||||
* Copyright 2019 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 ConnectionListenerBase extends Listener
|
||||
dependson(ConnectionService)
|
||||
abstract;
|
||||
|
||||
// 'PlayerConnected' is called the moment we detect a new player on a server.
|
||||
static function PlayerConnected(ConnectionService.Connection connection);
|
||||
|
||||
// 'PlayerDisconnected' is called the moment we
|
||||
// detect a player leaving the server.
|
||||
static function PlayerDisconnected(ConnectionService.Connection connection);
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedEvents = class'ConnectionEvents'
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
/**
|
||||
* This service tracks current connections to the server
|
||||
* as well as their basic information,
|
||||
* like IP or steam ID of connecting player.
|
||||
* Copyright 2019 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 ConnectionService extends Service;
|
||||
|
||||
// Stores basic information about a connection
|
||||
struct Connection
|
||||
{
|
||||
var public string networkAddress;
|
||||
var public string steamID;
|
||||
var public PlayerController controllerReference;
|
||||
// Reference to 'AcediaReplicationInfo' for this client,
|
||||
// in case it was created.
|
||||
var private AcediaReplicationInfo acediaRI;
|
||||
};
|
||||
|
||||
var private array<Connection> activeConnections;
|
||||
|
||||
// Shortcut to 'ConnectionEvents', so that we don't have to write
|
||||
// class'ConnectionEvents' every time.
|
||||
var const class<ConnectionEvents> events;
|
||||
|
||||
// Returning 'true' guarantees that 'controllerToCheck != none'
|
||||
// and either 'controllerToCheck.playerReplicationInfo != none'
|
||||
// or 'auxiliaryRepInfo != none'.
|
||||
private function bool IsHumanController(PlayerController controllerToCheck)
|
||||
{
|
||||
local PlayerReplicationInfo replicationInfo;
|
||||
if (controllerToCheck == none) return false;
|
||||
if (!controllerToCheck.bIsPlayer) return false;
|
||||
// Is this a WebAdmin that didn't yet set 'bIsPlayer = false'
|
||||
if (MessagingSpectator(controllerToCheck) != none) return false;
|
||||
// Check replication info
|
||||
replicationInfo = controllerToCheck.playerReplicationInfo;
|
||||
if (replicationInfo == none) return false;
|
||||
if (replicationInfo.bBot) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns index of the connection corresponding to the given controller.
|
||||
// Returns '-1' if no connection correspond to the given controller.
|
||||
// Returns '-1' if given controller is equal to 'none'.
|
||||
private function int GetConnectionIndex(PlayerController controllerToCheck)
|
||||
{
|
||||
local int i;
|
||||
if (controllerToCheck == none) return -1;
|
||||
for (i = 0; i < activeConnections.length; i += 1)
|
||||
{
|
||||
if (activeConnections[i].controllerReference == controllerToCheck)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Remove connections with now invalid ('none') player controller reference.
|
||||
private function RemoveBrokenConnections()
|
||||
{
|
||||
local int i;
|
||||
i = 0;
|
||||
while (i < activeConnections.length)
|
||||
{
|
||||
if (activeConnections[i].controllerReference == none)
|
||||
{
|
||||
if (activeConnections[i].acediaRI != none)
|
||||
{
|
||||
activeConnections[i].acediaRI.Destroy();
|
||||
}
|
||||
events.static.CallPlayerDisconnected(activeConnections[i]);
|
||||
activeConnections.Remove(i, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return connection, corresponding to a given player controller.
|
||||
public final function Connection GetConnection(PlayerController player)
|
||||
{
|
||||
local int connectionIndex;
|
||||
local Connection emptyConnection;
|
||||
connectionIndex = GetConnectionIndex(player);
|
||||
if (connectionIndex < 0) return emptyConnection;
|
||||
return activeConnections[connectionIndex];
|
||||
}
|
||||
|
||||
// Attempts to register a connection for this player controller.
|
||||
// Shouldn't be used outside of 'ConnectionService' module.
|
||||
// Returns 'true' if connection is registered (even if it was already added).
|
||||
public final function bool RegisterConnection(PlayerController player)
|
||||
{
|
||||
local Connection newConnection;
|
||||
if (!IsHumanController(player)) return false;
|
||||
if (GetConnectionIndex(player) >= 0) return true;
|
||||
newConnection.controllerReference = player;
|
||||
if (!class'Acedia'.static.GetInstance().IsServerOnly())
|
||||
{
|
||||
newConnection.acediaRI = Spawn(class'AcediaReplicationInfo', player);
|
||||
newConnection.acediaRI.linkOwner = player;
|
||||
}
|
||||
newConnection.networkAddress = player.GetPlayerNetworkAddress();
|
||||
newConnection.steamID = player.GetPlayerIDHash();
|
||||
activeConnections[activeConnections.length] = newConnection;
|
||||
events.static.CallPlayerConnected(newConnection);
|
||||
return true;
|
||||
}
|
||||
|
||||
public final function array<Connection> GetActiveConnections()
|
||||
{
|
||||
return activeConnections;
|
||||
}
|
||||
|
||||
event Tick(float delta)
|
||||
{
|
||||
RemoveBrokenConnections();
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
events = class'ConnectionEvents'
|
||||
requiredListeners(0) = class'MutatorListener_Connection'
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
/**
|
||||
* Overloaded mutator events listener to catch connecting players.
|
||||
* Copyright 2019 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 MutatorListener_Connection extends MutatorListenerBase
|
||||
abstract;
|
||||
|
||||
static function bool CheckReplacement(Actor other, out byte isSuperRelevant)
|
||||
{
|
||||
local KFSteamStatsAndAchievements playerSteamStatsAndAchievements;
|
||||
local PlayerController player;
|
||||
local ConnectionService service;
|
||||
// We are looking for 'KFSteamStatsAndAchievements' instead of
|
||||
// 'PlayerController' because, by the time they it's created,
|
||||
// controller should have a valid reference to 'PlayerReplicationInfo',
|
||||
// as well as valid network address and IDHash (steam id).
|
||||
// However, neither of those are properly initialized at the point when
|
||||
// 'CheckReplacement' is called for 'PlayerController'.
|
||||
//
|
||||
// Since 'KFSteamStatsAndAchievements'
|
||||
// is created soon after (at the same tick)
|
||||
// for each new `PlayerController`,
|
||||
// we'll be detecting new users right after server
|
||||
// detected and properly initialized them.
|
||||
playerSteamStatsAndAchievements = KFSteamStatsAndAchievements(other);
|
||||
if (playerSteamStatsAndAchievements == none) return true;
|
||||
service = ConnectionService(class'ConnectionService'.static.GetInstance());
|
||||
if (service == none) return true;
|
||||
|
||||
player = PlayerController(playerSteamStatsAndAchievements.owner);
|
||||
service.RegisterConnection(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedEvents = class'MutatorEvents'
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ function PreBeginPlay()
|
|||
super.PreBeginPlay();
|
||||
if (level != none && level.game != none)
|
||||
{
|
||||
level.game.AddMutator(string(class'Acedia'));
|
||||
level.game.AddMutator(string(class'Packages'));
|
||||
}
|
||||
Destroy();
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
/**
|
||||
* Listener for events related to testing.
|
||||
* Overloaded testing events listener to catch when tests that we run during
|
||||
* server loading finish.
|
||||
* Copyright 2020 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
|
|
@ -17,16 +18,26 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class TestingListenerBase extends Listener
|
||||
class TestingListener_AcediaLauncher extends TestingListenerBase
|
||||
abstract;
|
||||
|
||||
static function TestingBegan(array< class<TestCase> > testQueue) {}
|
||||
|
||||
static function CaseTested(class<TestCase> testQueue, TestCaseSummary result) {}
|
||||
|
||||
static function TestingEnded(
|
||||
array< class<TestCase> > testedCase,
|
||||
array<TestCaseSummary> results) {}
|
||||
array< class<TestCase> > testQueue,
|
||||
array<TestCaseSummary> results)
|
||||
{
|
||||
local int i;
|
||||
local string nextLine;
|
||||
local array<string> textSummary;
|
||||
textSummary = class'TestCaseSummary'.static.GenerateStringSummary(results);
|
||||
for (i = 0; i < textSummary.length; i += 1)
|
||||
{
|
||||
nextLine = _().text.ConvertString( textSummary[i],
|
||||
STRING_Formatted, STRING_Plain);
|
||||
Log(nextLine);
|
||||
}
|
||||
// No longer need to listen to testing events
|
||||
SetActive(false);
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
Reference in a new issue