Move most files to other Acedia packages

This commit is contained in:
Anton Tarasenko 2020-07-18 19:21:03 +07:00
commit c42edfb88f
93 changed files with 85 additions and 20255 deletions

View file

@ -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
{
}

View file

@ -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
{
}

View file

@ -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
{
}

View file

@ -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
}

View file

@ -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'
}

View file

@ -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
}

View file

@ -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'
}

View file

@ -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
{
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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"
}

View file

@ -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
}

View file

@ -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"
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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 = " "
}

View file

@ -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
{
}

View file

@ -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
{
}

View file

@ -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 namevalue 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
{
}

View file

@ -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
{
}

View file

@ -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"
}

View file

@ -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'
}

View file

@ -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
}

View file

@ -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'.

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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
}

View file

@ -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
{
}

View file

@ -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"
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
{
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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 = ""
}

View file

@ -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.

View file

@ -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

View file

@ -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
}

View file

@ -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'
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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
{
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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
{
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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
{
}

View file

@ -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
{
}

View file

@ -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')
}

View file

@ -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
}

View file

@ -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'
}

View file

@ -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
}

View file

@ -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());
}

View file

@ -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'
}

View file

@ -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"
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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'
}

View file

@ -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();
}

View file

@ -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
{