Prepare fixtures

This commit is contained in:
dkanus 2026-07-14 20:27:09 +07:00
parent 797e5ea192
commit 9c94356263
6021 changed files with 722805 additions and 22 deletions

View File

@ -11,56 +11,118 @@ use rottlib::diagnostics::Diagnostic;
use rottlib::lexer::TokenizedFile;
use rottlib::parser::Parser;
#[derive(Clone, Copy, Debug)]
enum ParseMode {
Expression,
SourceFile,
}
const PARSE_MODE: ParseMode = ParseMode::SourceFile;
const TEST_CASES: &[(&str, &str)] = &[
// P0057 - VarEditorSpecifierExpected
("files/P0057_01.uc", "var(Category,) int A;\n"),
("files/P0057_02.uc", "var(,) int A;\n"),
(
"files/P0057_01.uc",
concat!("class Test extends Actor;\n", "var(Category,) int A;\n"),
),
(
"files/P0057_02.uc",
concat!("class Test extends Actor;\n", "var(,) int A;\n"),
),
(
"files/P0057_03.uc",
"var(\n ,\n ,\n Category\n) int A;\n",
concat!(
"class Test extends Actor;\n",
"var(\n ,\n ,\n Category\n) int A;\n"
),
),
(
"files/P0057_04.uc",
"var(\n Category,\n\n\n) int A;\n",
concat!(
"class Test extends Actor;\n",
"var(\n Category,\n\n\n) int A;\n"
),
),
// P0058 - VarEditorSpecifierListMissingSeparator
("files/P0058_01.uc", "var(Category DisplayName) int A;\n"),
("files/P0058_02.uc", "var(\"Display\" \"Tooltip\") int A;\n"),
(
"files/P0058_01.uc",
concat!(
"class Test extends Actor;\n",
"var(Category DisplayName) int A;\n"
),
),
(
"files/P0058_02.uc",
concat!(
"class Test extends Actor;\n",
"var(\"Display\" \"Tooltip\") int A;\n"
),
),
(
"files/P0058_03.uc",
"var(\n Category\n DisplayName\n) int A;\n",
concat!(
"class Test extends Actor;\n",
"var(\n Category\n DisplayName\n) int A;\n"
),
),
(
"files/P0058_04.uc",
"var(\n Category\n\n \"Display Name\",\n Advanced\n) int A;\n",
concat!(
"class Test extends Actor;\n",
"var(\n Category\n\n \"Display Name\",\n Advanced\n) int A;\n"
),
),
// P0059 - VarEditorSpecifierListExpectedCommaOrClosingParenthesis
("files/P0059_01.uc", "var(Category :) int A;\n"),
("files/P0059_02.uc", "var(\"Display\" *) int A;\n"),
(
"files/P0059_01.uc",
concat!("class Test extends Actor;\n", "var(Category :) int A;\n"),
),
(
"files/P0059_02.uc",
concat!(
"class Test extends Actor;\n",
"var(\"Display\" *) int A;\n"
),
),
(
"files/P0059_03.uc",
"var(\n Category\n [\n) int A;\n",
concat!(
"class Test extends Actor;\n",
"var(\n Category\n [\n) int A;\n"
),
),
(
"files/P0059_04.uc",
"var(\n \"Display Name\"\n\n ]\n) int A;\n",
concat!(
"class Test extends Actor;\n",
"var(\n \"Display Name\"\n\n ]\n) int A;\n"
),
),
// P0060 - VarEditorSpecifierListMissingClosingParenthesis
("files/P0060_01.uc", "var(Category\n"),
(
"files/P0060_01.uc",
concat!("class Test extends Actor;\n", "var(Category\n"),
),
(
"files/P0060_02.uc",
"var(\n Category,\n \"Display Name\"\n",
concat!(
"class Test extends Actor;\n",
"var(\n Category,\n \"Display Name\"\n"
),
),
(
"files/P0060_03.uc",
"var(\n\n\n",
concat!("class Test extends Actor;\n", "var(\n\n\n"),
),
(
"files/P0060_04.uc",
"var(\n Category,\n\n \"Display Name\"\n\n",
concat!(
"class Test extends Actor;\n",
"var(\n Category,\n\n \"Display Name\"\n\n"
),
),
];
@ -183,11 +245,25 @@ fn main() {
if RUN_PARSER {
let mut parser = Parser::new(&tf, &arena);
let expr = parser.parse_expression();
if PRINT_PARSED_EXPR {
println!("Parsed expression:");
println!("{expr:#?}");
match PARSE_MODE {
ParseMode::Expression => {
let expr = parser.parse_expression();
if PRINT_PARSED_EXPR {
println!("Parsed expression:");
println!("{expr:#?}");
}
}
ParseMode::SourceFile => {
if let Err(error) = parser.parse_source_file() {
parser.report_error(error);
}
if PRINT_PARSED_EXPR {
println!("Parsed source file: <not printed>");
}
}
}
if parser.diagnostics.is_empty() {
@ -224,4 +300,4 @@ fn render_diagnostic(
_colors: bool,
) {
diag.render(file, file_name.unwrap_or("<default>"));
}
}

View File

@ -0,0 +1,413 @@
/**
* Base class for a game mode config, contains all the information Acedia's
* game modes must have, including settings
* (`includeFeature`, `includeFeatureAs` and `excludeFeature`)
* for picking used `Feature`s.
*
* Contains three types of methods:
* 1. Getters for its values;
* 2. `UpdateFeatureArray()` method for updating list of `Feature`s to
* be used based on game info's settings;
* 3. `Report...()` methods that perform various validation checks
* (and log them) on config data.
* Copyright 2021-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class BaseGameMode extends AcediaConfig
dependson(Packages)
abstract;
// Name of the game mode players will see in voting (formatted string)
var protected config string title;
// Preferable game length (plain string)
var protected config string length;
// Preferable difficulty level (plain string)
var protected config string difficulty;
// `Mutator`s to add with this game mode
var protected config array<string> includeMutator;
// `Feature`s to include (with "default" config)
var protected config array<string> includeFeature;
// `Feature`s to exclude from game mode, regardless of other settings
// (this one has highest priority)
var protected config array<string> excludeFeature;
struct FeatureConfigPair
{
var public string feature;
var public string config;
};
// `Feature`s to include (with specified config).
// Higher priority than `includeFeature`, but lower than `excludeFeature`.
var protected config array<FeatureConfigPair> includeFeatureAs;
var private LoggerAPI.Definition warnBadMutatorName, warnBadFeatureName;
protected function HashTable ToData()
{
local int i;
local HashTable result;
local HashTable nextPair;
local ArrayList nextArray;
result = _.collections.EmptyHashTable();
result.SetFormattedString(P("title"), title);
result.SetString(P("length"), length);
result.SetString(P("difficulty"), difficulty);
nextArray = _.collections.EmptyArrayList();
for (i = 0; i < includeFeature.length; i += 1) {
nextArray.AddString(includeFeature[i]);
}
result.SetItem(P("includeFeature"), nextArray);
_.memory.Free(nextArray);
nextArray = _.collections.EmptyArrayList();
for (i = 0; i < excludeFeature.length; i += 1) {
nextArray.AddItem(_.text.FromString(excludeFeature[i]));
}
result.SetItem(P("excludeFeature"), nextArray);
_.memory.Free(nextArray);
nextArray = _.collections.EmptyArrayList();
for (i = 0; i < includeMutator.length; i += 1) {
nextArray.AddItem(_.text.FromString(includeFeature[i]));
}
result.SetItem(P("includeMutator"), nextArray);
_.memory.Free(nextArray);
nextArray = _.collections.EmptyArrayList();
for (i = 0; i < includeFeatureAs.length; i += 1)
{
nextPair = _.collections.EmptyHashTable();
nextPair.SetString(P("feature"), includeFeatureAs[i].feature);
nextPair.SetString(P("config"), includeFeatureAs[i].config);
nextArray.AddItem(nextPair);
_.memory.Free(nextPair);
}
result.SetItem(P("includeFeatureAs"), nextArray);
_.memory.Free(nextArray);
return result;
}
protected function FromData(HashTable source)
{
local int i;
local ArrayList nextArray;
local HashTable nextPair;
if (source == none) {
return;
}
title = source.GetFormattedString(P("title"));
length = source.GetString(P("length"));
difficulty = source.GetString(P("difficulty"));
nextArray = source.GetArrayList(P("includeFeature"));
includeFeature = DynamicIntoStringArray(nextArray);
_.memory.Free(nextArray);
nextArray = source.GetArrayList(P("excludeFeature"));
excludeFeature = DynamicIntoStringArray(nextArray);
_.memory.Free(nextArray);
nextArray = source.GetArrayList(P("includeMutator"));
includeMutator = DynamicIntoStringArray(nextArray);
_.memory.Free(nextArray);
nextArray = source.GetArrayList(P("includeFeatureAs"));
if (nextArray == none) {
return;
}
includeFeatureAs.length = 0;
for (i = 0; i < nextArray.GetLength(); i += 1)
{
nextPair = nextArray.GetHashTable(i);
includeFeatureAs[i] = HashTableIntoPair(nextPair);
_.memory.Free(nextPair);
}
_.memory.Free(nextArray);
}
private final function FeatureConfigPair HashTableIntoPair(HashTable source)
{
local Text nextText;
local FeatureConfigPair result;
if (source == none) {
return result;
}
nextText = source.GetText(P("feature"));
if (nextText != none) {
result.feature = nextText.ToString();
}
nextText = source.GetText(P("config"));
if (nextText != none) {
result.config = nextText.ToString();
}
return result;
}
private final function array<string> DynamicIntoStringArray(ArrayList source)
{
local int i;
local Text nextText;
local array<string> result;
if (source == none) {
return result;
}
for (i = 0; i < source.GetLength(); i += 1)
{
nextText = source.GetText(i);
if (nextText != none) {
includeFeature[i] = nextText.ToString();
}
}
}
protected function array<Text> StringToTextArray(array<string> input)
{
local int i;
local array<Text> result;
for (i = 0; i < input.length; i += 1) {
result[i] = _.text.FromString(input[i]);
}
return result;
}
/**
* @return Name of the `GameInfo` class to be used with the caller game mode.
*/
public function Text GetGameTypeClass()
{
return none;
}
/**
* @return Human-readable name of the caller game mode.
* Players will see it as the name of the mode in the voting options.
*/
public function Text GetTitle()
{
return _.text.FromFormattedString(title);
}
/**
* @return Specified game length for the game mode.
* Interpretation of this value can depend on each particular game mode.
*/
public function Text GetLength()
{
return _.text.FromString(length);
}
/**
* @return Specified difficulty for the game mode.
* Interpretation of this value can depend on each particular game mode.
*/
public function Text GetDifficulty()
{
return _.text.FromString(difficulty);
}
/**
* Checks `Feature`-related settings (`includeFeature`, `includeFeatureAs` and
* `excludeFeature`) for correctness and reports any issues.
* Currently correctness check simply ensures that all listed `Feature`s
* actually exist.
*/
public function ReportIncorrectSettings(
array<Packages.FeatureConfigPair> featuresToEnable)
{
local int i;
local array<string> featureNames, featuresToReplace;
for (i = 0; i < featuresToEnable.length; i += 1) {
featureNames[i] = string(featuresToEnable[i].featureClass);
}
ValidateFeatureArray(includeFeature, featureNames, "includeFeatures");
ValidateFeatureArray(excludeFeature, featureNames, "excludeFeatures");
for (i = 0; i < includeFeatureAs.length; i += 1) {
featuresToReplace[i] = includeFeatureAs[i].feature;
}
ValidateFeatureArray(featuresToReplace, featureNames, "includeFeatureAs");
}
/**
* Checks `Mutator`-related settings (`includeMutator`) for correctness and
* reports any issues.
* Currently correctness check performs a simple validity check for mutator,
* to make sure it would not define a new option in server's URL.
*
* See `ValidateServerURLName()` for more information.
*/
public function ReportBadMutatorNames()
{
local int i;
for (i = 0; i < includeMutator.length; i += 1)
{
if (!ValidateServerURLName(includeMutator[i]))
{
_.logger.Auto(warnBadMutatorName)
.Arg(_.text.FromString(includeMutator[i]))
.Arg(_.text.FromString(string(name)));
}
}
}
/**
* Makes sure that a word to be used in server URL as a part of an option
* does not contain "," / "?" / "=" or whitespace.
* This is useful to make sure that user-specified mutator entries only add
* one mutator or option's key / values will not specify only one pair,
* avoiding "?opt1=value1?opt2=value2" entries.
*/
protected function bool ValidateServerURLName(string entry)
{
if (InStr(entry, "=") >= 0) return false;
if (InStr(entry, "?") >= 0) return false;
if (InStr(entry, ",") >= 0) return false;
if (InStr(entry, " ") >= 0) return false;
return true;
}
// Is every element `subset` present inside `whole`?
private function ValidateFeatureArray(
array<string> subset,
array<string> whole,
string arrayName)
{
local int i, j;
local bool foundItem;
for (i = 0; i < subset.length; i += 1)
{
foundItem = false;
for (j = 0; j < whole.length; j += 1)
{
if (subset[i] ~= whole[j])
{
foundItem = true;
break;
}
}
if (!foundItem)
{
_.logger.Auto(warnBadFeatureName)
.Arg(_.text.FromString(includeMutator[i]))
.Arg(_.text.FromString(string(name)))
.Arg(_.text.FromString(arrayName));
}
}
}
/**
* Updates passed `Feature` settings according to this game mode's settings.
*
* @param featuresToEnable Settings to update.
* `FeatureConfigPair` is a pair of `Feature` (`featureClass`) and its
* config's name (`configName`).
* If `configName` is set to `none`, then corresponding `Feature`
* should not be enabled.
* Otherwise it should be enabled with a specified config.
*/
public function UpdateFeatureArray(
out array<Packages.FeatureConfigPair> featuresToEnable)
{
local int i;
local Text newConfigName;
local string nextFeatureClassName;
for (i = 0; i < featuresToEnable.length; i += 1)
{
nextFeatureClassName = string(featuresToEnable[i].featureClass);
// `excludeFeature`
if (FeatureExcluded(nextFeatureClassName))
{
_.memory.Free(featuresToEnable[i].configName);
featuresToEnable[i].configName = none;
continue;
}
// `includeFeatureAs`
newConfigName = TryReplacingFeatureConfig(nextFeatureClassName);
if (newConfigName != none)
{
_.memory.Free(featuresToEnable[i].configName);
featuresToEnable[i].configName = newConfigName;
}
// `includeFeature`
if ( featuresToEnable[i].configName == none
&& FeatureInIncludedArray(nextFeatureClassName))
{
featuresToEnable[i].configName = P("default").Copy();
}
}
}
private function bool FeatureExcluded(string featureClassName)
{
local int i;
for (i = 0; i < excludeFeature.length; i += 1)
{
if (excludeFeature[i] ~= featureClassName) {
return true;
}
}
return false;
}
private function Text TryReplacingFeatureConfig(string featureClassName)
{
local int i;
for (i = 0; i < includeFeatureAs.length; i += 1)
{
if (includeFeatureAs[i].feature ~= featureClassName) {
return _.text.FromString(includeFeatureAs[i].config);
}
}
return none;
}
private function bool FeatureInIncludedArray(string featureClassName)
{
local int i;
for (i = 0; i < includeFeature.length; i += 1)
{
if (includeFeature[i] ~= featureClassName) {
return true;
}
}
return false;
}
public function array<Text> GetIncludedMutators()
{
local int i;
local array<string> validatedMutators;
for (i = 0; i < includeMutator.length; i += 1)
{
if (ValidateServerURLName(includeMutator[i])) {
validatedMutators[validatedMutators.length] = includeMutator[i];
}
}
return StringToTextArray(validatedMutators);
}
defaultproperties
{
configName = "AcediaGameModes"
warnBadMutatorName = (l=LOG_Warning,m="Mutator \"%1\" specified for game mode \"%2\" contains invalid characters and will be ignored. This is a configuration error, you should fix it.")
warnBadFeatureName = (l=LOG_Warning,m="Feature \"%1\" specified for game mode \"%2\" in array `%3` does not exist in enabled packages and will be ignored. This is a configuration error, you should fix it.")
}

View File

@ -0,0 +1,280 @@
/**
* The only implementation for `BaseGameMode` suitable for standard
* killing floor game types.
* Copyright 2021-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class GameMode extends BaseGameMode
perobjectconfig
config(AcediaGameModes);
struct GameOption
{
var public string key;
var public string value;
};
// Allow to specify additional server options for this game mode
var protected config array<GameOption> option;
// Specify `GameInfo`'s class to use, default is "KFMod.KFGameType"
// (plain string)
var protected config string gameTypeClass;
// Short version of the name of the game mode players will see in
// voting handler messages sometimes (plain string)
var protected config string acronym;
// Map prefix - only maps that start with specified prefix will be voteable for
// this game mode (plain string)
var protected config string mapPrefix;
// Aliases are an unnecessary overkill for difficulty names, so just define
// them in special `string` arrays.
// We accept not just these exact words, but any of their prefixes.
var private const array<string> beginnerSynonyms;
var private const array<string> normalSynonyms;
var private const array<string> hardSynonyms;
var private const array<string> suicidalSynonyms;
var private const array<string> hoeSynonyms;
var private LoggerAPI.Definition warnBadOption, warnDifficultyOption;
protected function DefaultIt()
{
title = "Acedia game mode";
length = "long";
difficulty = "Hell On Earth";
gameTypeClass = "KFMod.KFGameType";
acronym = "";
mapPrefix = "KF";
includeFeature.length = 0;
excludeFeature.length = 0;
includeMutator.length = 0;
option.length = 0;
}
protected function HashTable ToData()
{
local int i;
local ArrayList nextArray;
local HashTable result, nextPair;
result = super.ToData();
if (result == none) {
return none;
}
result.SetString(P("gameTypeClass"), gameTypeClass);
result.SetString(P("acronym"), acronym);
result.SetString(P("mapPrefix"), mapPrefix);
nextArray = _.collections.EmptyArrayList();
for (i = 0; i < option.length; i += 1)
{
nextPair = _.collections.EmptyHashTable();
nextPair.SetString(P("key"), option[i].key);
nextPair.SetString(P("value"), option[i].value);
nextArray.AddItem(nextPair);
_.memory.Free(nextPair);
}
result.SetItem(P("option"), nextArray);
_.memory.Free(nextArray);
return result;
}
protected function FromData(HashTable source)
{
local int i;
local GameOption nextGameOption;
local ArrayList nextArray;
local HashTable nextPair;
super.FromData(source);
if (source == none) {
return;
}
gameTypeClass = source.GetString(P("gameTypeClass"));
acronym = source.GetString(P("acronym"));
mapPrefix = source.GetString(P("mapPrefix"));
nextArray = source.GetArrayList(P("option"));
if (nextArray == none) {
return;
}
option.length = 0;
for (i = 0; i < nextArray.GetLength(); i += 1)
{
nextPair = HashTable(nextArray.GetItem(i));
if (nextPair == none) {
continue;
}
nextGameOption.key = nextPair.GetString(P("key"));
nextGameOption.value = nextPair.GetString(P("value"));
option[option.length] = nextGameOption;
_.memory.Free(nextPair);
}
_.memory.Free(nextArray);
}
public function Text GetGameTypeClass()
{
if (gameTypeClass == "") {
return P("KFMod.KFGameType").Copy();
}
else {
return _.text.FromString(gameTypeClass);
}
}
public function Text GetAcronym()
{
if (acronym == "") {
return _.text.FromString(string(name));
}
else {
return _.text.FromFormattedString(acronym);
}
}
public function Text GetMapPrefix()
{
if (acronym == "") {
return _.text.FromString("KF-");
}
else {
return _.text.FromString(mapPrefix);
}
}
/**
* Checks option-related settings (`option`) for correctness and reports
* any issues.
* Currently correctness check performs a simple validity check for mutator,
* to make sure it would not define a new option in server's URL.
*
* See `ValidateServerURLName()` in `BaseGameMode` for more information.
*/
public function ReportBadOptions()
{
local int i;
for (i = 0; i < option.length; i += 1)
{
if ( !ValidateServerURLName(option[i].key)
|| !ValidateServerURLName(option[i].value))
{
_.logger.Auto(warnBadOption)
.Arg(_.text.FromString(option[i].key))
.Arg(_.text.FromString(option[i].value))
.Arg(_.text.FromString(string(name)));
}
}
}
/**
* @return Server options as key-value pairs in an `HashTable`.
*/
public function HashTable GetOptions()
{
local int i;
local HashTable result;
local Text nextKey, nextValue;
result = _.collections.EmptyHashTable();
for (i = 0; i < option.length; i += 1)
{
if (!ValidateServerURLName(option[i].key)) continue;
if (!ValidateServerURLName(option[i].value)) continue;
if (option[i].key ~= "difficulty")
{
_.logger.Auto(warnDifficultyOption);
continue;
}
nextKey = _.text.FromString(option[i].key);
nextValue = _.text.FromString(option[i].value);
result.SetItem(nextKey, nextValue);
nextKey.FreeSelf();
nextValue.FreeSelf();
}
// Add difficulty option
nextValue = _.text.FromInt(GetNumericDifficulty());
result.SetItem(P("difficulty"), nextValue);
nextValue.FreeSelf();
return result;
}
// Convert `GameMode`'s difficulty's textual representation into
// KF's numeric one.
private final function int GetNumericDifficulty()
{
local int i;
local string lowerCaseDifficulty;
lowerCaseDifficulty = Locs(_.text.IntoString(GetDifficulty()));
for (i = 0; i < default.beginnerSynonyms.length; i += 1)
{
if (IsPrefixOf(lowerCaseDifficulty, default.beginnerSynonyms[i])) {
return 1;
}
}
for (i = 0; i < default.normalSynonyms.length; i += 1)
{
if (IsPrefixOf(lowerCaseDifficulty, default.normalSynonyms[i])) {
return 2;
}
}
for (i = 0; i < default.hardSynonyms.length; i += 1)
{
if (IsPrefixOf(lowerCaseDifficulty, default.hardSynonyms[i])) {
return 4;
}
}
for (i = 0; i < default.suicidalSynonyms.length; i += 1)
{
if (IsPrefixOf(lowerCaseDifficulty, default.suicidalSynonyms[i])) {
return 5;
}
}
for (i = 0; i < default.hoeSynonyms.length; i += 1)
{
if (IsPrefixOf(lowerCaseDifficulty, default.hoeSynonyms[i])) {
return 7;
}
}
return int(lowerCaseDifficulty);
}
protected final static function bool IsPrefixOf(string prefix, string value)
{
return (InStr(value, prefix) == 0);
}
defaultproperties
{
configName = "AcediaGameModes"
beginnerSynonyms(0) = "easy"
beginnerSynonyms(1) = "beginer"
beginnerSynonyms(2) = "beginner"
beginnerSynonyms(3) = "begginer"
beginnerSynonyms(4) = "begginner"
normalSynonyms(0) = "regular"
normalSynonyms(1) = "default"
normalSynonyms(2) = "normal"
hardSynonyms(0) = "harder" // "hard" is prefix of this, so it will count
hardSynonyms(1) = "difficult"
suicidalSynonyms(0) = "suicidal"
hoeSynonyms(0) = "hellonearth"
hoeSynonyms(1) = "hellon earth"
hoeSynonyms(2) = "hell onearth"
hoeSynonyms(3) = "hoe"
warnBadOption = (l=LOG_Warning,m="Option with key \"%1\" and value \"%2\" specified for game mode \"%3\" contains invalid characters and will be ignored. This is a configuration error, you should fix it.")
warnDifficultyOption = (l=LOG_Warning,m="Option with key \"Difficulty\" is specified. This key reserved and will be ignored. Difficulty value should be set through the game mode's \"Difficulty\" setting in \"AcediaGameModes.ini\" config. This is a configuration error, you should fix it.")
}

View File

@ -0,0 +1,274 @@
/**
* 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-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Packages extends Mutator
config(Acedia);
// Acedia's reference to a `Global` object.
var private Global _;
var private ServerGlobal _server;
var private ClientGlobal _client;
// Load Acedia on the client as well?
var private config bool clientside;
// Array of predefined services that must be started along with Acedia mutator.
var private config array<string> package;
// Set to `true` to activate Acedia's game modes system
var private config bool useGameModes;
// Responsible for setting up Acedia's game modes in current voting system
var VotingHandlerAdapter votingAdapter;
var Mutator_OnMutate_Signal onMutateSignal;
var Mutator_OnModifyLogin_Signal onModifyLoginSignal;
var Mutator_OnCheckReplacement_Signal onCheckReplacementSignal;
var private LoggerAPI.Definition infoFeatureEnabled;
var private LoggerAPI.Definition errNoServerLevelCore, errorCannotRunTests;
struct FeatureConfigPair
{
var public class<Feature> featureClass;
var public Text configName;
};
// "Constructor"
simulated function PreBeginPlay()
{
if (level.netMode == NM_DedicatedServer) {
InitializeServer();
}
else {
InitializeClient();
}
}
private simulated function InitializeClient()
{
_ = class'Global'.static.GetInstance();
class'ClientLevelCore'.static.CreateLevelCore(self);
}
private function InitializeServer()
{
local int i;
local LevelCore serverCore;
local GameMode currentGameMode;
local array<FeatureConfigPair> availableFeatures;
if (clientside) {
AddToPackageMap("Acedia");
}
CheckForGarbage();
// Launch and setup core Acedia
_ = class'Global'.static.GetInstance();
_server = class'ServerGlobal'.static.GetInstance();
_client = class'ClientGlobal'.static.GetInstance();
serverCore = class'ServerLevelCore'.static.CreateLevelCore(self);
for (i = 0; i < package.length; i += 1) {
_.environment.RegisterPackage_S(package[i]);
}
if (serverCore != none) {
_server.ConnectServerLevelCore();
}
else
{
_.logger.Auto(errNoServerLevelCore);
return;
}
if (class'TestingService'.default.runTestsOnStartUp) {
RunStartUpTests();
}
SetupMutatorSignals();
// Determine required features and launch them
availableFeatures = GetAutoConfigurationInfo();
if (useGameModes)
{
votingAdapter = VotingHandlerAdapter(
_.memory.Allocate(class'VotingHandlerAdapter'));
currentGameMode = votingAdapter.SetupGameModeAfterTravel();
if (currentGameMode != none) {
currentGameMode.UpdateFeatureArray(availableFeatures);
}
}
EnableFeatures(availableFeatures);
if (votingAdapter != none) {
votingAdapter.InjectIntoVotingHandler();
}
}
// "Finalizer"
function ServerTraveling(string URL, bool bItems)
{
if (votingAdapter != none)
{
votingAdapter.PrepareForServerTravel();
votingAdapter.RestoreVotingHandlerConfigBackup();
_.memory.Free(votingAdapter);
votingAdapter = none;
}
_.environment.ShutDown();
if (nextMutator != none) {
nextMutator.ServerTraveling(URL, bItems);
}
Destroy();
}
// Checks whether Acedia has left garbage after the previous map.
// This can lead to serious problems, so such diagnostic check is warranted.
private function CheckForGarbage()
{
local int leftoverObjectAmount;
local int leftoverActorAmount;
local int leftoverDBRAmount;
local AcediaObject nextObject;
local AcediaActor nextActor;
local DBRecord nextRecord;
foreach AllObjects(class'AcediaObject', nextObject) {
leftoverObjectAmount += 1;
}
foreach AllActors(class'AcediaActor', nextActor) {
leftoverActorAmount += 1;
}
foreach AllObjects(class'DBRecord', nextRecord) {
leftoverDBRAmount += 1;
}
if ( leftoverObjectAmount == 0 && leftoverActorAmount == 0
&& leftoverDBRAmount == 0)
{
Log("Acedia garbage check: nothing was found.");
}
else
{
Log("Acedia garbage check: garbage was found." @
"This can cause problems, report it.");
Log("Leftover object:" @ leftoverObjectAmount);
Log("Leftover actors:" @ leftoverActorAmount);
Log("Leftover database records:" @ leftoverDBRAmount);
}
}
public final function array<FeatureConfigPair> GetAutoConfigurationInfo()
{
local int i;
local array< class<Feature> > availableFeatures;
local FeatureConfigPair nextPair;
local array<FeatureConfigPair> result;
availableFeatures = _.environment.GetAvailableFeatures();
for (i = 0; i < availableFeatures.length; i += 1)
{
nextPair.featureClass = availableFeatures[i];
nextPair.configName = availableFeatures[i].static
.GetAutoEnabledConfig();
result[result.length] = nextPair;
}
return result;
}
private function EnableFeatures(array<FeatureConfigPair> features)
{
local int i;
for (i = 0; i < features.length; i += 1)
{
if (features[i].featureClass == none) continue;
if (features[i].configName == none) continue;
features[i].featureClass.static.EnableMe(features[i].configName);
_.logger.Auto(infoFeatureEnabled)
.Arg(_.text.FromString(string(features[i].featureClass)))
.Arg(features[i].configName); // consumes `configName`
}
}
// Fetches and sets up signals that `Mutator` needs to provide
private function SetupMutatorSignals()
{
local ServerUnrealService service;
service = ServerUnrealService(class'ServerUnrealService'.static.Require());
onMutateSignal = Mutator_OnMutate_Signal(
service.GetSignal(class'Mutator_OnMutate_Signal'));
onModifyLoginSignal = Mutator_OnModifyLogin_Signal(
service.GetSignal(class'Mutator_OnModifyLogin_Signal'));
onCheckReplacementSignal = Mutator_OnCheckReplacement_Signal(
service.GetSignal(class'Mutator_OnCheckReplacement_Signal'));
}
private final function RunStartUpTests()
{
local TestingService testService;
testService = TestingService(class'TestingService'.static.Require());
testService.PrepareTests();
if (testService.filterTestsByName) {
testService.FilterByName(testService.requiredName);
}
if (testService.filterTestsByGroup) {
testService.FilterByGroup(testService.requiredGroup);
}
if (!testService.Run()) {
_.logger.Auto(errorCannotRunTests);
}
}
/**
* Below `Mutator` events are redirected into appropriate signals.
*/
function bool CheckReplacement(Actor other, out byte isSuperRelevant)
{
if (onCheckReplacementSignal != none) {
return onCheckReplacementSignal.Emit(other, isSuperRelevant);
}
return true;
}
function Mutate(string command, PlayerController sendingController)
{
if (onMutateSignal != none) {
onMutateSignal.Emit(command, sendingController);
}
super.Mutate(command, sendingController);
}
function ModifyLogin(out string portal, out string options)
{
if (onModifyLoginSignal != none) {
onModifyLoginSignal.Emit(portal, options);
}
super.ModifyLogin(portal, options);
}
defaultproperties
{
clientside = false
useGameModes = false
// This is a server-only mutator
remoteRole = ROLE_SimulatedProxy
bAlwaysRelevant = true
// Mutator description
GroupName = "Package loader"
FriendlyName = "Acedia loader"
Description = "Launcher for Acedia packages"
infoFeatureEnabled = (l=LOG_Info,m="Feature `%1` enabled with config \"%2\".")
errNoServerLevelCore = (l=LOG_Error,m="Cannot create `ServerLevelCore`!")
errorCannotRunTests = (l=LOG_Error,m="Could not perform Acedia's tests.")
}

View File

@ -0,0 +1,36 @@
/**
* This actor's role is to add Acedia mutator on listen and dedicated servers.
* Copyright 2019-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class StartUp extends Actor;
function PreBeginPlay()
{
super.PreBeginPlay();
if (level != none && level.game != none) {
level.game.AddMutator(string(class'Packages'));
}
Destroy();
}
defaultproperties
{
// This is a server-only actor
remoteRole = ROLE_None
}

View File

@ -0,0 +1,364 @@
/**
* Acedia currently lacks its own means to provide a map/mode voting
* (and new voting mod with proper GUI would not be whitelisted anyway).
* This is why this class was made - to inject existing voting handlers with
* data from Acedia's game modes.
* Requires `GameInfo`'s voting handler to be derived from
* `XVotingHandler`, which is satisfied by pretty much every used handler.
* Copyright 2021-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class VotingHandlerAdapter extends AcediaObject
dependson(VotingHandler)
config(AcediaLauncherData);
/**
* All usage of this object should start with `InjectIntoVotingHandler()`
* method that will read all the `GameMode` configs and fill voting handler's
* config with their data, while making a backup of all values.
* Backup can be restored with `RestoreVotingHandlerConfigBackup()` method.
* How that affects the clients depends on whether restoration was done before,
* during or after the replication. It is intended to be done after
* server travel has started.
* the process of injection is to create an ordered list of game modes
* (`availableGameModes`) and generate appropriate voting handler's configs
* with `BuildVotingHandlerConfig()`, saving them in the same order inside
* the voting handler. Picked game mode is then determined by index of
* the picked voting handler's option.
*
* Additionally this class has a static internal state that allows it to
* transfer data along the server travel - it is used mainly to remember picked
* game mode and enforce game's difficulty by altering and restoring
* `GameInfo`'s variable.
* To make such transfer happen one must call `PrepareForServerTravel()` before
* server travel to set the internal static state and
* then `SetupGameModeAfterTravel()` after travel (when the new map is loading)
* to read (and forget) from internal state.
*/
// All available game modes for Acedia, loaded during initialization.
// This array is directly produces replacement for `XVotingHandler`'s
// `gameConfig` array and records of `availableGameModes` relate to those of
// `gameConfig` with the same index.
// So if we know that a voting option with a certain index was chosen -
// it means that user picked game mode from `availableGameModes` with
// the same index.
var private array<Text> availableGameModes;
// Finding voting handler is not cheap, so only do it once and then store it.
var private NativeActorRef votingHandlerReference;
// Save `VotingHandler`'s config to restore it before server travel -
// otherwise Acedia will alter its config
var private array<VotingHandler.MapVoteGameConfig> backupVotingHandlerConfig;
// Setting value of this flag to `true` indicates that map switching just
// occurred and we need to recover some information from the previous map.
var private config bool isServerTraveling;
// We should not rely on "VotingHandler" to inform us from which game mode its
// selected config option originated after server travel, so we need to
// remember it in this config variable before switching maps.
var private config string targetGameMode;
// Acedia's game modes intend on supporting difficulty switching, but
// `KFGameType` does not support appropriate flags, so we enforce default
// difficulty by overwriting default value of its `gameDifficulty` variable.
// But to not affect game's configs we must restore old value after new map is
// loaded. Store it in config variable for that.
var private config int storedGameLength;
// Aliases are an unnecessary overkill for difficulty names, so just define
// them in special `string` arrays.
// We accept not just these exact words, but any of their prefixes.
var private const array<string> shortSynonyms;
var private const array<string> normalSynonyms;
var private const array<string> longSynonyms;
var private const array<string> customSynonyms;
var private LoggerAPI.Definition fatNoXVotingHandler, fatBadGameConfigIndexVH;
var private LoggerAPI.Definition fatBadGameConfigIndexAdapter;
protected function Finalizer()
{
_.memory.Free(votingHandlerReference);
_.memory.FreeMany(availableGameModes);
votingHandlerReference = none;
availableGameModes.length = 0;
}
/**
* Replaces `XVotingHandler`'s configs with Acedia's game modes.
* Backup of replaced configs is made internally, so that they can be restored
* on map change.
*/
public final function InjectIntoVotingHandler()
{
local int i;
local GameMode nextGameMode;
local XVotingHandler votingHandler;
local array<VotingHandler.MapVoteGameConfig> newVotingHandlerConfig;
if (votingHandlerReference != none) {
return;
}
votingHandler = XVotingHandler(_server.unreal.FindActorInstance(
_server.unreal.GetGameType().VotingHandlerClass));
if (votingHandler == none)
{
_.logger.Auto(fatNoXVotingHandler);
return;
}
votingHandlerReference = _server.unreal.ActorRef(votingHandler);
class'GameMode'.static.Initialize();
availableGameModes = class'GameMode'.static.AvailableConfigs();
for (i = 0; i < availableGameModes.length; i += 1)
{
nextGameMode = GameMode(class'GameMode'.static
.GetConfigInstance(availableGameModes[i]));
newVotingHandlerConfig[i] = BuildVotingHandlerConfig(nextGameMode);
// Report omitted mutators / server options
nextGameMode.ReportBadMutatorNames();
nextGameMode.ReportBadOptions();
}
backupVotingHandlerConfig = votingHandler.gameConfig;
votingHandler.gameConfig = newVotingHandlerConfig;
}
private function VotingHandler.MapVoteGameConfig BuildVotingHandlerConfig(
GameMode gameMode)
{
local MutableText nextColoredName;
local VotingHandler.MapVoteGameConfig result;
result.gameClass = _.text.IntoString(gameMode.GetGameTypeClass());
result.prefix = _.text.IntoString(gameMode.GetMapPrefix());
nextColoredName = gameMode
.GetTitle()
.IntoMutableText()
.ChangeDefaultColor(_.color.white);
result.gameName = _.text.IntoColoredString(nextColoredName)
$ _.color.GetColorTag(_.color.White);
nextColoredName = gameMode
.GetAcronym()
.IntoMutableText()
.ChangeDefaultColor(_.color.white);
result.acronym = _.text.IntoColoredString(nextColoredName)
$ _.color.GetColorTag(_.color.White);
result.mutators = BuildMutatorString(gameMode);
result.options = BuildOptionsString(gameMode);
return result;
}
private function string BuildMutatorString(GameMode gameMode)
{
local int i;
local string result;
local array<Text> usedMutators;
usedMutators = gameMode.GetIncludedMutators();
for (i = 0; i < usedMutators.length; i += 1)
{
if (i > 0) {
result $= ",";
}
result $= _.text.IntoString(usedMutators[i]);
}
return result;
}
private function string BuildOptionsString(GameMode gameMode)
{
local bool optionWasAdded;
local string result;
local string nextKey, nextValue;
local CollectionIterator iter;
local HashTable options;
options = gameMode.GetOptions();
for (iter = options.Iterate(); !iter.HasFinished(); iter.Next())
{
nextKey = _.text.IntoString(Text(iter.GetKey()));
nextValue = _.text.IntoString(Text(iter.Get()));
if (optionWasAdded) {
result $= "?";
}
result $= (nextKey $ "=" $ nextValue);
optionWasAdded = true;
}
options.FreeSelf();
iter.FreeSelf();
return result;
}
/**
* Makes necessary preparations for the server travel.
*/
public final function PrepareForServerTravel()
{
local int pickedVHConfig;
local GameMode nextGameMode;
local string nextGameClassName;
local class<GameInfo> nextGameClass;
local class<KFGameType> nextKFGameType;
local XVotingHandler votingHandler;
if (votingHandlerReference == none) return;
votingHandler = XVotingHandler(votingHandlerReference.Get());
if (votingHandler == none) return;
// Server travel caused by something else than `XVotingHandler`
if (!votingHandler.bLevelSwitchPending) return;
pickedVHConfig = votingHandler.currentGameConfig;
if (pickedVHConfig < 0 || pickedVHConfig >= votingHandler.gameConfig.length)
{
_.logger.Auto(fatBadGameConfigIndexVH)
.ArgInt(pickedVHConfig)
.ArgInt(votingHandler.gameConfig.length);
return;
}
if (pickedVHConfig >= availableGameModes.length)
{
_.logger.Auto(fatBadGameConfigIndexAdapter)
.ArgInt(pickedVHConfig)
.ArgInt(availableGameModes.length);
return;
}
nextGameClassName = votingHandler.gameConfig[pickedVHConfig].gameClass;
if (string(_server.unreal.GetGameType().class) ~= nextGameClassName) {
nextGameClass = _server.unreal.GetGameType().class;
}
else
{
nextGameClass =
class<GameInfo>(_.memory.LoadClass_S(nextGameClassName));
}
isServerTraveling = true;
targetGameMode = availableGameModes[pickedVHConfig].ToString();
nextGameMode = GetConfigFromString(targetGameMode);
nextKFGameType = class<KFGameType>(nextGameClass);
if (nextKFGameType != none)
{
storedGameLength = nextKFGameType.default.kfGameLength;
nextKFGameType.default.kfGameLength =
GetNumericGameLength(nextGameMode);
}
nextGameClass.static.StaticSaveConfig();
SaveConfig();
}
/**
* Restore `GameInfo`'s settings after the server travel and
* apply selected `GameMode`.
*
* @return `GameMode` picked before server travel
* (the one that must be running now).
*/
public final function GameMode SetupGameModeAfterTravel()
{
local KFGameType kfGameType;
if (!isServerTraveling) {
return none;
}
kfGameType = _server.unreal.GetKFGameType();
if (kfGameType != none) {
kfGameType.default.kfGameLength = storedGameLength;
}
isServerTraveling = false;
_server.unreal.GetGameType().StaticSaveConfig();
SaveConfig();
return GetConfigFromString(targetGameMode);
}
/**
* Restores `XVotingHandler`'s config to the values that were overridden by
* `VHAdapter`'s `InjectIntoVotingHandler()` method.
*/
public final function RestoreVotingHandlerConfigBackup()
{
local XVotingHandler votingHandler;
if (votingHandlerReference == none) return;
votingHandler = XVotingHandler(votingHandlerReference.Get());
if (votingHandler == none) return;
votingHandler.gameConfig = backupVotingHandlerConfig;
votingHandler.default.gameConfig = backupVotingHandlerConfig;
votingHandler.SaveConfig();
}
// `GameMode`'s name as a `string` -> `GameMode` instance
private function GameMode GetConfigFromString(string configName)
{
local GameMode result;
local Text nextConfigName;
nextConfigName = _.text.FromString(configName);
result = GameMode(class'GameMode'.static.GetConfigInstance(nextConfigName));
_.memory.Free(nextConfigName);
return result;
}
// Convert `GameMode`'s difficulty's textual representation into
// KF's numeric one.
private final function int GetNumericGameLength(BaseGameMode gameMode)
{
local int i;
local string length;
length = Locs(_.text.IntoString(gameMode.GetLength()));
for (i = 0; i < default.shortSynonyms.length; i += 1)
{
if (IsPrefixOf(length, default.shortSynonyms[i])) {
return 0;
}
}
for (i = 0; i < default.normalSynonyms.length; i += 1)
{
if (IsPrefixOf(length, default.normalSynonyms[i])) {
return 1;
}
}
for (i = 0; i < default.longSynonyms.length; i += 1)
{
if (IsPrefixOf(length, default.longSynonyms[i])) {
return 2;
}
}
for (i = 0; i < default.customSynonyms.length; i += 1)
{
if (IsPrefixOf(length, default.customSynonyms[i])) {
return 3;
}
}
return 3;
}
protected final static function bool IsPrefixOf(string prefix, string value)
{
return (InStr(value, prefix) == 0);
}
defaultproperties
{
shortSynonyms(0) = "short"
normalSynonyms(0) = "normal"
normalSynonyms(1) = "medium"
normalSynonyms(2) = "regular"
longSynonyms(0) = "long"
customSynonyms(0) = "custom"
fatNoXVotingHandler = (l=LOG_Fatal,m="`XVotingHandler` class is missing. Make sure your server setup supports Acedia's game modes (by used voting handler derived from `XVotingHandler`).")
fatBadGameConfigIndexVH = (l=LOG_Fatal,m="`XVotingHandler`'s `currentGameConfig` variable value of %1 is out-of-bounds for `XVotingHandler.gameConfig` of length %2. Report this issue.")
fatBadGameConfigIndexAdapter = (l=LOG_Fatal,m="`XVotingHandler`'s `currentGameConfig` variable value of %1 is out-of-bounds for `VHAdapter` of length %2. Report this issue.")
}

View File

@ -0,0 +1,11 @@
<html>
<head><title>Index of /kf_sources/Acedia/Classes/</title></head>
<body>
<h1>Index of /kf_sources/Acedia/Classes/</h1><hr><pre><a href="../">../</a>
<a href="BaseGameMode.uc">BaseGameMode.uc</a> 08-Aug-2022 11:04 13630
<a href="GameMode.uc">GameMode.uc</a> 08-Aug-2022 11:50 9256
<a href="Packages.uc">Packages.uc</a> 08-Aug-2022 11:38 9297
<a href="StartUp.uc">StartUp.uc</a> 08-Aug-2022 06:17 1196
<a href="VotingHandlerAdapter.uc">VotingHandlerAdapter.uc</a> 11-Aug-2022 18:26 14483
</pre><hr></body>
</html>

View File

@ -0,0 +1,7 @@
<html>
<head><title>Index of /kf_sources/Acedia/</title></head>
<body>
<h1>Index of /kf_sources/Acedia/</h1><hr><pre><a href="../">../</a>
<a href="Classes/">Classes/</a> 29-Jun-2025 19:43 -
</pre><hr></body>
</html>

View File

@ -0,0 +1,163 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandFakers extends Command
dependsOn(VotingModel);
var private array<UserID> fakers;
protected static function StaticFinalizer() {
__().memory.FreeMany(default.fakers);
default.fakers.length = 0;
}
protected function BuildData(CommandDataBuilder builder) {
builder.Group(P("debug"));
builder.Summary(P("Adds fake voters for testing \"vote\" command."));
builder.Describe(P("Displays current fake voters."));
builder.SubCommand(P("amount"));
builder.Describe(P("Specify amount of faker that are allowed to vote."));
builder.ParamInteger(P("fakers_amount"));
builder.SubCommand(P("vote"));
builder.Describe(P("Make a vote as a faker."));
builder.ParamInteger(P("faker_number"));
builder.ParamBoolean(P("vote_for"));
}
protected function Executed(
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
if (arguments.subCommandName.IsEmpty()) {
DisplayCurrentFakers();
} else if (arguments.subCommandName.Compare(P("amount"), SCASE_INSENSITIVE)) {
ChangeAmount(arguments.parameters.GetInt(P("fakers_amount")));
} else if (arguments.subCommandName.Compare(P("vote"), SCASE_INSENSITIVE)) {
CastVote(
arguments.parameters.GetInt(P("faker_number")),
arguments.parameters.GetBool(P("vote_for")));
}
}
public final static function /*borrow*/ array<UserID> BorrowDebugVoters() {
return default.fakers;
}
private final function CastVote(int fakerID, bool voteFor) {
local Voting currentVoting;
if (fakerID < 0 || fakerID >= fakers.length) {
callerConsole
.UseColor(_.color.TextFailure)
.WriteLine(P("Faker number is out of bounds."));
return;
}
currentVoting = _.commands.GetCurrentVoting();
if (currentVoting == none) {
callerConsole
.UseColor(_.color.TextFailure)
.WriteLine(P("There is no voting active right now."));
return;
}
currentVoting.CastVoteByID(fakers[fakerID], voteFor);
_.memory.Free(currentVoting);
}
private final function ChangeAmount(int newAmount) {
local int i;
local Text nextIDName;
local UserID nextID;
local Voting currentVoting;
if (newAmount < 0) {
callerConsole
.UseColor(_.color.TextFailure)
.WriteLine(P("Cannot specify negative amount."));
}
if (newAmount == fakers.length) {
callerConsole
.UseColor(_.color.TextNeutral)
.WriteLine(P("Specified same amount of fakers."));
} else if (newAmount > fakers.length) {
for (i = fakers.length; i < newAmount; i += 1) {
nextIDName = _.text.FromString("DEBUG:FAKER:" $ i);
nextID = UserID(__().memory.Allocate(class'UserID'));
nextID.Initialize(nextIDName);
_.memory.Free(nextIDName);
fakers[fakers.length] = nextID;
}
} else {
for (i = fakers.length - 1; i >= newAmount; i -= 1) {
_.memory.Free(fakers[i]);
}
fakers.length = newAmount;
}
default.fakers = fakers;
currentVoting = _.commands.GetCurrentVoting();
if (currentVoting != none) {
currentVoting.SetDebugVoters(default.fakers);
_.memory.Free(currentVoting);
}
}
private function DisplayCurrentFakers() {
local int i;
local VotingModel.PlayerVoteStatus nextVoteStatus;
local MutableText nextNumber;
local Voting currentVoting;
if (fakers.length <= 0) {
callerConsole.WriteLine(P("No fakers!"));
return;
}
currentVoting =_.commands.GetCurrentVoting();
for (i = 0; i < fakers.length; i += 1) {
nextNumber = _.text.FromIntM(i);
callerConsole
.Write(P("Faker #"))
.Write(nextNumber)
.Write(P(": "));
if (currentVoting != none) {
nextVoteStatus = currentVoting.GetVote(fakers[i]);
}
switch (nextVoteStatus) {
case PVS_NoVote:
callerConsole.WriteLine(P("no vote"));
break;
case PVS_VoteFor:
callerConsole.UseColorOnce(_.color.TextPositive).WriteLine(P("vote for"));
break;
case PVS_VoteAgainst:
callerConsole.UseColorOnce(_.color.TextNegative).WriteLine(P("vote against"));
break;
default:
callerConsole.UseColorOnce(_.color.TextFailure).WriteLine(P("vote !ERROR!"));
}
_.memory.Free(nextNumber);
}
}
defaultproperties {
preferredName = "fakers"
}

View File

@ -0,0 +1,731 @@
/**
* Command for displaying help information about registered Acedia's commands.
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandHelp extends Command
dependson(LoggerAPI)
dependson(CommandAPI);
/**
* # `ACommandHelp`
*
* This built-in command is meant to do two things:
*
* 1. List all available commands and aliases related to them;
* 2. Display help pages for available commands (both by command and
* alias names).
*
* ## Implementation
*
* ### Aliases loading
*
* First thing this command tries to do upon creation is to read all
* command aliases and build a reverse map that allows to access aliases names
* by command + subcommand (even if latter one is empty) that these names
* refer to. This allows us to display relevant aliases names alongside
* command names. Map is stored inside `commandToAliasesMap` variable.
* (If aliases feature isn't yet available, `ACommandHelp` connects to
* `OnFeatureEnabled()` signal to do its job the moment required feature is
* enabled).
* This map is also made to be two-level one - it is...
*
* * a `HashTable` with command names as keys,
* * that points to `HashTable`s with subcommand names as keys,
* * that points at `ArrayList` of aliases.
*
* This allows us to also sort aliases by the subcommand name when displaying
* their list. This work is done by `FillCommandToAliasesMap()` method (that
* uses `ParseCommandNames()` method, which is also used for displaying
* command list).
*
* ### Command list displaying
*
* This is a simple process performed by `DisplayCommandLists()` that calls on
* a bunch of auxiliary `Print...()` methods.
*
* ### Displaying help pages
*
* Similar to the above, this is mostly a simple process that displays
* `Command`s' `CommandData` as a help page. Work is performed by
* `DisplayCommandHelpPages()` method that calls on a bunch of auxiliary
* `Print...()` methods, most notably `PrintHelpPageFor()` that can display
* help pages both for full commands, as well as only for a singular
* subcommand, which allows us to print proper help pages for command aliases
* that refer to a particular subcommand.
*/
// For each key (given by lower case command name) stores another `HashMap`
// that uses sub-command names as keys and returns `ArrayList` of aliases.
var private HashTable commandToAliasesMap;
var private User callerUser;
var public const int TSPACE, TCOMMAND_NAME_FALLBACK, TPLUS;
var public const int TOPEN_BRACKET, TCLOSE_BRACKET, TCOLON_SPACE;
var public const int TKEY, TDOUBLE_KEY, TCOMMA_SPACE, TBOOLEAN, TINDENT;
var public const int TBOOLEAN_TRUE_FALSE, TBOOLEAN_ENABLE_DISABLE;
var public const int TBOOLEAN_ON_OFF, TBOOLEAN_YES_NO;
var public const int TOPTIONS, TCMD_WITH_TARGET, TCMD_WITHOUT_TARGET;
var public const int TSEPARATOR, TLIST_REGIRESTED_CMDS, TEMPTY_GROUP;
var public const int TALIASES_FOR, TEMPTY, TDOT, TNO_COMMAND_BEGIN;
var public const int TNO_COMMAND_END, TEMPTY_GROUP_BEGIN, TEMPTY_GROUP_END;
protected function Constructor()
{
local Feature preenabledInstance;
super.Constructor();
// We need to update aliases map every time aliases feature is reenabled
_.environment.OnFeatureEnabled(self).connect = FillCommandToAliasesMap;
// If `Aliases_Feature` is already enabled - read its command aliases now
preenabledInstance = class'Aliases_Feature'.static.GetEnabledInstance();
FillCommandToAliasesMap(Aliases_Feature(preenabledInstance));
_.memory.Free(preenabledInstance);
}
protected function Finalizer()
{
super.Finalizer();
_.memory.Free(commandToAliasesMap);
commandToAliasesMap = none;
}
protected function BuildData(CommandDataBuilder builder)
{
builder.Group(P("core"));
builder.Summary(P("Displays detailed information about available commands."));
builder.OptionalParams();
builder.ParamTextList(P("commands"));
builder.Option(P("aliases"));
builder.Describe(P("When displaying available commands, specifying this flag will additionally"
@ "make command to display all of their available aliases."));
builder.Option(P("list"));
builder.Describe(P("Display available commands. Optionally command groups can be specified and"
@ "then only commands from such groups will be listed. Otherwise all commands will"
@ "be displayed."));
builder.OptionalParams();
builder.ParamTextList(P("groups"));
}
protected function Executed(
Command.CallData callData,
EPlayer callerPlayer,
CommandPermissions permissions
) {
local bool printedSomething;
local HashTable parameters, options;
local ArrayList commandsToDisplay, commandGroupsToDisplay;
callerUser = callerPlayer.GetIdentity();
parameters = callData.parameters;
options = callData.options;
// Print command list if "--list" option was specified
if (options.HasKey(P("list")))
{
commandGroupsToDisplay = options.GetArrayListBy(P("/list/groups"));
DisplayCommandLists(
commandGroupsToDisplay,
options.HasKey(P("aliases")));
_.memory.Free(commandGroupsToDisplay);
printedSomething = true;
}
// Help pages.
// Only need to print them if:
// 1. Any commands are specified as parameters;
// 2. No commands or "--list" option was specified, then we want to
// print a help page for this command.
if (!options.HasKey(P("list")) || parameters.HasKey(P("commands")))
{
commandsToDisplay = parameters.GetArrayList(P("commands"));
DisplayCommandHelpPages(commandsToDisplay, printedSomething);
_.memory.Free(commandsToDisplay);
}
_.memory.Free(callerUser);
callerUser = none;
}
// If instance of the `Aliases_Feature` is passed as an argument (allowing this
// method to be used as a slot for `OnFeatureEnabled` signal) and
// `commandAliasSource` is available - empties current `commandToAliasesMap`
// and refills it with available command aliases.
private final function FillCommandToAliasesMap(Feature enabledFeature)
{
local int i;
local Text aliasValue;
local array<Text> availableAliases;
local BaseAliasSource commandAliasSource;
local MutableText commandName, subcommandName;
if (enabledFeature == none) return;
if (enabledFeature.class != class'Aliases_Feature') return;
commandAliasSource = _.alias.GetCommandSource();
if (commandAliasSource == none) return;
// Drop map built by previous aliases (if it was even build up until now)
_.memory.Free(commandToAliasesMap);
commandToAliasesMap = _.collections.EmptyHashTable();
// Construct a command -> alias map by iterating over aliases
availableAliases = commandAliasSource.GetAllAliases();
for (i = 0; i < availableAliases.length; i += 1)
{
aliasValue = _.alias.ResolveCommand(availableAliases[i]);
ParseCommandNames(aliasValue, commandName, subcommandName);
aliasValue.FreeSelf();
InsertIntoAliasesMap(commandName, subcommandName, availableAliases[i]);
commandName.FreeSelf();
subcommandName.FreeSelf();
commandName = none;
subcommandName = none;
}
// Clean up
_.memory.FreeMany(availableAliases);
commandAliasSource.FreeSelf();
}
// Breaks command name as it is intended to be specified in command aliases
// ("<command>" or "<command>.<subcommand>") into command and subcommand,
// returning both as `out` parameters.
private final function ParseCommandNames(
BaseText source,
out MutableText commandName,
out MutableText subcommandName)
{
local Parser valueParser;
if (source == none) {
return;
}
if (subcommandName != none) {
subcommandName.FreeSelf();
}
valueParser = source.Parse();
subcommandName = valueParser
.MUntil(commandName, T(TDOT).GetCharacter(0))
.Match(T(TDOT))
.GetRemainderM();
_.memory.Free(valueParser);
}
// Assumes that `commandToAliasesMap` and its arguments are not `none`.
private final function InsertIntoAliasesMap(
BaseText commandName,
BaseText subcommandName,
BaseText alias)
{
local Text commandKey, subcommandKey;
local HashTable subcommandToAliasesMap;
local ArrayList aliasesArray;
commandKey = commandName.LowerCopy();
subcommandKey = subcommandName.LowerCopy();
subcommandToAliasesMap = commandToAliasesMap.GetHashTable(commandKey);
if (subcommandToAliasesMap == none) {
subcommandToAliasesMap = _.collections.EmptyHashTable();
}
commandToAliasesMap.SetItem(commandKey, subcommandToAliasesMap);
aliasesArray = subcommandToAliasesMap.GetArrayList(subcommandKey);
if (aliasesArray == none) {
aliasesArray = _.collections.EmptyArrayList();
}
subcommandToAliasesMap.SetItem(subcommandKey, aliasesArray);
if (aliasesArray.Find(alias) < 0) {
aliasesArray.AddItem(alias);
}
// Cleaning up local references to collections and keys
subcommandToAliasesMap.FreeSelf();
aliasesArray.FreeSelf();
commandKey.FreeSelf();
subcommandKey.FreeSelf();
}
private final function DisplayCommandLists(
ArrayList commandGroupsToDisplay,
bool displayAliases)
{
local int i;
local array<Text> commandNames, groupsNames;
if (commandGroupsToDisplay == none) {
groupsNames = _.commands.GetGroupsNames();
}
else
{
for (i = 0; i < commandGroupsToDisplay.GetLength(); i += 1) {
groupsNames[groupsNames.length] = commandGroupsToDisplay.GetText(i);
}
}
callerConsole.WriteLine(T(TLIST_REGIRESTED_CMDS));
for (i = 0; i < groupsNames.length; i += 1)
{
if (groupsNames[i] == none) {
continue;
}
commandNames = _.commands.GetCommandNamesInGroup(groupsNames[i]);
if (commandNames.length > 0)
{
callerConsole.UseColorOnce(_.color.TextSubHeader);
if (groupsNames[i].IsEmpty()) {
callerConsole.WriteLine(T(TEMPTY_GROUP));
}
else {
callerConsole.WriteLine(groupsNames[i]);
}
PrintCommandsNamesArray(
commandNames,
displayAliases);
_.memory.FreeMany(commandNames);
} else {
callerConsole.UseColor(_.color.TextFailure);
callerConsole.Write(T(TEMPTY_GROUP_BEGIN));
callerConsole.Write(groupsNames[i]);
callerConsole.WriteLine(T(TEMPTY_GROUP_END));
callerConsole.ResetColor();
}
}
_.memory.FreeMany(groupsNames);
}
private final function PrintCommandsNamesArray(
array<Text> commandsNamesArray,
bool displayAliases
) {
local int i;
local Command.Data nextData;
local CommandAPI.CommandConfigInfo nextCommandPair;
for (i = 0; i < commandsNamesArray.length; i += 1)
{
nextCommandPair = _.commands.ResolveCommandForUser(
commandsNamesArray[i],
callerUser);
if (nextCommandPair.instance != none && !nextCommandPair.usageForbidden) {
nextData = nextCommandPair.instance.BorrowData();
callerConsole
.UseColorOnce(_.color.textEmphasis)
.Write(commandsNamesArray[i])
.Write(T(TCOLON_SPACE))
.WriteLine(nextData.summary);
if (displayAliases) {
PrintCommandAliases(commandsNamesArray[i]);
}
}
_.memory.Free(nextCommandPair.instance);
}
}
private final function PrintCommandAliases(BaseText commandName)
{
local CollectionIterator iter;
local Text commandKey;
local Text nextSubCommand;
local ArrayList nextAliasesArray;
local HashTable subCommandToAliasesMap;
if (commandName == none) return;
if (commandToAliasesMap == none) return;
commandKey = commandName.LowerCopy();
subCommandToAliasesMap = commandToAliasesMap.GetHashTable(commandName);
commandKey.FreeSelf();
if (subCommandToAliasesMap == none) {
return;
}
// Display aliases to command itself first
nextAliasesArray = subCommandToAliasesMap.GetArrayList(T(TEMPTY));
PrintAliasesArray(commandName, none, nextAliasesArray);
_.memory.Free(nextAliasesArray);
// Then aliases to all of its subcommands, in no particular order
iter = subCommandToAliasesMap.Iterate();
while (!iter.HasFinished())
{
nextSubCommand = Text(iter.Getkey()); // cannot be `none`
if (nextSubCommand.IsEmpty())
{
nextSubCommand.FreeSelf();
iter.Next();
continue;
}
nextAliasesArray = ArrayList(iter.Get());
PrintAliasesArray(commandName, nextSubCommand, nextAliasesArray);
_.memory.Free(nextAliasesArray);
_.memory.Free(nextSubCommand);
iter.Next();
}
iter.FreeSelf();
}
private final function PrintAliasesArray(
BaseText commandName,
BaseText subcommandName,
ArrayList aliasesArray)
{
local int i;
local Text nextAlias;
if (commandName == none) return;
if (aliasesArray == none) return;
callerConsole
.Write(T(TALIASES_FOR))
.Write(T(TSPACE))
.UseColorOnce(_.color.TextEmphasis)
.Write(commandName);
if (subcommandName != none)
{
callerConsole
.Write(T(TSPACE))
.UseColorOnce(_.color.TextEmphasis)
.Write(subCommandName);
}
callerConsole.Write(T(TCOLON_SPACE));
for (i = 0; i < aliasesArray.GetLength(); i += 1)
{
if (i > 0) {
callerConsole.Write(T(TCOMMA_SPACE));
}
nextAlias = aliasesArray.GetText(i);
callerConsole.UseColorOnce(_.color.TextNeutral).Write(nextAlias);
nextAlias.FreeSelf();
}
callerConsole.WriteBlock();
}
private final function DisplayCommandHelpPages(ArrayList commandList, bool printedSomething) {
local int i;
local Text nextUserProvidedName;
local MutableText referredSubcommand;
local CommandAPI.CommandConfigInfo nextPair;
// If arguments were empty - at least display our own help page
if (commandList == none) {
nextPair.instance = self;
PrintHelpPageFor(usedName, none, nextPair);
return;
}
// Otherwise - print help for specified commands
for (i = 0; i < commandList.GetLength(); i += 1) {
nextUserProvidedName = commandList.GetText(i);
nextPair = GetCommandFromUserProvidedName(
nextUserProvidedName,
/*out*/ referredSubcommand);
if (nextPair.instance != none && !nextPair.usageForbidden) {
if (printedSomething) {
callerConsole.WriteLine(T(TSEPARATOR));
}
PrintHelpPageFor(
nextUserProvidedName,
referredSubcommand,
nextPair);
printedSomething = true;
} else if (nextPair.instance != none) {
callerConsole.UseColor(_.color.TextFailure);
callerConsole.Write(T(TNO_COMMAND_BEGIN));
callerConsole.Write(nextUserProvidedName);
callerConsole.WriteLine(T(TNO_COMMAND_END));
callerConsole.ResetColor();
}
_.memory.Free(nextPair.instance);
_.memory.Free(nextUserProvidedName);
_.memory.Free(referredSubcommand);
// `referredSubcommand` is passed as an `out` parameter on
// every iteration, so we need to prevent the possibility of its value
// being used.
// NOTE: `nextCommand` and `nextUserProvidedName` are just rewritten.
referredSubcommand = none;
}
}
// Returns `Command` based on the name, given by user.
// `referredSubcommand` is always overwritten (freed if non-`none` value
// is passed) and is used to return name of the subcommand for returned
// `Command` that is specified by `nextUserProvidedName` (only relevant for
// aliases that refer to a particular subcommand).
private final function CommandAPI.CommandConfigInfo GetCommandFromUserProvidedName(
BaseText nextUserProvidedName,
out MutableText referredSubcommand)
{
local CommandAPI.CommandConfigInfo result;
local Text commandAliasValue;
local MutableText parsedCommandName;
// Clear `out` parameter no matter what
if (referredSubcommand != none) {
referredSubcommand.FreeSelf();
referredSubcommand = none;
}
// Try getting command using `nextUserProvidedName` as a literal name
result = _.commands.ResolveCommandForUser(nextUserProvidedName, callerUser);
if (result.instance != none) {
return result;
}
// On failure - try resolving it as an alias
commandAliasValue = _.alias.ResolveCommand(nextUserProvidedName);
ParseCommandNames(commandAliasValue, parsedCommandName, referredSubcommand);
result = _.commands.ResolveCommandForUser(parsedCommandName, callerUser);
if ( result.instance == none
|| !result.instance.IsSubCommandAllowed(referredSubcommand, result.config)) {
_.memory.Free(result.instance);
return result;
}
// Empty subcommand name from the alias is essentially no subcommand name
if (referredSubcommand != none && referredSubcommand.IsEmpty()) {
referredSubcommand.FreeSelf();
referredSubcommand = none;
}
_.memory.Free2(commandAliasValue, parsedCommandName);
return result;
}
private final function PrintHelpPageFor(
BaseText commandAlias,
BaseText referredSubcommand,
CommandAPI.CommandConfigInfo commandPair
) {
local Text commandNameLowerCase, commandNameUpperCase;
// Get capitalized command name
commandNameUpperCase = commandAlias.UpperCopy();
// Print header: name + basic info
callerConsole.UseColor(_.color.textHeader)
.Write(commandNameUpperCase)
.UseColor(_.color.textDefault);
commandNameUpperCase.FreeSelf();
if (commandPair.instance.BorrowData().requiresTarget) {
callerConsole.WriteLine(T(TCMD_WITH_TARGET));
}
else {
callerConsole.WriteLine(T(TCMD_WITHOUT_TARGET));
}
// Print commands and options
commandNameLowerCase = commandAlias.LowerCopy();
PrintCommands(commandPair, commandNameLowerCase, referredSubcommand);
commandNameLowerCase.FreeSelf();
PrintOptions(commandPair.instance.BorrowData());
// Clean up
callerConsole.ResetColor().Flush();
}
private final function PrintCommands(
CommandAPI.CommandConfigInfo commandPair,
BaseText commandName,
BaseText referredSubcommand
) {
local int i;
local array<Command.SubCommand> subCommands;
subCommands = commandPair.instance.BorrowData().subCommands;
for (i = 0; i < subCommands.length; i += 1) {
if (referredSubcommand == none || referredSubcommand.Compare(subCommands[i].name)) {
if (commandPair.instance.IsSubCommandAllowed(subCommands[i].name, commandPair.config)) {
PrintSubCommand(subCommands[i], commandName, referredSubcommand != none);
}
}
}
}
private final function PrintSubCommand(
SubCommand subCommand,
BaseText commandName,
bool skipSubcommandName)
{
// Command + parameters
// Command name + sub command name
callerConsole.UseColor(_.color.textEmphasis)
.Write(commandName)
.Write(T(TSPACE));
if ( !skipSubcommandName
&& subCommand.name != none
&& !subCommand.name.IsEmpty())
{
callerConsole.Write(subCommand.name).Write(T(TSPACE));
}
callerConsole.UseColor(_.color.textDefault);
// Parameters
PrintParameters(subCommand.required, subCommand.optional);
callerConsole.Flush();
// Description
if (subCommand.description != none && !subCommand.description.IsEmpty()) {
callerConsole.WriteBlock(subCommand.description);
}
}
private final function PrintOptions(Command.Data data)
{
local int i;
local array<Option> options;
options = data.options;
if (options.length <= 0) {
return;
}
callerConsole
.UseColor(_.color.textSubHeader)
.WriteLine(T(TOPTIONS))
.UseColor(_.color.textDefault);
for (i = 0; i < options.length; i += 1) {
PrintOption(options[i]);
}
}
private final function PrintOption(Option option)
{
local Text shortNameAsText;
// Option short and long names with added key characters
shortNameAsText = _.text.FromCharacter(option.shortName);
callerConsole
.UseColor(_.color.textEmphasis)
.Write(T(TKEY)).Write(shortNameAsText) // "-"
.UseColor(_.color.textDefault)
.Write(T(TCOMMA_SPACE)) // ", "
.UseColor(_.color.textEmphasis)
.Write(T(TDOUBLE_KEY)).Write(option.longName) // "--"
.UseColor(_.color.textDefault);
shortNameAsText.FreeSelf();
// Parameters
if (option.required.length != 0 || option.optional.length != 0)
{
callerConsole.Write(T(TSPACE));
PrintParameters(option.required, option.optional);
}
callerConsole.Flush();
// Description
if (option.description != none && !option.description.IsEmpty()) {
callerConsole.WriteBlock(option.description);
}
}
private final function PrintParameters(
array<Parameter> required,
array<Parameter> optional)
{
local int i;
// Print required
for (i = 0; i < required.length; i += 1)
{
PrintParameter(required[i]);
if (i < required.length - 1) {
callerConsole.Write(T(TSPACE));
}
}
if (optional.length <= 0) {
return;
}
// Print optional
callerConsole.Write(T(TSPACE)).Write(T(TOPEN_BRACKET));
for (i = 0; i < optional.length; i += 1)
{
PrintParameter(optional[i]);
if (i < optional.length - 1) {
callerConsole.Write(T(TSPACE));
}
}
callerConsole.Write(T(TCLOSE_BRACKET));
}
private final function PrintParameter(Parameter parameter)
{
switch (parameter.type)
{
case CPT_Boolean:
callerConsole.UseColor(_.color.typeBoolean);
break;
case CPT_Integer:
callerConsole.UseColor(_.color.typeNumber);
break;
case CPT_Number:
callerConsole.UseColor(_.color.typeNumber);
break;
case CPT_Text:
case CPT_Remainder:
callerConsole.UseColor(_.color.typeString);
break;
case CPT_Object:
case CPT_JSON:
case CPT_Array:
case CPT_PLAYERS:
callerConsole.UseColor(_.color.typeLiteral);
break;
default:
callerConsole.UseColor(_.color.textDefault);
}
callerConsole.Write(parameter.displayName);
if (parameter.allowsList) {
callerConsole.Write(T(TPLUS));
}
callerConsole.UseColor(_.color.textDefault);
}
defaultproperties
{
TSPACE = 0
stringConstants(0) = " "
TPLUS = 1
stringConstants(1) = "(+)"
TOPEN_BRACKET = 2
stringConstants(2) = "["
TCLOSE_BRACKET = 3
stringConstants(3) = "]"
TKEY = 4
stringConstants(4) = "-"
TDOUBLE_KEY = 5
stringConstants(5) = "--"
TCOMMA_SPACE = 6
stringConstants(6) = ", "
TCOLON_SPACE = 7
stringConstants(7) = ": "
TINDENT = 8
stringConstants(8) = " "
TBOOLEAN = 9
stringConstants(9) = "boolean"
TBOOLEAN_TRUE_FALSE = 10
stringConstants(10) = "true/false"
TBOOLEAN_ENABLE_DISABLE = 11
stringConstants(11) = "enable/disable"
TBOOLEAN_ON_OFF = 12
stringConstants(12) = "on/off"
TBOOLEAN_YES_NO = 13
stringConstants(13) = "yes/no"
TCMD_WITH_TARGET = 14
stringConstants(14) = ": This command requires target to be specified."
TCMD_WITHOUT_TARGET = 15
stringConstants(15) = ": This command does not require target to be specified."
TOPTIONS = 16
stringConstants(16) = "OPTIONS"
TSEPARATOR = 17
stringConstants(17) = "============================="
TLIST_REGIRESTED_CMDS = 18
stringConstants(18) = "{$TextHeader List of registered commands}"
TEMPTY_GROUP = 19
stringConstants(19) = "Empty group"
TALIASES_FOR = 20
stringConstants(20) = "Aliases for"
TEMPTY = 21
stringConstants(21) = ""
TDOT = 22
stringConstants(22) = "."
TNO_COMMAND_BEGIN = 23
stringConstants(23) = "Command `"
TNO_COMMAND_END = 24
stringConstants(24) = "` not found!"
TEMPTY_GROUP_BEGIN = 25
stringConstants(25) = "No commands in group \""
TEMPTY_GROUP_END = 26
stringConstants(26) = "\"!"
preferredName = "help"
}

View File

@ -0,0 +1,69 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandNotify extends Command
dependsOn(ChatApi);
protected function BuildData(CommandDataBuilder builder) {
builder.Group(P("core"));
builder.Summary(P("Notifies players with provided message."));
builder.ParamText(P("message"));
builder.OptionalParams();
builder.ParamNumber(P("duration"));
builder.Describe(P("Notify to players message with distinct header and body."));
builder.RequireTarget();
builder.Option(P("title"));
builder.Describe(P("Specify the optional title of the notification."));
builder.ParamText(P("title"));
builder.Option(P("channel"));
builder.Describe(P("Specify the optional channel. A channel is a grouping mechanism used to"
@ "control the display of related notifications. Only last message from the same channel is"
@ "stored in queue."));
builder.ParamText(P("channel_name"));
}
protected function ExecutedFor(
EPlayer target,
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local Text title, message, plainTitle, plainMessage;
plainMessage = arguments.parameters.GetText(P("message"));
if (arguments.options.HasKey(P("title"))) {
plainTitle = arguments.options.GetTextBy(P("/title/title"));
}
title = _.text.FromFormatted(plainTitle);
message = _.text.FromFormatted(plainMessage);
target.Notify(
title,
message,
arguments.parameters.GetFloat(P("duration")),
arguments.options.GetTextBy(P("/channel/channel_name")));
_.memory.Free4(title, message, plainTitle, plainMessage);
}
defaultproperties {
preferredName = "notify"
}

View File

@ -0,0 +1,197 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandSideEffects extends Command;
// Maps `UserID` to `ArrayList` with side effects listed for that player last time
var private HashTable displayedLists;
protected function Constructor() {
super.Constructor();
displayedLists = _.collections.EmptyHashTable();
}
protected function Finalizer() {
super.Finalizer();
_.memory.Free(displayedLists);
displayedLists = none;
}
protected function BuildData(CommandDataBuilder builder) {
builder.Group(P("debug"));
builder.Summary(P("Displays information about current side effects."));
builder.Describe(P("This command allows to display current side effects, optionally filtering"
@ "them by specified package names."));
builder.OptionalParams();
builder.ParamTextList(P("package_names"));
builder.SubCommand(P("show"));
builder.Describe(P("This sub-command is only usable after side effects have been shown"
@ "at least once. It takes an index from the last displayed list and displays a verbose"
@ "information about it."));
builder.ParamInteger(P("side_effect_number"));
builder.Option(P("verbose"));
builder.Describe(P("Display verbose information about each side effect."));
}
protected function Executed(
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local UserID playerID;
local array<SideEffect> relevantSideEffects;
local ArrayList packagesList, storedSideEffectsList;
playerID = instigator.GetUserID();
if (arguments.subCommandName.IsEmpty()) {
relevantSideEffects = _.sideEffects.GetAll();
packagesList = arguments.parameters.GetArrayList(P("package_names"));
FilterSideEffects(/*out*/ relevantSideEffects, packagesList);
_.memory.Free(packagesList);
DisplaySideEffects(relevantSideEffects, arguments.options.HasKey(P("verbose")));
// Store new side effect list
storedSideEffectsList = _.collections.NewArrayList(relevantSideEffects);
displayedLists.SetItem(playerID, storedSideEffectsList);
_.memory.FreeMany(relevantSideEffects);
_.memory.Free(storedSideEffectsList);
} else {
ShowInfoFor(playerID, arguments.parameters.GetInt(P("side_effect_number")));
}
_.memory.Free(playerID);
}
private function FilterSideEffects(out array<SideEffect> sideEffects, ArrayList allowedPackages) {
local int i, j;
local int packagesLength;
local bool matchedPackage;
local Text nextSideEffectPackage, nextAllowedPackage;
if (allowedPackages == none) return;
if (allowedPackages.GetLength() <= 0) return;
packagesLength = allowedPackages.GetLength();
while (i < sideEffects.length) {
nextSideEffectPackage = sideEffects[i].GetPackage();
matchedPackage = false;
for (j = 0; j < packagesLength; j += 1) {
nextAllowedPackage = allowedPackages.GetText(j);
if (nextAllowedPackage.Compare(nextSideEffectPackage, SCASE_INSENSITIVE)) {
matchedPackage = true;
_.memory.Free(nextAllowedPackage);
break;
}
_.memory.Free(nextAllowedPackage);
}
if (!matchedPackage) {
sideEffects.Remove(i, 1);
} else {
i += 1;
}
_.memory.Free(nextSideEffectPackage);
}
}
private function DisplaySideEffects(array<SideEffect> toDisplay, bool verbose) {
local int i;
local MutableText nextPrefix;
if (toDisplay.length <= 0) {
callerConsole.Write(F("List of side effects is {$TextNeutral empty}."));
}
for (i = 0; i < toDisplay.length; i += 1) {
nextPrefix = _.text.FromIntM(i + 1);
nextPrefix.Append(P("."));
DisplaySideEffect(toDisplay[i], nextPrefix, verbose);
_.memory.Free(nextPrefix);
}
}
private function DisplaySideEffect(SideEffect toDisplay, BaseText prefix, bool verbose) {
local Text effectName, effectDescription, effectPackage, effectSource, effectStatus;
if (toDisplay == none) {
return;
}
if (prefix != none) {
callerConsole.Write(prefix);
callerConsole.Write(P(" "));
}
effectName = toDisplay.GetName();
effectPackage = toDisplay.GetPackage();
effectSource = toDisplay.GetSource();
effectStatus = toDisplay.GetStatus();
callerConsole.UseColor(_.color.TextEmphasis);
callerConsole.Write(P("["));
callerConsole.Write(effectPackage);
callerConsole.Write(P(" \\ "));
callerConsole.Write(effectSource);
callerConsole.Write(P("] "));
callerConsole.ResetColor();
callerConsole.Write(effectName);
callerConsole.Write(P(" {"));
callerConsole.Write(effectStatus);
callerConsole.WriteLine(P("}"));
if (verbose) {
effectDescription = toDisplay.GetDescription();
callerConsole.WriteBlock(effectDescription);
}
_.memory.Free5(effectName, effectDescription, effectPackage, effectSource, effectStatus);
}
private function ShowInfoFor(UserID playerID, int sideEffectIndex) {
local SideEffect toDisplay;
local ArrayList sideEffectList;
if (playerID == none) {
return;
}
if (sideEffectIndex <= 0) {
callerConsole.WriteLine(F("Specified side effect index {$TextNegative isn't positive}!"));
return;
}
sideEffectList = displayedLists.GetArrayList(playerID);
if (sideEffectList == none) {
callerConsole.WriteLine(F("{$TextNegative Cannot display} side effect by index without"
@ "first listing them. Call {$TextEmphasis sideeffects} command without"
@ "{$TextEmphasis show} subcommand first."));
return;
}
if (sideEffectIndex > sideEffectList.GetLength()) {
callerConsole.WriteLine(F("Specified side effect index is {$TextNegative out of bounds}."));
_.memory.Free(sideEffectList);
return;
}
// Above we checked that `sideEffectIndex` lies within `[0; sideEffectList.GetLength()]` segment
// This means that `sideEffectIndex - 1` points at non-`none` value
toDisplay = SideEffect(sideEffectList.GetItem(sideEffectIndex - 1));
if (!_.sideEffects.IsRegistered(toDisplay)) {
callerConsole.UseColorOnce(_.color.TextWarning);
callerConsole.WriteLine(P("Selected side effect is no longer active!"));
}
DisplaySideEffect(toDisplay, none, true);
_.memory.Free2(toDisplay, sideEffectList);
}
defaultproperties {
preferredName = "sideeffects"
}

View File

@ -0,0 +1,220 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ACommandVote extends Command
dependson(CommandAPI)
dependson(VotingModel);
var private CommandDataBuilder dataBuilder;
protected function Constructor() {
ResetVotingInfo();
_.commands.OnVotingAdded(self).connect = AddVotingInfo;
_.commands.OnVotingRemoved(self).connect = HandleRemovedVoting;
_.chat.OnVoiceMessage(self).connect = VoteWithVoice;
}
protected function Finalizer() {
super.Finalizer();
_.memory.Free(dataBuilder);
dataBuilder = none;
_.commands.OnVotingAdded(self).Disconnect();
_.commands.OnVotingRemoved(self).Disconnect();
_.chat.OnVoiceMessage(self).Disconnect();
}
protected function BuildData(CommandDataBuilder builder) {
builder.Group(P("core"));
builder.Summary(P("Allows players to initiate any available voting."
@ "Voting options themselves are specified as sub-commands."));
builder.Describe(P("Default command simply displaces information about current vote."));
dataBuilder.SubCommand(P("yes"));
builder.Describe(P("Vote `yes` on the current vote."));
dataBuilder.SubCommand(P("no"));
builder.Describe(P("Vote `no` on the current vote."));
builder.Option(P("force"));
builder.Describe(P("Tries to force voting to end immediately with the desired result."));
}
protected function Executed(
CallData arguments,
EPlayer instigator,
CommandPermissions permissions
) {
local bool forcingVoting;
local VotingModel.ForceEndingType forceType;
local Voting currentVoting;
forcingVoting = arguments.options.HasKey(P("force"));
currentVoting = _.commands.GetCurrentVoting();
if (arguments.subCommandName.IsEmpty()) {
DisplayInfoAboutVoting(instigator, currentVoting);
} else if (arguments.subCommandName.Compare(P("yes"), SCASE_INSENSITIVE)) {
CastVote(currentVoting, instigator, true);
forceType = FET_Success;
} else if (arguments.subCommandName.Compare(P("no"), SCASE_INSENSITIVE)) {
CastVote(currentVoting, instigator, false);
forceType = FET_Failure;
} else if (StartVoting(arguments, currentVoting, instigator)) {
_.memory.Free(currentVoting);
currentVoting = _.commands.GetCurrentVoting();
forceType = FET_Success;
} else {
forcingVoting = false;
}
if (currentVoting != none && !currentVoting.HasEnded() && forcingVoting) {
if (currentVoting.ForceEnding(instigator, forceType) == FEO_Forbidden) {
callerConsole
.WriteLine(F("You {$TextNegative aren't allowed} to forcibly end current voting"));
}
}
_.memory.Free(currentVoting);
}
private final function VoteWithVoice(EPlayer sender, ChatApi.BuiltInVoiceMessage message) {
local Voting currentVoting;
currentVoting = _.commands.GetCurrentVoting();
if (message == BIVM_AckYes) {
CastVote(currentVoting, sender, true);
}
if (message == BIVM_AckNo) {
CastVote(currentVoting, sender, false);
}
_.memory.Free(currentVoting);
}
/// Adds sub-command information about given voting with a given name.
public final function AddVotingInfo(class<Voting> processClass, Text processName) {
if (processName == none) return;
if (processClass == none) return;
if (dataBuilder == none) return;
dataBuilder.SubCommand(processName);
processClass.static.AddInfo(dataBuilder);
commandData = dataBuilder.BorrowData();
}
public final function HandleRemovedVoting(class<Voting> votingClass) {
local int i;
local array<Text> votingsNames;
ResetVotingInfo();
// Rebuild the whole voting data
votingsNames = _.commands.GetAllVotingsNames();
for (i = 0; i < votingsNames.length; i += 1) {
AddVotingInfo(_.commands.GetVotingClass(votingsNames[i]), votingsNames[i]);
}
_.memory.FreeMany(votingsNames);
}
/// Clears all sub-command information added from [`Voting`]s.
public final function ResetVotingInfo() {
_.memory.Free(dataBuilder);
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
BuildData(dataBuilder);
commandData = dataBuilder.BorrowData();
}
private final function DisplayInfoAboutVoting(EPlayer instigator, Voting currentVoting) {
if (currentVoting == none) {
callerConsole.WriteLine(P("No voting is active right now."));
} else {
currentVoting.PrintVotingInfoFor(instigator);
}
}
private final function CastVote(Voting currentVoting, EPlayer voter, bool voteForSuccess) {
if (currentVoting != none) {
currentVoting.CastVote(voter, voteForSuccess);
} else {
callerConsole.UseColor(_.color.TextWarning).WriteLine(P("No voting is active right now."));
}
}
// Assumes all arguments aren't `none`.
private final function bool StartVoting(
CallData arguments,
Voting currentVoting,
EPlayer instigator
) {
local Voting newVoting;
local User callerUser;
local CommandAPI.VotingConfigInfo pair;
local CommandAPI.StartVotingResult result;
callerUser = instigator.GetIdentity();
pair = _.commands.ResolveVotingForUser(arguments.subCommandName, callerUser);
_.memory.Free(callerUser);
if (pair.votingClass == none) {
callerConsole
.UseColor(_.color.TextFailure)
.Write(P("Unknown voting option \""))
.Write(arguments.subCommandName)
.WriteLine(P("\""));
return false;
}
if (pair.usageForbidden) {
callerConsole
.UseColor(_.color.TextFailure)
.Write(P("You aren't allowed to start \""))
.Write(arguments.subCommandName)
.WriteLine(P("\" voting"));
return false;
}
result = _.commands.StartVoting(pair, arguments.parameters);
Log("Result:" @ result);
// Handle errors.
// `SVR_UnknownVoting` is impossible, since we've already checked that
// `pair.votingClass != none`)
if (result == SVR_AlreadyInProgress) {
callerConsole
.UseColor(_.color.TextWarning)
.WriteLine(P("Another voting is already in progress!"));
return false;
}
if (result == SVR_NoVoters) {
callerConsole
.UseColor(_.color.TextWarning)
.WriteLine(P("There are no players eligible for that voting."));
return false;
}
// Cast a vote from instigator
newVoting = _.commands.GetCurrentVoting();
if (newVoting != none) {
newVoting.CastVote(instigator, true);
} else {
callerConsole
.UseColor(_.color.TextFailure)
.WriteLine(P("Voting should be available, but it isn't."
@ "This is unexpected, something broke terribly."));
_.memory.Free(newVoting);
return false;
}
_.memory.Free(newVoting);
return true;
}
defaultproperties {
preferredName = "vote"
}

View File

@ -0,0 +1,116 @@
/**
* Object that is meant to describe a squad of zeds for Killing Floor spawning
* system. Zeds can be either added individually or through native squad
* definitions "3A1B2D1G1H".
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ASquad extends AcediaObject;
struct ZedCountPair
{
var Text template;
var int count;
};
/**
* Resets caller squad, removing all zeds inside.
*/
public function Reset();
/**
* Changes amount of zeds with given template inside the caller squad.
*
* @param template Template for which to change stored count.
* @param delta By how much to change zed count in squad. Negative
* values are allowed.
*/
public function ChangeCount(BaseText template, int delta);
/**
* Changes amount of zeds with given template inside the caller squad.
*
* @param template Template for which to change stored count.
* @param delta By how much to change zed count in squad. Negative
* values are allowed.
*/
public final function ChangeCount_S(string template, int delta)
{
local MutableText wrapper;
wrapper = _.text.FromStringM(template);
ChangeCount(wrapper, delta);
wrapper.FreeSelf();
}
/**
* Adds zeds into the squad from the text definition format.
*
* For example "3clot5crawer1fp" will add 3 clots, 5 crawlers and 1 fleshpound
* into the squad (names are resolved via entity aliases).
* Counts inside a definition cannot be negative.
*
* @param definition Text definition of the addition to squad.
*/
public function AddFromDefinition(BaseText definition);
/**
* Adds zeds into the squad from the text definition format.
*
* For example "3clot5crawer1fp" will add 3 clots, 5 crawlers and 1 fleshpound
* into the squad (names are resolved via entity aliases).
* Counts inside a definition cannot be negative.
*
* @param definition Text definition of the addition to squad.
*/
public final function AddFromDefinition_S(string definition)
{
local MutableText wrapper;
wrapper = _.text.FromStringM(definition);
AddFromDefinition(wrapper);
wrapper.FreeSelf();
}
/**
* Returns list of zeds inside caller squad as an array of zed template and
* the count of that zed in a squad.
*
* @return Array of pairs of template and corresponding zed count inside
* a squad. Guaranteed that there cannot be two array elements with
* the same template.
*/
public function array<ZedCountPair> GetZedList();
/**
* Returns amount of zeds of the given template inside the caller squad.
*
* @return Current amount of zeds of the given template inside the caller
* squad.
*/
public function int GetZedCount(BaseText template);
/**
* Returns current total zed count inside a squad.
*
* @return Current total zed count inside a caller squad.
*/
public function int GetTotalZedCount();
defaultproperties
{
}

View File

@ -0,0 +1,101 @@
/**
* Subset of functionality for dealing with basic game world interactions:
* searching for entities, tracing, etc..
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AWorldComponent extends AcediaObject
abstract;
/**
* Traces world for entities starting from `start` point and continuing into
* the direction `direction` for a "far distance". What is considered
* a "far distance" depends on the implementation (can potentially be infinite
* distance).
*
* @param start Point from which to start tracing.
* @param direction Direction alongside which to trace.
* @return `TracingIterator` that will iterate among `EPlaceable` interfaces
* for traced entities. Iteration is done in order from the entity closest
* to `start` point.
*/
public function TracingIterator Trace(Vector start, Rotator direction);
/**
* Traces world for entities starting from `start` point and until `end` point.
*
* @param start Point from which to start tracing.
* @param end Point at which to stop tracing.
* @return `TracingIterator` that will iterate among `EPlaceable` interfaces
* for traced entities. Iteration is done in order from the entity closest
* to `start` point.
*/
public function TracingIterator TraceBetween(Vector start, Vector end);
/**
* Traces world for entities starting from the `player`'s camera position and
* along the direction of his sight.
*
* This method works for both players with and without pawns. For player with
* a pawn it produce identical results to `TraceSight()` method.
*
* @param player Player alongside whos sight method is supposed to trace.
* @return `TracingIterator` that will iterate among `EPlaceable` interfaces
* for traced entities. Iteration is done in order from the entity closest
* to `player`'s camera location.
*/
public function TracingIterator TracePlayerSight(EPlayer player);
/**
* Traces world for entities starting from the `pawn`'s camera position and
* along the direction of his sight.
*
* @param pawn Pawn alongside whos sight method is supposed to trace.
* @return `TracingIterator` that will iterate among `EPlaceable` interfaces
* for traced entities. Iteration is done in order from the entity closest
* to `pawn`'s eyes location.
*/
public function TracingIterator TraceSight(EPawn pawn);
/**
* Spawns a new `EPlaceable` based on the given `template` at a given location
* `location`, facing it into the given direction `direction`.
*
* @param template Describes entity (supporting `EPlaceable` interface) to
* spawn into the world.
* @param location At what location to spawn that entity.
* @param direction In what direction spawned entity must face.
* @return `EPlaceable` interface for the spawned entity. `none` if spawning it
* has failed. If method returns interface to a non-existent entity, it
* means that entity has successfully spawned, but then something handled
* its spawning and destroyed it.
*/
public function EPlaceable Spawn(
BaseText template,
optional Vector location,
optional Rotator direction);
/**
* Returns iterator for going through all entities in the game world.
*
* @return `EntityIterator` that will iterate through world entities.
*/
public function EntityIterator Entities();
defaultproperties
{
}

View File

@ -0,0 +1,49 @@
/**
* Base class for describing what API Acedia should load into its client- and
* server- `...Global`s objects.
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaAdapter extends AcediaObject
abstract;
/**
* # `AcediaAdapter`
*
* Acedia provides a set of APIs through `...Global`s objects
* `_` (for everything), `_server` (for servers) and `_client` (for clients).
* The functionality in common API set `_` is hardcoded, but Acedia allows
* users to provide their own implementation of `_server`'s and/or `_client`'s
* functionality to extend them, offer compatibility with other mods, etc..
* This replacement is done through `AcediaAdapter` classes that serve as
* a collection of API classes to be used in `_server` or `_client`.
* All one needs to do to use a different set of server/client APIs is to
* specify desired `AcediaAdapter` before loading server/client core.
*/
var public const class<TimeAPI> /* surprise! */ timeAPIClass;
/*And
like that!*//*
and gain!*/
var public/*A*//*B*/ const class<DBAPI> dbAPIClass;
defaultproperties
{
}/*

View File

@ -0,0 +1,365 @@
/**
* Acedia makes extensive use of `perobjectconfig` for storing information
* in ini config files. Their data is usually stored in ini config files as:
* "[<config_object_name> <data_class_name>]".
* Making a child class for `AcediaConfig` defines "<data_class_name>" and
* contents of appropriate section. Then any such class can have multiple
* records with different "<config_object_name>" values. The only requirement
* is that "<config_object_name>" must be considered *valid* by
* `BaseText.IsValidName()` standards.
* Copyright 2021-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaConfig extends AcediaObject
dependson(HashTable)
abstract;
/**
* # `AcediaConfig`
*
* This class deals with several issues related to use of such objects,
* stemming from the lack of documentation:
*
* 1. Not all `Object` names are a usable.
* [Beyond Unreal wiki](
* https://wiki.beyondunreal.com/Legacy:PerObjectConfig)
* lists a couple of limitations: whitespace and ']' character.
* However there are more, including '.'.
* We limit available character set to ASCII latin letters, digits and
* the dot ('.') / underscore ('_'). Dot is a forbidden character, but it
* is often used in class names and, therefore, was added via workaround:
* it is automatically converted into colon ':' character to allow its
* storage inside ini config files. It also will not lead to the name
* conflicts, since colon is a forbidden character for `AcediaConfig`.
* Unreal Engine also doesn't handle long object names very well:
* if it considers "[<config_object_name> <data_class_name>]" too long, it
* might cut some of the trailing letters of what is between brackets '['
* and ']' basically mangling class' name. Since such config header seems
* to be able to contain at least 100 character (at least in our tests),
* we deal with that issue by limiting name to 50 characters at most.
* Depending on the class' name it might still cause problems, so don't
* make them too long.
* 2. Behavior of loading `perobjectconfig`-objects a second time is wonky and
* is fixed by `AcediaConfig`: it provides concrete behavior guarantees
* for all of its config object-managing methods.
*/
// All config objects of a particular class only get loaded once
// per session (unless new one is created) and then accessed through this
// collection.
// This array stores `AcediaConfig` values with `Text` keys in
// a case-insensitive way (by converting keys into lower case).
// In case it has a `none` value stored under some key - it means that value
// was detected in config, but not yet loaded.
// Only its default value is ever used.
var private HashTable existingConfigs;
// TODO: comment and add static cleanup
var private array<AcediaConfig> clearQueue;
var private bool syncScheduled;
// Stores name of the config where settings are to be stored.
// Must correspond to value in `config(...)` modifier in class definition.
var protected const string configName;
// Set this to `true` if you implement `ToData()` / `FromData()` pair of
// methods.
// This will tell Acedia that your config can be converted into
// JSON-compatible types.
var public const bool supportsDataConversion;
/**
* These methods must be overloaded to store and load all the config
* variables inside an `HashTable` collection. How exactly to store
* them is up to each config class to decide, as long as it allows conversion
* into JSON (see `JSONAPI.IsCompatible()` for details).
* Note that `HashTable` reference `FromData()` receives is
* not necessarily the same one your `ToData()` method returns - any particular
* value boxes can be replaced with value references and vice versa.
* NOTE: DO NOT use `P()`, `C()`, `F()` or `T()` methods for keys or
* values in collections you return. All keys and values will be automatically
* deallocated when necessary, so these methods for creating `Text` values are
* not suitable.
*/
protected function HashTable ToData() { return none; }
protected function FromData(HashTable source) {}
/**
* This method must be overloaded to setup default values for all config
* variables. You should use it instead of the `defaultproperties` block.
*/
protected function DefaultIt() {}
/**
* This reads all of the `AcediaConfig`'s settings objects into internal
* storage. Must be called before any other methods. Actual loading might be
* postponed until a particular config is needed.
*/
public static function Initialize()
{
local int i;
local Text nextName, lowerName;
local array<string> names;
if (default.existingConfigs != none) {
return;
}
default.existingConfigs = __().collections.EmptyHashTable();
names = GetPerObjectNames( default.configName, string(default.class.name),
MaxInt);
for (i = 0; i < names.length; i += 1)
{
if (names[i] == "") {
continue;
}
nextName = __().text.FromString(NameToActualVersion(names[i]));
if (nextName.IsValidName())
{
lowerName = nextName.LowerCopy();
default.existingConfigs.SetItem(lowerName, none);
lowerName.FreeSelf();
}
nextName.FreeSelf();
}
}
private static function string NameToStorageVersion(string configObjectName)
{
return Repl(configObjectName, ".", ":");
}
private static function string NameToActualVersion(string configObjectName)
{
return Repl(configObjectName, ":", ".");
}
/**
* Creates a brand new config object with a given name.
*
* Fails if config object with that name already exists.
* Config name must be considered *valid* by `BaseText.IsValidName()`
* standards.
*
* Always writes new config inside the ini file on disk.
*
* @param name Name of the new config object.
* Must be considered *valid* by `BaseText.IsValidName()`
* standards, otherwise method will fail.
* @return `false` iff config object name `name` already exists
* or `name` is invalid for config object.
*/
public final static function bool NewConfig(BaseText name)
{
local AcediaConfig newConfig;
if (name == none) return false;
if (!name.IsValidName()) return false;
if (default.existingConfigs == none) return false;
name = name.LowerCopy();
if (default.existingConfigs.HasKey(name))
{
name.FreeSelf();
return false;
}
newConfig =
new(none, NameToStorageVersion(name.ToString())) default.class;
newConfig._ = __();
newConfig.DefaultIt();
newConfig.SyncSave();
default.existingConfigs.SetItem(name, newConfig);
name.FreeSelf();
return true;
}
/**
* Checks if a config object with a given name exists.
*
* @param name Name of the new config object.
* Must be considered *valid* by `BaseText.IsValidName()` standards.
* @return `true` iff new config object was created.
*/
public final static function bool Exists(BaseText name)
{
local bool result;
if (name == none) return false;
if (!name.IsValidName()) return false;
if (default.existingConfigs == none) return false;
name = name.LowerCopy();
result = default.existingConfigs.HasKey(name);
name.FreeSelf();
return result;
}
/**
* Deletes config object with a given name.
*
* If given config object exists, this method cannot fail.
* `Exists()` is guaranteed to return `false` after this method call.
*
* Always removes any present config entries from ini files.
*
* @param name Name of the config object to delete.
*/
public final static function DeleteConfig(BaseText name)
{
local AcediaConfig value;
if (name == none) return;
if (default.existingConfigs == none) return;
name = name.LowerCopy();
value = AcediaConfig(default.existingConfigs.TakeItem(name));
if (value != none)
{
__().scheduler.RequestDiskAccess(default.existingConfigs).connect =
HandleClearQueue;
default.clearQueue[default.clearQueue.length] = value;
}
__().memory.Free(name);
}
/**
* Returns array containing names of all available config objects.
*
* @return Array with names of all available config objects.
* Guaranteed to not contain `none` values.
*/
public static function array<Text> AvailableConfigs()
{
local array<Text> emptyResult;
if (default.existingConfigs != none) {
return default.existingConfigs.GetTextKeys();
}
return emptyResult;
}
/**
* Returns `AcediaConfig` of caller class with name `name`.
*
* @param name Name of the config object, whos settings data is to
* be loaded. Must be considered *valid* by `BaseText.IsValidName()`
* standards.
* @return `AcediaConfig` of caller class with name `name`.
* Returns `none` if config with given name doesn't exist.
*/
public final static function AcediaConfig GetConfigInstance(BaseText name)
{
local HashTable.Entry configEntry;
if (name == none) return none;
if (!name.IsValidName()) return none;
if (default.existingConfigs == none) return none;
name = name.LowerCopy();
configEntry = default.existingConfigs.GetEntry(name);
if (configEntry.value == none && configEntry.key != none)
{
configEntry.value =
new(none, NameToStorageVersion(name.ToString())) default.class;
configEntry.value._ = __();
default.existingConfigs.SetItem(configEntry.key, configEntry.value);
}
__().memory.Free(name);
// We return value, so do not deallocate it
__().memory.Free(configEntry.key);
return AcediaConfig(configEntry.value);
}
/**
* Loads Acedia's representation of settings data of a particular config
* object, given by the `name`.
*
* Should only be called if caller class has `supportsDataConversion` set to
* `true`.
*
* @param name Name of the config object, whos data is to be loaded.
* Name must be considered *valid* by `BaseText.IsValidName()` standards.
* @return Data of a particular config object, given by the `name`.
* Expected to be in format that allows for JSON serialization
* (see `JSONAPI.IsCompatible()` for details).
* Returns `none` if config with specified name is missing (or their class
* was not yet initialized: see `self.Initialize()` method).
*/
public final static function HashTable LoadData(BaseText name)
{
local HashTable result;
local AcediaConfig requiredConfig;
requiredConfig = GetConfigInstance(name);
if (requiredConfig != none) {
result = requiredConfig.ToData();
}
return result;
}
/**
* Saves Acedia's representation of settings data (`data`) for a particular
* config object, given by the `name`.
*
* Should only be called if caller class has `supportsDataConversion` set to
* `true`.
*
* @param name Name of the config object, whos data is to be modified.
* Name must be considered *valid* by `BaseText.IsValidName()` standards.
* @param data New data for config variables. Expected to be in format that
* allows for JSON deserialization (see `JSONAPI.IsCompatible()` for
* details).
*/
public final static function SaveData(BaseText name, HashTable data)
{
local AcediaConfig requiredConfig;
requiredConfig = GetConfigInstance(name);
if (requiredConfig != none)
{
requiredConfig.FromData(data);
requiredConfig.SyncSave();
}
}
/**
* Synchronizes current in-memory data by saving it onto the disk (into
* the config file). Can be performed asynchronously (actual saving can be
* postponed for performance reasons).
*/
public final function SyncSave()
{
if (syncScheduled) {
return;
}
syncScheduled = true;
__().scheduler.RequestDiskAccess(default.existingConfigs).connect = DoSync;
}
// Does actual saving
private final function DoSync()
{
syncScheduled = false;
SaveConfig();
}
private final static function HandleClearQueue()
{
if (default.clearQueue.length <= 0) {
return;
}
default.clearQueue[0].ClearConfig();
default.clearQueue.Remove(0, 1);
}
defaultproperties
{
supportsDataConversion = false
usesObjectPool = false
}

View File

@ -0,0 +1,518 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaEnvironment extends AcediaObject
config(AcediaSystem);
//! API for management of running [`Feature`]s and loaded packages.
//!
//! Instance of this class will be used by Acedia to manage resources available
//! from different packages like [`Feature`]s and such other etc..
//! This is mostly necessary to implement Acedia loader (and, possibly,
//! its alternatives) that would load available packages and enable [`Feature`]s
//! admin wants to be enabled.
/// Flag indicating whether Acedia has already undergone its shutdown process.
var private bool acediaShutDown;
/// Global data that can be changed by any active mods, allowing limited
/// interaction between them.
var private HashTable globalData;
/// Collection of all registered package manifests, representing different sets
/// of features and resources.
var private array< class<_manifest> > availablePackages;
/// List of all feature classes available from currently registered packages.
var private array< class<Feature> > availableFeatures;
/// Instances of features that are currently enabled and active within
/// this environment.
var private array<Feature> enabledFeatures;
var private array<int> enabledFeaturesLifeVersions;
/// Suffix appended to package names when searching for their manifest classes.
var private string manifestSuffix;
/// Indicates if the system should run in debug mode, enabling extra
/// development-related functionality.
var private const config bool debugMode;
var private LoggerAPI.Definition infoRegisteringPackage, infoAlreadyRegistered;
var private LoggerAPI.Definition errNotRegistered, errFeatureAlreadyEnabled;
var private LoggerAPI.Definition warnFeatureAlreadyEnabled;
var private LoggerAPI.Definition errFeatureClassAlreadyEnabled, errNoGlobals;
var private SimpleSignal onShutdownSignal;
var private SimpleSignal onShutdownSystemSignal;
var private Environment_FeatureEnabled_Signal onFeatureEnabledSignal;
var private Environment_FeatureDisabled_Signal onFeatureDisabledSignal;
protected function Constructor() {
globalData = _.collections.EmptyHashTable();
// Always register our core package
RegisterPackage_S("AcediaCore");
onShutdownSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
onShutdownSystemSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
onFeatureEnabledSignal = Environment_FeatureEnabled_Signal(
_.memory.Allocate(class'Environment_FeatureEnabled_Signal'));
onFeatureDisabledSignal = Environment_FeatureDisabled_Signal(
_.memory.Allocate(class'Environment_FeatureDisabled_Signal'));
}
protected function Finalizer() {
_.memory.Free(globalData);
_.memory.Free(onShutdownSignal);
_.memory.Free(onShutdownSystemSignal);
_.memory.Free(onFeatureEnabledSignal);
_.memory.Free(onFeatureDisabledSignal);
}
/// Signal that will be emitted right before Acedia shuts down.
///
/// At the point of emission all APIs should still exist and function.
///
/// # Signature
///
/// void <slot>()
public final /*signal*/ function SimpleSlot OnShutDown(AcediaObject receiver) {
return SimpleSlot(onShutdownSignal.NewSlot(receiver));
}
/// Signal that will be emitted during Acedia shut down.
///
/// System API will use it to clean up after themselves, so one shouldn't rely
/// on using them.
///
/// There is no reason to use this signal unless you're reimplementing one of
/// the APIs.
/// Otherwise you probably want to use [`AcediaEnvironment::OnShutDown()`]
/// signal instead.
///
/// # Signature
///
/// void <slot>()
public final /*signal*/ function SimpleSlot OnShutDownSystem(AcediaObject receiver) {
return SimpleSlot(onShutdownSystemSignal.NewSlot(receiver));
}
/// Signal that will be emitted right after a new [`Feature`] is enabled and its
/// [`Feature::OnEnabled()`] method was called.
///
/// # Signature
///
/// void <slot>(Feature enabledFeature)
///
/// [`enabledFeature`] argument is an instance of [`Feature`] that was just
/// enabled.
public final /*signal*/ function Environment_FeatureEnabled_Slot OnFeatureEnabled(
AcediaObject receiver
) {
return Environment_FeatureEnabled_Slot(onFeatureEnabledSignal.NewSlot(receiver));
}
/// Signal that will be emitted right after when a [`Feature`] is disabled
/// and its [`Feature::OnDisabled()`] method was called.
///
/// # Signature
///
/// void <slot>(class<Feature> disabledFeatureClass)
///
/// Argument [`disabledFeatureClass`] describes class of the [`Feature`]
/// instance that was just disabled.
public final /*signal*/ function Environment_FeatureDisabled_Slot OnFeatureDisabled(
AcediaObject receiver
) {
return Environment_FeatureDisabled_Slot(onFeatureDisabledSignal.NewSlot(receiver));
}
/// Shuts AcediaCore down, performing all the necessary clean up.
public final function Shutdown() {
local LevelCore core;
if (acediaShutDown) {
return;
}
DisableAllFeatures();
onShutdownSignal.Emit();
onShutdownSystemSignal.Emit();
core = class'ServerLevelCore'.static.GetInstance();
if (core != none) {
core.Destroy();
}
core = class'ClientLevelCore'.static.GetInstance();
if (core != none) {
core.Destroy();
}
_.DropCoreAPI();
acediaShutDown = true;
}
/// Registers an Acedia package with a given name.
///
/// Returns `true` if package was successfully registered, `false` if it either
/// does not exist, was already registered or [`packageName`] is `none`.
///
/// # Errors
///
/// Will log an error if the package has failed to get registered (it is either
/// missing or not an Acedia package).
public final function bool RegisterPackage(BaseText packageName) {
local class<_manifest> manifestClass;
if (packageName == none) {
return false;
}
_.logger.Auto(infoRegisteringPackage).Arg(packageName.Copy());
manifestClass = class<_manifest>(DynamicLoadObject(
packageName.ToString() $ manifestSuffix, class'Class', true));
if (manifestClass == none) {
_.logger.Auto(errNotRegistered).Arg(packageName.Copy());
return false;
}
if (IsManifestRegistered(manifestClass)) {
_.logger.Auto(infoAlreadyRegistered).Arg(packageName.Copy());
return false;
}
availablePackages[availablePackages.length] = manifestClass;
ReadManifest(manifestClass);
return true;
}
/// Registers an Acedia package with a given name.
///
/// Returns `true` if package was successfully registered, `false` if it either
/// does not exist or was already registered.
///
/// # Errors
///
/// Will log an error if the package has failed to get registered (it is either
/// missing or not an Acedia package).
public final function RegisterPackage_S(string packageName) {
local Text wrapper;
wrapper = _.text.FromString(packageName);
RegisterPackage(wrapper);
_.memory.Free(wrapper);
}
private final function bool IsManifestRegistered(class<_manifest> manifestClass) {
local int i;
for (i = 0; i < availablePackages.length; i += 1) {
if (manifestClass == availablePackages[i]) {
return true;
}
}
return false;
}
private final function ReadManifest(class<_manifest> manifestClass) {
local int i;
for (i = 0; i < manifestClass.default.features.length; i += 1) {
if (manifestClass.default.features[i] == none) {
continue;
}
manifestClass.default.features[i].static.LoadConfigs();
availableFeatures[availableFeatures.length] = manifestClass.default.features[i];
}
for (i = 0; i < manifestClass.default.testCases.length; i += 1) {
class'TestingService'.static.RegisterTestCase(manifestClass.default.testCases[i]);
}
}
/// Returns `true` iff AcediaCore is running in the debug mode.
///
/// AcediaCore's debug mode allows features to enable functionality that is only
/// useful during development.
/// Whether AcediaCore is running in a debug mode is decided at launch and
/// cannot be changed.
public final function bool IsDebugging() {
return debugMode;
}
/// Returns all packages registered in the caller [`AcediaEnvironment`].
public final function array< class<_manifest> > GetAvailablePackages() {
return availablePackages;
}
/// Returns all [`Feature`]s available in the caller [`AcediaEnvironment`].
public final function array< class<Feature> > GetAvailableFeatures() {
return availableFeatures;
}
/// Returns all currently enabled [`Feature`]s.
public final function array<Feature> GetEnabledFeatures() {
local int i;
for (i = 0; i < enabledFeatures.length; i += 1) {
enabledFeatures[i].NewRef();
}
return enabledFeatures;
}
// CleanRemove [`Feature`]s that got deallocated.
// This shouldn't happen unless someone messes up.
private final function CleanEnabledFeatures()
{
local int i;
while (i < enabledFeatures.length) {
if (enabledFeatures[i].GetLifeVersion() != enabledFeaturesLifeVersions[i]) {
enabledFeatures.Remove(i, 1);
} else {
i += 1;
}
}
}
/// Checks if [`Feature`] of given class is enabled.
///
/// Even if If feature of class [`featureClass`] is enabled, it's not
/// necessarily that the instance you have reference to is enabled.
///
/// Although unlikely, it is possible that someone spawned another instance of
/// the same class that isn't considered enabled.
/// If you want to check whether some particular instance of given class
/// [`featureClass`] is enabled, use [`AcediaEnvironment::IsFeatureEnabled()`]
/// method instead.
public final function bool IsFeatureClassEnabled(class<Feature> featureClass) {
local int i;
if (featureClass == none) {
return false;
}
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1) {
if (featureClass == enabledFeatures[i].class) {
return true;
}
}
return false;
}
/// Checks if given `Feature` instance is enabled.
///
/// If you want to check if any instance instance of given class
/// [`classToCheck`] is enabled (and not [`feature`] specifically),
/// use [`AcediaEnvironment::IsFeatureClassEnabled()`] method instead.
public final function bool IsFeatureEnabled(Feature feature) {
local int i;
if (feature == none) return false;
if (!feature.IsAllocated()) return false;
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1) {
if (feature == enabledFeatures[i]) {
return true;
}
}
return false;
}
/// Returns enabled [`Feature`] instance of the given class.
///
/// Returns `none` only if [`featureClass`] is not enabled (or also `none`).
public final function Feature GetEnabledFeature(class<Feature> featureClass) {
local int i;
if (featureClass == none) {
return none;
}
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1) {
if (featureClass == enabledFeatures[i].class) {
enabledFeatures[i].NewRef();
return enabledFeatures[i];
}
}
return none;
}
/// Enables given [`Feature`] instance [`newEnabledFeature`] with a given
/// config.
/// Does not change a config for already enabled feature, failing instead.
///
/// Returns `true` if given [`newEnabledFeature`] was enabled and `false`
/// otherwise (including if feature of the same class has already been enabled).
///
/// # Errors
///
/// Will log an error if feature with the same class as [`newEnabledFeature`] is
/// already enabled.
/// In case already enabled feature is the same instance as
/// [`newEnabledFeature`] a warning will be logged instead.
public final function bool EnableFeature(Feature newEnabledFeature, optional BaseText configName) {
local int i;
if (newEnabledFeature == none) return false;
if (!newEnabledFeature.IsAllocated()) return false;
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1) {
if (newEnabledFeature.class == enabledFeatures[i].class) {
if (newEnabledFeature == enabledFeatures[i]) {
_.logger
.Auto(warnFeatureAlreadyEnabled)
.Arg(_.text.FromClass(newEnabledFeature.class));
}
else {
_.logger
.Auto(errFeatureClassAlreadyEnabled)
.Arg(_.text.FromClass(newEnabledFeature.class));
}
return false;
}
}
newEnabledFeature.NewRef();
enabledFeatures[enabledFeatures.length] = newEnabledFeature;
enabledFeaturesLifeVersions[enabledFeaturesLifeVersions.length] =
newEnabledFeature.GetLifeVersion();
newEnabledFeature.EnableInternal(configName);
onFeatureEnabledSignal.Emit(newEnabledFeature);
return true;
}
/// Disables given [`Feature`] instance [`featureToDisable`].
///
/// Returns `true` if given [`newEnabledFeature`] was disabled and `false`
/// otherwise (including if it already was disabled).
public final function bool DisableFeature(Feature featureToDisable) {
local int i;
if (featureToDisable == none) return false;
if (!featureToDisable.IsAllocated()) return false;
CleanEnabledFeatures();
for (i = 0; i < enabledFeatures.length; i += 1) {
if (featureToDisable == enabledFeatures[i]) {
enabledFeatures.Remove(i, 1);
enabledFeaturesLifeVersions.Remove(i, 1);
featureToDisable.DisableInternal();
onFeatureDisabledSignal.Emit(featureToDisable.class);
_.memory.Free(featureToDisable);
return true;
}
}
return false;
}
/// Disables all currently enabled [`Feature`]s.
///
/// Mainly intended for the clean up when Acedia shuts down.
public final function DisableAllFeatures() {
local int i;
local array<Feature> featuresCopy;
CleanEnabledFeatures();
// Move features from `enabledFeatures`, which marks them as disabled,
// allowing us to successfully call their `DisableInternal()` methods.
featuresCopy = enabledFeatures;
enabledFeatures.length = 0;
enabledFeaturesLifeVersions.length = 0;
for (i = 0; i < featuresCopy.length; i += 1) {
featuresCopy[i].DisableInternal();
onFeatureDisabledSignal.Emit(featuresCopy[i].class);
}
_.memory.FreeMany(featuresCopy);
}
/// Returns globally defined value with a specified name.
///
/// Returns `none` if [`valueName`] is `none`.
///
/// # Error
///
/// This shouldn't happen, but in case global data object wasn't created,
/// logs an error.
/// Also returns `none` no matter the argument.
public final function AcediaObject GetGlobalValue(BaseText valueName) {
if (valueName == none) {
return none;
}
if (globalData == none) {
_.logger.Auto(errNoGlobals);
return none;
}
return globalData.GetItem(valueName);
}
/// Returns globally defined value with a specified name.
///
/// # Error
///
/// This shouldn't happen, but in case global data object wasn't created,
/// logs an error.
/// Also returns `none` no matter the argument.
public final function AcediaObject GetGlobalValue_S(string valueName) {
local Text wrapper;
local AcediaObject value;
wrapper = _.text.FromString(valueName);
value = GetGlobalValue(wrapper);
_.memory.Free(wrapper);
return value;
}
/// Sets globally defined value for a specified name.
///
/// Does nothing if [`valueName`] is `none`.
///
/// # Error
///
/// This shouldn't happen, but in case global data object wasn't created,
/// logs an error.
public final function SetGlobalValue(BaseText valueName, AcediaObject value) {
if (valueName == none) {
return;
}
if (globalData == none) {
_.logger.Auto(errNoGlobals);
return;
}
globalData.SetItem(valueName, value);
}
/// Sets globally defined value for a specified name.
///
/// # Error
///
/// This shouldn't happen, but in case global data object wasn't created,
/// logs an error.
public final function SetGlobalValue_S(string valueName, AcediaObject value) {
local Text wrapper;
wrapper = _.text.FromString(valueName);
SetGlobalValue(wrapper, value);
_.memory.Free(wrapper);
}
defaultproperties {
manifestSuffix = ".Manifest"
debugMode = false
infoRegisteringPackage = (l=LOG_Info,m="Registering package \"%1\".")
infoAlreadyRegistered = (l=LOG_Info,m="Package \"%1\" is already registered.")
errNotRegistered = (l=LOG_Error,m="Package \"%1\" has failed to be registered.")
warnFeatureAlreadyEnabled = (l=LOG_Warning,m="Trying to enable already enabled instance of `Feature` class `%1`.")
errFeatureClassAlreadyEnabled = (l=LOG_Error,m="Different instance of the same `Feature` class `%1` is already enabled.")
errNoGlobals = (l=LOG_Error,m="Global data isn't initialize for `AcediaEnvironment`.")
}

View File

@ -0,0 +1,166 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2020-2024 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 AcediaObjectPool extends Object
config(AcediaSystem);
//! Acedia's implementation for object pool.
//!
//! Unlike generic built in [`Engine::ObjectPool`], that can only store objects
//! of one specific class, it specializes in a single class to allow for both
//! faster allocation and faster deallocation (we don't need to look for
//! an object of particular class to return an unused instance).
//!
//! Allows to set a maximum capacity in a config.
/// Represents config entry about pool capacity.
///
/// This struct and it's associated array [`poolSizeOverwrite`] allows server
/// admins to rewrite
/// the pool capacity for each class.
struct PoolSizeSetting {
var class<AcediaObject> objectClass;
var int maxPoolSize;
};
var private config const array<PoolSizeSetting> poolSizeOverwrite;
// Class of objects that this `AcediaObjectPool` stores.
// if `== none`, - object pool is considered uninitialized.
var private class<AcediaObject> storedClass;
// Capacity for object pool that we are using.
// Obtained from [`poolSizeOverwrite`] during initialization and cannot be changed later.
var private int usedMaxPoolSize;
// Actual storage, functions on LIFO principle.
var private array<AcediaObject> objectPool;
// Determines default object pool size for the initialization.
private final function int GetMaxPoolSizeForClass(class<AcediaObject> classToCheck) {
local int i;
local int result;
if (classToCheck != none) {
result = classToCheck.default.defaultMaxPoolSize;
}
else {
result = -1;
}
// Try to replace it with server's settings
for (i = 0; i < poolSizeOverwrite.length; i += 1) {
if (poolSizeOverwrite[i].objectClass == classToCheck) {
result = poolSizeOverwrite[i].maxPoolSize;
break;
}
}
return result;
}
/// Initializes caller object pool to store objects of the given class.
///
/// Returns `true` if initialization completed, `false` otherwise (including if
/// it was already completed with passed [`classToStore`]).
///
/// If successful, this action is irreversible: same pool cannot be
/// re-initialized.
///
/// [`forcedPoolSize`] defines max pool size for the caller
/// [`AcediaObjectPool`].
/// Leaving it at default `0` value will cause method to auto-determine
/// the size: gives priority to the [`poolSizeOverwrite`] config array;
/// if not specified, uses [`AcediaObject`]'s
/// [`AcediaObject::defaultMaxPoolSize`]
/// (ignoring [`AcediaObject::usesObjectPool`] setting).
public final function bool Initialize(
class<AcediaObject> classToStore,
optional int forcedPoolSize
) {
if (storedClass != none) return false;
if (classToStore == none) return false;
// If does not matter that we've set those variables until
// we set `storedClass`.
if (forcedPoolSize == 0) {
usedMaxPoolSize = GetMaxPoolSizeForClass(classToStore);
}
else {
usedMaxPoolSize = forcedPoolSize;
}
if (usedMaxPoolSize == 0) {
return false;
}
storedClass = classToStore;
return true;
}
/// Returns class of objects stored inside the caller [`AcediaObjectPool`].
///
/// `none` means object pool was not initialized.
public final function class<AcediaObject> GetClassOfStoredObjects() {
return storedClass;
}
/// Clear the storage of all its contents.
public final function Clear() {
objectPool.length = 0;
}
/// Adds object to the caller storage (that needs to be initialized to store
/// [`newObject.class`] classes).
///
/// Returns `true` on success and `false` on failure (can happen if passed
/// [`newObject`] reference was invalid, caller storage is not initialized yet
/// or reached it's capacity).
///
/// For performance purposes does not do duplicates checks, this should be
/// verified from outside [`AcediaObjectPool`].
///
/// Performs type checks and only allows objects of the class that caller
/// [`AcediaObjectPool`] was initialized for.
public final function bool Store(AcediaObject newObject) {
if (newObject == none) return true;
if (newObject.class != storedClass) return false;
if (usedMaxPoolSize >= 0 && objectPool.length >= usedMaxPoolSize) {
return false;
}
objectPool[objectPool.length] = newObject;
return true;
}
/// Returns last stored object from the pool, removing it from that pool in
/// the process.
///
/// Only returns `none` if caller `AcediaObjectPool` is either empty or not
/// initialized.
public final function AcediaObject Fetch() {
local AcediaObject result;
if (storedClass == none) return none;
if (objectPool.length <= 0) return none;
result = objectPool[objectPool.length - 1];
objectPool.length = objectPool.length - 1;
return result;
}
defaultproperties {
}

View File

@ -0,0 +1,40 @@
/**
* Facilitates some core replicated functions between client and server.
* Copyright 2019-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AcediaReplicationInfo extends AcediaActor;
var public PlayerController linkOwner;
replication
{
reliable if (role == ROLE_Authority)
linkOwner;
}
defaultproperties
{
remoteRole = ROLE_SimulatedProxy
netUpdateFrequency = 10
bAlwaysRelevant = true
bStatic = false
bNoDelete = false
bHidden = true
bOnlyDirtyReplication = true
bSkipActorPropertyReplication = true
}

View File

@ -0,0 +1,294 @@
/**
* Service for safe `Actor` storage, aimed to resolve two issues that arise
* from storing references to `Actor`s inside non-`Actor` objects:
* 1. This can potentially prevent the whole level from being
* garbage-collected on level change, leading to excessive memory use
* and crashes;
* 2. This can lead to stored `Actor` temporarily being put into
* a "faulty state", where any sort of access to that `Actor`
* (except converting it into a `string` ¯\_()_/¯) will lead to
* server/game crashing.
* This `Service` resolves these issues by storing `Actor`s in such a way
* that they can be relatively quickly obtained via a simple struct value
* (`ActorReference`) that can be safely stored inside any `Object`.
* It is recommended to use `ActorRef` / `ActorBox` instead of accessing
* this `Service` directly.
* Copyright 2021 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 ActorService extends Service;
/**
* This service works as follows: it contains an array filled with `Actor`s
* that will dynamically grow in size whenever more space is needed for new
* `Actor`s. To refer to the stored `Actor` we put it's index in the storage
* array in `ActorReference` struct.
*
* The main problem with that approach is that once `Actor` is destroyed or
* removed- it leaves a "hole" inside our storage and if we keep simply adding
* new `Actor`s at the end of the storage array - we will eventually use
* too much space, even if we only store a limited amount of `Actor`s at any
* given point in time.
* To avoid this problem we also maintain an array of "empty indices" that
* remembers where the "holes" are located and can let us re-allocate them to
* place new `Actor`s in, without needlessly increasing storage's size.
*
* Another problem is that now two different `Actor`s can occupy the same
* spot at different points in time. To deal with that we will also record
* an `Actor`'s "version" for each storage index and increment it each time
* a new `Actor` is stored at that index. We will also store that "version"
* inside `ActorReference` to help us invalidate references to old `Actor`s
* that were stored there before.
*
* NOTE that this `Service` avoids doing any sort of manual cleaning,
* instead relying on the proper interface (`ActorRef` / `ActorBox`) to do it.
*/
/**
* Reference to the `Actor` storage inside this service.
* Reference is only considered invalid after an `Actor` it was referring
* to was removed from the storage (possibly by being replaced with
* another `Actor`).
*/
struct ActorReference
{
var private int index;
var private int version;
};
/* Arrays `actorRecords`, `actorVersions` and `indexAllocationFlag` could
* be represented as an array of a single struct with 3 element, but instead,
* for performance reasons, were separated into three distinct arrays - one for
* each variable.
* This means that they must at all times have the same length and
* elements with the same index should be considered to belong to
* the same record.
*/
// Storage for `Actor`s. It's size can grow, but it cannot shrink, instead
// marking some of array's indices as vacant
// (by putting them into `emptyIndices`).
var private array<Actor> actorRecords;
// Defines a "version" of a corresponding `Actor`.
var private array<int> actorVersions;
// Marks whether values at a particular index are currently used to
// store some `Actor` (`1`) or not (`0`).
// Without these flags we would have no way of knowing whether a spot in
// the array was already freed and recorded in `emptyIndices`, since stored
// `Actor`s can turn into `none` by themselves upon destruction.
var private array<byte> indexAllocationFlag;
// A set of empty indices - that once contained stored an `Actor`, but can
// now be reused.
// Used like a LIFO-queue to quickly find an empty spot in `actorRecords`.
var private array<int> emptyIndices;
// Finds an vacant index in our records (expands array if all indices are
// already taken) and puts `candidate` there.
// Does not do any checks for whether `candidate != none`.
private final function int InsertAtEmptyIndex(Actor candidate)
{
local int newIndex;
if (emptyIndices.length > 0)
{
newIndex = emptyIndices[emptyIndices.length - 1];
emptyIndices.length = emptyIndices.length - 1;
actorVersions[newIndex] += 1;
}
else
{
newIndex = actorRecords.length;
actorVersions[newIndex] = 0;
}
actorRecords[newIndex] = candidate;
indexAllocationFlag[newIndex] = 1;
return newIndex;
}
// Forces `indexToFree` to become vacant, not storing any `Actor`.
private final function FreeIndex(int indexToFree)
{
if (indexAllocationFlag[indexToFree] > 0)
{
actorRecords[indexToFree] = none;
emptyIndices[emptyIndices.length] = indexToFree;
}
// No need to bump `actorVersions[indexToFree]` here, since any refences to
// this index will automatically be invalidated by this:
indexAllocationFlag[indexToFree] = 0;
}
/**
* Adds a new `Actor` to the storage.
*
* Any reference created with `AddActor()` must be release with
* `RemoveActor()`.
*
* This method does not attempt to check whether `newActor` is already stored
* in `ActorService`. Therefore it is possible for the same actor to be stored
* multiple times under different `ActorReference` records.
*
* Can only fail if provided `Actor` is either equal to `none`.
*
* Note that while this storage is supposed to be "safe", meaning that it
* aims to reduce probability of `Actor`-related crashes, it assumes that
* passed `Actor`s are not already "broken", as there is no way to check
* for that.
* `Actor`s that were not stored in a non-`Actor` object as a non-local
* variable should not be broken.
*
* @param newActor `Actor` to store. If it's equal to `none`, then
* this method will do nothing.
* @return `ActorReference` struct that can be used to retrieve stored
* `Actor` later. Returned reference will point at `none` once stored
* `Actor` gets destroyed or removed via the `RemoveActor()` call.
*/
public final function ActorReference AddActor(Actor newActor)
{
local int newIndex;
local ActorReference result;
// This will make the very first check inside `ValidateIndexVersion()` fail
result.index = -1;
if (newActor == none) {
return result;
}
newIndex = InsertAtEmptyIndex(newActor);
result.index = newIndex;
result.version = actorVersions[newIndex];
return result;
}
/**
* Returns an `Actor` that provided `reference` points at.
*
* @param reference Reference to the `Actor` this method should return.
* @return `Actor`, stored in caller `ActorService` with `reference`.
* If stored `Actor` was already destroyed or removed, this method will
* return `none`.
*/
public final function Actor GetActor(ActorReference reference)
{
local int index;
local Actor result;
index = reference.index;
if (!ValidateIndexVersion(index, reference.version)) {
return none;
}
// While this can be considered a side-effect in a getter, removing `none`
// from the storage will not affect how `reference` behaves, since it can
// only refer to `none` from now on.
result = actorRecords[index];
if (result == none)
{
FreeIndex(index);
return none;
}
return result;
}
/**
* Replaces an `Actor`, that provided `reference` points at, with a `newActor`,
* returning a new `ActorReference` and invalidating the old one (`reference`),
* making it to refer to `none`.
*
* This method is guaranteed to be functionally identical (as far as public,
* not encapsulated, interface is concerned) to calling two methods:
* `RemoveActor(reference)` and `AddActor(newActor)`. But is expected to
* provide be more efficient.
*
* @param reference Reference to `Actor` that should be replaced.
* @param newActor New `Actor` to store.
* @return Reference to the `newActor` inside the storage.
*/
public final function ActorReference UpdateActor(
ActorReference reference,
Actor newActor)
{
local int index;
index = reference.index;
// Nothing to remove
if (!ValidateIndexVersion(index, reference.version)) {
return AddActor(newActor);
}
// Nothing to add
if (newActor == none) {
RemoveActor(reference);
}
else
{
// If we need to both remove and add, we can just replace an `Actor`
// and bump the stored version to invalidate copies of the passed
// `reference`:
actorRecords[index] = newActor;
actorVersions[index] += 1;
reference.version += 1;
}
return reference;
}
/**
* Unconditionally removes an `Actor` recorded with `reference` from storage.
*
* Cannot fail.
*
* If referred `Actor` is recorded in storage under several different
* references - only this reference will be affected. The rest are still going
* to refer to that `Actor`.
*
* @param reference Reference to `Actor` to be removed.
*/
public final function RemoveActor(ActorReference reference)
{
if (ValidateIndexVersion(reference.index, reference.version)) {
FreeIndex(reference.index);
}
}
/**
* Returns size of this storage.
*
* @param totalSize Default value (`false`) means that method should return
* amount of currently stored `Actor`s, while `true` means how much space
* this storage takes up (in the sense that the actual amount in bytes is
* `O(GetSize(true))` in big-O notation).
* @return Size of this storage.
*/
public final function int GetSize(optional bool totalSize)
{
if (totalSize) {
return actorRecords.length;
}
return actorRecords.length - emptyIndices.length;
}
// Validates passed index and `Actor`'s version, taken from
// the `ActorReference`: returns `true` iff `Actor` referred to by them
// currently exists in this storage (even if it's now equal to `none`).
private final function bool ValidateIndexVersion(
int refIndex,
int refVersion)
{
if (refIndex < 0) return false;
if (refIndex >= actorRecords.length) return false;
if (actorVersions[refIndex] != refVersion) return false;
return (indexAllocationFlag[refIndex] > 0);
}
defaultproperties
{
}

View File

@ -0,0 +1,385 @@
/**
* This class implements an alias database that stores aliases inside
* standard config ini-files.
* Copyright 2020-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AliasSource extends BaseAliasSource
dependson(HashTable)
abstract;
// 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;
// (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<AliasesStorage> 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<AliasesStorage> loadedAliasObjects;
// 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 HashTable aliasHash;
// Faster access to all aliases, corresponding to a certain value.
// This `HashTable` stores same data as `aliasHash`, but "in reverse":
// for each value as a "key" it stored `ArrayList` of corresponding aliases.
var private HashTable valueHash;
// `true` means that this `AliasSource` is awaiting saving into its config
var private bool pendingSaveToConfig;
var private LoggerAPI.Definition errIncorrectAliasPair, warnDuplicateAlias;
var private LoggerAPI.Definition warnInvalidAlias;
// Load and hash all the data `AliasSource` creation.
protected function Constructor()
{
// If this check fails - caller alias source is fundamentally broken
// and requires mod to be fixed
if (!ASSERT_AliasesClassIsOwnedByThisSource()) {
return;
}
// Load and hash
loadedAliasObjects = aliasesClass.static.LoadAllObjects();
aliasHash = _.collections.EmptyHashTable();
valueHash = _.collections.EmptyHashTable();
HashValidAliasesFromRecord();
HashValidAliasesFromPerObjectConfig();
}
protected function Finalizer()
{
loadedAliasObjects.length = 0;
_.memory.Free(aliasHash);
aliasHash = none;
if (pendingSaveToConfig) {
SaveConfig();
}
}
// Ensures that our `Aliases` class is properly linked with this
// source's class. Logs failure otherwise.
private function bool ASSERT_AliasesClassIsOwnedByThisSource()
{
if (aliasesClass == none) return true;
if (aliasesClass.default.sourceClass == class) return true;
_.logger.Auto(errIncorrectAliasPair).ArgClass(class);
return false;
}
// Load hashes from `AliasSource`'s config (`record` array)
private function HashValidAliasesFromRecord()
{
local int i;
local Text aliasAsText, valueAsText;
for (i = 0; i < record.length; i += 1)
{
aliasAsText = _.text.FromString(record[i].alias);
valueAsText = _.text.FromString(record[i].value);
InsertAliasIntoHash(aliasAsText, valueAsText);
aliasAsText.FreeSelf();
valueAsText.FreeSelf();
}
}
// Load hashes from `Aliases` objects' config
private function HashValidAliasesFromPerObjectConfig()
{
local int i, j;
local Text nextValue;
local array<Text> valueAliases;
for (i = 0; i < loadedAliasObjects.length; i += 1)
{
nextValue = loadedAliasObjects[i].GetValue();
valueAliases = loadedAliasObjects[i].GetAliases();
for (j = 0; j < valueAliases.length; j += 1) {
InsertAliasIntoHash(valueAliases[j], nextValue);
}
nextValue.FreeSelf();
_.memory.FreeMany(valueAliases);
}
}
public static function bool AreValuesCaseSensitive()
{
// Almost all built-in aliases are aliases to class names (or templates)
// and the rest are colors. Both are case-insensitive, so returning `false`
// is a good default implementation. Child classes can just change this
// value, if they need.
return false;
}
public function array<Text> GetAllAliases()
{
return aliasHash.GetTextKeys();
}
public function array<Text> GetAliases(BaseText value)
{
local int i;
local Text storedValue;
local ArrayList aliasesArray;
local array<Text> result;
storedValue = NormalizeValue(value);
aliasesArray = valueHash.GetArrayList(storedValue);
storedValue.FreeSelf();
if (aliasesArray == none) {
return result;
}
for (i = 0; i < aliasesArray.GetLength(); i += 1) {
result[result.length] = aliasesArray.GetText(i);
}
return result;
}
// "Normalizes" value:
// 1. Converts it into lower case if `AreValuesCaseSensitive()` returns
// `true`;
// 2. Converts in into `Text` in case passed value is `MutableText`, so
// that hash table is actually usable.
private function Text NormalizeValue(BaseText value)
{
if (value == none) {
return none;
}
if (AreValuesCaseSensitive()) {
return value.Copy();
}
return value.LowerCopy();
}
// Inserts alias into `aliasHash`, cleaning previous keys/values in case
// they already exist.
// Takes care of lower case conversion to store aliases in `aliasHash`
// in a case-insensitive way. Depending on `AreValuesCaseSensitive()`, can also
// convert values to lower case.
private function InsertAliasIntoHash(BaseText alias, BaseText value)
{
local Text storedAlias;
local Text storedValue;
local Text existingValue;
local ArrayList valueAliases;
if (alias == none) return;
if (value == none) return;
if (!alias.IsValidName())
{
_.logger.Auto(warnInvalidAlias)
.ArgClass(class)
.Arg(alias.Copy());
return;
}
storedAlias = alias.LowerCopy();
existingValue = aliasHash.GetText(storedAlias);
if (aliasHash.HasKey(storedAlias))
{
_.logger.Auto(warnDuplicateAlias)
.ArgClass(class)
.Arg(alias.Copy())
.Arg(existingValue);
}
_.memory.Free(existingValue);
storedValue = NormalizeValue(value);
// Add to `aliasHash`: alias -> value
aliasHash.SetItem(storedAlias, storedValue);
// Add to `valueHash`: value -> alias
valueAliases = valueHash.GetArrayList(storedValue);
if (valueAliases == none) {
valueAliases = _.collections.EmptyArrayList();
}
valueAliases.AddItem(storedAlias);
valueHash.SetItem(storedValue, valueAliases);
// Clean up
storedAlias.FreeSelf();
storedValue.FreeSelf();
}
public function bool HasAlias(BaseText alias)
{
local bool result;
local Text storedAlias;
if (alias == none) {
return false;
}
storedAlias = alias.LowerCopy();
result = aliasHash.HasKey(storedAlias);
storedAlias.FreeSelf();
return result;
}
public function Text Resolve(
BaseText alias,
optional bool copyOnFailure)
{
local Text result;
local Text storedAlias;
if (alias == none) {
return none;
}
storedAlias = alias.LowerCopy();
result = aliasHash.GetText(storedAlias);
storedAlias.FreeSelf();
if (result != none) {
return result;
}
if (copyOnFailure) {
return alias.Copy();
}
return none;
}
public function bool AddAlias(BaseText aliasToAdd, BaseText aliasValue)
{
local Text storedAlias;
if (aliasToAdd == none) return false;
if (aliasValue == none) return false;
if (!aliasToAdd.IsValidName()) return false;
// Check if alias already exists and if yes - remove it
storedAlias = aliasToAdd.LowerCopy();
if (aliasHash.HasKey(storedAlias)) {
RemoveAlias(aliasToAdd);
}
storedAlias.FreeSelf();
// Add alias-value pair
AddToConfigRecords(aliasToAdd.ToString(), aliasValue.ToString());
InsertAliasIntoHash(aliasToAdd, aliasValue);
return true;
}
public function bool RemoveAlias(BaseText aliasToRemove)
{
local Text storedAlias, storedValue;
local ArrayList valueAliases;
if (aliasToRemove == none) return false;
if (!aliasToRemove.IsValidName()) return false;
storedAlias = aliasToRemove.LowerCopy();
storedValue = aliasHash.GetText(storedAlias);
if (storedValue == none)
{
storedAlias.FreeSelf();
return false;
}
aliasHash.RemoveItem(aliasToRemove);
// Since we've found `storedValue`, this couldn't possibly be `none` if
// "same data invariant" is preserved (see their declaration)
valueAliases = valueHash.GetArrayList(storedValue);
if (valueAliases != none) {
valueAliases.RemoveItem(storedAlias, true);
}
if (valueAliases != none && valueAliases.GetLength() <= 0)
{
valueHash.SetItem(storedValue, none);
valueAliases = none;
}
_.memory.Free(valueAliases);
RemoveFromConfigRecords(aliasToRemove.ToString());
return true;
}
// Takes `string`s that represents alias to remove in proper case (lower for
// aliases and for values it depends on the caller source's settings):
// aliases are supposed to be ASCII, so `string` should handle it and its
// comparison just fine
private function AddToConfigRecords(string alias, string value)
{
local AliasValuePair newPair;
newPair.alias = alias;
newPair.value = value;
record[record.length] = newPair;
// Request saving
if (!pendingSaveToConfig)
{
pendingSaveToConfig = true;
_.scheduler.RequestDiskAccess(self).connect = SaveSelf;
}
}
// Takes `string` that represents alias to remove in lower case: aliases are
// supposed to be ASCII, so `string` should handle it and its comparison just
// fine
private function RemoveFromConfigRecords(string aliasToRemove)
{
local int i;
local bool removedAliasFromRecord;
// Aliases are supposed to be ASCII, so `string` should handle it and its
// comparison just fine
while (i < record.length)
{
if (aliasToRemove ~= record[i].alias)
{
record.Remove(i, 1);
removedAliasFromRecord = true;
}
else {
i += 1;
}
}
// Since admins can fuck up and add duplicate aliases, we need to
// thoroughly check every alias object
for (i = 0; i < loadedAliasObjects.length; i += 1) {
loadedAliasObjects[i].RemoveAlias_S(aliasToRemove);
}
// Alias objects can request disk access themselves, so only record if
// needed for the record
if (removedAliasFromRecord && !pendingSaveToConfig)
{
pendingSaveToConfig = true;
_.scheduler.RequestDiskAccess(self).connect = SaveSelf;
}
}
private function SaveSelf()
{
pendingSaveToConfig = false;
SaveConfig();
}
defaultproperties
{
// Source main parameters
aliasesClass = class'Aliases'
errIncorrectAliasPair = (l=LOG_Error,m="`AliasSource`-`Aliases` class pair is incorrectly setup for source `%1`. Omitting it.")
warnDuplicateAlias = (l=LOG_Warning,m="Alias source `%1` has duplicate record for alias \"%2\". This is likely due to an erroneous config. \"%3\" value will be used.")
warnInvalidAlias = (l=LOG_Warning,m="Alias source `%1` has record with invalid alias \"%2\". This is likely due to an erroneous config. This alias will be discarded.")
}

View File

@ -0,0 +1,141 @@
/**
* Config object for `Aliases_Feature`.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Aliases extends FeatureConfig
perobjectconfig
config(AcediaAliases);
struct CustomSourceRecord
{
var string name;
var class<AliasSource> source;
};
var public config class<AliasSource> weaponAliasSource;
var public config class<AliasSource> colorAliasSource;
var public config class<AliasSource> featureAliasSource;
var public config class<AliasSource> entityAliasSource;
var public config class<AliasSource> commandAliasSource;
var public config array<CustomSourceRecord> customSource;
protected function HashTable ToData()
{
local int i;
local Text nextKey;
local HashTable data, otherSourcesData;
data = __().collections.EmptyHashTable();
// Add named aliases
data.SetString(P("weapon"), string(weaponAliasSource));
data.SetString(P("color"), string(colorAliasSource));
data.SetString(P("feature"), string(featureAliasSource));
data.SetString(P("entity"), string(entityAliasSource));
data.SetString(P("command"), string(commandAliasSource));
// Add the rest
otherSourcesData = __().collections.EmptyHashTable();
for (i = 0; i < customSource.length; i += 1)
{
nextKey = _.text.FromString(customSource[i].name);
otherSourcesData.SetString(nextKey, string(customSource[i].source));
nextKey.FreeSelf();
}
data.SetItem(P("other"), otherSourcesData);
otherSourcesData.FreeSelf();
return data;
}
protected function FromData(HashTable source)
{
local HashTable otherSourcesData;
if (source == none) {
return;
}
// We cast `class` into `string`
// (e.g. `string(class'AcediaAliases_Weapons')`)
// instead of writing full name of the class so that code is independent
// from this package's name, making it easier to change later
weaponAliasSource = class<AliasSource>(_.memory.LoadClass_S(
source.GetString(P("weapon"), string(class'WeaponAliasSource'))));
colorAliasSource = class<AliasSource>(_.memory.LoadClass_S(
source.GetString(P("color"), string(class'ColorAliasSource'))));
featureAliasSource = class<AliasSource>(_.memory.LoadClass_S(
source.GetString(P("feature"), string(class'FeatureAliasSource'))));
entityAliasSource = class<AliasSource>(_.memory.LoadClass_S(
source.GetString(P("entity"), string(class'EntityAliasSource'))));
commandAliasSource = class<AliasSource>(_.memory.LoadClass_S(
source.GetString(P("command"), string(class'CommandAliasSource'))));
otherSourcesData = source.GetHashTable(P("other"));
if (otherSourcesData != none)
{
ReadOtherSources(otherSourcesData);
otherSourcesData.FreeSelf();
}
}
// Doesn't check whether `otherSources` is `none`
protected function ReadOtherSources(HashTable otherSourcesData)
{
local CustomSourceRecord newRecord;
local BaseText keyAsText, valueAsText;
local AcediaObject key, value;
local HashTableIterator iter;
customSource.length = 0;
iter = HashTableIterator(otherSourcesData.Iterate());
iter.LeaveOnlyNotNone();
for (iter = iter; !iter.HasFinished(); iter.Next())
{
key = iter.GetKey();
value = iter.GetKey();
keyAsText = BaseText(key);
valueAsText = BaseText(value);
if (keyAsText != none && valueAsText != none)
{
newRecord.name = keyAsText.ToString();
newRecord.source = class<AliasSource>(
_.memory.LoadClass_S(valueAsText.ToString()));
if (newRecord.source != none) {
customSource[customSource.length] = newRecord;
}
}
_.memory.Free(key);
_.memory.Free(value);
}
}
protected function DefaultIt()
{
customSource.length = 0;
weaponAliasSource = class'WeaponAliasSource';
colorAliasSource = class'ColorAliasSource';
featureAliasSource = class'FeatureAliasSource';
entityAliasSource = class'EntityAliasSource';
commandAliasSource = class'CommandAliasSource';
}
defaultproperties
{
configName = "AcediaAliases"
weaponAliasSource = class'WeaponAliasSource'
colorAliasSource = class'ColorAliasSource'
featureAliasSource = class'FeatureAliasSource'
entityAliasSource = class'EntityAliasSource'
commandAliasSource = class'CommandAliasSource'
}

View File

@ -0,0 +1,420 @@
/**
* Provides convenient access to Aliases-related functions.
* Copyright 2020-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AliasesAPI extends AcediaObject
dependson(LoggerAPI);
// To avoid bothering with fetching `Aliases_Feature` each time we need to
// access an alias source, we save all the basic ones separately and
// `Aliases_Feature` can simply trigger their updates whenever necessary via
// `AliasesAPI._reloadSources()` function.
var private BaseAliasSource weaponAliasSource;
var private BaseAliasSource colorAliasSource;
var private BaseAliasSource featureAliasSource;
var private BaseAliasSource entityAliasSource;
var private BaseAliasSource commandAliasSource;
public function _reloadSources()
{
local Aliases_Feature feature;
_.memory.Free(weaponAliasSource);
_.memory.Free(colorAliasSource);
_.memory.Free(featureAliasSource);
_.memory.Free(entityAliasSource);
_.memory.Free(commandAliasSource);
weaponAliasSource = none;
colorAliasSource = none;
featureAliasSource = none;
entityAliasSource = none;
commandAliasSource = none;
feature = Aliases_Feature(
class'Aliases_Feature'.static.GetEnabledInstance());
if (feature == none) {
return;
}
weaponAliasSource = feature.GetWeaponSource();
colorAliasSource = feature.GetColorSource();
featureAliasSource = feature.GetFeatureSource();
entityAliasSource = feature.GetEntitySource();
commandAliasSource = feature.GetCommandSource();
_.memory.Free(feature);
}
/**
* Provides an easier access to the instance of the custom `BaseAliasSource`
* with a given name `sourceName`.
*
* Custom alias sources can be added manually by the admin through the
* `Aliases_Feature` config file.
*
* @param sourceName Name that alias source was added as to
* `Aliases_Feature`.
* @return Instance of the requested `BaseAliasSource`,
* `none` if `sourceName` is `none`, does not refer to any alias source
* or `Aliases_Feature` is disabled.
*/
public final function BaseAliasSource GetCustomSource(BaseText sourceName)
{
local Aliases_Feature feature;
if (sourceName == none) {
return none;
}
feature =
Aliases_Feature(class'Aliases_Feature'.static.GetEnabledInstance());
if (feature != none) {
return feature.GetCustomSource(sourceName);
}
return none;
}
/**
* Returns `BaseAliasSource` that is designated in configuration files as
* a source for weapon aliases.
*
* @return Reference to the `BaseAliasSource` that contains weapon aliases.
* Can return `none` if no source for weapons was configured or
* the configured source is incorrectly defined.
*/
public final function BaseAliasSource GetWeaponSource()
{
if (weaponAliasSource != none) {
weaponAliasSource.NewRef();
}
return weaponAliasSource;
}
/**
* Returns `BaseAliasSource` that is designated in configuration files as
* a source for color aliases.
*
* @return Reference to the `BaseAliasSource` that contains color aliases.
* Can return `none` if no source for colors was configured or
* the configured source is incorrectly defined.
*/
public final function BaseAliasSource GetColorSource()
{
if (colorAliasSource != none) {
colorAliasSource.NewRef();
}
return colorAliasSource;
}
/**
* Returns `BaseAliasSource` that is designated in configuration files as
* a source for feature aliases.
*
* @return Reference to the `BaseAliasSource` that contains feature aliases.
* Can return `none` if no source for features was configured or
* the configured source is incorrectly defined.
*/
public final function BaseAliasSource GetFeatureSource()
{
if (featureAliasSource != none) {
featureAliasSource.NewRef();
}
return featureAliasSource;
}
/**
* Returns `BaseAliasSource` that is designated in configuration files as
* a source for entity aliases.
*
* @return Reference to the `BaseAliasSource` that contains entity aliases.
* Can return `none` if no source for entities was configured or
* the configured source is incorrectly defined.
*/
public final function BaseAliasSource GetEntitySource()
{
if (entityAliasSource != none) {
entityAliasSource.NewRef();
}
return entityAliasSource;
}
/**
* Returns `BaseAliasSource` that is designated in configuration files as
* a source for command aliases.
*
* Command aliases values can have the format of "<command>" or
* "<command> <sub-command>".
*
* @return Reference to the `BaseAliasSource` that contains command aliases.
* Can return `none` if no source for command was configured or
* the configured source is incorrectly defined.
*/
public final function BaseAliasSource GetCommandSource()
{
if (commandAliasSource != none) {
commandAliasSource.NewRef();
}
return commandAliasSource;
}
private final function Text ResolveWithSource(
BaseText alias,
BaseAliasSource source,
optional bool copyOnFailure)
{
local Text result;
local Text trimmedAlias;
if (alias == none) {
return none;
}
if (alias.StartsWith(P("$"))) {
trimmedAlias = alias.Copy(1);
}
else {
trimmedAlias = alias.Copy();
}
if (source != none) {
result = source.Resolve(trimmedAlias, copyOnFailure);
}
else if (copyOnFailure) {
result = trimmedAlias.Copy();
}
trimmedAlias.FreeSelf();
return result;
}
/**
* Tries to look up a value stored for given alias in an `BaseAliasSource`
* configured to store weapon aliases.
*
* In Acedia aliases are typically prefixed with '$' to indicate that user
* means to enter alias. This method is able to handle both aliases with and
* without that prefix. This does not lead to conflicts, because '$' cannot
* be a valid part of any alias.
*
* 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.
*
* @param alias Alias, for which method will attempt to
* look up a value. Case-insensitive.
* @param copyOnFailure Whether method should return copy of original
* `alias` value in case caller source did not have any records
* corresponding to `alias`. If `alias` was specified with '$' prefix -
* it will be discarded.
* @return If look up was successful - value, associated with the given
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
* flag: `copyOnFailure == false` means method will return `none`
* and `copyOnFailure == true` means method will return `alias.Copy()`.
* If `alias == none` method always returns `none`.
*/
public final function Text ResolveWeapon(
BaseText alias,
optional bool copyOnFailure)
{
return ResolveWithSource(alias, weaponAliasSource, copyOnFailure);
}
/**
* Tries to look up a value stored for given alias in an `BaseAliasSource`
* configured to store color aliases.
*
* In Acedia aliases are typically prefixed with '$' to indicate that user
* means to enter alias. This method is able to handle both aliases with and
* without that prefix. This does not lead to conflicts, because '$' cannot
* be a valid part of any alias.
*
* 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.
*
* @param alias Alias, for which method will attempt to
* look up a value. Case-insensitive.
* @param copyOnFailure Whether method should return copy of original
* `alias` value in case caller source did not have any records
* corresponding to `alias`. If `alias` was specified with '$' prefix -
* it will be discarded.
* @return If look up was successful - value, associated with the given
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
* flag: `copyOnFailure == false` means method will return `none`
* and `copyOnFailure == true` means method will return `alias.Copy()`.
* If `alias == none` method always returns `none`.
*/
public final function Text ResolveColor(
BaseText alias,
optional bool copyOnFailure)
{
return ResolveWithSource(alias, colorAliasSource, copyOnFailure);
}
/**
* Tries to look up a value stored for given alias in an `BaseAliasSource`
* configured to store feature aliases.
*
* In Acedia aliases are typically prefixed with '$' to indicate that user
* means to enter alias. This method is able to handle both aliases with and
* without that prefix. This does not lead to conflicts, because '$' cannot
* be a valid part of any alias.
*
* Lookup of alias can fail if either alias does not exist in feature alias
* source or feature alias source itself does not exist
* (due to either faulty configuration or incorrect definition).
* To determine if feature alias source exists you can check
* `_.alias.GetFeatureSource()` value.
*
* @param alias Alias, for which method will attempt to
* look up a value. Case-insensitive.
* @param copyOnFailure Whether method should return copy of original
* `alias` value in case caller source did not have any records
* corresponding to `alias`. If `alias` was specified with '$' prefix -
* it will be discarded.
* @return If look up was successful - value, associated with the given
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
* flag: `copyOnFailure == false` means method will return `none`
* and `copyOnFailure == true` means method will return `alias.Copy()`.
* If `alias == none` method always returns `none`.
*/
public final function Text ResolveFeature(
BaseText alias,
optional bool copyOnFailure)
{
return ResolveWithSource(alias, featureAliasSource, copyOnFailure);
}
/**
* Tries to look up a value stored for given alias in an `BaseAliasSource`
* configured to store entity aliases.
*
* In Acedia aliases are typically prefixed with '$' to indicate that user
* means to enter alias. This method is able to handle both aliases with and
* without that prefix. This does not lead to conflicts, because '$' cannot
* be a valid part of any alias.
*
* Lookup of alias can fail if either alias does not exist in entity alias
* source or entity alias source itself does not exist
* (due to either faulty configuration or incorrect definition).
* To determine if entity alias source exists you can check
* `_.alias.GetEntitySource()` value.
*
* @param alias Alias, for which method will attempt to
* look up a value. Case-insensitive.
* @param copyOnFailure Whether method should return copy of original
* `alias` value in case caller source did not have any records
* corresponding to `alias`. If `alias` was specified with '$' prefix -
* it will be discarded.
* @return If look up was successful - value, associated with the given
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
* flag: `copyOnFailure == false` means method will return `none`
* and `copyOnFailure == true` means method will return `alias.Copy()`.
* If `alias == none` method always returns `none`.
*/
public final function Text ResolveEntity(
BaseText alias,
optional bool copyOnFailure)
{
return ResolveWithSource(alias, entityAliasSource, copyOnFailure);
}
/**
* Tries to look up a value stored for given alias in an `BaseAliasSource`
* configured to store command aliases.
*
* Command aliases values can have the format of "<command>" or
* "<command> <sub-command>".
*
* In Acedia aliases are typically prefixed with '$' to indicate that user
* means to enter alias. This method is able to handle both aliases with and
* without that prefix. This does not lead to conflicts, because '$' cannot
* be a valid part of any alias.
*
* Lookup of alias can fail if either alias does not exist in command alias
* source or command alias source itself does not exist
* (due to either faulty configuration or incorrect definition).
* To determine if command alias source exists you can check
* `_.alias.GetCommandSource()` value.
*
* @param alias Alias, for which method will attempt to
* look up a value. Case-insensitive.
* @param copyOnFailure Whether method should return copy of original
* `alias` value in case caller source did not have any records
* corresponding to `alias`. If `alias` was specified with '$' prefix -
* it will be discarded.
* @return If look up was successful - value, associated with the given
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
* flag: `copyOnFailure == false` means method will return `none`
* and `copyOnFailure == true` means method will return `alias.Copy()`.
* If `alias == none` method always returns `none`.
*/
public final function Text ResolveCommand(
BaseText alias,
optional bool copyOnFailure)
{
return ResolveWithSource(alias, commandAliasSource, copyOnFailure);
}
/**
* Tries to look up a value stored for given alias in a custom alias source
* with a given name `sourceName`.
*
* In Acedia aliases are typically prefixed with '$' to indicate that user
* means to enter alias. This method is able to handle both aliases with and
* without that prefix. This does not lead to conflicts, because '$' cannot
* be a valid part of any alias.
*
* Custom alias sources are any type of alias source that isn't built-in into
* Acedia. They can either be added manually by the admin through config file.
*
* Lookup of alias can fail if either alias does not exist in specified
* alias source or that alias source itself does not exist.
* To determine if specified alias source exists you can check
* `_.alias.GetCustomSource()` value.
*
* @param alias Alias, for which method will attempt to
* look up a value. Case-insensitive.
* @param copyOnFailure Whether method should return copy of original
* `alias` value in case caller source did not have any records
* corresponding to `alias`. If `alias` was specified with '$' prefix -
* it will be discarded.
* @return If look up was successful - value, associated with the given
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
* flag: `copyOnFailure == false` means method will return `none`
* and `copyOnFailure == true` means method will return `alias.Copy()`.
* If `alias == none` method always returns `none`.
*/
public final function Text ResolveCustom(
BaseText sourceName,
BaseText alias,
optional bool copyOnFailure)
{
local BaseAliasSource customSource;
customSource = GetCustomSource(sourceName);
if (customSource == none)
{
if (copyOnFailure && alias != none) {
return alias.Copy();
}
return none;
}
return ResolveWithSource(alias, customSource, copyOnFailure);
}
defaultproperties
{
}

View File

@ -0,0 +1,205 @@
/**
* 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 `AliasesStorage` object can store several aliases for a single
* value.
* It is recommended that you do not try to access these objects directly.
* `AliasesStorage` is abstract, so it can never be created, for any
* actual use create a child class. Suggested name scheme is
* "<something>Aliases", like "ColorAliases" or "WeaponAliases".
* 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).
* [INTERNAL] This is an internal object and should not be directly modified,
* its only allowed use is documented way to create new alias sources.
* Copyright 2019-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class AliasesStorage extends AcediaObject
perObjectConfig
config(AcediaAliases)
abstract;
// Aliases, recorded by this `AliasesStorage` object that all refer to
// the same value, defined by this object's name `string(self.name)`
var protected config array<string> alias;
// Name of the configurational file (without extension) where
// this `AliasSource`'s data will be stored
var protected const string configName;
// Link to the `AliasSource` that uses `AliasesStorage` objects of this class.
// To ensure that any `AliasesStorage` sub-class only belongs to one
// `AliasSource`.
var public const class<AliasSource> sourceClass;
// `true` means that this `AliasesStorage` object is awaiting saving into its
// config
var private bool pendingSaveToConfig;
// Since:
// 1. '.'s in values are converted into ':' for storage purposes;
// 2. We have to store values in `string` to make use of config files.
// we need methods to convert between "storage" (`string`)
// and "actual" (`Text`) value version.
// `ToStorageVersion()` and `ToActualVersion()` do that.
private final static function string ToStorageVersion(BaseText actualValue)
{
return Repl(actualValue.ToString(), ".", ":");
}
// See comment to `ToStorageVersion()`
private final static function Text ToActualVersion(string storageValue)
{
return __().text.FromString(Repl(storageValue, ":", "."));
}
/**
* Loads all `AliasesStorage` objects from their config file
* (defined in paired `AliasSource` class).
*
* This is an internal method and returned `AliasesStorage` objects require
* a special treatment: they aren't meant to be used with Acedia's reference
* counting - never release their references.
*
* @return Array of all `Aliases` objects, loaded from their config file.
*/
public static final function array<AliasesStorage> LoadAllObjects()
{
local int i;
local array<string> objectNames;
local array<AliasesStorage> loadedAliasObjects;
objectNames = GetPerObjectNames(default.configName,
string(default.class.name), MaxInt);
for (i = 0; i < objectNames.length; i += 1) {
loadedAliasObjects[i] = LoadObjectByName(objectNames[i]);
}
return loadedAliasObjects;
}
// Loads a new `AliasesStorage` object by it's given name (`objectName`).
private static final function AliasesStorage LoadObjectByName(
string objectName)
{
local AliasesStorage result;
// Since `MemoryAPI` for now does not support specifying names
// to created objects - do some manual dark magic and
// initialize this shit ourselves. Don't repeat this at home, kids.
result = new(none, objectName) default.class;
result._constructor();
return result;
}
/**
* Loads a new `AliasesStorage` object based on the value (`aliasesValue`)
* of it's aliases.
*
* @param aliasesValue Value that aliases in this `Aliases` object will
* correspond to.
* @return Instance of `AliasesStorage` object with a given name.
*/
public static final function AliasesStorage LoadObject(BaseText aliasesValue)
{
if (aliasesValue != none) {
return LoadObjectByName(ToStorageVersion(aliasesValue));
}
return none;
}
/**
* Returns value that caller's `Aliases` object's aliases point to.
*
* @return Value, stored by this object.
*/
public final function Text GetValue()
{
return ToActualVersion(string(self.name));
}
/**
* Returns array of aliases that caller `AliasesStorage` tells us point to
* it's value.
*
* @return Array of all aliases, stored by caller `AliasesStorage` object.
*/
public final function array<Text> GetAliases()
{
local int i;
local array<Text> textAliases;
for (i = 0; i < alias.length; i += 1) {
textAliases[i] = _.text.FromString(alias[i]);
}
return textAliases;
}
/**
* [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 `AliasesStorage` object.
* Expected to be in lower case.
*/
public final function RemoveAlias_S(string aliasToRemove)
{
local int i;
local bool removedAlias;
while (i < alias.length)
{
if (aliasToRemove ~= alias[i])
{
alias.Remove(i, 1);
removedAlias = true;
}
else {
i += 1;
}
}
if (!pendingSaveToConfig)
{
pendingSaveToConfig = true;
_.scheduler.RequestDiskAccess(self).connect = SaveOrClear;
}
}
/**
* 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.
*/
private final function SaveOrClear()
{
if (alias.length <= 0) {
ClearConfig();
}
else {
SaveConfig();
}
pendingSaveToConfig = false;
}
defaultproperties
{
sourceClass = class'AliasSource'
configName = "AcediaAliases"
}

View File

@ -0,0 +1,263 @@
/**
* This feature provides a mechanism to define commands that automatically
* parse their arguments into standard Acedia collection. It also allows to
* manage them (and specify limitation on how they can be called) in a
* centralized manner.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Aliases_Feature extends Feature
dependson(Aliases);
struct ClassSourcePair
{
var class<AliasSource> class;
var AliasSource source;
};
// We don't want to reload `AliasSource`s several times when this feature is
// disabled and re-enabled or its config is swapped, so we keep all loaded
// sources here at all times and look them up from here
var private array<ClassSourcePair> loadedSources;
// Loaded `AliasSource`s
var private AliasSource weaponAliasSource;
var private AliasSource colorAliasSource;
var private AliasSource featureAliasSource;
var private AliasSource entityAliasSource;
var private AliasSource commandAliasSource;
// Everything else
var private HashTable customSources;
var private LoggerAPI.Definition errCannotLoadAliasSource, errEmptyName;
var private LoggerAPI.Definition errDuplicateCustomSource;
protected function OnEnabled()
{
_.alias._reloadSources();
}
protected function OnDisabled()
{
DropSources();
_.alias._reloadSources();
}
private function DropSources()
{
_.memory.Free(weaponAliasSource);
weaponAliasSource = none;
_.memory.Free(colorAliasSource);
colorAliasSource = none;
_.memory.Free(featureAliasSource);
featureAliasSource = none;
_.memory.Free(entityAliasSource);
entityAliasSource = none;
_.memory.Free(commandAliasSource);
commandAliasSource = none;
_.memory.Free(customSources);
customSources = none;
}
// Create each `AliasSource` instance only once to avoid any possible
// nonsense with loading named objects several times: alias sources don't use
// `AcediaConfig`s, so they don't automatically avoid named config reloading
private function AliasSource GetSource(class<AliasSource> sourceClass)
{
local int i;
local AliasSource newSource;
local ClassSourcePair newPair;
if (sourceClass == none) {
return none;
}
for (i = 0; i < loadedSources.length; i += 1)
{
if (loadedSources[i].class == sourceClass) {
return loadedSources[i].source;
}
}
newSource = AliasSource(_.memory.Allocate(sourceClass));
if (newSource != none)
{
newPair.class = sourceClass;
newPair.source = newSource;
// One reference we store, one we return
newSource.NewRef();
}
else {
_.logger.Auto(errCannotLoadAliasSource).ArgClass(sourceClass);
}
return newSource;
}
protected function SwapConfig(FeatureConfig config)
{
local Aliases newConfig;
newConfig = Aliases(config);
if (newConfig == none) {
return;
}
weaponAliasSource = GetSource(newConfig.weaponAliasSource);
colorAliasSource = GetSource(newConfig.colorAliasSource);
featureAliasSource = GetSource(newConfig.featureAliasSource);
entityAliasSource = GetSource(newConfig.entityAliasSource);
commandAliasSource = GetSource(newConfig.commandAliasSource);
LoadCustomSources(newConfig.customSource);
}
private function LoadCustomSources(
array<Aliases.CustomSourceRecord> configCustomSources)
{
local int i;
local bool reportedEmptyName;
local Text nextKey;
local AliasSource nextSource, conflictingSource;
_.memory.Free(customSources);
customSources = _.collections.EmptyHashTable();
for (i = 0; i < configCustomSources.length; i += 1)
{
if (configCustomSources[i].name == "" && !reportedEmptyName)
{
reportedEmptyName = true;
_.logger.Auto(errEmptyName);
}
nextKey = _.text.FromString(configCustomSources[i].name);
// We only store `AliasSource`s
conflictingSource = AliasSource(customSources.GetItem(nextKey));
if (conflictingSource != none)
{
_.logger.Auto(errDuplicateCustomSource)
.ArgClass(conflictingSource.class)
.Arg(nextKey) // Releases `nextKey`
.ArgClass(configCustomSources[i].source);
conflictingSource.FreeSelf();
continue;
}
nextSource = GetSource(configCustomSources[i].source);
if (nextSource != none)
{
customSources.SetItem(nextKey, nextSource);
nextSource.FreeSelf();
}
nextKey.FreeSelf();
}
}
/**
* Returns `AliasSource` for weapon aliases.
*
* @return `AliasSource`, configured to store weapon aliases.
*/
public function AliasSource GetWeaponSource()
{
if (weaponAliasSource != none) {
weaponAliasSource.Newref();
}
return weaponAliasSource;
}
/**
* Returns `AliasSource` for color aliases.
*
* @return `AliasSource`, configured to store color aliases.
*/
public function AliasSource GetColorSource()
{
if (colorAliasSource != none) {
colorAliasSource.Newref();
}
return colorAliasSource;
}
/**
* Returns `AliasSource` for feature aliases.
*
* @return `AliasSource`, configured to store feature aliases.
*/
public function AliasSource GetFeatureSource()
{
if (featureAliasSource != none) {
featureAliasSource.Newref();
}
return featureAliasSource;
}
/**
* Returns `AliasSource` for entity aliases.
*
* @return `AliasSource`, configured to store entity aliases.
*/
public function AliasSource GetEntitySource()
{
if (entityAliasSource != none) {
entityAliasSource.Newref();
}
return entityAliasSource;
}
/**
* Returns `AliasSource` for command aliases.
*
* @return `AliasSource`, configured to store command aliases.
*/
public function AliasSource GetCommandSource()
{
if (commandAliasSource != none) {
commandAliasSource.Newref();
}
return commandAliasSource;
}
/**
* Returns custom `AliasSource` with a given name.
*
* @return Custom `AliasSource`, configured with a given name `sourceName`.
*/
public function AliasSource GetCustomSource(BaseText sourceName)
{
if (sourceName == none) {
return none;
}
// We only store `AliasSource`s
return AliasSource(customSources.GetItem(sourceName));
}
/**
* Returns custom `AliasSource` with a given name `sourceName.
*
* @return Custom `AliasSource`, configured with a given name `sourceName`.
*/
public function AliasSource GetCustomSource_S(string sourceName)
{
local Text wrapper;
local AliasSource result;
wrapper = _.text.FromString(sourceName);
result = GetCustomSource(wrapper);
wrapper.FreeSelf();
return result;
}
defaultproperties
{
configClass = class'Aliases'
errEmptyName = (l=LOG_Error,m="Empty name provided for the custom alias source. This is likely due to an erroneous config.")
errCannotLoadAliasSource = (l=LOG_Error,m="Failed to load alias source class `%1`.")
errDuplicateCustomSource = (l=LOG_Error,m="Custom alias source `%1` is already registered with name '%2'. Alias source `%3` with the same name will be ignored.")
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,86 @@
/**
* Iterator for iterating over `ArrayList`'s items.
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ArrayListIterator extends CollectionIterator
dependson(ArrayList);
var private ArrayList relevantCollection;
var private int currentIndex;
var private bool skipNoneReferences;
protected function Finalizer()
{
relevantCollection = none;
skipNoneReferences = false;
}
public function bool Initialize(Collection relevantArray)
{
currentIndex = 0;
relevantCollection = ArrayList(relevantArray);
if (relevantCollection == none) {
return false;
}
return true;
}
public function LeaveOnlyNotNone()
{
skipNoneReferences = true;
}
public function bool Next()
{
local int collectionLength;
if (!skipNoneReferences)
{
currentIndex += 1;
return HasFinished();
}
collectionLength = relevantCollection.GetLength();
while (currentIndex < collectionLength)
{
currentIndex += 1;
if (!relevantCollection.IsNone(currentIndex)) {
return HasFinished();
}
}
return HasFinished();
}
public function AcediaObject Get()
{
return relevantCollection.GetItem(currentIndex);
}
public function AcediaObject GetKey()
{
return _.box.int(currentIndex);
}
public function bool HasFinished()
{
return currentIndex >= relevantCollection.GetLength();
}
defaultproperties
{
}

View File

@ -0,0 +1,110 @@
/**
* Config object for `Avarice_Feature`.
* Copyright 2021 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 Avarice extends FeatureConfig
perobjectconfig
config(AcediaAvarice);
struct AvariceLinkRecord
{
var string name;
var string address;
};
// List all the names (and addresses) of all Avarice instances Acedia must
// connect to.
// `name` parameter is a useful (case-insensitive) identifier that
// can be used in other configs to point at each link.
// `address` must have form "host:port", where "host" is either ip or
// domain name and "port" is a numerical port value.
var public config array<AvariceLinkRecord> link;
// In case Avarice utility is launched after link started trying open
// the connection - that connection attempt will fail. To fix that link must
// try connecting again.
// This variable sets the time (in seconds), after which link will
// re-attempt opening connection. Setting value too low can prevent any
// connection from opening, setting it too high might make you wait for
// connection too long.
var public config float reconnectTime;
protected function HashTable ToData()
{
local int i;
local HashTable data;
local HashTable linkData;
local ArrayList linksArray;
data = __().collections.EmptyHashTable();
data.SetFloat(P("reconnectTime"), reconnectTime, true);
linksArray = __().collections.EmptyArrayList();
data.SetItem(P("link"), linksArray);
for (i = 0; i < link.length; i += 1)
{
linkData = __().collections.EmptyHashTable();
linkData.SetString(P("name"), link[i].name);
linkData.SetString(P("address"), link[i].address);
linksArray.AddItem(linkData);
linkData.FreeSelf();
}
linksArray.FreeSelf();
return data;
}
protected function FromData(HashTable source)
{
local int i;
local ArrayList linksArray;
local HashTable nextLink;
local AvariceLinkRecord nextRecord;
if (source == none) {
return;
}
reconnectTime = source.GetFloat(P("reconnectTime"));
link.length = 0;
linksArray = source.GetArrayList(P("link"));
if (linksArray == none) {
return;
}
for (i = 0; i < linksArray.GetLength(); i += 1)
{
nextLink = linksArray.GetHashTable(i);
if (nextLink == none) {
continue;
}
nextRecord.name = nextLink.GetString(P("name"));
nextRecord.address = nextLink.GetString(P("address"));
link[i] = nextRecord;
_.memory.Free(nextLink);
}
_.memory.Free(linksArray);
}
protected function DefaultIt()
{
local AvariceLinkRecord defaultRecord;
reconnectTime = 10.0;
link.length = 0;
defaultRecord.name = "avarice";
defaultRecord.address = "127.0.0.1:1234";
link[0] = defaultRecord;
}
defaultproperties
{
configName = "AcediaAvarice"
}

View File

@ -0,0 +1,77 @@
/**
* API for Avarice functionality of Acedia.
* Copyright 2020 - 2021 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 AvariceAPI extends AcediaObject;
/**
* Method that returns all the `AvariceLink` created by `Avarice` feature.
*
* @return Array of links created by this feature.
* Guaranteed to not contain `none` values.
* Empty if `Avarice` feature is currently disabled.
*/
public final function array<AvariceLink> GetAllLinks()
{
local Avarice_Feature avariceFeature;
local array<AvariceLink> emptyResult;
avariceFeature =
Avarice_Feature(class'Avarice_Feature'.static.GetEnabledInstance());
if (avariceFeature != none) {
return avariceFeature.GetAllLinks();
}
return emptyResult;
}
/**
* Finds and returns `AvariceLink` by its name, specified in "AcediaAvarice"
* config, if it exists.
*
* @param linkName Name of the `AvariceLink` to find.
* @return `AvariceLink` corresponding to name `linkName`.
* If `linkName == none` or `AvariceLink` with such name does not exist -
* returns `none`.
*/
public final function AvariceLink GetLink(BaseText linkName)
{
local int i;
local Text nextName;
local array<AvariceLink> allLinks;
if (linkName == none) {
return none;
}
allLinks = GetAllLinks();
for (i = 0; i < allLinks.length; i += 1)
{
if (allLinks[i] == none) {
continue;
}
nextName = allLinks[i].GetName();
if (linkName.Compare(nextName, SCASE_INSENSITIVE))
{
_.memory.Free(nextName);
return allLinks[i];
}
_.memory.Free(nextName);
}
return none;
}
defaultproperties
{
}

View File

@ -0,0 +1,443 @@
/**
* Provide interface for the connection to Avarice application.
* It's parameters are defined in Acedia's config.
* Class provides methods to obtain its configuration information
* (name, address, port), methods to check and change the status of connection,
* signals to handle arriving messages and ways to send messages back.
* Copyright 2021 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 AvariceLink extends AcediaObject;
/**
* Objects of this class are supposed to be obtained via the
* `AvariceAPI.GetLink()` method. Available links are automatically initialized
* based on the configs and their parameters cannot be changed.
* It is also possible to spawn a link of your own by creating an object of
* this class (`AvariceLink`) and calling `Initialize()` method with
* appropriate parameters. To start the link then simply call `StartUp()`.
* But such links will not appear in the list of available links in
* `AvariceAPI`.
*/
// Actual work of dealing with network input/output is done in
// the `AvariceTcpStream` `Actor` class that is stored inside this reference
var private NativeActorRef tcpStream;
// `tcpStream` communicates with this class by informing it about specific
// events. This enum describes all of their types.
enum AvariceNetworkMessage
{
// Connection with Avarice established - can happen several times in case
// connection is interrupted
ANM_Connected,
// We have lost connection with Avarice, but normally will attempt to
// reconnect back
ANM_Disconnected,
// JSON message received
ANM_Message,
// Connection died: either was manually closed, host address could not
// be resolved or invalid data was received from Avarice
ANM_Death
};
// Name of this link, specified in the config
var private Text linkName;
// Host of the Avarice instance we are connecting to
var private Text linkHost;
// Port used by the Avarice instance we are connecting to
var private int linkPort;
var private SimpleSignal onConnectedSignal;
var private SimpleSignal onDisconnectedSignal;
var private SimpleSignal onDeathSignal;
// We want to have a separate signal for each message "service", since most
// users of `AvariceLink` would only care about one particular service.
// To achieve that we use this array as a "service name" <-> "signal" map.
var private HashTable serviceSignalMap;
var private const int TSERVICE_PREFIX, TTYPE_PREFIX;
var private const int TPARAMS_PREFIX, TMESSAGE_SUFFIX;
var private LoggerAPI.Definition fatalCannotSpawn;
protected function Constructor()
{
onConnectedSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
onDisconnectedSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
onDeathSignal = SimpleSignal(_.memory.Allocate(class'SimpleSignal'));
serviceSignalMap = _.collections.EmptyHashTable();
}
protected function Finalizer()
{
local Actor storedStream;
_.memory.Free(onConnectedSignal);
_.memory.Free(onDisconnectedSignal);
_.memory.Free(onDeathSignal);
_.memory.Free(serviceSignalMap);
_.memory.Free(linkName);
_.memory.Free(linkHost);
onConnectedSignal = none;
onDisconnectedSignal = none;
onDeathSignal = none;
serviceSignalMap = none;
linkName = none;
linkHost = none;
linkPort = 0;
if (tcpStream == none) {
return;
}
storedStream = tcpStream.Get();
if (storedStream != none) {
storedStream.Destroy();
}
tcpStream.FreeSelf();
tcpStream = none;
}
/**
* Initializes this caller `AvariceLink` with config data.
*
* Can only successfully (for that `name` and `host` must not be `none`)
* be called once.
*
* @param name Alias (case-insensitive) of caller `AvariceLink`.
* Must not be `none`.
* @param host Host of the Avarice instance that caller `AvariceLink` is
* connecting to. Must not be `none`.
* @param name Port used by the Avarice instance that caller `AvariceLink`
* is connecting to.
*/
public final function Initialize(BaseText name, BaseText host, int port)
{
if (tcpStream != none) return;
if (name == none) return;
if (host == none) return;
linkName = name.Copy();
linkHost = host.Copy();
linkPort = port;
tcpStream = _server.unreal.ActorRef(none);
}
/**
* Signal that will be emitted whenever caller `AvariceLink` connects to
* Avarice. This event can be emitted multiple times if case link temporarily
* loses it's TCP connection or if connection is killed off due to errors
* (or manually).
*
* [Signature]
* void <slot>()
*/
/* SIGNAL */
public final function SimpleSlot OnConnected(AcediaObject receiver)
{
return SimpleSlot(onConnectedSignal.NewSlot(receiver));
}
/**
* Signal that will be emitted whenever caller `AvariceLink` disconnects from
* Avarice. Disconnects can temporarily be cause by network issue and
* `AvariceLink` will attempt to restore it's connection. To detect when
* connection was permanently severed use `OnDeath` signal instead.
*
* [Signature]
* void <slot>()
*/
/* SIGNAL */
public final function SimpleSlot OnDisconnected(AcediaObject receiver)
{
return SimpleSlot(onDisconnectedSignal.NewSlot(receiver));
}
/**
* Signal that will be emitted whenever connection is closed and dropped:
* either due to bein unable to resolve host's address, receiving incorrect
* input from Avarice or someone manually closing it.
*
* [Signature]
* void <slot>()
*/
/* SIGNAL */
public final function SimpleSlot OnDeath(AcediaObject receiver)
{
return SimpleSlot(onDeathSignal.NewSlot(receiver));
}
/**
* Signal that will be emitted whenever caller `AvariceLink` disconnects from
* Avarice. Disconnects can temporarily be cause by network issue and
* `AvariceLink` will attempt to restore it's connection. To detect when
* connection was permanently severed use `OnDeath` signal instead.
*
* @param service Name of the service, whos messages one wants to receive.
* `none` will be treated as an empty `Text`.
*
* [Signature]
* void <slot>(AvariceLink link, AvariceMessage message)
* @param link Link that has received message.
* @param message Received message.
* Can be any JSON-compatible value (see `JSONAPI.IsCompatible()`
* for more information).
*/
/* SIGNAL */
public final function Avarice_OnMessage_Slot OnMessage(
AcediaObject receiver,
BaseText service)
{
return Avarice_OnMessage_Slot(GetServiceSignal(service).NewSlot(receiver));
}
private final function Avarice_OnMessage_Signal GetServiceSignal(
BaseText service)
{
local Avarice_OnMessage_Signal result;
if (service != none) {
service = service.Copy();
}
else {
service = Text(_.memory.Allocate(class'Text'));
}
result = Avarice_OnMessage_Signal(serviceSignalMap.GetItem(service));
if (result == none)
{
result = Avarice_OnMessage_Signal(
_.memory.Allocate(class'Avarice_OnMessage_Signal'));
serviceSignalMap.SetItem(service, result);
_.memory.Free(result);
}
else {
service.FreeSelf();
}
return result;
}
/**
* Starts caller `AvariceLink`, making it attempt to connect to the Avarice
* with parameters that should be first specified by the `Initialize()` call.
*
* Does nothing if the caller `AvariceLink` is either not initialized or
* is already active (`IsActive() == true`).
*/
public final function StartUp()
{
local AvariceTcpStream newStream;
local LevelCore core;
if (tcpStream == none) return;
if (tcpStream.Get() != none) return;
core = __level().GetLevelCore();
if (core == none) return;
newStream = AvariceTcpStream(core.Allocate(class'AvariceTcpStream'));
if (newStream == none)
{
// `linkName` has to be defined if `tcpStream` is defined
_.logger.Auto(fatalCannotSpawn).Arg(linkName.Copy());
return;
}
tcpStream.Set(newStream);
newStream.StartUp(self, class'Avarice_Feature'.static.GetReconnectTime());
}
/**
* Shuts down any connections related to the caller `AvariceLink`.
*
* Does nothing if the caller `AvariceLink` is either not initialized or
* is already inactive (`IsActive() == false`).
*/
public final function ShutDown()
{
local Actor storedStream;
if (tcpStream == none) return;
storedStream = tcpStream.Get();
if (storedStream == none) return;
storedStream.Destroy();
tcpStream.Set(none);
}
/**
* Checks whether caller `AvariceLink` is currently active: either connected or
* currently attempts to connect to Avarice.
*
* See also `IsConnected()`.
*
* @return `true` if caller `AvariceLink` is either connected or currently
* attempting to connect to Avarice. `false` otherwise.
*/
public final function bool IsActive()
{
if (tcpStream == none) {
return false;
}
return tcpStream.Get() != none;
}
/**
* Checks whether caller `AvariceLink` is currently connected or to Avarice.
*
* See also `IsActive()`.
*
* @return `true` iff caller `AvariceLink` is currently connected to Avarice.
*/
public final function bool IsConnected()
{
local AvariceTcpStream storedStream;
if (tcpStream == none) {
return false;
}
storedStream = AvariceTcpStream(tcpStream.Get());
return storedStream.linkState == STATE_Connected;
}
/**
* Returns name caller `AvariceLink` was initialized with.
* Defined through the config files.
*
* @return Name of the caller `AvariceLink`.
* `none` iff caller link was not yet initialized.
*/
public final function Text GetName()
{
if (linkName != none) {
return linkName.Copy();
}
// `linkName` cannot be `none` after `Initialize()` call
return none;
}
/**
* Returns host name (without port number) caller `AvariceLink` was
* initialized with. Defined through the config files.
*
* See `GetPort()` method for port number.
*
* @return Host name of the caller `AvariceLink`.
* `none` iff caller link was not yet initialized.
*/
public final function Text GetHost()
{
if (linkHost != none) {
return linkHost.Copy();
}
// `linkName` cannot be `none` after `Initialize()` call
return none;
}
/**
* Returns port number caller `AvariceLink` was initialized with.
* Defined through the config files.
*
* @return Host name of the caller `AvariceLink`.
* If caller link was not yet initialized, method makes no guarantees
* about returned number.
*/
public final function int GetPort()
{
return linkPort;
}
/**
* Send a message to the Avarice that caller `AvariceLink` is connected to.
*
* Message can only be set if caller `AvariceLink` was initialized and is
* currently connected (see `IsConnected()`) to Avarice.
*
* @param service Name of the service this message is addressed.
* As an example, to address a database one would specify its name,
* like "db". Cannot be `none`.
* @param type Name of this message. As an example, to address
* a database, one would specify here WHAT that database must do,
* like "get" to fetch some data. Cannot be `none`.
* @param parameters Parameters of the command. Can be any value that is
* JSON-compatible (see `JSONAPI.IsCompatible()` for details).
* @return `true` if message was successfully sent and `false` otherwise.
* Note that this method returning `true` does not necessarily mean that
* message has arrived (which is impossible to know at this moment),
* instead simply saying that network call to send data was successful.
* Avarice does not provide any mechanism to verify message arrival, so if
* you need that confirmation - it is necessary that service you are
* addressing make a reply.
*/
public final function bool SendMessage(
BaseText service,
BaseText type,
AcediaObject parameters)
{
local Mutabletext parametesAsJSON;
local MutableText message;
local AvariceTcpStream storedStream;
if (tcpStream == none) return false;
if (service == none) return false;
if (type == none) return false;
storedStream = AvariceTcpStream(tcpStream.Get());
if (storedStream == none) return false;
if (storedStream.linkState != STATE_Connected) return false;
parametesAsJSON = _.json.Print(parameters);
if (parametesAsJSON == none) return false;
message = _.text.Empty();
message.Append(T(TSERVICE_PREFIX))
.Append(_.json.Print(service))
.Append(T(TTYPE_PREFIX))
.Append(_.json.Print(type))
.Append(T(TPARAMS_PREFIX))
.Append(parametesAsJSON)
.Append(T(TMESSAGE_SUFFIX));
storedStream.SendMessage(message);
message.FreeSelf();
parametesAsJSON.FreeSelf();
return true;
}
// This is a public method, but it is not a part of
// `AvariceLink` interface.
// It is used as a communication channel with `AvariceTcpStream` and
// should not be called outside of that class.
public final function ReceiveNetworkMessage(
AvariceNetworkMessage message,
optional AvariceMessage avariceMessage)
{
if (message == ANM_Connected) {
onConnectedSignal.Emit();
}
else if (message == ANM_Disconnected) {
onDisconnectedSignal.Emit();
}
else if (message == ANM_Message && avariceMessage != none) {
GetServiceSignal(avariceMessage.service).Emit(self, avariceMessage);
}
else if (message == ANM_Death) {
onDeathSignal.Emit();
tcpStream.Set(none);
}
}
defaultproperties
{
TSERVICE_PREFIX = 0
stringConstants(0) = "&{\"s\":"
TTYPE_PREFIX = 1
stringConstants(1) = ",\"t\":"
TPARAMS_PREFIX = 2
stringConstants(2) = ",\"p\":"
TMESSAGE_SUFFIX = 3
stringConstants(3) = "&}"
fatalCannotSpawn = (l=LOG_Error,m="Cannot spawn new actor of class `AvariceTcpStream`, avarice link \"%1\" will not be created")
}

View File

@ -0,0 +1,97 @@
/**
* Object that represents a message received from Avarice.
* For performance's sake it does not provide a getter/setter interface and
* exposes public fields instead. However, for Acedia to correctly function
* you are not supposed modify those fields in any way, only using them to
* read necessary data.
* All `AvariceMessage`'s fields will be automatically deallocated, so if
* you need their data - you have to make a copy, instead of simply storing
* a reference to them.
* Copyright 2021 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 AvariceMessage extends AcediaObject;
// Every message from Avarice has following structure:
// { "s": "<service_name>", "t": "<command_type>", "p": <any_json_value> }
// Value of the "s" field
var public Text service;
// Value of the "t" field
var public Text type;
// Value of the "p" field
var public AcediaObject parameters;
var private HashTable messageTemplate;
var private const int TS, TT, TP;
public static function StaticConstructor()
{
if (StaticConstructorGuard()) return;
super.StaticConstructor();
default.messageTemplate = __().collections.EmptyHashTable();
ResetTemplate(default.messageTemplate);
}
protected function Finalizer()
{
__().memory.Free(type);
__().memory.Free(service);
__().memory.Free(parameters);
type = none;
service = none;
parameters = none;
}
private static final function ResetTemplate(HashTable template)
{
if (template == none) {
return;
}
template.SetItem(T(default.TS), none);
template.SetItem(T(default.TT), none);
template.SetItem(T(default.TP), none);
}
public final function MutableText ToText()
{
local MutableText result;
local HashTable template;
if (type == none) return none;
if (service == none) return none;
template = default.messageTemplate;
ResetTemplate(template);
template.SetItem(T(TT), type);
template.SetItem(T(TS), service);
if (parameters != none) {
template.SetItem(T(TP), parameters);
}
result = _.json.Print(template);
return result;
}
defaultproperties
{
TS = 0
stringConstants(0) = "s"
TT = 1
stringConstants(1) = "t"
TP = 2
stringConstants(2) = "p"
}

View File

@ -0,0 +1,149 @@
/**
* Helper class meant for reading byte stream sent to us by the Avarice
* application.
* Avarice sends us utf8-encoded JSONs one-by-one, prepending each of them
* with 4 bytes (big endian) that encode the length of the following message.
* Copyright 2021 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 AvariceStreamReader extends AcediaObject;
// Are we currently reading length of the message (`true`) or
// the message itself (`false`)?
var private bool readingLength;
// How many byte we have read so far.
// Resets to zero when we finish reading either length of the message or
// the message itself.
var private int readBytes;
// Expected length of the next message
var private int nextMessageLength;
// Message read so far
var private ByteArrayRef nextMessage;
// All the messages we have fully read, but did not yet return
var private array<MutableText> outputQueue;
// For converting read messages into `MutableText`
var private Utf8Decoder decoder;
// Set to `true` if Avarice input was somehow unacceptable.
// Cannot be recovered from.
var private bool hasFailed;
// Maximum allowed size of JSON message sent from avarice;
// Anything more than that is treated as a mistake.
// TODO: make this configurable
var private const int MAX_MESSAGE_LENGTH;
protected function Constructor()
{
readingLength = true;
nextMessage = ByteArrayRef(_.memory.Allocate(class'ByteArrayRef'));
decoder = Utf8Decoder(_.memory.Allocate(class'Utf8Decoder'));
}
protected function Finalizer()
{
_.memory.FreeMany(outputQueue);
_.memory.Free(nextMessage);
_.memory.Free(decoder);
outputQueue.length = 0;
nextMessage = none;
decoder = none;
hasFailed = false;
}
/**
* Adds next `byte` from the input Avarice stream to the reader.
*
* If input stream signals that message we have to read is too long
* (longer then `MAX_MESSAGE_LENGTH`) - enters a failed state and will
* no longer accept any input. Failed status can be checked with
* `Failed()` method.
* Otherwise cannot fail.
*
* @param nextByte Next byte from the Avarice input stream.
* @return `false` if caller `AvariceStreamReader` is in a failed state
* (including if it entered one after pushing this byte)
* and `true` otherwise.
*/
public final function bool PushByte(byte nextByte)
{
if (hasFailed) {
return false;
}
if (readingLength)
{
// Make space for the next 8 bits by shifting previously recorded ones
nextMessageLength = nextMessageLength << 8;
nextMessageLength += nextByte;
readBytes += 1;
if (readBytes >= 4)
{
readingLength = false;
readBytes = 0;
}
// Message either too long or so long it overfilled `MaxInt`
if ( nextMessageLength > MAX_MESSAGE_LENGTH
|| nextMessageLength < 0)
{
hasFailed = true;
return false;
}
return true;
}
nextMessage.AddItem(nextByte);
readBytes += 1;
if (readBytes >= nextMessageLength)
{
outputQueue[outputQueue.length] = decoder.Decode(nextMessage);
nextMessage.Empty();
readingLength = true;
readBytes = 0;
nextMessageLength = 0;
}
return true;
}
/**
* Returns all complete messages read so far.
*
* Even if caller `AvariceStreamReader` entered a failed state - this method
* will return all the messages read before it has failed.
*
* @return aAl complete messages read from Avarice stream so far
*/
public final function array<MutableText> PopMessages()
{
local array<MutableText> result;
result = outputQueue;
outputQueue.length = 0;
return result;
}
/**
* Is caller `AvariceStreamReader` in a failed state?
* See `PushByte()` method for details.
*
* @return `true` iff caller `AvariceStreamReader` has failed.
*/
public final function bool Failed()
{
return hasFailed;
}
defaultproperties
{
MAX_MESSAGE_LENGTH = 26214400 // 25 * 1024 * 1024 = 25MB
}

View File

@ -0,0 +1,387 @@
/**
* Acedia's `TcpLink` class for connecting to Avarice.
* This class should be considered an internal implementation detail and not
* accessed directly.
* Copyright 2021 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 AvariceTcpStream extends TcpLink
dependson(LoggerAPI);
var private Global _;
// Reference to the link that has spawned us, to pass on messages.
var private AvariceLink ownerLink;
// Information needed for connection
var private string linkHost;
var private int linkPort;
var private IpAddr remoteAddress;
// If `OpenNoSteam()` (inside `OpenAddress()`) call is made before Avarice
// application started - connection will not succeed. Because of that
// `AvariceTcpStream` will keep restarting its attempt to connect if it waited
// for connection to go through for long enough.
// This variable is set when `AvariceTcpStream` is initialized and it
// defines how long we have to wait before reconnection attempt.
var private float reconnectInterval;
// This variable track how much time has passed since `OpenNoSteam()` call.
var private float timeSpentConnecting;
// For reconnections we need to remember whether we have already bound our
// port, otherwise it will lead to errors in logs.
var private bool portBound;
// Array used to read to and write from TCP connection.
// All the native methods use it, so avoid creating it locally in our methods.
var private byte buffer[255];
// Used to convert our messages in a way appropriate for the network
var private Utf8Encoder encoder;
// Used to read and correctly interpret byte stream from Avarice
var private AvariceStreamReader avariceReader;
// Arbitrary value indicating that next byte sequence from us reports amount of
// bytes received so far
var private byte HEAD_BYTES_RECEIVED;
// Arbitrary value indicating that next byte sequence from us contains
// JSON message (prepended by it's length)
var private byte HEAD_MESSAGE;
// Byte mask to extract lowest byte from the `int`s:
// 00000000 00000000 00000000 11111111
var private int byteMask;
// `Text` values to be used as keys for getting information from
// received messages
var private Text keyS, keyT, keyP;
var private LoggerAPI.Definition infoConnected;
var private LoggerAPI.Definition infoDisconnected;
var private LoggerAPI.Definition fatalNoLink;
var private LoggerAPI.Definition fatalBadPort;
var private LoggerAPI.Definition fatalCannotBindPort;
var private LoggerAPI.Definition fatalCannotResolveHost;
var private LoggerAPI.Definition fatalCannotConnect;
var private LoggerAPI.Definition fatalInvaliddUTF8;
var private LoggerAPI.Definition fatalInvalidMessage;
// Starts this link, to stop it - simply destroy it
public final function StartUp(AvariceLink link, float reconnectTime)
{
if (link == none)
{
Destroy();
return;
}
ownerLink = link;
// Apparently `TcpLink` ignores default values for these variables,
// so we set them here
linkMode = MODE_Binary;
receiveMode = RMODE_Manual;
// This actor does not have `AcediaActor` for a parent, so manually
// define `_` for convenience
_ = class'Global'.static.GetInstance();
// Necessary constants
keyS = _.text.FromString("s");
keyT = _.text.FromString("t");
keyP = _.text.FromString("p");
// For decoding input and encoding output
encoder = Utf8Encoder(_.memory.Allocate(class'Utf8Encoder'));
avariceReader =
AvariceStreamReader(_.memory.Allocate(class'AvariceStreamReader'));
linkHost = _.text.IntoString(link.GetHost());
linkPort = link.GetPort();
reconnectInterval = reconnectTime;
TryConnecting();
}
private final function TryConnecting()
{
if (linkPort <= 0)
{
_.logger.Auto(fatalBadPort)
.ArgInt(linkPort)
.Arg(ownerLink.GetName());
Destroy();
return;
}
// `linkPort` is a port we are connecting to, which is different
// from the port we have to bind to use `OpenNoSteam()` method
if (!portBound && BindPort(, true) <= 0)
{
_.logger.Auto(fatalCannotBindPort)
.Arg(ownerLink.GetName());
Destroy();
return;
}
portBound = true;
// Try to read `linkHost` as an IP address first, in case of failure -
// try to resolve it from the host name
StringToIpAddr(linkHost, remoteAddress);
remoteAddress.port = linkPort;
if (remoteAddress.addr == 0) {
Resolve(linkHost);
}
else {
OpenAddress();
}
timeSpentConnecting = 0.0;
}
event Resolved(IpAddr resolvedAddress)
{
remoteAddress.addr = resolvedAddress.addr;
OpenAddress();
}
event ResolveFailed()
{
_.logger.Auto(fatalCannotResolveHost).Arg(_.text.FromString(linkHost));
Destroy();
}
private final function OpenAddress()
{
if (!OpenNoSteam(remoteAddress))
{
_.logger.Auto(fatalCannotConnect).Arg(ownerLink.GetName());
Destroy();
}
}
event Opened()
{
_.logger.Auto(infoConnected).Arg(ownerLink.GetName());
if (ownerLink != none) {
ownerLink.ReceiveNetworkMessage(ANM_Connected);
}
else {
_.logger.Auto(fatalNoLink).Arg(ownerLink.GetName());
}
}
event Closed()
{
_.logger.Auto(infoDisconnected).Arg(ownerLink.GetName());
if (ownerLink != none) {
ownerLink.ReceiveNetworkMessage(ANM_Connected);
}
else {
_.logger.Auto(fatalNoLink).Arg(ownerLink.GetName());
}
portBound = false;
timeSpentConnecting = 0.0;
}
event Destroyed()
{
if (_ != none)
{
_.memory.Free(avariceReader);
_.memory.Free(encoder);
_.memory.Free(keyS);
_.memory.Free(keyT);
_.memory.Free(keyP);
if (ownerLink != none) {
ownerLink.ReceiveNetworkMessage(ANM_Death);
}
else {
_.logger.Auto(fatalNoLink).Arg(ownerLink.GetName());
}
}
}
event Tick(float delta)
{
if (linkState == STATE_Connected) {
HandleIncomingData();
}
else
{
timeSpentConnecting += delta;
if (timeSpentConnecting >= reconnectInterval)
{
Close();
TryConnecting();
timeSpentConnecting = 0.0;
}
}
}
private final function HandleIncomingData()
{
local int i, totalReceivedBytes;
local AvariceMessage nextMessage;
local array<MutableText> receivedMessages;
local int bytesRead;
bytesRead = ReadBinary(255, buffer);
while (bytesRead > 0)
{
totalReceivedBytes += bytesRead;
for (i = 0; i < bytesRead; i += 1) {
avariceReader.PushByte(buffer[i]);
}
bytesRead = ReadBinary(255, buffer);
}
if (avariceReader.Failed())
{
_.logger.Auto(fatalInvaliddUTF8).Arg(ownerLink.GetName());
Destroy();
return;
}
// Tell Avarice how many bytes we have received, so it can send
// more information
if (totalReceivedBytes > 0) {
SendReceived(totalReceivedBytes);
}
receivedMessages = avariceReader.PopMessages();
for (i = 0; i < receivedMessages.length; i += 1)
{
nextMessage = MessageFromText(receivedMessages[i]);
// This means received message is invalid,
// which means whatever we are connected to is feeding us invalid data,
// which means connection should be cut immediately
if (nextMessage == none)
{
_.logger.Auto(fatalInvalidMessage).Arg(ownerLink.GetName());
_.memory.Free(nextMessage);
_.memory.FreeMany(receivedMessages);
Destroy();
return;
}
ownerLink.ReceiveNetworkMessage(ANM_Message, nextMessage);
_.memory.Free(nextMessage);
}
_.memory.FreeMany(receivedMessages);
}
public final function SendMessage(BaseText textMessage)
{
local int i;
local int nextByte;
local ByteArrayRef message;
local int messageLength;
if (textMessage == none) {
return;
}
message = encoder.Encode(textMessage);
messageLength = message.GetLength();
// Signal that we are sending next message
buffer[0] = HEAD_MESSAGE;
// Next four bytes (with indices 1, 2, 3 and 4) must contain length of
// the message's contents as a 4-byte unsigned integer in big endian.
// UnrealScript does not actually have an unsigned integer type, but
// even `int`'s positive value range is enough, since avarice server
// will not accept message length that is even close to `MaxInt`.
buffer[4] = messageLength & byteMask;
messageLength -= buffer[4];
messageLength = messageLength >> 8;
buffer[3] = messageLength & byteMask;
messageLength -= buffer[3];
messageLength = messageLength >> 8;
buffer[2] = messageLength & byteMask;
messageLength -= buffer[2];
messageLength = messageLength >> 8;
buffer[1] = messageLength;
// Record the rest of the message in chunks of `255`, since `SendBinary()`
// can only send this much at once
nextByte = 5; // We have already added 5 bytes in the code above
messageLength = message.GetLength();
for (i = 0; i < messageLength; i += 1)
{
buffer[nextByte] = message.GetItem(i);
nextByte += 1;
if (nextByte >= 255)
{
nextByte = 0;
SendBinary(255, buffer);
}
}
// Cycle above only sent full chunks of `255`, so send the remainder now
if (nextByte > 0) {
SendBinary(nextByte, buffer);
}
message.FreeSelf();
}
private final function SendReceived(int received)
{
// Signal that we are sending amount of bytes received this tick
buffer[0] = HEAD_BYTES_RECEIVED;
// Next four bytes (with indices 1 and 2) must contain amount of bytes
// received as a 2-byte unsigned integer in big endian
buffer[2] = received & byteMask;
received -= buffer[2];
received = received >> 8;
buffer[1] = received;
SendBinary(3, buffer);
}
private final function AvariceMessage MessageFromText(BaseText message)
{
local Parser parser;
local AvariceMessage result;
local HashTable parsedMessage;
local AcediaObject item;
if (message == none) {
return none;
}
parser = _.text.Parse(message);
parsedMessage = _.json.ParseHashTableWith(parser);
parser.FreeSelf();
if (parsedMessage == none) {
return none;
}
result = AvariceMessage(_.memory.Allocate(class'AvariceMessage'));
item = parsedMessage.TakeItem(keyS);
if (item == none || item.class != class'Text')
{
_.memory.Free(item);
_.memory.Free(parsedMessage);
_.memory.Free(result);
return none;
}
result.service = Text(item);
item = parsedMessage.TakeItem(keyT);
if (item == none || item.class != class'Text')
{
_.memory.Free(item);
_.memory.Free(parsedMessage);
_.memory.Free(result);
return none;
}
result.type = Text(item);
result.parameters = parsedMessage.TakeItem(keyP);
_.memory.Free(parsedMessage);
return result;
}
defaultproperties
{
HEAD_BYTES_RECEIVED = 85
HEAD_MESSAGE = 42
byteMask = 255 // Only lowest 8 bits are `1`
infoConnected = (l=LOG_Info,m="Avarice link \"%1\" connected")
infoDisconnected = (l=LOG_Info,m="Avarice link \"%1\" disconnected")
fatalNoLink = (l=LOG_Fatal,m="Unexpected internal `none` value for Avarice link \"%1\"")
fatalBadPort = (l=LOG_Fatal,m="Bad port \"%1\" specified for Avarice link \"%2\"")
fatalCannotBindPort = (l=LOG_Fatal,m="Cannot bind port for Avarice link \"%1\"")
fatalCannotResolveHost = (l=LOG_Fatal,m="Cannot resolve host \"%1\" for Avarice link \"%2\"")
fatalCannotConnect = (l=LOG_Fatal,m="Connection for Avarice link \"%1\" was rejected")
fatalInvaliddUTF8 = (l=LOG_Fatal,m="Avarice link \"%1\" has received invalid UTF8, aborting connection")
fatalInvalidMessage = (l=LOG_Fatal,m="Avarice link \"%1\" has received invalid message, aborting connection")
}

View File

@ -0,0 +1,168 @@
/**
* This feature makes it possible to use TCP connection to exchange
* messages (represented by JSON objects) with external applications.
* There are some peculiarities to UnrealEngine's `TCPLink`, so to simplify
* communication process for external applications, they are expected to
* connect to the server through the "Avarice" utility that can accept a stream
* of utf8-encoded JSON messageand feed them to our `TCPLink` (we use child
* class `AvariceTcpStream`) in a way it can receive them.
* Every message sent to us must have the following structure:
* { "s": "<service_name>", "t": "<command_type>", "p": <any_json_value> }
* where
* * <service_name> describes a particular source of messages
* (it can be a name of the database or an alias for
* a connected application);
* * <command_type> simply states the name of a command, for a database it
* can be "get", "set", "delete", etc..
* * <any_json_value> can be an arbitrary json value and can be used to
* pass any additional information along with the message.
* Acedia provides a special treatment for any messages that have their
* service set to "echo" - it always returns them back as-is, except for the
* message type that gets set to "end".
* Copyright 2021 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 Avarice_Feature extends Feature
dependson(Avarice);
var private /*config*/ array<Avarice.AvariceLinkRecord> link;
var private /*config*/ float reconnectTime;
// The feature itself is dead simple - it simply creates list of
// `AvariceLink` objects, according to its settings, and stores them
var private array<AvariceLink> createdLinks;
var private const int TECHO, TEND, TCOLON;
var private LoggerAPI.Definition errorBadAddress;
protected function OnEnabled()
{
local int i;
local Text name;
local MutableText host;
local int port;
local AvariceLink nextLink;
for (i = 0; i < link.length; i += 1)
{
name = _.text.FromString(link[i].name);
if (ParseAddress(link[i].address, host, port))
{
nextLink = AvariceLink(_.memory.Allocate(class'AvariceLink'));
nextLink.Initialize(name, host, port);
nextLink.StartUp();
nextLink.OnMessage(self, T(TECHO)).connect = EchoHandler;
createdLinks[createdLinks.length] = nextLink;
}
else
{
_.logger.Auto(errorBadAddress)
.Arg(_.text.FromString(link[i].address))
.Arg(_.text.FromString(link[i].name));
}
_.memory.Free(name);
_.memory.Free(host);
}
}
protected function OnDisabled()
{
_.memory.FreeMany(createdLinks);
}
protected function SwapConfig(FeatureConfig config)
{
local Avarice newConfig;
newConfig = Avarice(config);
if (newConfig == none) {
return;
}
link = newConfig.link;
reconnectTime = newConfig.reconnectTime;
// For static `GetReconnectTime()` method
default.reconnectTime = reconnectTime;
}
// Reply back any messages from "echo" service
private function EchoHandler(AvariceLink link, AvariceMessage message)
{
link.SendMessage(T(TECHO), T(TEND), message.parameters);
}
private final function bool ParseAddress(
string address,
out MutableText host,
out int port)
{
local bool success;
local Parser parser;
parser = _.text.ParseString(address);
parser.Skip()
.MUntil(host, T(TCOLON).GetCharacter(0))
.Match(T(TCOLON))
.MUnsignedInteger(port)
.Skip();
success = parser.Ok() && parser.GetRemainingLength() == 0;
parser.FreeSelf();
return success;
}
/**
* Method that returns all the `AvariceLink` created by this feature.
*
* @return Array of links created by this feature.
* Guaranteed to not contain `none` values.
*/
public final function array<AvariceLink> GetAllLinks()
{
local int i;
while (i < createdLinks.length)
{
if (createdLinks[i] == none) {
createdLinks.Remove(i, 1);
}
else {
i += 1;
}
}
return createdLinks;
}
/**
* Returns its current `reconnectTime` setting that describes amount of time
* between connection attempts.
*
* @return Value of `reconnectTime` config variable.
*/
public final static function float GetReconnectTime()
{
return default.reconnectTime;
}
defaultproperties
{
configClass = class'Avarice'
// `Text` constants
TECHO = 0
stringConstants(0) = "echo"
TEND = 1
stringConstants(1) = "end"
TCOLON = 2
stringConstants(2) = ":"
// Log messages
errorBadAddress = (l=LOG_Error,m="Cannot parse address \"%1\" for \"%2\"")
}

View File

@ -0,0 +1,38 @@
/**
* Signal class implementation for `Avarice`'s `OnMessage` signal.
* Copyright 2021 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 Avarice_OnMessage_Signal extends Signal;
public final function Emit(AvariceLink link, AvariceMessage message)
{
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
Avarice_OnMessage_Slot(nextSlot).connect(link, message);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
relatedSlotClass = class'Avarice_OnMessage_Slot'
}

View File

@ -0,0 +1,40 @@
/**
* Slot class implementation for `Avarice`'s `OnMessage` signal.
* Copyright 2021 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 Avarice_OnMessage_Slot extends Slot;
delegate connect(AvariceLink link, AvariceMessage message)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

View File

@ -0,0 +1,311 @@
/**
* Aliases allow users to define human-readable and easier to use
* "synonyms" to some symbol sequences (mainly names of UnrealScript classes).
* This is an interface class that can be implemented in various different
* ways.
* Several `AliasSource`s are supposed to exist separately, each storing
* aliases of particular kind: for weapon, zeds, colors, etc..
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class BaseAliasSource extends AcediaObject
abstract;
/**
* # `AliasSource`
*
* Interface for any alias source instance that includes basic methods for
* alias lookup and adding/removal.
*
* ## Aliases
*
* Aliases in Acedia are usually defined as either `$<alias_name>`.
* `<alias_name>` can only contain ASCII latin letters and digits.
* `<alias_name>` is meant to define a human-readable name for something
* (e.g. "m14" for `KFMod.M14EBRBattleRifle`).
* Aliases are *case-insensitive*.
* '$' prefix is used to emphasize that what user is specifying is,
* in fact, an alias and it *is not* actually important for aliases feature
* and API: only `<alias_name>` is used. However, for convenience's sake,
* `AliasesAPI` usually recognizes aliases both with and without '$' prefix.
* Alias sources (classes derived from `BaseAliasSource`), however should only
* handle resolving `<alias_name>`, treating '$' prefix as a mistake in alias
* parameter.
*
* ## Implementation
*
* As far as implementation goes, it's up to you how your own child alias
* source class is configured to obtain its aliases, but `Aliases_Feature`
* should only create a single instance for every source class (although
* nothing can prevent other mods from creating more instances, so we cannot
* give any guarantees).
* Methods that add or remove aliases are allowed to fail for whatever
* reason is valid for your source's case (it might forbid adding aliases
* at all), as long as they return `false`.
* Although all built-in aliases are storing case-insensitive values,
* `BaseAliasSource` does not demand that and allows to configure this behavior
* via `AreValuesCaseSensitive()`. This is important for `GetAliases()` method
* that returns all aliases referring to a given value.
*/
/**
* Returns whether caller alias source class stores case-sensitive values.
*
* This information is necessary for organizing lookup of aliases by
* a given value. Aliases themselves are always case-insensitive.
*
* This method should not change returned value for any fixed class.
*
* @return `true` if stored values are case-sensitive and `false` if they are
* case-insensitive.
*/
public static function bool AreValuesCaseSensitive();
/**
* Returns all aliases that are stored in the caller source/
*
* @return Array of all aliases inside the caller alias source. All `Text`
* references are guaranteed to not be `none` or duplicated.
*/
public function array<Text> GetAllAliases();
/**
* Returns all aliases that are stored in the caller source/
*
* @return Array of all aliases inside the caller alias source. All `Text`
* references are guaranteed to not be `none` or duplicated.
*/
public function array<string> GetAllAliases_S()
{
local int i;
local array<Text> resultWithTexts;
local array<string> result;
resultWithTexts = GetAllAliases();
for (i = 0; i < resultWithTexts.length; i += 1) {
result[result.length] = resultWithTexts[i].ToString();
}
_.memory.FreeMany(resultWithTexts);
return result;
}
/**
* Returns all aliases that represent given value `value`.
*
* @param value Value for which to return all its aliases.
* Whether it is treated as case-sensitive is decided by
* `AreValuesCaseSensitive()` method, but all default alias sources are
* case-insensitive.
* @return Array of all aliases that refer to the given `value` inside
* the caller alias source. All `Text` references are guaranteed to not be
* `none` or duplicated.
*/
public function array<Text> GetAliases(BaseText value);
/**
* Returns all aliases that represent given value `value`.
*
* @param value Value for which to return all its aliases.
* Whether it is treated as case-sensitive is decided by
* `AreValuesCaseSensitive()` method, but all default alias sources are
* case-insensitive.
* @return Array of all aliases that refer to the given `value` inside
* the caller alias source. All `string` references are guaranteed to not
* be duplicated.
*/
public function array<string> GetAliases_S(string value)
{
local int i;
local Text valueAsText;
local array<Text> resultWithTexts;
local array<string> result;
valueAsText = _.text.FromString(value);
resultWithTexts = GetAliases(valueAsText);
_.memory.Free(valueAsText);
for (i = 0; i < resultWithTexts.length; i += 1) {
result[result.length] = resultWithTexts[i].ToString();
}
_.memory.FreeMany(resultWithTexts);
return result;
}
/**
* Checks if given alias is present in caller `AliasSource`.
*
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
* method.
*
* @param alias Alias to check, case-insensitive.
* @return `true` if present, `false` otherwise.
*/
public function bool HasAlias(BaseText alias);
/**
* Checks if given alias is present in caller `AliasSource`.
*
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
* method.
*
* @param alias Alias to check, case-insensitive.
* @return `true` if present, `false` otherwise.
*/
public function bool HasAlias_S(string alias)
{
local bool result;
local Text aliasAsText;
aliasAsText = _.text.FromString(alias);
result = HasAlias(aliasAsText);
_.memory.Free(aliasAsText);
return result;
}
/**
* Returns value stored for the given alias in caller `AliasSource`
* (as well as it's `Aliases` objects).
*
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
* method.
*
* @param alias Alias, for which method will attempt to return
* a value. Case-insensitive. If given `alias` starts with "$" character -
* that character will be removed before resolving that alias.
* @param copyOnFailure Whether method should return copy of original
* `alias` value in case caller source did not have any records
* corresponding to `alias`.
* @return If look up was successful - value, associated with the given
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
* flag: `copyOnFailure == false` means method will return `none`
* and `copyOnFailure == true` means method will return `alias.Copy()`.
* If `alias == none` method always returns `none`.
*/
public function Text Resolve(BaseText alias, optional bool copyOnFailure);
/**
* Returns value stored for the given alias in caller `AliasSource`
* (as well as it's `Aliases` objects).
*
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
* method.
*
* @param alias Alias, for which method will attempt to return
* a value. Case-insensitive. If given `alias` starts with "$" character -
* that character will be removed before resolving that alias.
* @param copyOnFailure Whether method should return copy of original
* `alias` value in case caller source did not have any records
* corresponding to `alias`.
* @return If look up was successful - value, associated with the given
* alias `alias`. If lookup was unsuccessful, it depends on `copyOnFailure`
* flag: `copyOnFailure == false` means method will return empty `string`
* and `copyOnFailure == true` means method will return `alias`.
*/
public function string Resolve_S(
string alias,
optional bool copyOnFailure)
{
local Text resultAsText;
local Text aliasAsText;
aliasAsText = _.text.FromString(alias);
resultAsText = Resolve(aliasAsText, copyOnFailure);
return _.text.IntoString(resultAsText);
}
/**
* 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 or `aliasValue == none`.
*
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
* method.
*
* @param aliasToAdd Alias that you want to add to caller source.
* Alias names are case-insensitive.
* @param aliasValue Intended value of this alias.
* @return `true` if alias was added and `false` otherwise (alias was invalid).
*/
public function bool AddAlias(BaseText aliasToAdd, BaseText aliasValue);
/**
* 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.
*
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
* method.
*
* @param aliasToAdd Alias that you want to add to caller source.
* Alias names are case-insensitive.
* @param aliasValue Intended value of this alias.
* @return `true` if alias was added and `false` otherwise (alias was invalid).
*/
public function bool AddAlias_S(string aliasToAdd, string aliasValue)
{
local bool result;
local Text aliasAsText, valueAsText;
aliasAsText = _.text.FromString(aliasToAdd);
valueAsText = _.text.FromString(aliasValue);
result = AddAlias(aliasAsText, valueAsText);
_.memory.Free(aliasAsText);
_.memory.Free(valueAsText);
return result;
}
/**
* Removes alias (all records with it, in case of duplicates) from
* the caller `AliasSource`.
*
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
* method.
*
* @param aliasToRemove Alias that you want to remove from caller source.
* @return `true` if an alias was present in the source and was deleted and
* `false` if there was no specified alias in the first place.
*/
public function bool RemoveAlias(BaseText aliasToRemove);
/**
* Removes alias (all records with it, in case of duplicates) from
* the caller `AliasSource`.
*
* NOTE: having '$' prefix is considered to be invalid for `alias` by this
* method.
*
* @param aliasToRemove Alias that you want to remove from caller source.
* @return `true` if an alias was present in the source and was deleted and
* `false` if there was no specified alias in the first place.
*/
public function bool RemoveAlias_S(string aliasToRemove)
{
local bool result;
local Text aliasAsText;
aliasAsText = _.text.FromString(aliasToRemove);
result = RemoveAlias(aliasAsText);
_.memory.Free(aliasAsText);
return result;
}
defaultproperties
{
}

View File

@ -0,0 +1,654 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class BigInt extends AcediaObject
dependson(MathApi);
/// A simple big integer implementation.
///
/// [`BigInt`]'s main purpose is to allow Acedia's databases to store integers of arbitrary size.
/// It can be used for long arithmetic computations, but it was mainly meant as a players'
/// statistics counter and, therefore, not optimized for performing large amount of operations.
/// [`BigInt`] data as a struct - meant to be used to store [`BigInt`]'s values inside
/// the local databases.
struct BigIntData {
var bool negative;
var array<byte> digits;
};
/// Result of comparison for [`BigInt`]s with each other.
enum BigIntCompareResult
{
BICR_Less,
BICR_Equal,
BICR_Greater
};
/// Does stored [`BigInt`] have a negative sign?
var private bool negative;
/// Digits array, from least to most significant. For example, for 13524:
///
/// ```
/// `digits[0] = 4`
/// `digits[1] = 2`
/// `digits[2] = 5`
/// `digits[3] = 3`
/// `digits[4] = 1`
/// ```
///
/// Valid [`BigInt`] should not have this array empty: zero should be represented by an array with
/// a single `0`-element.
/// This isn't a most efficient representation for [`BigInt`], but it's easy to convert to and from
/// decimal representation.
///
/// # Invariants
///
/// This array must not have leading (in the sense of significance) zeroes.
/// That is, last element of the array should not be a `0`.
/// The only exception if if stored value is `0`, then `digits` must consist of
/// a single `0` element.
var private array<byte> digits;
/// Constants useful for converting [`BigInt`] back to [`int`], while avoiding overflow.
/// We can add less digits than that without any fear of overflow.
const DIGITS_IN_MAX_INT = 10;
/// Maximum [`int`] value is `2147483647`, so in case most significant digit is 10th and is `2`
/// (so number has a form of `2xxxxxxxxx`), to check for overflow we only need to compare
/// combination of the rest of the digits with this constant.
const ALMOST_MAX_INT = 147483647;
/// To add last digit we add/subtract that digit multiplied by this value.
const LAST_DIGIT_ORDER = 1000000000;
protected function Constructor() {
SetZero();
}
protected function Finalizer() {
negative = false;
digits.length = 0;
}
// Auxiliary method to set current value to zero
private function SetZero() {
negative = false;
digits.length = 1;
digits[0] = 0;
}
// Minimal [`int`] value `-2,147,483,648` is somewhat of a pain to handle, so just use this
// auxiliary pre-made constructor for it
private function SetMinimalNegative() {
negative = true;
digits.length = 10;
digits[0] = 8;
digits[1] = 4;
digits[2] = 6;
digits[3] = 3;
digits[4] = 8;
digits[5] = 4;
digits[6] = 7;
digits[7] = 4;
digits[8] = 1;
digits[9] = 2;
}
// Removes unnecessary zeroes from leading digit positions `digits`.
// Does not change contained value.
private final function TrimLeadingZeroes() {
local int i, zeroesToRemove;
// Finds how many leading zeroes there is.
// Since `digits` stores digits from least to most significant, we need to check from the end of
// `digits` array.
for (i = digits.length - 1; i >= 0; i -= 1) {
if (digits[i] != 0) {
break;
}
zeroesToRemove += 1;
}
// `digits` must not be empty, enforce `0` value in that case
if (zeroesToRemove >= digits.length) {
SetZero();
}
else {
digits.length = digits.length - zeroesToRemove;
}
}
/// Changes current value of [`BigInt`] to given value.
public final function Set(BigInt value)
{
if (value != none) {
value.TrimLeadingZeroes();
digits = value.digits;
negative = value.negative;
}
}
/// Changes current value of [`BigInt`] to given value.
public final function SetInt(int value) {
local MathApi.IntegerDivisionResult divisionResult;
negative = false;
digits.length = 0;
if (value < 0) {
// Treat special case of minimal [`int`] value `-2,147,483,648` that
// won't fit into positive [`int`] as special and use pre-made
// specialized constructor `CreateMinimalNegative()`
if (value < -maxInt) {
SetMinimalNegative();
return;
} else {
negative = true;
value *= -1;
}
}
if (value == 0) {
digits[0] = 0;
} else {
while (value > 0) {
divisionResult = __().math.IntegerDivision(value, 10);
value = divisionResult.quotient;
digits[digits.length] = divisionResult.remainder;
}
}
TrimLeadingZeroes();
}
/// Changes current value of [`BigInt`] to the value, given by decimal representation.
///
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given
/// as an argument, caller's value won't change.
/// Returns `true` in case of success and `false` otherwise.
public final function bool SetDecimal(BaseText value) {
local int i;
local byte nextDigit;
local bool newNegative;
local array<byte> newDigits;
local Parser parser;
local Basetext.Character nextCharacter;
if (value == none) {
return false;
}
parser = value.Parse();
newNegative = ParseSign(parser);
// Reset to valid state whether sign was consumed or not
parser.Confirm();
parser.R();
newDigits.length = parser.GetRemainingLength();
// Parse new one
i = newDigits.length - 1;
while (!parser.HasFinished()){
// This should not happen, but just in case
if (i < 0) {
break;
}
parser.MCharacter(nextCharacter);
nextDigit = __().text.CharacterToInt(nextCharacter, 10);
if (nextDigit < 0) {
return false;
}
newDigits[i] = nextDigit;
i -= 1;
}
parser.FreeSelf();
digits = newDigits;
negative = newNegative;
TrimLeadingZeroes();
return true;
}
// Tries to parse either `+` or `-` and returns `true` iff it parsed `-`.
// If neither got parsed, `parser` will enter failed state.
// Assumes `parser` isn't `none`.
private final function bool ParseSign(Parser parser) {
parser.Match(P("-"));
negative = parser.Ok();
if (parser.Ok()) {
negative = true;
}
else {
parser.R();
parser.Match(P("+"));
}
return negative;
}
/// Changes current value of [`BigInt`] to the value, given by decimal representation.
///
/// If invalid decimal representation (digits only, possibly with leading sign) is given as
/// an argument, caller's value won't change.
/// Returns `true` in case of success and `false` otherwise.
public final function bool SetDecimal_S(string value) {
local bool result;
local MutableText wrapper;
wrapper = __().text.FromStringM(value);
result = SetDecimal(wrapper);
wrapper.FreeSelf();
return result;
}
// Auxiliary method for comparing two [`BigInt`]s by their absolute value.
private function BigIntCompareResult _compareAbsolute(BigInt other) {
local int i;
local array<byte> otherDigits;
otherDigits = other.digits;
if (digits.length == otherDigits.length)
{
for (i = digits.length - 1; i >= 0; i -= 1)
{
if (digits[i] < otherDigits[i]) {
return BICR_Less;
}
if (digits[i] > otherDigits[i]) {
return BICR_Greater;
}
}
return BICR_Equal;
}
if (digits.length < otherDigits.length) {
return BICR_Less;
}
return BICR_Greater;
}
/// Compares caller [`BigInt`] to [`other`].
///
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
/// it means that caller [`BigInt`] is smaller that `other`.
/// If argument is `none`, then it is considered to be less ([`BICR_Less`]) than caller [`BigInt`].
public function BigIntCompareResult Compare(BigInt other) {
local BigIntCompareResult resultForModulus;
if (other == none) return BICR_Less;
if (negative && !other.negative) return BICR_Less;
if (!negative && other.negative) return BICR_Greater;
resultForModulus = _compareAbsolute(other);
if (resultForModulus == BICR_Equal) return BICR_Equal;
if (negative && (resultForModulus == BICR_Greater)) return BICR_Less;
if (!negative && (resultForModulus == BICR_Less)) return BICR_Less;
return BICR_Greater;
}
/// Compares caller [`BigInt`] to [`other`].
///
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
/// it means that caller [`BigInt`] is smaller that `other`.
public function BigIntCompareResult CompareInt(int other) {
local BigInt wrapper;
local BigIntCompareResult result;
wrapper = _.math.ToBigInt(other);
result = Compare(wrapper);
wrapper.FreeSelf();
return result;
}
/// Compares caller [`BigInt`] to a decimal representation of a number.
///
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
/// it means that caller [`BigInt`] is smaller that `other`.
/// If argument is `none` or is an invalid decimal representation (digits only, possibly with
/// leading sign, then it is considered to be less ([`BICR_Less`]) than caller [`BigInt`].
public function BigIntCompareResult CompareDecimal(BaseText other) {
local BigInt wrapper;
local BigIntCompareResult result;
wrapper = _.math.MakeBigInt(other);
result = Compare(wrapper);
_.memory.Free(wrapper);
return result;
}
/// Compares caller [`BigInt`] to a decimal representation of a number.
///
/// [`BigIntCompareResult`] representing the result of comparison is returned as a result.
/// It describes how caller [`BigInt`] relates to the `other`, e.g. if `BICR_Less` was returned then
/// it means that caller [`BigInt`] is smaller that `other`.
/// If argument is an invalid decimal representation (digits only, possibly with leading sign, then
/// it is considered to be less ([`BICR_Less`]) than caller [`BigInt`].
public function BigIntCompareResult CompareDecimal_S(string other) {
local BigInt wrapper;
local BigIntCompareResult result;
wrapper = _.math.MakeBigInt_S(other);
result = Compare(wrapper);
wrapper.FreeSelf();
return result;
}
// Adds absolute values of caller [`BigInt`] and [`other`] with no changes to the sign.
private function _add(BigInt other) {
local int i;
local byte carry, digitSum;
local array<byte> otherDigits;
if (other == none) {
return;
}
otherDigits = other.digits;
if (digits.length < otherDigits.length) {
digits.length = otherDigits.length;
} else {
otherDigits.length = digits.length;
}
carry = 0;
for (i = 0; i < digits.length; i += 1) {
digitSum = digits[i] + otherDigits[i] + carry;
digits[i] = _.math.Remainder(digitSum, 10);
carry = (digitSum - digits[i]) / 10;
}
if (carry > 0) {
digits[digits.length] = carry;
}
// No leading zeroes can be created here, so no need to trim
}
// Subtracts absolute value of [`other`] from the caller [`BigInt`], flipping caller's sign in case
// `other`'s absolute value is bigger.
private function _sub(BigInt other) {
local int i;
local int carry, nextDigit;
local array<byte> minuendDigits, subtrahendDigits;
local BigIntCompareResult resultForModulus;
if (other == none) {
return;
}
resultForModulus = _compareAbsolute(other);
if (resultForModulus == BICR_Equal) {
SetZero();
return;
}
if (resultForModulus == BICR_Less) {
negative = !negative;
minuendDigits = other.digits;
subtrahendDigits = digits;
} else {
minuendDigits = digits;
subtrahendDigits = other.digits;
}
digits.length = minuendDigits.length;
subtrahendDigits.length = minuendDigits.length;
carry = 0;
for (i = 0; i < digits.length; i += 1) {
nextDigit = int(minuendDigits[i]) - int(subtrahendDigits[i]) + carry;
if (nextDigit < 0) {
nextDigit += 10;
carry = -1;
} else {
carry = 0;
}
digits[i] = nextDigit;
}
TrimLeadingZeroes();
}
/// Adds another value to the caller [`BigInt`].
///
/// If argument is `none`, then given method does nothing.
public function Add(BigInt other) {
if (other == none) {
return;
}
if (negative == other.negative) {
_add(other);
} else {
_sub(other);
}
}
/// Adds another value to the caller [`BigInt`].
public function AddInt(int other) {
local BigInt otherObject;
otherObject = _.math.ToBigInt(other);
Add(otherObject);
_.memory.Free(otherObject);
}
/// Adds decimal representation of the number to the caller [`BigInt`].
///
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given -
/// does nothing.
public function AddDecimal(BaseText other) {
local BigInt otherObject;
if (other == none) {
return;
}
otherObject = _.math.MakeBigInt(other);
Add(otherObject);
_.memory.Free(otherObject);
}
/// Adds decimal representation of the number to the caller [`BigInt`].
///
/// If invalid decimal representation (digits only, possibly with leading sign) is given -
/// does nothing.
public function AddDecimal_S(string other) {
local BigInt otherObject;
otherObject = _.math.MakeBigInt_S(other);
Add(otherObject);
_.memory.Free(otherObject);
}
/// Subtracts another value to the caller [`BigInt`].
///
/// If argument is `none`, then given method does nothing.
public function Subtract(BigInt other) {
if (negative != other.negative) {
_add(other);
} else {
_sub(other);
}
}
/// Adds another value to the caller [`BigInt`].
public function SubtractInt(int other) {
local BigInt otherObject;
otherObject = _.math.ToBigInt(other);
Subtract(otherObject);
_.memory.Free(otherObject);
}
/// Subtracts decimal representation of the number to the caller [`BigInt`].
///
/// If `none` or invalid decimal representation (digits only, possibly with leading sign) is given -
/// does nothing.
public function SubtractDecimal(BaseText other) {
local BigInt otherObject;
if (other == none) {
return;
}
otherObject = _.math.MakeBigInt(other);
Subtract(otherObject);
_.memory.Free(otherObject);
}
/// Adds decimal representation of the number to the caller [`BigInt`].
///
/// If invalid decimal representation (digits only, possibly with leading sign) is given -
/// does nothing.
public function SubtractDecimal_S(string other) {
local BigInt otherObject;
otherObject = _.math.MakeBigInt_S(other);
Subtract(otherObject);
_.memory.Free(otherObject);
}
/// Checks if caller [`BigInt`] is negative.
///
/// Returns if stored value is negative and `false` otherwise.
/// Zero is not considered negative number.
public function bool IsNegative() {
// Handle special case of zero first (it ignores `negative` flag)
if (digits.length == 1 && digits[0] == 0) {
return false;
}
return negative;
}
/// Converts caller [`BigInt`] into [`int`] representation.
///
/// In case stored value is outside `int`'s value range
/// (`[-maxInt-1, maxInt] == [-2147483648; 2147483647]`), method returns either maximal or minimal
// possible value, depending on the [`BigInt`]'s sign.
public function int ToInt() {
local int i;
local int accumulator;
local int safeDigitsAmount;
if (digits.length <= 0) {
return 0;
}
if (digits.length > DIGITS_IN_MAX_INT) {
if (negative) {
return (-maxInt - 1);
} else {
return maxInt;
}
}
// At most `DIGITS_IN_MAX_INT - 1` iterations
safeDigitsAmount = Min(DIGITS_IN_MAX_INT - 1, digits.length);
for (i = safeDigitsAmount - 1; i >= 0; i -= 1) {
accumulator *= 10;
accumulator += digits[i];
}
if (negative) {
accumulator *= -1;
}
accumulator = AddUnsafeDigitToInt(accumulator);
return accumulator;
}
/// Adding `DIGITS_IN_MAX_INT - 1` will never lead to an overflow, but adding the next digit can,
/// so we need to handle it differently and more carefully.
/// Assumes `digits.length <= DIGITS_IN_MAX_INT`.
private function int AddUnsafeDigitToInt(int accumulator) {
local int unsafeDigit;
local bool noOverflow;
if (digits.length < DIGITS_IN_MAX_INT) {
return accumulator;
}
unsafeDigit = digits[DIGITS_IN_MAX_INT - 1];
// `maxInt` stats with `2`, so if last/unsafe digit is either `0` or `1`, there is no overflow,
// otherwise - check rest of the digits
noOverflow = (unsafeDigit < 2);
if (unsafeDigit == 2) {
// Include `maxInt` and `-maxInt-1` (minimal possible value) into an overflow too - this way
// we still give a correct result, but do not have to worry about `int`-arithmetic error
noOverflow = noOverflow
|| (negative && (accumulator > -ALMOST_MAX_INT - 1))
|| (!negative && (accumulator < ALMOST_MAX_INT));
}
if (noOverflow) {
if (negative) {
accumulator -= unsafeDigit * LAST_DIGIT_ORDER;
}
else {
accumulator += unsafeDigit * LAST_DIGIT_ORDER;
}
return accumulator;
}
// Handle overflow
if (negative) {
return (-maxInt - 1);
}
return maxInt;
}
/// Converts caller [`BigInt`] into [`Text`] representation.
public function Text ToText() {
return ToText_M().IntoText();
}
/// Converts caller [`BigInt`] into [`MutableText`] representation.
public function MutableText ToText_M() {
local int i;
local MutableText result;
result = _.text.Empty();
if (negative) {
result.AppendCharacter(_.text.GetCharacter("-"));
}
for (i = digits.length - 1; i >= 0; i -= 1) {
result.AppendCharacter(_.text.CharacterFromCodePoint(digits[i] + 48));
}
return result;
}
/// Converts caller [`BigInt`] into [`string`] representation.
public function string ToString() {
local int i;
local string result;
if (negative) {
result = "-";
}
for (i = digits.length - 1; i >= 0; i -= 1) {
result = result $ digits[i];
}
return result;
}
/// Restores [`BigInt`] from the [`BigIntData`] value.
///
/// This method is created to make an efficient way to store [`BigInt`].
public function FromData(BigIntData data) {
local int i;
negative = data.negative;
digits = data.digits;
// Deal with possibly erroneous data
for (i = 0; i < digits.length; i += 1) {
if (digits[i] > 9) {
digits[i] = 9;
}
}
}
/// Converts caller [`BigInt`]'s value into [`BigIntData`].
///
/// This method is created to make an efficient way to store [`BigInt`].
public function BigIntData ToData() {
local BigIntData result;
result.negative = negative;
result.digits = digits;
return result;
}
defaultproperties {
}

View File

@ -0,0 +1,415 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ChatApi extends AcediaObject;
///! API for accessing chat-related events.
///!
///! # Implementation
///!
///! Signal functions that track text chat messages `OnMessage()` and `OnMessageFor()` simply
///! hook into [`BroadcastApi`] the first time such signal is requested.
///!
///! Signal function [`OnVoiceMessage()`] for tracking voice replaces a function in
///! [`KFPlayerController`] to track when they are replicated.
///! Then replaced function informs [`ChatApi`] about new voice message transmissions via
///! internal [`_EmitOnVoiceMessage()`] method.
/// Lists voice messages built-in in the game.
enum BuiltInVoiceMessage {
// Support
BIVM_SupportMedic,
BIVM_SupportHelp,
BIVM_SupportAskForMoney,
BIVM_SupportAskForWeapon,
// Acknowledgements
BIVM_AckYes,
BIVM_AckNo,
BIVM_AckThanks,
BIVM_AckSorry,
// Alert
BIVM_AlretLookOut,
BIVM_AlretRun,
BIVM_AlretWaitForMe,
BIVM_AlretWeldTheDoors,
BIVM_AlretLetsHoleUpHere,
BIVM_AlretFollowMe,
// Direction
BIVM_DirectionGetToTheTrader,
BIVM_DirectionGoUpstairs,
BIVM_DirectionGoDownstairs,
BIVM_DirectionGetOutside,
BIVM_DirectionGetInside,
// Insult
BIVM_InsultSpecimens,
BIVM_InsultPlayers,
// Trader
BIVM_TraderCheckWhereTheShopIs,
BIVM_TraderGetClose,
BIVM_TraderShopOpen,
BIVM_TraderShopOpenFinal,
BIVM_Trader30SecondsUntilShopCloses,
BIVM_TraderShopClosed,
BIVM_TraderCompliment,
BIVM_TraderNotEnoughMoney,
BIVM_TraderCannotCarry,
BIVM_TraderHurryUp1,
BIVM_TraderHurryUp2,
// Auto
BIVM_AutoWelding,
BIVM_AutoUnwelding,
BIVM_AutoReload,
BIVM_AutoOutOfAmmo,
BIVM_AutoDosh,
BIVM_AutoStandStillTryingToHealYou,
BIVM_AutoLowOnHealth,
BIVM_AutoBloatAcid,
BIVM_AutoPatriarchCloack,
BIVM_AutoPatriarchMinigun,
BIVM_AutoPatriarchRocketLauncher,
BIVM_AutoGrabbedByClot,
BIVM_AutoFleshpoundSpotted,
BIVM_AutoGorefastSpotted,
BIVM_AutoScrakeSpotted,
BIVM_AutoSirenSpotten,
BIVM_AutoSirenScream,
BIVM_AutoStalkerSpotted,
BIVM_AutoCrawlertSpotted,
BIVM_AutoMeleeKilledAStalker,
BIVM_AutoUsingFlamethrower,
BIVM_AutoEquipHuntingShotgun,
BIVM_AutoEquipHandcannons,
BIVM_AutoEquipLAW,
BIVM_AutoEquipFireaxe,
// Fallback
BIVM_Unknown
};
/// Killing Floor's native voice message is defined by `name` and `byte` pair.
/// This struct is added to allow returning them as a pair.
struct NativeVoiceMessage {
var name type;
var byte index;
};
/// Tracks whether we've already connected to broadcast signals.
var protected bool connectedToBroadcastAPI;
/// Tracks whether we've already replaced a function that allows us to catch voice messages.
var private bool replacedSendVoiceMessage;
/// Auxiliary constants that store amount of values in [`BuiltInVoiceMessage`] before
/// a certain group.
/// Used for conversion between native voice messages and [`BuiltInVoiceMessage`]
var private const int VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS;
var private const int VOICE_MESSAGES_BEFORE_ALERTS;
var private const int VOICE_MESSAGES_BEFORE_DIRECTIONS;
var private const int VOICE_MESSAGES_BEFORE_INSULTS;
var private const int VOICE_MESSAGES_BEFORE_TRADER;
var private const int VOICE_MESSAGES_BEFORE_AUTO;
var private const int VOICE_MESSAGES_TOTAL;
var protected ChatAPI_OnMessage_Signal onMessageSignal;
var protected ChatAPI_OnMessageFor_Signal onMessageForSignal;
var protected ChatAPI_OnVoiceMessage_Signal onVoiceMessageSignal;
protected function Constructor() {
onMessageSignal = ChatAPI_OnMessage_Signal(_.memory.Allocate(class'ChatAPI_OnMessage_Signal'));
onMessageForSignal =
ChatAPI_OnMessageFor_Signal(_.memory.Allocate(class'ChatAPI_OnMessageFor_Signal'));
onVoiceMessageSignal =
ChatAPI_OnVoiceMessage_Signal(_.memory.Allocate(class'ChatAPI_OnVoiceMessage_Signal'));
}
protected function Finalizer() {
_.memory.Free(onMessageSignal);
_.memory.Free(onMessageForSignal);
_.memory.Free(onVoiceMessageSignal);
onMessageSignal = none;
onMessageForSignal = none;
onVoiceMessageSignal = none;
_server.unreal.broadcasts.OnHandleText(self).Disconnect();
_server.unreal.broadcasts.OnHandleTextFor(self).Disconnect();
connectedToBroadcastAPI = false;
}
/// Signal that will be emitted when a player sends a message into the chat.
///
/// Allows to modify message before sending it, as well as prevent it from being sent at all.
///
/// Return `false` to prevent message from being sent.
/// If `false` is returned, signal propagation to the remaining handlers will also be interrupted.
///
/// # Slot description
///
/// bool <slot>(EPlayer sender, MutableText message, bool teamMessage)
///
/// ## Parameters
///
/// * [`sender`]: `EPlayer` that has sent the message.
/// * [`message`]: Message that `sender` has sent.
/// This is a mutable variable and can be modified from message will be sent.
/// * [`teamMessage`]: Is this a team message (to be sent only to players on the same team)?
///
/// ## Returns
///
/// Return `false` to prevent this message from being sent at all and `true` otherwise.
/// Message will be sent only if all handlers will return `true`.
public /*signal*/ function ChatAPI_OnMessage_Slot OnMessage(AcediaObject receiver) {
TryConnectingBroadcastSignals();
return ChatAPI_OnMessage_Slot(onMessageSignal.NewSlot(receiver));
}
/// Signal that will be emitted when a player sends a message into the chat.
///
/// Allows to modify message before sending it, as well as prevent it from being sent at all.
///
/// Return `false` to prevent message from being sent to a specific player.
/// If `false` is returned, signal propagation to the remaining handlers will also be interrupted.
///
/// # Slot description
///
/// bool <slot>(EPlayer receiver, EPlayer sender, BaseText message)
///
/// ## Parameters
///
/// * [`receiver`]: `EPlayer` that will receive the message.
/// * [`sender`]: `EPlayer` that has sent the message.
/// * [`message`]: Message that `sender` has sent. This is an immutable variable and cannot
/// be changed at this point. Use `OnMessage()` signal function for that.
///
/// ## Returns
///
/// Return `false` to prevent this message from being sent to a particular player and
/// `true` otherwise.
/// Message will be sent only if all handlers will return `true`.
/// However decision whether to send message or not is made for every player separately.
public /*signal*/ function ChatAPI_OnMessageFor_Slot OnMessageFor(AcediaObject receiver) {
TryConnectingBroadcastSignals();
return ChatAPI_OnMessageFor_Slot(onMessageForSignal.NewSlot(receiver));
}
/// Signal that will be emitted when a player sends a voice message.
///
/// # Slot description
///
/// void <slot>(EPlayer sender, ChatApi.BuiltInVoiceMessage message)
///
/// ## Parameters
///
/// * [`sender`]: `EPlayer` that has sent the voice message.
/// * [`message`]: Message that `sender` has sent.
public /*signal*/ function ChatAPI_OnVoiceMessage_Slot OnVoiceMessage(AcediaObject receiver) {
if (!replacedSendVoiceMessage) {
_.unflect.ReplaceFunction_S(
"KFMod.KFPlayerController.SendVoiceMessage",
"AcediaCore.Unflect_ChatApi_Controller.SendVoiceMessage",
"`ChatApi` was required to catch voice messages");
replacedSendVoiceMessage = true;
}
return ChatAPI_OnVoiceMessage_Slot(onVoiceMessageSignal.NewSlot(receiver));
}
public /*internal*/ function NativeVoiceMessage _enumIntoNativeVoiceMessage(
BuiltInVoiceMessage voiceMessage
) {
local int enumValue;
local NativeVoiceMessage result;
enumValue = int(voiceMessage);
if (enumValue < VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS) {
result.type = 'SUPPORT';
result.index = enumValue;
} else if (enumValue < VOICE_MESSAGES_BEFORE_ALERTS) {
result.type = 'ACK';
result.index = enumValue - VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS;
} else if (enumValue < VOICE_MESSAGES_BEFORE_DIRECTIONS) {
result.type = 'ALERT';
result.index = enumValue - VOICE_MESSAGES_BEFORE_ALERTS;
} else if (enumValue < VOICE_MESSAGES_BEFORE_INSULTS) {
result.type = 'DIRECTION';
result.index = enumValue - VOICE_MESSAGES_BEFORE_DIRECTIONS;
} else if (enumValue < VOICE_MESSAGES_BEFORE_TRADER) {
result.type = 'INSULT';
result.index = enumValue - VOICE_MESSAGES_BEFORE_INSULTS;
} else if (enumValue < VOICE_MESSAGES_BEFORE_AUTO) {
result.type = 'TRADER';
result.index = enumValue - VOICE_MESSAGES_BEFORE_TRADER;
if (result.index >= 5) {
result.index += 1;
}
} else if (enumValue < VOICE_MESSAGES_TOTAL) {
result.type = 'AUTO';
result.index = enumValue - VOICE_MESSAGES_BEFORE_AUTO;
}
return result;
}
public /*internal*/ function BuiltInVoiceMessage _nativeVoiceMessageIntoEnum(
NativeVoiceMessage voiceMessage
) {
switch (voiceMessage.type) {
case 'SUPPORT':
if (voiceMessage.index < 4) {
return BuiltInVoiceMessage(voiceMessage.index);
}
break;
case 'ACK':
if (voiceMessage.index < 4) {
return BuiltInVoiceMessage(
VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS + voiceMessage.index);
}
break;
case 'ALERT':
if (voiceMessage.index < 6) {
return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_ALERTS + voiceMessage.index);
}
break;
case 'DIRECTION':
if (voiceMessage.index < 5) {
return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_DIRECTIONS + voiceMessage.index);
}
break;
case 'INSULT':
if (voiceMessage.index < 2) {
return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_INSULTS + voiceMessage.index);
}
break;
case 'TRADER':
if (voiceMessage.index < 12) {
if (voiceMessage.index < 5) {
return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_TRADER + voiceMessage.index);
} else if (voiceMessage.index > 5) {
return BuiltInVoiceMessage(
VOICE_MESSAGES_BEFORE_TRADER + voiceMessage.index - 1);
}
}
break;
case 'AUTO':
if (voiceMessage.index < 25) {
return BuiltInVoiceMessage(VOICE_MESSAGES_BEFORE_AUTO + voiceMessage.index);
}
break;
default:
return BIVM_Unknown;
}
return BIVM_Unknown;
}
public final /*internal*/ /*native*/ function _EmitOnVoiceMessage(
PlayerController sender,
name messageType,
byte messageID
) {
local EPlayer wrapper;
local NativeVoiceMessage nativeVoiceMessage;
local BuiltInVoiceMessage builtInVoiceMessage;
if (sender == none) return;
wrapper = _.players.FromController(sender);
if (wrapper == none) return;
nativeVoiceMessage.type = messageType;
nativeVoiceMessage.index = messageID;
builtInVoiceMessage = _nativeVoiceMessageIntoEnum(nativeVoiceMessage);
if (builtInVoiceMessage != BIVM_Unknown) {
onVoiceMessageSignal.Emit(wrapper, builtInVoiceMessage);
}
_.memory.Free(wrapper);
}
private final function TryConnectingBroadcastSignals() {
if (connectedToBroadcastAPI) {
return;
}
connectedToBroadcastAPI = true;
_server.unreal.broadcasts.OnHandleText(self).connect = HandleText;
_server.unreal.broadcasts.OnHandleTextFor(self).connect = HandleTextFor;
}
private function bool HandleText(
Actor sender,
out string message,
name messageType,
bool teamMessage
) {
local bool result;
local MutableText messageAsText;
local EPlayer senderPlayer;
// We only want to catch chat messages from a player
if (messageType != 'Say' && messageType != 'TeamSay') return true;
senderPlayer = _.players.FromController(PlayerController(sender));
if (senderPlayer == none) return true;
messageAsText = __().text.FromColoredStringM(message);
result = onMessageSignal.Emit(senderPlayer, messageAsText, teamMessage);
message = messageAsText.ToColoredString();
// To correctly display chat messages we want to drop default color tag
// at the beginning (the one `ToColoredString()` adds if first character
// has no defined color).
// This is a compatibility consideration with vanilla UI that expects
// uncolored text. Not removing initial color tag will make chat text
// appear black.
if (!messageAsText.GetFormatting(0).isColored) {
message = Mid(message, 4);
}
_.memory.Free(messageAsText);
_.memory.Free(senderPlayer);
return result;
}
private function bool HandleTextFor(
PlayerController receiver,
Actor sender,
out string message,
name messageType
) {
local bool result;
local Text messageAsText;
local EPlayer senderPlayer, receiverPlayer;
// We only want to catch chat messages from another player
if (messageType != 'Say' && messageType != 'TeamSay') return true;
senderPlayer = _.players.FromController(PlayerController(sender));
if (senderPlayer == none) return true;
receiverPlayer = _.players.FromController(receiver);
if (receiverPlayer == none) {
_.memory.Free(senderPlayer);
return true;
}
messageAsText = __().text.FromColoredString(message);
result = onMessageForSignal.Emit(receiverPlayer, senderPlayer, messageAsText);
_.memory.Free(messageAsText);
_.memory.Free(senderPlayer);
_.memory.Free(receiverPlayer);
return result;
}
defaultproperties {
VOICE_MESSAGES_BEFORE_ACKNOWLEDGEMENTS = 4
VOICE_MESSAGES_BEFORE_ALERTS = 8
VOICE_MESSAGES_BEFORE_DIRECTIONS = 14
VOICE_MESSAGES_BEFORE_INSULTS = 19
VOICE_MESSAGES_BEFORE_TRADER = 21
VOICE_MESSAGES_BEFORE_AUTO = 32
VOICE_MESSAGES_TOTAL = 57
}

View File

@ -0,0 +1,49 @@
/**
* Signal class implementation for `ChatAPI`'s `OnMessageFor` signal.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ChatAPI_OnMessageFor_Signal extends Signal;
public final function bool Emit(
EPlayer receiver,
EPlayer sender,
BaseText message)
{
local Slot nextSlot;
local bool nextReply;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
nextReply = ChatAPI_OnMessageFor_Slot(nextSlot)
.connect(receiver, sender, message);
if (!nextReply && !nextSlot.IsEmpty())
{
CleanEmptySlots();
return false;
}
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties
{
relatedSlotClass = class'ChatAPI_OnMessageFor_Slot'
}

View File

@ -0,0 +1,41 @@
/**
* Slot class implementation for `CharAPI`'s `OnMessageFor` signal.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ChatAPI_OnMessageFor_Slot extends Slot;
delegate bool connect(EPlayer receiver, EPlayer sender, BaseText message)
{
DummyCall();
return true;
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

View File

@ -0,0 +1,49 @@
/**
* Signal class implementation for `ChatAPI`'s `OnMessage` signal.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ChatAPI_OnMessage_Signal extends Signal;
public final function bool Emit(
EPlayer sender,
MutableText message,
bool teamMessage)
{
local Slot nextSlot;
local bool nextReply;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
nextReply = ChatAPI_OnMessage_Slot(nextSlot)
.connect(sender, message, teamMessage);
if (!nextReply && !nextSlot.IsEmpty())
{
CleanEmptySlots();
return false;
}
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties
{
relatedSlotClass = class'ChatAPI_OnMessage_Slot'
}

View File

@ -0,0 +1,44 @@
/**
* Slot class implementation for `ChatAPI`'s `OnMessage` signal.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ChatAPI_OnMessage_Slot extends Slot;
delegate bool connect(
EPlayer sender,
MutableText message,
bool teamMessage)
{
DummyCall();
return true;
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

View File

@ -0,0 +1,39 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ChatAPI_OnVoiceMessage_Signal extends Signal
dependsOn(ChatApi);
public final function Emit(EPlayer sender, ChatApi.BuiltInVoiceMessage message) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
ChatAPI_OnVoiceMessage_Slot(nextSlot).connect(sender, message);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties {
relatedSlotClass = class'ChatAPI_OnVoiceMessage_Slot'
}

View File

@ -0,0 +1,41 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class ChatAPI_OnVoiceMessage_Slot extends Slot;
delegate connect(
EPlayer sender,
ChatApi.BuiltInVoiceMessage message
) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

View File

@ -0,0 +1,306 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CmdItemsTool extends AcediaObject
dependson(CommandAPI)
abstract;
//! This is a base class for auxiliary objects that will be used for storing
//! named [`Command`] instances and [`Voting`] classes: they both have in common
//! the need to remember who was authorized to use them (i.e. which user group)
//! and with what permissions (i.e. name of the config that contains appropriate
//! permissions).
//!
//! Aside from trivial accessors to its data, it also provides a way to resolve
//! the best permissions available to the user by finding the most priviledged
//! group he belongs to.
//!
//! NOTE: child classes must implement `MakeCard()` method and can override
//! `DiscardCard()` method to catch events of removing items from storage.
/// Allows to specify a base class requirement for this tool - only classes
/// that were derived from it can be stored inside.
var protected const class<AcediaObject> ruleBaseClass;
/// Names of user groups that can decide permissions for items,
/// in order of importance: from most significant to the least significant.
/// This is used for resolving the best permissions for each user.
var private array<Text> permissionGroupOrder;
/// Maps item names to their [`ItemCards`] with information about which groups
/// are authorized to use this particular item.
var private HashTable registeredCards;
var LoggerAPI.Definition errItemInvalidName;
var LoggerAPI.Definition errItemDuplicate;
protected function Constructor() {
registeredCards = _.collections.EmptyHashTable();
}
protected function Finalizer() {
_.memory.Free(registeredCards);
_.memory.FreeMany(permissionGroupOrder);
registeredCards = none;
permissionGroupOrder.length = 0;
}
/// Registers given item class under the specified (case-insensitive) name.
///
/// If name parameter is omitted (specified as `none`) or is an invalid name
/// (according to [`BaseText::IsValidName()`] method), then item class will not
/// be registered.
///
/// Returns `true` if item was successfully registered and `false` otherwise`.
///
/// # Errors
///
/// If provided name that is invalid or already taken by a different item -
/// a warning will be logged and item class won't be registered.
public function bool AddItemClass(class<AcediaObject> itemClass, BaseText itemName) {
local Text itemKey;
local ItemCard newCard, existingCard;
if (itemClass == none) return false;
if (itemName == none) return false;
if (registeredCards == none) return false;
if (ruleBaseClass == none || !ClassIsChildOf(itemClass, ruleBaseClass)) {
return false;
}
// The item name is transformed into lowercase, immutable value.
// This facilitates the use of item names as keys in a [`HashTable`],
// enabling case-insensitive matching.
itemKey = itemName.LowerCopy();
if (itemKey == none || !itemKey.IsValidName()) {
_.logger.Auto(errItemInvalidName).ArgClass(itemClass).Arg(itemKey);
return false;
}
// Guaranteed to only store cards
existingCard = ItemCard(registeredCards.GetItem(itemName));
if (existingCard != none) {
_.logger.Auto(errItemDuplicate)
.ArgClass(existingCard.GetItemClass())
.Arg(itemKey)
.ArgClass(itemClass);
_.memory.Free(existingCard);
return false;
}
newCard = MakeCard(itemClass, itemName);
registeredCards.SetItem(itemKey, newCard);
_.memory.Free2(itemKey, newCard);
return true;
}
/// Removes item of given class from the list of registered items.
///
/// Removing once registered item is not an action that is expected to
/// be performed under normal circumstances and does not have an efficient
/// implementation (it is linear on the current amount of items).
///
/// Returns `true` if successfully removed registered item class and
/// `false` otherwise (either item wasn't registered or caller tool
/// initialized).
public function bool RemoveItemClass(class<AcediaObject> itemClass) {
local int i;
local CollectionIterator iter;
local ItemCard nextCard;
local array<Text> keysToRemove;
if (itemClass == none) return false;
if (registeredCards == none) return false;
// Removing items during iterator breaks an iterator, so first we find
// all the keys to remove
iter = registeredCards.Iterate();
iter.LeaveOnlyNotNone();
while (!iter.HasFinished()) {
// Guaranteed to only be `ItemCard`
nextCard = ItemCard(iter.Get());
if (nextCard.GetItemClass() == itemClass) {
keysToRemove[keysToRemove.length] = Text(iter.GetKey());
DiscardCard(nextCard);
}
_.memory.Free(nextCard);
iter.Next();
}
iter.FreeSelf();
// Actual clean up everything in `keysToRemove`
for (i = 0; i < keysToRemove.length; i += 1) {
registeredCards.RemoveItem(keysToRemove[i]);
}
_.memory.FreeMany(keysToRemove);
return (keysToRemove.length > 0);
}
/// Allows to specify the order of the user group in terms of privilege for
/// accessing stored items. Only specified groups will be used when resolving
/// appropriate permissions config name for a user.
public final function SetPermissionGroupOrder(array<Text> groupOrder) {
local int i;
_.memory.FreeMany(permissionGroupOrder);
permissionGroupOrder.length = 0;
for (i = 0; i < groupOrder.length; i += 1) {
if (groupOrder[i] != none) {
permissionGroupOrder[permissionGroupOrder.length] = groupOrder[i].Copy();
}
}
}
/// Specifies what permissions (given by the config name) given user group has
/// when using an item with a specified name.
///
/// Method must be called after item with a given name is added.
///
/// If this config name is specified as `none`, then "default" will be
/// used instead. For non-`none` values, only an invalid name (according to
/// [`BaseText::IsValidName()`] method) will prevent the group from being
/// registered.
///
/// Method will return `true` if group was successfully authorized and `false`
/// otherwise (either group already authorized or no item with specified name
/// was added in the caller tool so far).
///
/// # Errors
///
/// If specified group was already authorized to use card's item, then it
/// will log a warning message about it.
public function bool AuthorizeUsage(BaseText itemName, BaseText groupName, BaseText configName) {
local bool result;
local ItemCard relevantCard;
if (configName != none && !configName.IsValidName()) {
return false;
}
relevantCard = GetCard(itemName);
if (relevantCard != none) {
result = relevantCard.AuthorizeGroupWithConfig(groupName, configName);
_.memory.Free(relevantCard);
}
return result;
}
/// Returns struct with item class (+ instance, if one was stored) for a given
/// case in-sensitive item name and name of the config with best permissions
/// available to the player with provided ID.
///
/// Function only returns `none` for item class if item with a given name
/// wasn't found.
/// Config name being `none` with non-`none` item class in the result means
/// that user with provided ID doesn't have permissions for using the item at
/// all.
public final function CommandAPI.ItemConfigInfo ResolveItem(BaseText itemName, BaseText textID) {
local int i;
local ItemCard relevantCard;
local CommandAPI.ItemConfigInfo result;
relevantCard = GetCard(itemName);
if (relevantCard == none) {
// At this point contains `none` for all values -> indicates a failure
// to find item in storage
return result;
}
result.instance = relevantCard.GetItem();
result.class = relevantCard.GetItemClass();
if (textID == none) {
return result;
}
// Look through all `permissionGroupOrder` in order to find most priviledged
// group that user with `textID` belongs to
for (i = 0; i < permissionGroupOrder.length && result.configName == none; i += 1) {
if (_.users.IsSteamIDInGroup(textID, permissionGroupOrder[i])) {
result.configName = relevantCard.GetConfigNameForGroup(permissionGroupOrder[i]);
}
}
_.memory.Free(relevantCard);
return result;
}
/// Returns all item classes that are stored inside caller tool.
///
/// Doesn't check for duplicates (although with a normal usage, there shouldn't
/// be any).
public final function array< class<AcediaObject> > GetAllItemClasses() {
local array< class<AcediaObject> > result;
local ItemCard value;
local CollectionIterator iter;
for (iter = registeredCards.Iterate(); !iter.HasFinished(); iter.Next()) {
value = ItemCard(iter.Get());
if (value != none) {
result[result.length] = value.GetItemClass();
}
_.memory.Free(value);
}
iter.FreeSelf();
return result;
}
/// Returns array of names of all available items.
public final function array<Text> GetItemsNames() {
local array<Text> emptyResult;
if (registeredCards != none) {
return registeredCards.GetTextKeys();
}
return emptyResult;
}
/// Called each time a new card is to be created and stored.
///
/// Must be reimplemented by child classes.
protected function ItemCard MakeCard(class<AcediaObject> itemClass, BaseText itemName) {
return none;
}
/// Called each time a certain card is to be removed from storage.
///
/// Must be reimplemented by child classes
/// (reimplementations SHOULD NOT DEALLOCATE `toDiscard`).
protected function DiscardCard(ItemCard toDiscard) {
}
/// Find item card for the item that was stored with a specified
/// case-insensitive name
///
/// Function only returns `none` if item with a given name wasn't found
/// (or `none` was provided as an argument).
protected final function ItemCard GetCard(BaseText itemName) {
local Text itemKey;
local ItemCard relevantCard;
if (itemName == none) return none;
if (registeredCards == none) return none;
/// The item name is transformed into lowercase, immutable value.
/// This facilitates the use of item names as keys in a [`HashTable`],
/// enabling case-insensitive matching.
itemKey = itemName.LowerCopy();
relevantCard = ItemCard(registeredCards.GetItem(itemKey));
_.memory.Free(itemKey);
return relevantCard;
}
defaultproperties {
errItemInvalidName = (l=LOG_Error,m="Attempt at registering item with class `%1` under an invalid name \"%2\" will be ignored.")
errItemDuplicate = (l=LOG_Error,m="Command `%1` is already registered with name '%2'. Attempt at registering command `%3` with the same name will be ignored.")
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
/**
* Base class for collection iterators, an auxiliary object for iterating
* through objects stored inside an Acedia's collection.
* Iterators expect that collection remains unchanged while they
* are iterating through it. Otherwise their behavior becomes undefined.
* Copyright 2020-2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CollectionIterator extends Iter
abstract;
/**
* Initialized caller `Iterator` to iterate over a given collection.
*
* `Iterator` should only be initialized once, reinitializing `Iterator` is
* considered an undefined behavior. Collection's `Iterate()` method is
* a preferred method to create initialized `Iterator`.
*
* Initialization is not guaranteed to be successful, - each iterator class
* corresponds to a particular collection and it's not `none` reference must
* be used as an argument.
*
* @param relevantCollection `Collection` over which items `Iterator`
* must iterate.
* @param `true` if iteration was successful and `false` otherwise.
*/
public function bool Initialize(Collection relevantCollection);
/**
* Returns key of current value pointed to by an iterator.
*
* NOTE: this method is guaranteed to return reference to the key used in
* relevant `Collection` if it actually stores key objects inside.
* However for other `Collection`s this method may create a "replacement"
* object (like `ArrayList` creating `IntBox` to simply return integer index).
*
* Does not advance iteration: use `Next()` to pick next value.
*
* @return Key of the current value being iterated over.
* If `Iterator()` has finished iterating over all values or
* was not initialized - returns `none`.
*/
public function AcediaObject GetKey();
defaultproperties
{
}

View File

@ -0,0 +1,96 @@
/**
* Convenience API that provides methods for quickly creating collections.
* 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 CollectionsAPI extends AcediaObject;
/**
* Creates a new `ArrayList`, optionally filling it with objects from
* a given native array.
*
* @param objectArray Objects to place inside created `ArrayList`;
* if empty (by default) - new, empty `ArrayList` will be returned.
* Objects will be added in the same order as in `objectArray`.
* @param managed Flag that indicates whether objects from
* `objectArray` argument should be added as managed.
* By default `false` - they would not be managed.
* @return New `ArrayList`, optionally filled with contents of
* `objectArray`. Guaranteed to be not `none` and to not contain any items
* outside of `objectArray`.
*/
public final function ArrayList NewArrayList(array<AcediaObject> objectArray)
{
local int i;
local ArrayList result;
result = ArrayList(_.memory.Allocate(class'ArrayList'));
for (i = 0; i < objectArray.length; i += 1) {
result.AddItem(objectArray[i]);
}
return result;
}
/**
* Creates a new empty `ArrayList`.
*
* @return New empty instance of `ArrayList`.
*/
public final function ArrayList EmptyArrayList()
{
return ArrayList(_.memory.Allocate(class'ArrayList'));
}
/**
* Creates a new `HashTable`, optionally filling it with entries
* (key/value pairs) from a given native array.
*
* @param entriesArray Entries (key/value pairs) to place inside created
* `HashTable`; if empty (by default) - new,
* empty `HashTable` will be returned.
* @param managed Flag that indicates whether values from
* `entriesArray` argument should be added as managed.
* By default `false` - they would not be managed.
* @return New `HashTable`, optionally filled with contents of
* `entriesArray`. Guaranteed to be not `none` and to not contain any items
* outside of `entriesArray`.
*/
public final function HashTable NewHashTable(
array<HashTable.Entry> entriesArray)
{
local int i;
local HashTable result;
result = HashTable(_.memory.Allocate(class'HashTable'));
for (i = 0; i < entriesArray.length; i += 1) {
result.SetItem(entriesArray[i].key, entriesArray[i].value);
}
return result;
}
/**
* Creates a new empty `HashTable`.
*
* @return New empty instance of `HashTable`.
*/
public final function HashTable EmptyHashTable()
{
return HashTable(_.memory.Allocate(class'HashTable'));
}
defaultproperties
{
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
/**
* Source intended for color aliases.
* Copyright 2020 - 2021 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
{
aliasesClass = class'ColorAliases'
}

View File

@ -0,0 +1,28 @@
/**
* Per-object-configuration intended for color aliases.
* Copyright 2020 - 2021 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 AliasesStorage
perObjectConfig
config(AcediaAliases_Colors);
defaultproperties
{
configName = "AcediaAliases_Colors"
sourceClass = class'ColorAliasSource'
}

View File

@ -0,0 +1,805 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Command extends AcediaObject
dependson(BaseText);
//! This class is meant to represent a command type.
//!
//! Command class provides an automated way to add a command to a server through
//! AcediaCore's features. It takes care of:
//!
//! 1. Verifying that player has passed correct (expected parameters);
//! 2. Parsing these parameters into usable values (both standard, built-in
//! types like `bool`, `int`, `float`, etc. and more advanced types such
//! as players lists and JSON values);
//! 3. Allowing you to easily specify a set of players you are targeting by
//! supporting several ways to refer to them, such as *by name*, *by id*
//! and *by selector* (@ and @self refer to caller player, @all refers
//! to all players).
//! 4. It can be registered inside AcediaCore's commands feature and be
//! automatically called through the unified system that supports *chat*
//! and *mutate* inputs (as well as allowing you to hook in any other
//! input source);
//! 5. Will also automatically provide a help page through built-in "help"
//! command;
//! 6. Subcommand support - when one command can have several distinct
//! functions, depending on how its called (e.g. "inventory add" vs
//! "inventory remove"). These subcommands have a special treatment in
//! help pages, which makes them more preferable, compared to simply
//! matching first `Text` argument;
//! 7. Add support for "options" - additional flags that can modify commands
//! behavior and behave like usual command options "--force"/"-f".
//! Their short versions can even be combined:
//! "give@ $ebr --ammo --force" can be rewritten as "give@ $ebr -af".
//! And they can have their own parameters: "give@all --list sharp".
//!
//! # Implementation
//!
//! The idea of `Command`'s implementation is simple: command is basically the
//! `Command.Data` struct that is filled via `CommandDataBuilder`.
//! Whenever command is called it uses `CommandParser` to parse user's input
//! based on its `Command.Data` and either report error (in case of failure) or
//! pass make `Executed()`/`ExecutedFor()` calls (in case of success).
//!
//! When command is called is decided by `Commands_Feature` that tracks possible
//! user inputs (and provides `HandleInput()`/`HandleInputWith()` methods for
//! adding custom command inputs). That feature basically parses first part of
//! the command: its name (not the subcommand's names) and target players
//! (using `PlayersParser`, but only if command is targeted).
//!
//! Majority of the command-related code either serves to build `Command.Data`
//! or to parse command input by using it (`CommandParser`).
/// Possible errors that can arise when parsing command parameters from user
/// input
enum ErrorType {
/// No error
CET_None,
/// Bad parser was provided to parse user input (this should not be possible)
CET_BadParser,
/// Sub-command name was not specified or was incorrect
/// (this should not be possible)
CET_NoSubCommands,
/// Specified sub-command does not exist
/// (only relevant when it is enforced for parser, e.g. by an alias)
CET_BadSubCommand,
/// Required param for command / option was not specified
CET_NoRequiredParam,
CET_NoRequiredParamForOption,
/// Unknown option key was specified
CET_UnknownOption,
/// Unknown short option key was specified
CET_UnknownShortOption,
/// Same option appeared twice in one command call
CET_RepeatedOption,
/// Part of user's input could not be interpreted as a part of
/// command's call
CET_UnusedCommandParameters,
/// In one short option specification (e.g. '-lah') several options require
/// parameters: this introduces ambiguity and is not allowed
CET_MultipleOptionsWithParams,
/// Targets are specified incorrectly (for targeted commands only)
CET_IncorrectTargetList,
// No targets are specified (for targeted commands only)
CET_EmptyTargetList
};
/// Structure that contains all the information about how `Command` was called.
struct CallData {
/// Targeted players (if applicable)
var public array<EPlayer> targetPlayers;
/// Specified sub-command and parameters/options
var public Text subCommandName;
/// Provided parameters and specified options
var public HashTable parameters;
var public HashTable options;
/// Errors that occurred during command call processing are described by
/// error type.
var public ErrorType parsingError;
/// Optional error textual name of the object (parameter, option, etc.)
/// that caused it.
var public Text errorCause;
};
/// Possible types of parameters.
enum ParameterType {
/// Parses into `BoolBox`
CPT_Boolean,
/// Parses into `IntBox`
CPT_Integer,
/// Parses into `FloatBox`
CPT_Number,
/// Parses into `Text`
CPT_Text,
/// Special parameter that consumes the rest of the input into `Text`
CPT_Remainder,
/// Parses into `HashTable`
CPT_Object,
/// Parses into `ArrayList`
CPT_Array,
/// Parses into any JSON value
CPT_JSON,
/// Parses into an array of specified players
CPT_Players
};
/// Possible forms a boolean variable can be used as.
/// Boolean parameter can define it's preferred format, which will be used for
/// help page generation.
enum PreferredBooleanFormat {
PBF_TrueFalse,
PBF_EnableDisable,
PBF_OnOff,
PBF_YesNo
};
// Defines a singular command parameter
struct Parameter {
/// Display name (for the needs of help page displaying)
var Text displayName;
/// Type of value this parameter would store
var ParameterType type;
/// Does it take only a singular value or can it contain several of them,
/// written in a list
var bool allowsList;
/// Variable name that will be used as a key to store parameter's value
var Text variableName;
/// (For `CPT_Boolean` type variables only) - preferred boolean format,
/// used in help pages
var PreferredBooleanFormat booleanFormat;
/// `CPT_Text` can be attempted to be auto-resolved as an alias from some
/// source during parsing.
/// For command to attempt that, this field must be not-`none` and contain
/// the name of the alias source (either "weapon", "color", "feature",
/// "entity" or some kind of custom alias source name).
///
/// Only relevant when given value is prefixed with "$" character.
var Text aliasSourceName;
};
/// Defines a sub-command of a this command
/// (specified as "<command> <sub_command>").
///
/// Using sub-command is not optional, but if none defined
/// (in `BuildData()`) / specified by the player - an empty (`name.IsEmpty()`)
/// one is automatically created / used.
struct SubCommand {
/// Name of the sub command. Cannot be `none`.
var Text name;
/// Human-readable description of the subcommand. Can be `none`.
var Text description;
/// List of required parameters of this [`Command`].
var array<Parameter> required;
/// List of optional parameters of this [`Command`].
var array<Parameter> optional;
};
/// Defines command's option (options are specified by "--long" or "-l").
/// Options are independent from sub-commands.
struct Option {
/// [`Option`]'s short name, i.e. a single letter "f" that can be specified
/// in, e.g. "-laf" type option listings
var BaseText.Character shortName;
/// [`Option`]'s full name, e.g. "--force".
var Text longName;
/// Human-readable description of the option. Can be `none`.
var Text description;
/// List of required parameters of this [`Command::Option`].
var array<Parameter> required;
/// List of required parameters of this [`Command::Option`].
var array<Parameter> optional;
};
/// Structure that defines what sub-commands and options command has
/// (and what parameters they take)
struct Data {
/// Command group this command belongs to
var protected Text group;
/// Short summary of what command does (recommended to
/// keep it to 80 characters)
var protected Text summary;
/// Available subcommands.
var protected array<SubCommand> subCommands;
/// Available options, common to all subcommands.
var protected array<Option> options;
/// `true` iff related [`Command`] targets players.
var protected bool requiresTarget;
};
var protected Data commandData;
/// Setting variable that defines a name that will be chosen for command by
/// default.
var protected const string preferredName;
/// Name that was used to register this command.
var protected Text usedName;
/// Settings variable that defines a class to be used for this [`Command`]'s
/// permissions config
var protected const class<CommandPermissions> permissionsConfigClass;
// We do not really ever need to create more than one instance of each class
// of `Command`, so we will simply store and reuse one created instance.
var private Command mainInstance;
/// When command is being executed we create several instances of
/// `ConsoleWriter` that can be used for command output.
/// They will also be automatically deallocated once command is executed.
///
/// DO NOT modify them or deallocate any of them manually.
///
/// This should make output more convenient and standardized.
///
/// 1. `publicConsole` - sends messages to all present players;
/// 2. `callerConsole` - sends messages to the player that called the command;
/// 3. `targetConsole` - sends messages to the player that is currently being
/// targeted (different each call of `ExecutedFor()` and `none` during
/// `Executed()` call);
/// 4. `othersConsole` - sends messaged to every player that is neither
/// "caller" or "target".
var protected ConsoleWriter publicConsole, othersConsole;
var protected ConsoleWriter callerConsole, targetConsole;
protected function Constructor() {
local CommandDataBuilder dataBuilder;
if (permissionsConfigClass != none) {
permissionsConfigClass.static.Initialize();
}
dataBuilder = CommandDataBuilder(_.memory.Allocate(class'CommandDataBuilder'));
// Let user fill-in the rest
BuildData(dataBuilder);
commandData = dataBuilder.BorrowData();
dataBuilder.FreeSelf();
dataBuilder = none;
}
protected function Finalizer() {
local int i;
local array<SubCommand> subCommands;
local array<Option> options;
DeallocateConsoles();
_.memory.Free(usedName);
_.memory.Free(commandData.summary);
usedName = none;
commandData.summary = none;
subCommands = commandData.subCommands;
for (i = 0; i < options.length; i += 1) {
_.memory.Free(subCommands[i].name);
_.memory.Free(subCommands[i].description);
CleanParameters(subCommands[i].required);
CleanParameters(subCommands[i].optional);
subCommands[i].required.length = 0;
subCommands[i].optional.length = 0;
}
commandData.subCommands.length = 0;
options = commandData.options;
for (i = 0; i < options.length; i += 1) {
_.memory.Free(options[i].longName);
_.memory.Free(options[i].description);
CleanParameters(options[i].required);
CleanParameters(options[i].optional);
options[i].required.length = 0;
options[i].optional.length = 0;
}
commandData.options.length = 0;
}
/// Initializes command, providing it with a specific name.
///
/// Argument cannot be `none`, otherwise initialization fails.
/// [`Command`] can only be successfully initialized once.
public final function bool Initialize(BaseText commandName) {
if (commandName == none) return false;
if (usedName != none) return false;
usedName = commandName.LowerCopy();
return true;
}
/// Overload this method to use `builder` to define parameters and options for
/// your command.
protected function BuildData(CommandDataBuilder builder){}
/// Overload this method to perform required actions when your command is
/// called.
///
/// [`arguments`] is a `struct` filled with parameters that your command has
/// been called with. Guaranteed to not be in error state.
/// [`instigator`] is a player that instigated this execution.
/// [`permissions`] is a config with permissions for this command call.
protected function Executed(
CallData arguments,
EPlayer instigator,
CommandPermissions permissions) {}
/// Overload this method to perform required actions when your command is called
/// with a given player as a target.
///
/// If several players have been specified - this method will be called once
/// for each.
///
/// If your command does not require a target - this method will not be called.
///
/// [`target`] is a player that this command must perform an action on.
/// [`arguments`] is a `struct` filled with parameters that your command has
/// been called with. Guaranteed to not be in error state.
/// [`instigator`] is a player that instigated this execution.
/// [`permissions`] is a config with permissions for this command call.
protected function ExecutedFor(
EPlayer target,
CallData arguments,
EPlayer instigator,
CommandPermissions permissions) {}
/// Returns an instance of command (of particular class) that is stored
/// "as a singleton" in command's class itself. Do not deallocate it.
public final static function Command GetInstance() {
if (default.mainInstance == none) {
default.mainInstance = Command(__().memory.Allocate(default.class));
}
return default.mainInstance;
}
/// Forces command to process (parse) player's input, producing a structure with
/// parsed data in Acedia's format instead.
///
/// Use `Execute()` for actually performing command's actions.
///
/// [`subCommandName`] can be optionally specified to use as sub-command.
/// If this argument's value is `none` - sub-command name will be parsed from
/// the `parser`'s data.
///
/// Returns `CallData` structure that contains all the information about
/// parameters specified in `parser`'s contents.
/// Returned structure contains objects that must be deallocated, which can
/// easily be done by the auxiliary `DeallocateCallData()` method.
public final function CallData ParseInputWith(
Parser parser,
EPlayer callerPlayer,
optional BaseText subCommandName
) {
local array<EPlayer> targetPlayers;
local CommandParser commandParser;
local CallData callData;
if (parser == none || !parser.Ok()) {
callData.parsingError = CET_BadParser;
return callData;
}
// Parse targets and handle errors that can arise here
if (commandData.requiresTarget) {
targetPlayers = ParseTargets(parser, callerPlayer);
if (!parser.Ok()) {
callData.parsingError = CET_IncorrectTargetList;
return callData;
}
if (targetPlayers.length <= 0) {
callData.parsingError = CET_EmptyTargetList;
return callData;
}
}
// Parse parameters themselves
commandParser = CommandParser(_.memory.Allocate(class'CommandParser'));
callData = commandParser.ParseWith(
parser,
commandData,
callerPlayer,
subCommandName);
callData.targetPlayers = targetPlayers;
commandParser.FreeSelf();
return callData;
}
/// Executes caller `Command` with data provided by `callData` if it is in
/// a correct state and reports error to `callerPlayer` if `callData` is
/// invalid.
///
/// Returns `true` if command was successfully executed and `false` otherwise.
/// Execution is considered successful if `Execute()` call was made, regardless
/// of whether `Command` can actually perform required action.
/// For example, giving a weapon to a player can fail because he does not have
/// enough space in his inventory, but it will still be considered a successful
/// execution as far as return value is concerned.
///
/// [`permissions`] argument is supposed to specify permissions with which this
/// command runs.
/// If [`permissionsConfigClass`] is `none`, it must always be `none`.
/// If [`permissionsConfigClass`] is not `none`, then [`permissions`] argument
/// being `none` should mean running with minimal priviledges.
public final function bool Execute(
CallData callData,
EPlayer callerPlayer,
CommandPermissions permissions
) {
local int i;
local array<EPlayer> targetPlayers;
if (callerPlayer == none) return false;
if (!callerPlayer.IsExistent()) return false;
// Report or execute
if (callData.parsingError != CET_None) {
ReportError(callData, callerPlayer);
return false;
}
targetPlayers = callData.targetPlayers;
publicConsole = _.console.ForAll();
callerConsole = _.console.For(callerPlayer);
callerConsole
.Write(P("Executing command `"))
.Write(usedName)
.Say(P("`"));
// `othersConsole` should also exist in time for `Executed()` call
othersConsole = _.console.ForAll().ButPlayer(callerPlayer);
Executed(callData, callerPlayer, permissions);
_.memory.Free(othersConsole);
if (commandData.requiresTarget) {
for (i = 0; i < targetPlayers.length; i += 1) {
targetConsole = _.console.For(targetPlayers[i]);
othersConsole = _.console
.ForAll()
.ButPlayer(callerPlayer)
.ButPlayer(targetPlayers[i]);
ExecutedFor(targetPlayers[i], callData, callerPlayer, permissions);
_.memory.Free(othersConsole);
_.memory.Free(targetConsole);
}
}
othersConsole = none;
targetConsole = none;
DeallocateConsoles();
return true;
}
/// Auxiliary method that cleans up all data and deallocates all objects inside provided structure.
public final static function DeallocateCallData(/* take */ CallData callData) {
__().memory.Free(callData.subCommandName);
__().memory.Free(callData.parameters);
__().memory.Free(callData.options);
__().memory.Free(callData.errorCause);
__().memory.FreeMany(callData.targetPlayers);
if (callData.targetPlayers.length > 0) {
callData.targetPlayers.length = 0;
}
}
private final function CleanParameters(array<Parameter> parameters) {
local int i;
for (i = 0; i < parameters.length; i += 1) {
_.memory.Free(parameters[i].displayName);
_.memory.Free(parameters[i].variableName);
_.memory.Free(parameters[i].aliasSourceName);
}
}
/// Returns name (in lower case) of the caller command class.
public final static function Text GetPreferredName() {
return __().text.FromString(Locs(default.preferredName));
}
/// Returns name (in lower case) of the caller command class.
public final static function string GetPreferredName_S() {
return Locs(default.preferredName);
}
/// Returns name (in lower case) of the caller command class.
public final function Text GetName() {
if (usedName == none) {
return P("").Copy();
}
return usedName.LowerCopy();
}
/// Returns name (in lower case) of the caller command class.
public final function string GetName_S() {
if (usedName == none) {
return "";
}
return _.text.IntoString(/*take*/ usedName.LowerCopy());
}
/// Returns group name (in lower case) of the caller command class.
public final function Text GetGroupName() {
if (commandData.group == none) {
return P("").Copy();
}
return commandData.group.LowerCopy();
}
/// Returns group name (in lower case) of the caller command class.
public final function string GetGroupName_S() {
if (commandData.group == none) {
return "";
}
return _.text.IntoString(/*take*/ commandData.group.LowerCopy());
}
/// Loads permissions config with a given name for the caller [`Command`] class.
///
/// Permission configs describe allowed usage of the [`Command`].
/// Basic settings are contained inside [`CommandPermissions`], but commands
/// should derive their own child classes for storing their settings.
///
/// Returns `none` if caller [`Command`] class didn't specify custom permission
/// settings class or provided name is invalid (according to
/// [`BaseText::IsValidName()`]).
/// Otherwise guaranteed to return a config reference.
public final static function CommandPermissions LoadConfig(BaseText configName) {
if (configName == none) return none;
if (default.permissionsConfigClass == none) return none;
// This creates default config if it is missing
default.permissionsConfigClass.static.NewConfig(configName);
return CommandPermissions(default.permissionsConfigClass.static
.GetConfigInstance(configName));
}
/// Loads permissions config with a given name for the caller [`Command`] class.
///
/// Permission configs describe allowed usage of the [`Command`].
/// Basic settings are contained inside [`CommandPermissions`], but commands
/// should derive their own child classes for storing their settings.
///
/// Returns `none` if caller [`Command`] class didn't specify custom permission
/// settings class or provided name is invalid (according to
/// [`BaseText::IsValidName()`]).
/// Otherwise guaranteed to return a config reference.
public final static function CommandPermissions LoadConfig_S(string configName) {
local MutableText wrapper;
local CommandPermissions result;
wrapper = __().text.FromStringM(configName);
result = LoadConfig(wrapper);
__().memory.Free(wrapper);
return result;
}
/// Returns subcommands of caller [`Command`] according to the provided
/// permissions.
///
/// If provided `none` as permissions, returns all available sub commands.
public final function array<Text> GetSubCommands(optional CommandPermissions permissions) {
local int i, j;
local bool addSubCommand;
local array<string> forbiddenCommands;
local array<Text> result;
forbiddenCommands = permissions.forbiddenSubCommands;
if (permissions != none) {
forbiddenCommands = permissions.forbiddenSubCommands;
}
for (i = 0; i < commandData.subCommands.length; i += 1) {
addSubCommand = true;
for (j = 0; j < forbiddenCommands.length; j += 1) {
if (commandData.subCommands[i].name.ToString() ~= forbiddenCommands[j]) {
addSubCommand = false;
break;
}
}
if (addSubCommand) {
result[result.length] = commandData.subCommands[i].name.LowerCopy();
}
}
return result;
}
/// Returns sub commands of caller [`Command`] according to the provided
/// permissions.
///
/// If provided `none` as permissions, returns all available sub commands.
public final function array<string> GetSubCommands_S(optional CommandPermissions permissions) {
return _.text.IntoStrings(GetSubCommands(permissions));
}
/// Checks whether a given sub command (case insensitive) is allowed to be
/// executed with given permissions.
///
/// If `none` is passed as either argument, returns `true`.
///
/// Doesn't check for the existence of sub command, only that permissions do not
/// explicitly forbid it.
/// In case non-existing subcommand is passed as an argument, the result
/// should be considered undefined.
public final function bool IsSubCommandAllowed(
BaseText subCommand,
CommandPermissions permissions
) {
if (subCommand == none) return true;
if (permissions == none) return true;
return IsSubCommandAllowed_S(subCommand.ToString(), permissions);
}
/// Checks whether a given sub command (case insensitive) is allowed to be
/// executed with given permissions.
///
/// If `none` is passed for permissions, always returns `true`.
///
/// Doesn't check for the existence of sub command, only that permissions do not
/// explicitly forbid it.
/// In case non-existing sub command is passed as an argument, the result
/// should be considered undefined.
public final function bool IsSubCommandAllowed_S(
string subCommand,
CommandPermissions permissions
) {
local int i;
local array<string> forbiddenCommands;
if (permissions == none) {
return true;
}
forbiddenCommands = permissions.forbiddenSubCommands;
for (i = 0; i < forbiddenCommands.length; i += 1) {
if (subCommand ~= forbiddenCommands[i]) {
return false;
}
}
return true;
}
/// Returns `Command.Data` struct that describes caller `Command`.
///
/// Returned struct contains `Text` references that are used internally by
/// the `Command` and not their copies.
///
/// Generally this is undesired approach and leaves `Command` more vulnerable to
/// modification, but copying all the data inside would not only introduce
/// a largely pointless computational overhead, but also would require some
/// cumbersome logic.
/// This might change in the future, so deallocating any objects in the returned
/// `struct` would lead to undefined behavior.
public final function Data BorrowData() {
return commandData;
}
private final function DeallocateConsoles() {
if (publicConsole != none && publicConsole.IsAllocated()) {
_.memory.Free(publicConsole);
}
if (callerConsole != none && callerConsole.IsAllocated()) {
_.memory.Free(callerConsole);
}
if (targetConsole != none && targetConsole.IsAllocated()) {
_.memory.Free(targetConsole);
}
if (othersConsole != none && othersConsole.IsAllocated()) {
_.memory.Free(othersConsole);
}
publicConsole = none;
callerConsole = none;
targetConsole = none;
othersConsole = none;
}
/// Reports given error to the `callerPlayer`, appropriately picking
/// message color
private final function ReportError(CallData callData, EPlayer callerPlayer) {
local Text errorMessage;
local ConsoleWriter console;
if (callerPlayer == none) return;
if (!callerPlayer.IsExistent()) return;
// Setup console color
console = callerPlayer.BorrowConsole();
if (callData.parsingError == CET_EmptyTargetList) {
console.UseColor(_.color.textWarning);
} else {
console.UseColor(_.color.textFailure);
}
// Send message
errorMessage = PrintErrorMessage(callData);
console.Say(errorMessage);
errorMessage.FreeSelf();
// Restore console color
console.ResetColor().Flush();
}
private final function Text PrintErrorMessage(CallData callData) {
local Text result;
local MutableText builder;
builder = _.text.Empty();
switch (callData.parsingError) {
case CET_BadParser:
builder.Append(P("Internal error occurred: invalid parser"));
break;
case CET_NoSubCommands:
builder.Append(P("Ill defined command: no subcommands"));
break;
case CET_BadSubCommand:
builder
.Append(P("Ill defined sub-command: "))
.Append(callData.errorCause);
break;
case CET_NoRequiredParam:
builder
.Append(P("Missing required parameter: "))
.Append(callData.errorCause);
break;
case CET_NoRequiredParamForOption:
builder
.Append(P("Missing required parameter for option: "))
.Append(callData.errorCause);
break;
case CET_UnknownOption:
builder
.Append(P("Invalid option specified: "))
.Append(callData.errorCause);
break;
case CET_UnknownShortOption:
builder.Append(P("Invalid short option specified"));
break;
case CET_RepeatedOption:
builder
.Append(P("Option specified several times: "))
.Append(callData.errorCause);
break;
case CET_UnusedCommandParameters:
builder.Append(P("Part of command could not be parsed: "))
.Append(callData.errorCause);
break;
case CET_MultipleOptionsWithParams:
builder
.Append(P("Multiple short options in one declarations require parameters: "))
.Append(callData.errorCause);
break;
case CET_IncorrectTargetList:
builder
.Append(P("Target players are incorrectly specified."))
.Append(callData.errorCause);
break;
case CET_EmptyTargetList:
builder
.Append(P("List of target players is empty"))
.Append(callData.errorCause);
break;
default:
}
result = builder.Copy();
builder.FreeSelf();
return result;
}
// Auxiliary method for parsing list of targeted players.
// Assumes given parser is not `none` and not in a failed state.
// If parsing failed, guaranteed to return an empty array.
private final function array<EPlayer> ParseTargets(Parser parser, EPlayer callerPlayer) {
local array<EPlayer> targetPlayers;
local PlayersParser targetsParser;
targetsParser = PlayersParser(_.memory.Allocate(class'PlayersParser'));
targetsParser.SetSelf(callerPlayer);
targetsParser.ParseWith(parser);
if (parser.Ok()) {
targetPlayers = targetsParser.GetPlayers();
}
targetsParser.FreeSelf();
return targetPlayers;
}
defaultproperties {
preferredName = ""
permissionsConfigClass = none
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
/**
* Source intended for command aliases.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandAliasSource extends AliasSource
config(AcediaAliases_Commands);
defaultproperties
{
aliasesClass = class'CommandAliases'
}

View File

@ -0,0 +1,28 @@
/**
* Per-object-configuration intended for command aliases.
* Copyright 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandAliases extends AliasesStorage
perObjectConfig
config(AcediaAliases_Commands);
defaultproperties
{
configName = "AcediaAliases_Commands"
sourceClass = class'CommandAliasSource'
}

View File

@ -0,0 +1,939 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandDataBuilder extends AcediaObject
dependson(Command);
//! This is an auxiliary class for convenient creation of [`Command::Data`]
//! using a builder pattern.
//!
//! ## Implementation
//!
//! We will store all defined data in two ways:
//!
//! 1. Selected data: data about parameters for subcommand/option that is
//! currently being filled;
//! 2. Prepared data: data that was already filled as "selected data" then
//! stored in these records. Whenever we want to switch to filling another
//! subcommand/option or return already prepared data we must dump
//! "selected data" into "prepared data" first and then return the latter.
//!
//! Builder object is automatically created when new `Command` instance is
//! allocated and doesn't normally need to be allocated by hand.
// "Prepared data"
var private Text commandName, commandGroup;
var private Text commandSummary;
var private array<Command.SubCommand> subcommands;
var private array<Command.Option> options;
var private bool requiresTarget;
// Auxiliary arrays signifying that we've started adding optional parameters
// into appropriate `subcommands` and `options`.
//
// All optional parameters must follow strictly after required parameters and
// so, after user have started adding optional parameters to subcommand/option,
// we prevent them from adding required ones (to that particular
// command/option).
var private array<byte> subcommandsIsOptional;
var private array<byte> optionsIsOptional;
// "Selected data"
// `false` means we have selected sub-command, `true` - option
var private bool selectedItemIsOption;
// `name` for sub-commands, `longName` for options
var private Text selectedItemName;
// Description of selected sub-command/option
var private Text selectedDescription;
// Are we filling optional parameters (`true`)? Or required ones (`false`)?
var private bool selectionIsOptional;
// Array of parameters we are currently filling (either required or optional)
var private array<Command.Parameter> selectedParameterArray;
var private LoggerAPI.Definition errLongNameTooShort, errShortNameTooLong;
var private LoggerAPI.Definition warnSameLongName, warnSameShortName;
protected function Constructor() {
// Fill empty subcommand (no special key word) by default
SubCommand(P(""));
}
protected function Finalizer() {
subcommands.length = 0;
subcommandsIsOptional.length = 0;
options.length = 0;
optionsIsOptional.length = 0;
selectedParameterArray.length = 0;
commandName = none;
commandGroup = none;
commandSummary = none;
selectedItemName = none;
selectedDescription = none;
requiresTarget = false;
selectedItemIsOption = false;
selectionIsOptional = false;
}
/// Method that starts defining a new sub-command.
///
/// Creates new sub-command with a given name (if it's missing) and then selects
/// sub-command with a given name to add parameters to.
///
/// [`name`] defines name of the sub-command user wants, case-sensitive.
/// If `none` is passed, this method will do nothing.
public final function SubCommand(BaseText name) {
local int subcommandIndex;
if (name == none) {
return;
}
if (!selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(name)) {
return;
}
RecordSelection();
subcommandIndex = FindSubCommandIndex(name);
if (subcommandIndex < 0) {
MakeEmptySelection(name, false);
return;
}
// Load appropriate prepared data, if it exists for
// sub-command with name `name`
selectedItemIsOption = false;
selectedItemName = subcommands[subcommandIndex].name;
selectedDescription = subcommands[subcommandIndex].description;
selectionIsOptional = subcommandsIsOptional[subcommandIndex] > 0;
if (selectionIsOptional) {
selectedParameterArray = subcommands[subcommandIndex].optional;
} else {
selectedParameterArray = subcommands[subcommandIndex].required;
}
}
/// Method that starts defining a new option.
///
/// This method checks if some of the recorded options are in conflict with
/// given `longName` and `shortName` (already using one and only one of them).
/// In case there is no conflict, it creates new option with specified long and
/// short names (if such option is missing) and selects option with a long name
/// `longName` to add parameters to.
///
/// [`longName`] defines long name of the option, case-sensitive (for using
/// an option in form "--..."). Must be at least two characters long.
/// [`shortName`] defines short name of the option, case-sensitive (for using
/// an option in form "-..."). Must be exactly one character.
///
/// # Errors
///
/// Errors will be logged in case either of arguments are `none`, have
/// inappropriate length or are in conflict with each other.
public final function Option(BaseText longName, optional BaseText shortName) {
local int optionIndex;
local BaseText.Character shortNameAsCharacter;
// Unlike for `SubCommand()`, we need to ensure that option naming is
// correct and does not conflict with existing options
// (user might attempt to add two options with same long names and
// different short ones).
shortNameAsCharacter = GetValidShortName(longName, shortName);
if ( !_.text.IsValidCharacter(shortNameAsCharacter)
|| VerifyNoOptionNamingConflict(longName, shortNameAsCharacter)) {
// ^ `GetValidShortName()` and `VerifyNoOptionNamingConflict()`
// are responsible for logging warnings/errors
return;
}
SelectOption(longName);
// Set short name for new options
optionIndex = FindOptionIndex(longName);
if (optionIndex < 0) {
// We can only be here if option was created for the first time
RecordSelection();
// So now it cannot fail
optionIndex = FindOptionIndex(longName);
options[optionIndex].shortName = shortNameAsCharacter;
}
}
/// Adds description to the selected sub-command / option.
///
/// Highlights parts of the description in-between "`" characters.
///
/// Does nothing if nothing is yet selected.
public final function Describe(BaseText description) {
local int fromIndex, toIndex;
local BaseText.Formatting keyWordFormatting;
local bool lookingForEnd;
local MutableText coloredDescription;
if (description == none) {
return;
}
keyWordFormatting = _.text.FormattingFromColor(_.color.TextEmphasis);
coloredDescription = description.MutableCopy();
while (true) {
if (lookingForEnd) {
toIndex = coloredDescription.IndexOf(P("`"), fromIndex + 1);
} else {
fromIndex = coloredDescription.IndexOf(P("`"), toIndex + 1);
}
if (toIndex < 0 || fromIndex < 0) {
break;
}
if (lookingForEnd) {
coloredDescription.ChangeFormatting(
keyWordFormatting,
fromIndex,
toIndex - fromIndex + 1);
lookingForEnd = false;
} else {
lookingForEnd = true;
}
}
coloredDescription.Replace(P("`"), P(""));
if (lookingForEnd) {
coloredDescription.ChangeFormatting(keyWordFormatting, fromIndex);
}
_.memory.Free(selectedDescription);
selectedDescription = coloredDescription.IntoText();
}
/// Sets new group of `Command.Data` under construction.
///
/// Group name is meant to be shared among several commands, allowing user to
/// filter or fetch commands of a certain group.
/// Group name is case-insensitive.
public final function Group(BaseText newName) {
if (newName != none && newName == commandGroup) {
return;
}
_.memory.Free(commandGroup);
if (newName != none) {
commandGroup = newName.Copy();
} else {
commandGroup = none;
}
}
/// Sets new summary of `Command.Data` under construction.
///
/// Summary gives a short description of the command on the whole that will
/// be displayed when "help" command is listing available command
public final function Summary(BaseText newSummary) {
if (newSummary != none && newSummary == commandSummary) {
return;
}
_.memory.Free(commandSummary);
if (newSummary != none) {
commandSummary = newSummary.Copy();
} else {
commandSummary = none;
}
}
/// Makes caller builder to mark `Command.Data` under construction to require
/// a player target.
public final function RequireTarget() {
requiresTarget = true;
}
/// Any parameters added to currently selected sub-command / option after
/// calling this method will be marked as optional.
///
/// Further calls when the same sub-command / option is selected will do
/// nothing.
public final function OptionalParams()
{
if (selectionIsOptional) {
return;
}
// Record all required parameters first, otherwise there would be no way
// to distinguish between them and optional parameters
RecordSelection();
selectionIsOptional = true;
selectedParameterArray.length = 0;
}
/// Returns data that has been constructed so far by the caller
/// [`CommandDataBuilder`].
///
/// Does not reset progress.
public final function Command.Data BorrowData() {
local Command.Data newData;
// TODO: is this copying needed?
RecordSelection();
newData.group = commandGroup;
newData.summary = commandSummary;
newData.subcommands = subcommands;
newData.options = options;
newData.requiresTarget = requiresTarget;
return newData;
}
// Adds new parameter to selected sub-command / option
private final function PushParameter(Command.Parameter newParameter) {
selectedParameterArray[selectedParameterArray.length] = newParameter;
}
// Fills `Command.ParameterType` struct with given values
// (except boolean format).
// Assumes `displayName != none`.
private final function Command.Parameter NewParameter(
BaseText displayName,
Command.ParameterType parameterType,
bool isListParameter,
optional BaseText variableName
) {
local Command.Parameter newParameter;
newParameter.displayName = displayName.Copy();
newParameter.type = parameterType;
newParameter.allowsList = isListParameter;
if (variableName != none) {
newParameter.variableName = variableName.Copy();
} else {
newParameter.variableName = displayName.Copy();
}
return newParameter;
}
/// Adds new boolean parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`format`] defines preferred format of boolean values.
/// Command parser will still accept boolean values in any form, this setting
/// only affects how parameter will be displayed in generated help.
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command
/// input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamBoolean(
BaseText name,
optional Command.PreferredBooleanFormat format,
optional BaseText variableName
) {
local Command.Parameter newParam;
if (name != none) {
newParam = NewParameter(name, CPT_Boolean, false, variableName);
newParam.booleanFormat = format;
PushParameter(newParam);
}
}
/// Adds new integer list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`format`] defines preferred format of boolean values.
/// Command parser will still accept boolean values in any form, this setting
/// only affects how
/// parameter will be displayed in generated help.
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamBooleanList(
BaseText name,
optional Command.PreferredBooleanFormat format,
optional BaseText variableName
) {
local Command.Parameter newParam;
if (name != none) {
newParam = NewParameter(name, CPT_Boolean, true, variableName);
newParam.booleanFormat = format;
PushParameter(newParam);
}
}
/// Adds new integer parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamInteger(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Integer, false, variableName));
}
}
/// Adds new integer list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter (it would appear in
/// the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamIntegerList(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Integer, true, variableName));
}
}
/// Adds new numeric parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamNumber(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Number, false, variableName));
}
}
/// Adds new numeric list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamNumberList(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Number, true, variableName));
}
}
/// Adds new text parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
///
/// [`aliasSourceName`] defines name of the alias source that must be used to
/// auto-resolve this parameter's value. `none` means that parameter will be
/// recorded as-is, any other value (either "weapon", "color", "feature",
/// "entity" or some kind of custom alias source name) will make values prefixed
/// with "$" to be resolved as custom aliases.
/// In case auto-resolving is used, value will be recorded as a `HasTable` with
/// two fields: "alias" - value provided by user and (in case "$" prefix was
/// used) "value" - actual resolved value of an alias.
/// If alias has failed to be resolved, `none` will be stored as a value.
public final function ParamText(
BaseText name,
optional BaseText variableName,
optional BaseText aliasSourceName
) {
local Command.Parameter newParameterValue;
if (name == none) {
return;
}
newParameterValue = NewParameter(name, CPT_Text, false, variableName);
if (aliasSourceName != none) {
newParameterValue.aliasSourceName = aliasSourceName.Copy();
}
PushParameter(newParameterValue);
}
/// Adds new text list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter (it would appear in the
/// generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed. If left `none`, - will coincide with
/// `name` parameter.
///
/// [`aliasSourceName`] defines name of the alias source that must be used to
/// auto-resolve this parameter's value. `none` means that parameter will be
/// recorded as-is, any other value (either "weapon", "color", "feature",
/// "entity" or some kind of custom alias source name) will make values prefixed
/// with "$" to be resolved as custom aliases.
/// In case auto-resolving is used, value will be recorded as a `HasTable` with
/// two fields: "alias" - value provided by user and (in case "$" prefix was
/// used) "value" - actual resolved value of an alias.
/// If alias has failed to be resolved, `none` will be stored as a value.
public final function ParamTextList(
BaseText name,
optional BaseText variableName,
optional BaseText aliasSourceName
) {
local Command.Parameter newParameterValue;
if (name == none) {
return;
}
newParameterValue = NewParameter(name, CPT_Text, true, variableName);
if (aliasSourceName != none) {
newParameterValue.aliasSourceName = aliasSourceName.Copy();
}
PushParameter(newParameterValue);
}
/// Adds new remainder parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// Remainder parameter is a special parameter that will simply consume all
/// remaining command's input as-is.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamRemainder(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Remainder, false, variableName));
}
}
/// Adds new JSON object parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamObject(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Object, false, variableName));
}
}
/// Adds new JSON object list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamObjectList(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Object, true, variableName));
}
}
/// Adds new JSON array parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamArray(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Array, false, variableName));
}
}
/// Adds new JSON array list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamArrayList(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_Array, true, variableName));
}
}
/// Adds new JSON value parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamJSON(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_JSON, false, variableName));
}
}
/// Adds new JSON value list parameter (required or optional depends on whether
/// `OptionalParams()` call happened) to the currently selected
/// sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamJSONList(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_JSON, true, variableName));
}
}
/// Adds new parameter that defines a set of players (required or optional
/// depends on whether `OptionalParams()` call happened) to the currently
/// selected sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamPlayers(BaseText name, optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_PLAYERS, false, variableName));
}
}
/// Adds new parameter that defines a list of sets of players (required or
/// optional depends on whether `OptionalParams()` call happened) to
/// the currently selected sub-command / option.
///
/// Only fails if provided `name` is `none`.
///
/// List parameters expect user to enter one or more value of the same type as
/// command's arguments.
///
/// [`name`] will become the name of the parameter
/// (it would appear in the generated "help" command info).
///
/// [`variableName`] will become key for this parameter's value in `HashTable`
/// after user's command input is parsed.
/// If left `none`, - will coincide with `name` parameter.
public final function ParamPlayersList(BaseText name,optional BaseText variableName) {
if (name != none) {
PushParameter(NewParameter(name, CPT_PLAYERS, true, variableName));
}
}
// Find index of sub-command with a given name `name` in `subcommands`.
// `-1` if there's not sub-command with such name.
// Case-sensitive.
private final function int FindSubCommandIndex(BaseText name) {
local int i;
if (name == none) {
return -1;
}
for (i = 0; i < subcommands.length; i += 1) {
if (name.Compare(subcommands[i].name)) {
return i;
}
}
return -1;
}
// Find index of option with a given name `name` in `options`.
// `-1` if there's not sub-command with such name.
// Case-sensitive.
private final function int FindOptionIndex(BaseText longName) {
local int i;
if (longName == none) {
return -1;
}
for (i = 0; i < options.length; i += 1) {
if (longName.Compare(options[i].longName)) {
return i;
}
}
return -1;
}
// Creates an empty selection record for subcommand or option with name (long name) `name`.
// Doe not check whether subcommand/option with that name already exists.
// Copies passed `name`, assumes that it is not `none`.
private final function MakeEmptySelection(BaseText name, bool selectedOption) {
selectedItemIsOption = selectedOption;
selectedItemName = name.Copy();
selectedDescription = none;
selectedParameterArray.length = 0;
selectionIsOptional = false;
}
// Select option with a given long name `longName` from `options`.
// If there is no option with specified `longName` in prepared data - creates new record in
// selection, otherwise copies previously saved data.
// Automatically saves previously selected data into prepared data.
// Copies `name` if it has to create new record.
private final function SelectOption(BaseText longName) {
local int optionIndex;
if (longName == none) {
return;
}
if (selectedItemIsOption && selectedItemName != none && selectedItemName.Compare(longName)) {
return;
}
RecordSelection();
optionIndex = FindOptionIndex(longName);
if (optionIndex < 0) {
MakeEmptySelection(longName, true);
return;
}
// Load appropriate prepared data, if it exists for
// option with long name `longName`
selectedItemIsOption = true;
selectedItemName = options[optionIndex].longName;
selectedDescription = options[optionIndex].description;
selectionIsOptional = optionsIsOptional[optionIndex] > 0;
if (selectionIsOptional) {
selectedParameterArray = options[optionIndex].optional;
} else {
selectedParameterArray = options[optionIndex].required;
}
}
// Saves currently selected data into prepared data.
private final function RecordSelection() {
if (selectedItemName == none) {
return;
}
if (selectedItemIsOption) {
RecordSelectedOption();
} else {
RecordSelectedSubCommand();
}
}
// Saves selected sub-command into prepared records.
// Assumes that command and not an option is selected.
private final function RecordSelectedSubCommand() {
local int selectedSubCommandIndex;
local Command.SubCommand newSubcommand;
if (selectedItemName == none) {
return;
}
selectedSubCommandIndex = FindSubCommandIndex(selectedItemName);
if (selectedSubCommandIndex < 0) {
selectedSubCommandIndex = subcommands.length;
subcommands[selectedSubCommandIndex] = newSubcommand;
}
subcommands[selectedSubCommandIndex].name = selectedItemName;
subcommands[selectedSubCommandIndex].description = selectedDescription;
if (selectionIsOptional) {
subcommands[selectedSubCommandIndex].optional = selectedParameterArray;
subcommandsIsOptional[selectedSubCommandIndex] = 1;
} else {
subcommands[selectedSubCommandIndex].required = selectedParameterArray;
subcommandsIsOptional[selectedSubCommandIndex] = 0;
}
}
// Saves currently selected option into prepared records.
// Assumes that option and not an command is selected.
private final function RecordSelectedOption() {
local int selectedOptionIndex;
local Command.Option newOption;
if (selectedItemName == none) {
return;
}
selectedOptionIndex = FindOptionIndex(selectedItemName);
if (selectedOptionIndex < 0) {
selectedOptionIndex = options.length;
options[selectedOptionIndex] = newOption;
}
options[selectedOptionIndex].longName = selectedItemName;
options[selectedOptionIndex].description = selectedDescription;
if (selectionIsOptional) {
options[selectedOptionIndex].optional = selectedParameterArray;
optionsIsOptional[selectedOptionIndex] = 1;
} else {
options[selectedOptionIndex].required = selectedParameterArray;
optionsIsOptional[selectedOptionIndex] = 0;
}
}
// Validates names (printing errors in case of failure) for the option.
// Long name must be at least 2 characters long.
// Short name must be either:
// 1. exactly one character long;
// 2. `none`, which leads to deriving `shortName` from `longName`
// as a first character.
// Anything else will result in logging a failure and rejection of
// the option altogether.
// Returns `none` if validation failed and chosen short name otherwise
// (if `shortName` was used for it - it's value will be copied).
private final function BaseText.Character GetValidShortName(
BaseText longName,
BaseText shortName
) {
// Validate `longName`
if (longName == none) {
return _.text.GetInvalidCharacter();
}
if (longName.GetLength() < 2) {
_.logger.Auto(errLongNameTooShort).ArgClass(class).Arg(longName.Copy());
return _.text.GetInvalidCharacter();
}
// Validate `shortName`,
// deriving if from `longName` if necessary & possible
if (shortName == none) {
return longName.GetCharacter(0);
}
if (shortName.IsEmpty() || shortName.GetLength() > 1) {
_.logger.Auto(errShortNameTooLong).ArgClass(class).Arg(longName.Copy());
return _.text.GetInvalidCharacter();
}
return shortName.GetCharacter(0);
}
// Checks that if any option record has a long/short name from a given pair of
// names (`longName`, `shortName`), then it also has another one.
//
// i.e. we cannot have several options with identical names:
// (--silent, -s) and (--sick, -s).
private final function bool VerifyNoOptionNamingConflict(
BaseText longName,
BaseText.Character shortName
) {
local int i;
local bool sameShortNames, sameLongNames;
// To make sure we will search through the up-to-date `options`,
// record selection into prepared records.
RecordSelection();
for (i = 0; i < options.length; i += 1) {
sameShortNames = _.text.AreEqual(shortName, options[i].shortName);
sameLongNames = longName.Compare(options[i].longName);
if (sameLongNames && !sameShortNames) {
_.logger.Auto(warnSameLongName).ArgClass(class).Arg(longName.Copy());
return true;
}
if (!sameLongNames && sameShortNames) {
_.logger.Auto(warnSameLongName).ArgClass(class).Arg(_.text.FromCharacter(shortName));
return true;
}
}
return false;
}
defaultproperties
{
errLongNameTooShort = (l=LOG_Error,m="Command `%1` is trying to register an option with a name that is way too short (<2 characters). Option will be discarded: %2")
errShortNameTooLong = (l=LOG_Error,m="Command `%1` is trying to register an option with a short name that doesn't consist of just one character. Option will be discarded: %2")
warnSameLongName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same long name \"%2\", but different short names. This should not happen, do not expect correct behavior.")
warnSameShortName = (l=LOG_Error,m="Command `%1` is trying to register several options with the same short name \"%2\", but different long names. This should not have happened, do not expect correct behavior.")
}

View File

@ -0,0 +1,249 @@
/**
* Config class for storing map lists.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandList extends AcediaConfig
perObjectConfig
config(AcediaCommands);
//! `CommandList` describes a set of commands and votings that can be made
//! available to users inside Commands feature
//!
//! Optionally, permission configs can be specified for commands and votings,
//! allowing server admins to create command lists for different groups player
//! with the same commands, but different permissions.
// For storing `class<Command>` - `string` pairs in the config
struct CommandConfigStoragePair {
var public class<Command> cmd;
var public string config;
};
// For storing `class` - `string` pairs in the config
struct VotingConfigStoragePair {
var public class<Voting> vtn;
var public string config;
};
// For returning `class` - `Text` pairs into other Acedia classes
struct EntityConfigPair {
var public class<AcediaObject> class;
var public Text config;
};
/// Allows to specify if this list should only be added when server is running
/// in debug mode.
/// `true` means yes, `false` means that list will always be available.
var public config bool debugOnly;
/// Adds a command of specified class with a "default" permissions config.
var public config array< class<Command> > command;
/// Adds a command of specified class with specified permissions config
var public config array<CommandConfigStoragePair> commandWith;
/// Adds a voting of specified class with a "default" permissions config
var public config array< class<Voting> > voting;
/// Adds a voting of specified class with specified permissions config
var public config array<VotingConfigStoragePair> votingWith;
public final function array<EntityConfigPair> GetCommandData() {
local int i;
local EntityConfigPair nextPair;
local array<EntityConfigPair> result;
for (i = 0; i < command.length; i += 1) {
if (command[i] != none) {
nextPair.class = command[i];
result[result.length] = nextPair;
}
}
for (i = 0; i < commandWith.length; i += 1) {
if (commandWith[i].cmd != none) {
nextPair.class = commandWith[i].cmd;
if (commandWith[i].config != "") {
nextPair.config = _.text.FromString(commandWith[i].config);
}
result[result.length] = nextPair;
// Moved into the `result`
nextPair.config = none;
}
}
return result;
}
public final function array<EntityConfigPair> GetVotingData() {
local int i;
local EntityConfigPair nextPair;
local array<EntityConfigPair> result;
for (i = 0; i < voting.length; i += 1) {
if (voting[i] != none) {
nextPair.class = voting[i];
result[result.length] = nextPair;
}
}
for (i = 0; i < votingWith.length; i += 1) {
if (votingWith[i].vtn != none) {
nextPair.class = votingWith[i].vtn;
if (votingWith[i].config != "") {
nextPair.config = _.text.FromString(votingWith[i].config);
}
result[result.length] = nextPair;
// Moved into the `result`
nextPair.config = none;
}
}
return result;
}
protected function HashTable ToData() {
local int i;
local ArrayList entityArray;
local HashTable result, innerPair;
result = _.collections.EmptyHashTable();
result.SetBool(P("debugOnly"), debugOnly);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < command.length; i += 1) {
entityArray.AddString(string(command[i]));
}
result.SetItem(P("commands"), entityArray);
_.memory.Free(entityArray);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < voting.length; i += 1) {
entityArray.AddString(string(voting[i]));
}
result.SetItem(P("votings"), entityArray);
_.memory.Free(entityArray);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < commandWith.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("command"), string(commandWith[i].cmd));
innerPair.SetString(P("config"), commandWith[i].config);
entityArray.AddItem(innerPair);
_.memory.Free(innerPair);
}
result.SetItem(P("commandsWithConfig"), entityArray);
_.memory.Free(entityArray);
entityArray = _.collections.EmptyArrayList();
for (i = 0; i < votingWith.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("voting"), string(votingWith[i].vtn));
innerPair.SetString(P("config"), votingWith[i].config);
entityArray.AddItem(innerPair);
_.memory.Free(innerPair);
}
result.SetItem(P("votingsWithConfig"), entityArray);
_.memory.Free(entityArray);
return result;
}
protected function FromData(HashTable source) {
local int i;
local ArrayList entityArray;
local HashTable innerPair;
local class<Command> nextCommandClass;
local class<Voting> nextVotingClass;
local CommandConfigStoragePair nextCommandPair;
local VotingConfigStoragePair nextVotingPair;
if (source == none) {
return;
}
debugOnly = source.GetBool(P("debugOnly"));
command.length = 0;
entityArray = source.GetArrayList(P("commands"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
nextCommandClass = class<Command>(_.memory.LoadClass_S(entityArray.GetString(i)));
if (nextCommandClass != none) {
command[command.length] = nextCommandClass;
}
}
}
_.memory.Free(entityArray);
voting.length = 0;
entityArray = source.GetArrayList(P("votings"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
nextVotingClass = class<Voting>(_.memory.LoadClass_S(entityArray.GetString(i)));
if (nextVotingClass != none) {
voting[voting.length] = nextVotingClass;
}
}
}
_.memory.Free(entityArray);
commandWith.length = 0;
entityArray = source.GetArrayList(P("commandsWithConfig"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
innerPair = entityArray.GetHashTable(i);
if (innerPair == none) {
continue;
}
nextCommandPair.cmd =
class<Command>(_.memory.LoadClass_S(innerPair.GetString(P("command"))));
nextCommandPair.config = innerPair.GetString(P("config"));
_.memory.Free(innerPair);
if (nextCommandPair.cmd != none) {
commandWith[commandWith.length] = nextCommandPair;
}
}
}
_.memory.Free(entityArray);
votingWith.length = 0;
entityArray = source.GetArrayList(P("votingsWithConfig"));
if (entityArray != none) {
for (i = 0; i < entityArray.GetLength(); i += 1) {
innerPair = entityArray.GetHashTable(i);
if (innerPair == none) {
continue;
}
nextVotingPair.vtn =
class<Voting>(_.memory.LoadClass_S(innerPair.GetString(P("voting"))));
nextVotingPair.config = innerPair.GetString(P("config"));
_.memory.Free(innerPair);
if (nextVotingPair.vtn != none) {
votingWith[votingWith.length] = nextVotingPair;
}
}
}
_.memory.Free(entityArray);
}
protected function DefaultIt() {
debugOnly = false;
command.length = 0;
commandWith.length = 0;
voting.length = 0;
votingWith.length = 0;
command[0] = class'ACommandHelp';
command[1] = class'ACommandVote';
}
defaultproperties {
configName = "AcediaCommands"
supportsDataConversion = true
debugOnly = false
command(0) = class'ACommandHelp'
command(1) = class'ACommandVote'
}

View File

@ -0,0 +1,983 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandParser extends AcediaObject
dependson(Command);
//! Class specialized for parsing user input of the command's call into
//![ `Command.CallData`] structure with the information about all parsed
//! arguments.
//!
//! [`CommandParser`] is not made to parse the whole input:
//!
//! * Command's name needs to be parsed and resolved as an alias before using
//! this parser - it won't do this hob for you;
//! * List of targeted players must also be parsed using [`PlayersParser`] -
//! [`CommandParser`] won't do this for you;
//! * Optionally one can also decide on the referred subcommand and pass it into
//! [`ParseWith()`] method. If subcommand's name is not passed -
//! [`CommandParser`] will try to parse it itself.
//! This feature is used to add support for subcommand aliases.
//!
//! However, above steps are handled by [`Commands_Feature`] and one only needs to
//! call that feature's [`HandleInput()`] methods to pass user input with command
//! call line there.
//!
//! # Usage
//!
//! Allocate [`CommandParser`] and call [`ParseWith()`] method, providing it with:
//!
//! 1. [`Parser`], filled with command call input;
//! 2. Command's data that describes subcommands, options and their parameters
//! for the command, which call we are parsing;
//! 3. (Optionally) [`EPlayer`] reference to the player that initiated
//! the command call;
//! 4. (Optionally) Subcommand to be used - this will prevent [`CommandParser`]
//! from parsing subcommand name itself. Used for implementing aliases that
//! refer to a particular subcommand.
//!
//! # Implementation
//!
//! [`CommandParser`] stores both its state and command data, relevant to parsing,
//! as its member variables during the whole parsing process, instead of passing
//! that data around in every single method.
//!
//! We will give a brief overview of how around 20 parsing methods below are
//! interconnected.
//!
//! The only public method [`ParseWith()`] is used to start parsing and it uses
//! [`PickSubCommand()`] to first try and figure out what sub command is
//! intended by user's input.
//!
//! Main bulk of the work is done by [`ParseParameterArrays()`] method, for
//! simplicity broken into two [`ParseRequiredParameterArray()`] and
//! [`ParseOptionalParameterArray()`] methods that can parse
//! parameters for both command itself and it's options.
//!
//! They go through arrays of required and optional parameters, calling
//! [`ParseParameter()`] for each parameters, which in turn can make several
//! calls of [`ParseSingleValue()`] to parse parameters' values: it is called
//! once for single-valued parameters, but possibly several times for list
//! parameters that can contain several values.
//!
//! So main parsing method looks something like:
//!
//! ```
//! ParseParameterArrays() {
//! loop ParseParameter() {
//! loop ParseSingleValue()
//! }
//! }
//! ```
//!
//! [`ParseSingleValue()`] is essentially that redirects it's method call to
//! another, more specific, parsing method based on the parameter type.
//!
//! Finally, to allow users to specify options at any point in command, we call
//! [`TryParsingOptions()`] at the beginning of every [`ParseSingleValue()`]
//! (the only parameter that has higher priority than options is
//! [`CPT_Remainder`]), since option definition can appear at any place between
//! parameters. We also call `TryParsingOptions()` *after* we've parsed all
//! command's parameters, since that case won't be detected by parsing them
//! *before* every parameter.
//!
//! [`TryParsingOptions()`] itself simply tries to detect "-" and "--" prefixes
//! (filtering out negative numeric values) and then redirect the call to either
//! of more specialized methods: [`ParseLongOption()`] or
//! [`ParseShortOption()`], that can in turn make another
//! [`ParseParameterArrays()`] call, if specified option has parameters.
//!
//! NOTE: [`ParseParameterArrays()`] can only nest in itself once, since option
//! declaration always interrupts previous option's parameter list.
//! Rest of the methods perform simple auxiliary functions.
// Describes which parameters we are currently parsing, classifying them
// as either "necessary" or "extra".
//
// E.g. if last require parameter is a list of integers,
// then after parsing first integer we are:
//
// * Still parsing required *parameter* "integer list";
// * But no more integers are *necessary* for successful parsing.
//
// Therefore we consider parameter "necessary" if the lack of it will
// result in failed parsing and "extra" otherwise.
enum ParsingTarget {
// We are in the process of parsing required parameters, that must all
// be present.
// This case does not include parsing last required parameter: it needs
// to be treated differently to track when we change from "necessary" to
// "extra" parameters.
CPT_NecessaryParameter,
// We are parsing last necessary parameter.
CPT_LastNecessaryParameter,
// We are not parsing extra parameters that can be safely omitted.
CPT_ExtraParameter,
};
// Parser filled with user input.
var private Parser commandParser;
// Data for sub-command specified by both command we are parsing
// and user's input; determined early during parsing.
var private Command.SubCommand pickedSubCommand;
// Options available for the command we are parsing.
var private array<Command.Option> availableOptions;
// Result variable we are filling during the parsing process,
// should be `none` outside of [`self.ParseWith()`] method call.
var private Command.CallData nextResult;
// Parser for player parameters, setup with a caller for current parsing
var private PlayersParser currentPlayersParser;
// Current [`ParsingTarget`], see it's enum description for more details
var private ParsingTarget currentTarget;
// `true` means we are parsing parameters for a command's option and
// `false` means we are parsing command's own parameters
var private bool currentTargetIsOption;
// If we are parsing parameters for an option (`currentTargetIsOption == true`)
// this variable will store that option's data.
var private Command.Option targetOption;
// Last successful state of [`commandParser`].
var Parser.ParserState confirmedState;
// Options we have so far encountered during parsing, necessary since we want
// to forbid specifying th same option more than once.
var private array<Command.Option> usedOptions;
// Literals that can be used as boolean values
var private array<string> booleanTrueEquivalents;
var private array<string> booleanFalseEquivalents;
var LoggerAPI.Definition errNoSubCommands;
protected function Finalizer() {
Reset();
}
/// Parses user's input given in [`parser`] using command's information given by
/// [`commandData`].
///
/// Optionally, sub-command can be specified for the [`CommandParser`] to use
/// via [`specifiedSubCommand`] argument.
/// If this argument's value is `none` - it will be parsed from [`parser`]'s
/// data instead.
///
/// Returns results of parsing, described by [`Command.CallData`].
/// Returned object is guaranteed to be not `none`.
public final function Command.CallData ParseWith(
Parser parser,
Command.Data commandData,
EPlayer callerPlayer,
optional BaseText specifiedSubCommand
) {
local HashTable commandParameters;
// Temporary object to return `nextResult` while setting variable to `none`
local Command.CallData toReturn;
nextResult.parameters = _.collections.EmptyHashTable();
nextResult.options = _.collections.EmptyHashTable();
if (commandData.subCommands.length == 0) {
DeclareError(CET_NoSubCommands, none);
toReturn = nextResult;
Reset();
return toReturn;
}
if (parser == none || !parser.Ok()) {
DeclareError(CET_BadParser, none);
toReturn = nextResult;
Reset();
return toReturn;
}
commandParser = parser;
availableOptions = commandData.options;
currentPlayersParser =
PlayersParser(_.memory.Allocate(class'PlayersParser'));
currentPlayersParser.SetSelf(callerPlayer);
// (subcommand) (parameters, possibly with options) and nothing else!
PickSubCommand(commandData, specifiedSubCommand);
nextResult.subCommandName = pickedSubCommand.name.Copy();
commandParameters = ParseParameterArrays(pickedSubCommand.required, pickedSubCommand.optional);
AssertNoTrailingInput(); // make sure there is nothing else
if (commandParser.Ok()) {
nextResult.parameters = commandParameters;
} else {
_.memory.Free(commandParameters);
}
// Clean up
toReturn = nextResult;
Reset();
return toReturn;
}
// Zero important variables
private final function Reset() {
local Command.CallData blankCallData;
_.memory.Free(currentPlayersParser);
currentPlayersParser = none;
// We didn't create this one and are not meant to free it either
commandParser = none;
nextResult = blankCallData;
currentTarget = CPT_NecessaryParameter;
currentTargetIsOption = false;
usedOptions.length = 0;
}
// Auxiliary method for recording errors
private final function DeclareError(Command.ErrorType type, optional BaseText cause) {
nextResult.parsingError = type;
if (cause != none) {
nextResult.errorCause = cause.Copy();
}
if (commandParser != none) {
commandParser.Fail();
}
}
// Assumes `commandParser != none`, is in successful state.
//
// Picks a sub command based on it's contents (parser's pointer must be before
// where subcommand's name is specified).
//
// If [`specifiedSubCommand`] is not `none` - will always use that value instead
// of parsing it from [`commandParser`].
private final function PickSubCommand(Command.Data commandData, BaseText specifiedSubCommand) {
local int i;
local MutableText candidateSubCommandName;
local Command.SubCommand emptySubCommand;
local array<Command.SubCommand> allSubCommands;
allSubCommands = commandData.subCommands;
if (allSubcommands.length == 0) {
_.logger.Auto(errNoSubCommands).ArgClass(class);
pickedSubCommand = emptySubCommand;
return;
}
// Get candidate name
confirmedState = commandParser.GetCurrentState();
if (specifiedSubCommand != none) {
candidateSubCommandName = specifiedSubCommand.MutableCopy();
} else {
commandParser.Skip().MUntil(candidateSubCommandName,, true);
}
// Try matching it to sub commands
pickedSubCommand = allSubcommands[0];
if (candidateSubCommandName.IsEmpty()) {
candidateSubCommandName.FreeSelf();
return;
}
for (i = 0; i < allSubcommands.length; i += 1) {
if (candidateSubCommandName.Compare(allSubcommands[i].name)) {
candidateSubCommandName.FreeSelf();
pickedSubCommand = allSubcommands[i];
return;
}
}
// We will only reach here if we did not match any sub commands,
// meaning that whatever consumed by[ `candidateSubCommandName`] probably
// has a different meaning.
commandParser.RestoreState(confirmedState);
}
// Assumes `commandParser` is not `none`
// Declares an error if `commandParser` still has any input left
private final function AssertNoTrailingInput() {
local Text remainder;
if (!commandParser.Ok()) return;
if (commandParser.Skip().GetRemainingLength() <= 0) return;
remainder = commandParser.GetRemainder();
DeclareError(CET_UnusedCommandParameters, remainder);
remainder.FreeSelf();
}
// Assumes `commandParser` is not `none`.
// Parses given required and optional parameters along with any possible option
// declarations.
// Returns `HashTable` filled with (variable, parsed value) pairs.
// Failure is equal to `commandParser` entering into a failed state.
private final function HashTable ParseParameterArrays(
array<Command.Parameter> requiredParameters,
array<Command.Parameter> optionalParameters
) {
local HashTable parsedParameters;
if (!commandParser.Ok()) {
return none;
}
parsedParameters = _.collections.EmptyHashTable();
// Parse parameters
ParseRequiredParameterArray(parsedParameters, requiredParameters);
ParseOptionalParameterArray(parsedParameters, optionalParameters);
// Parse trailing options
while (TryParsingOptions());
return parsedParameters;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses given required parameters along with any possible option declarations into given
// `parsedParameters` `HashTable`.
private final function ParseRequiredParameterArray(
HashTable parsedParameters,
array<Command.Parameter> requiredParameters
) {
local int i;
if (!commandParser.Ok()) {
return;
}
currentTarget = CPT_NecessaryParameter;
while (i < requiredParameters.length) {
if (i == requiredParameters.length - 1) {
currentTarget = CPT_LastNecessaryParameter;
}
// Parse parameters one-by-one, reporting appropriate errors
if (!ParseParameter(parsedParameters, requiredParameters[i])) {
// Any failure to parse required parameter leads to error
if (currentTargetIsOption) {
DeclareError( CET_NoRequiredParamForOption,
targetOption.longName);
} else {
DeclareError( CET_NoRequiredParam,
requiredParameters[i].displayName);
}
return;
}
i += 1;
}
currentTarget = CPT_ExtraParameter;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses given optional parameters along with any possible option declarations
// into given `parsedParameters` hash table.
private final function ParseOptionalParameterArray(
HashTable parsedParameters,
array<Command.Parameter> optionalParameters
) {
local int i;
if (!commandParser.Ok()) {
return;
}
while (i < optionalParameters.length) {
confirmedState = commandParser.GetCurrentState();
// Parse parameters one-by-one, reporting appropriate errors
if (!ParseParameter(parsedParameters, optionalParameters[i])) {
// Propagate errors
if (nextResult.parsingError != CET_None) {
return;
}
// Failure to parse optional parameter is fine if
// it is caused by that parameters simply missing
commandParser.RestoreState(confirmedState);
break;
}
i += 1;
}
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses one given parameter along with any possible option declarations into
// given `parsedParameters` `HashTable`.
//
// Returns `true` if we've successfully parsed given parameter without any
// errors.
private final function bool ParseParameter(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local bool parsedEnough;
confirmedState = commandParser.GetCurrentState();
while (ParseSingleValue(parsedParameters, expectedParameter)) {
if (currentTarget == CPT_LastNecessaryParameter) {
currentTarget = CPT_ExtraParameter;
}
parsedEnough = true;
// We are done if there is either no more input or we only needed
// to parse a single value
if (!expectedParameter.allowsList) {
return true;
}
if (commandParser.Skip().HasFinished()) {
return true;
}
confirmedState = commandParser.GetCurrentState();
}
// We only succeeded in parsing if we've parsed enough for
// a given parameter and did not encounter any errors
if (parsedEnough && nextResult.parsingError == CET_None) {
commandParser.RestoreState(confirmedState);
return true;
}
// Clean up any values `ParseSingleValue` might have recorded
parsedParameters.RemoveItem(expectedParameter.variableName);
return false;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses a single value for a given parameter (e.g. one integer for integer or
// integer list parameter types) along with any possible option declarations
// into given `parsedParameters`.
//
// Returns `true` if we've successfully parsed a single value without
// any errors.
private final function bool ParseSingleValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
// Before parsing any other value we need to check if user has specified any options instead.
//
// However this might lead to errors if we are already parsing necessary parameters of another
// option: we must handle such situation and report an error.
if (currentTargetIsOption) {
// There is no problem is option's parameter is remainder
if (expectedParameter.type == CPT_Remainder) {
return ParseRemainderValue(parsedParameters, expectedParameter);
}
if (currentTarget != CPT_ExtraParameter && TryParsingOptions()) {
DeclareError(CET_NoRequiredParamForOption, targetOption.longName);
return false;
}
}
while (TryParsingOptions());
// First we try `CPT_Remainder` parameter, since it is a special case that
// consumes all further input
if (expectedParameter.type == CPT_Remainder) {
return ParseRemainderValue(parsedParameters, expectedParameter);
}
// Propagate errors after parsing options
if (nextResult.parsingError != CET_None) {
return false;
}
// Try parsing one of the variable types
if (expectedParameter.type == CPT_Boolean) {
return ParseBooleanValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Integer) {
return ParseIntegerValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Number) {
return ParseNumberValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Text) {
return ParseTextValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Remainder) {
return ParseRemainderValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Object) {
return ParseObjectValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Array) {
return ParseArrayValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_JSON) {
return ParseJSONValue(parsedParameters, expectedParameter);
} else if (expectedParameter.type == CPT_Players) {
return ParsePlayersValue(parsedParameters, expectedParameter);
}
return false;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses a single boolean value into given `parsedParameters` hash table.
private final function bool ParseBooleanValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local int i;
local bool isValidBooleanLiteral;
local bool booleanValue;
local MutableText parsedLiteral;
commandParser.Skip().MUntil(parsedLiteral,, true);
if (!commandParser.Ok()) {
_.memory.Free(parsedLiteral);
return false;
}
// Try to match parsed literal to any recognizable boolean literals
for (i = 0; i < booleanTrueEquivalents.length; i += 1) {
if (parsedLiteral.CompareToString(booleanTrueEquivalents[i], SCASE_INSENSITIVE)) {
isValidBooleanLiteral = true;
booleanValue = true;
break;
}
}
for (i = 0; i < booleanFalseEquivalents.length; i += 1) {
if (isValidBooleanLiteral) {
break;
}
if (parsedLiteral.CompareToString(booleanFalseEquivalents[i], SCASE_INSENSITIVE)) {
isValidBooleanLiteral = true;
booleanValue = false;
}
}
parsedLiteral.FreeSelf();
if (!isValidBooleanLiteral) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, _.box.bool(booleanValue));
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single integer value into given `parsedParameters` hash table.
private final function bool ParseIntegerValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local int integerValue;
commandParser.Skip().MInteger(integerValue);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, _.box.int(integerValue));
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single number (float) value into given `parsedParameters`
// hash table.
private final function bool ParseNumberValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local float numberValue;
commandParser.Skip().MNumber(numberValue);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, _.box.float(numberValue));
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single `Text` value into given `parsedParameters`
// hash table.
private final function bool ParseTextValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local bool failedParsing;
local MutableText textValue;
local Parser.ParserState initialState;
local HashTable resolvedPair;
// (needs some work for reading formatting `string`s from `Text` objects)
initialState = commandParser.Skip().GetCurrentState();
// Try manually parsing as a string literal first, since then we will
// allow empty `textValue` as a result
commandParser.MStringLiteral(textValue);
failedParsing = !commandParser.Ok();
// Otherwise - empty values are not allowed
if (failedParsing) {
_.memory.Free(textValue);
commandParser.RestoreState(initialState).MString(textValue);
failedParsing = (!commandParser.Ok() || textValue.IsEmpty());
}
if (failedParsing) {
_.memory.Free(textValue);
commandParser.Fail();
return false;
}
resolvedPair = AutoResolveAlias(textValue, expectedParameter.aliasSourceName);
if (resolvedPair != none) {
RecordParameter(parsedParameters, expectedParameter, resolvedPair);
_.memory.Free(textValue);
} else {
RecordParameter(parsedParameters, expectedParameter, textValue.IntoText());
}
return true;
}
// Resolves alias and returns it, along with the resolved value, if parameter
// was specified to be auto-resolved.
// Returns `none` otherwise.
private final function HashTable AutoResolveAlias(MutableText textValue, Text aliasSourceName) {
local HashTable result;
local Text resolvedValue, immutableValue;
if (textValue == none) return none;
if (aliasSourceName == none) return none;
// Always create `HashTable` with at least "alias" key
result = _.collections.EmptyHashTable();
immutableValue = textValue.Copy();
result.SetItem(P("alias"), immutableValue);
_.memory.Free(immutableValue);
// Add "value" key only after we've checked for "$" prefix
if (!textValue.StartsWithS("$")) {
result.SetItem(P("value"), immutableValue);
return result;
}
if (aliasSourceName.Compare(P("weapon"))) {
resolvedValue = _.alias.ResolveWeapon(textValue, true);
} else if (aliasSourceName.Compare(P("color"))) {
resolvedValue = _.alias.ResolveColor(textValue, true);
} else if (aliasSourceName.Compare(P("feature"))) {
resolvedValue = _.alias.ResolveFeature(textValue, true);
} else if (aliasSourceName.Compare(P("entity"))) {
resolvedValue = _.alias.ResolveEntity(textValue, true);
} else {
resolvedValue = _.alias.ResolveCustom(aliasSourceName, textValue, true);
}
result.SetItem(P("value"), resolvedValue);
_.memory.Free(resolvedValue);
return result;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses a single `Text` value into given `parsedParameters` hash table,
// consuming all remaining contents.
private final function bool ParseRemainderValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local MutableText value;
commandParser.Skip().MUntil(value);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, value.IntoText());
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
//
// Parses a single JSON object into given `parsedParameters` hash table.
private final function bool ParseObjectValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local HashTable objectValue;
objectValue = _.json.ParseHashTableWith(commandParser);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, objectValue);
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single JSON array into given `parsedParameters` hash table.
private final function bool ParseArrayValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local ArrayList arrayValue;
arrayValue = _.json.ParseArrayListWith(commandParser);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, arrayValue);
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single JSON value into given `parsedParameters`
// hash table.
private final function bool ParseJSONValue(
HashTable parsedParameters,
Command.Parameter expectedParameter
) {
local AcediaObject jsonValue;
jsonValue = _.json.ParseWith(commandParser);
if (!commandParser.Ok()) {
return false;
}
RecordParameter(parsedParameters, expectedParameter, jsonValue);
return true;
}
// Assumes `commandParser` and `parsedParameters` are not `none`.
// Parses a single JSON value into given `parsedParameters` hash table.
private final function bool ParsePlayersValue(
HashTable parsedParameters,
Command.Parameter expectedParameter)
{
local ArrayList resultPlayerList;
local array<EPlayer> targetPlayers;
currentPlayersParser.ParseWith(commandParser);
if (commandParser.Ok()) {
targetPlayers = currentPlayersParser.GetPlayers();
} else {
return false;
}
resultPlayerList = _.collections.NewArrayList(targetPlayers);
_.memory.FreeMany(targetPlayers);
RecordParameter(parsedParameters, expectedParameter, resultPlayerList);
return true;
}
// Assumes `parsedParameters` is not `none`.
//
// Records `value` for a given `parameter` into a given `parametersArray`.
// If parameter is not a list type - simply records `value` as value under
// `parameter.variableName` key.
// If parameter is a list type - pushed value at the end of an array, recorded at
// `parameter.variableName` key (creating it if missing).
//
// All recorded values are managed by `parametersArray`.
private final function RecordParameter(
HashTable parametersArray,
Command.Parameter parameter,
/*take*/ AcediaObject value
) {
local ArrayList parameterVariable;
if (!parameter.allowsList) {
parametersArray.SetItem(parameter.variableName, value);
_.memory.Free(value);
return;
}
parameterVariable = ArrayList(parametersArray.GetItem(parameter.variableName));
if (parameterVariable == none) {
parameterVariable = _.collections.EmptyArrayList();
}
parameterVariable.AddItem(value);
_.memory.Free(value);
parametersArray.SetItem(parameter.variableName, parameterVariable);
_.memory.Free(parameterVariable);
}
// Assumes `commandParser` is not `none`.
//
// Tries to parse an option declaration (along with all of it's parameters) with
// `commandParser`.
//
// Returns `true` on success and `false` otherwise.
//
// In case of failure to detect option declaration also reverts state of
// `commandParser` to that before `TryParsingOptions()` call.
// However, if option declaration was present, but invalid (or had invalid
// parameters) parser will be left in a failed state.
private final function bool TryParsingOptions() {
local int temporaryInt;
if (!commandParser.Ok()) {
return false;
}
confirmedState = commandParser.GetCurrentState();
// Long options
commandParser.Skip().Match(P("--"));
if (commandParser.Ok()) {
return ParseLongOption();
}
// Filter out negative numbers that start similarly to short options:
// -3, -5.7, -.9
commandParser
.RestoreState(confirmedState)
.Skip()
.Match(P("-"))
.MUnsignedInteger(temporaryInt, 10, 1);
if (commandParser.Ok()) {
commandParser.RestoreState(confirmedState);
return false;
}
commandParser.RestoreState(confirmedState).Skip().Match(P("-."));
if (commandParser.Ok()) {
commandParser.RestoreState(confirmedState);
return false;
}
// Short options
commandParser.RestoreState(confirmedState).Skip().Match(P("-"));
if (commandParser.Ok()) {
return ParseShortOption();
}
commandParser.RestoreState(confirmedState);
return false;
}
// Assumes `commandParser` is not `none`.
//
// Tries to parse a long option name along with all of it's possible parameters
// with `commandParser`.
//
// Returns `true` on success and `false` otherwise. At the point this method is
// called, option declaration is already assumed to be detected and any failure
// implies parsing error (ending in failed `Command.CallData`).
private final function bool ParseLongOption() {
local int i, optionIndex;
local MutableText optionName;
commandParser.MUntil(optionName,, true);
if (!commandParser.Ok()) {
return false;
}
while (optionIndex < availableOptions.length) {
if (optionName.Compare(availableOptions[optionIndex].longName)) break;
optionIndex += 1;
}
if (optionIndex >= availableOptions.length) {
DeclareError(CET_UnknownOption, optionName);
optionName.FreeSelf();
return false;
}
for (i = 0; i < usedOptions.length; i += 1) {
if (optionName.Compare(usedOptions[i].longName)) {
DeclareError(CET_RepeatedOption, optionName);
optionName.FreeSelf();
return false;
}
}
//usedOptions[usedOptions.length] = availableOptions[optionIndex];
optionName.FreeSelf();
return ParseOptionParameters(availableOptions[optionIndex]);
}
// Assumes `commandParser` and `nextResult` are not `none`.
//
// Tries to parse a short option name along with all of it's possible parameters
// with `commandParser`.
//
// Returns `true` on success and `false` otherwise. At the point this
// method is called, option declaration is already assumed to be detected
// and any failure implies parsing error (ending in failed `Command.CallData`).
private final function bool ParseShortOption() {
local int i;
local bool pickedOptionWithParameters;
local MutableText optionsList;
commandParser.MUntil(optionsList,, true);
if (!commandParser.Ok()) {
optionsList.FreeSelf();
return false;
}
for (i = 0; i < optionsList.GetLength(); i += 1) {
if (nextResult.parsingError != CET_None) break;
pickedOptionWithParameters =
AddOptionByCharacter(
optionsList.GetCharacter(i),
optionsList,
pickedOptionWithParameters)
|| pickedOptionWithParameters;
}
optionsList.FreeSelf();
return (nextResult.parsingError == CET_None);
}
// Assumes `commandParser` and `nextResult` are not `none`.
//
// Auxiliary method that adds option by it's short version's character
// `optionCharacter`.
//
// It also accepts `optionSourceList` that describes short option expression
// (e.g. "-rtV") from
// which it originated for error reporting and `forbidOptionWithParameters`
// that, when set to `true`, forces this method to cause the
// `CET_MultipleOptionsWithParams` error if new option has non-empty parameters.
//
// Method returns `true` if added option had non-empty parameters and `false`
// otherwise.
//
// Any parsing failure inside this method always causes
// `nextError.DeclareError()` call, so you can use `nextResult.IsSuccessful()`
// to check if method has failed.
private final function bool AddOptionByCharacter(
BaseText.Character optionCharacter,
BaseText optionSourceList,
bool forbidOptionWithParameters
) {
local int i;
local bool optionHasParameters;
// Prevent same option appearing twice
for (i = 0; i < usedOptions.length; i += 1) {
if (_.text.AreEqual(optionCharacter, usedOptions[i].shortName)) {
DeclareError(CET_RepeatedOption, usedOptions[i].longName);
return false;
}
}
// If it's a new option - look it up in all available options
for (i = 0; i < availableOptions.length; i += 1) {
if (!_.text.AreEqual(optionCharacter, availableOptions[i].shortName)) {
continue;
}
usedOptions[usedOptions.length] = availableOptions[i];
optionHasParameters = (availableOptions[i].required.length > 0
|| availableOptions[i].optional.length > 0);
// Enforce `forbidOptionWithParameters` flag restriction
if (optionHasParameters && forbidOptionWithParameters) {
DeclareError(CET_MultipleOptionsWithParams, optionSourceList);
return optionHasParameters;
}
// Parse parameters (even if they are empty) and bail
commandParser.Skip();
ParseOptionParameters(availableOptions[i]);
break;
}
if (i >= availableOptions.length) {
DeclareError(CET_UnknownShortOption);
}
return optionHasParameters;
}
// Auxiliary method for parsing option's parameters (including empty ones).
// Automatically fills `nextResult` with parsed parameters (or `none` if option
// has no parameters).
// Assumes `commandParser` and `nextResult` are not `none`.
private final function bool ParseOptionParameters(Command.Option pickedOption) {
local HashTable optionParameters;
// If we are already parsing other option's parameters and did not finish
// parsing all required ones - we cannot start another option
if (currentTargetIsOption && currentTarget != CPT_ExtraParameter) {
DeclareError(CET_NoRequiredParamForOption, targetOption.longName);
return false;
}
if (pickedOption.required.length == 0 && pickedOption.optional.length == 0) {
nextResult.options.SetItem(pickedOption.longName, none);
return true;
}
currentTargetIsOption = true;
targetOption = pickedOption;
optionParameters = ParseParameterArrays(
pickedOption.required,
pickedOption.optional);
currentTargetIsOption = false;
if (commandParser.Ok()) {
nextResult.options.SetItem(pickedOption.longName, optionParameters);
_.memory.Free(optionParameters);
return true;
}
_.memory.Free(optionParameters);
return false;
}
defaultproperties {
booleanTrueEquivalents(0) = "true"
booleanTrueEquivalents(1) = "enable"
booleanTrueEquivalents(2) = "on"
booleanTrueEquivalents(3) = "yes"
booleanFalseEquivalents(0) = "false"
booleanFalseEquivalents(1) = "disable"
booleanFalseEquivalents(2) = "off"
booleanFalseEquivalents(3) = "no"
errNoSubCommands = (l=LOG_Error,m="`GetSubCommand()` method was called on a command `%1` with zero defined sub-commands.")
}

View File

@ -0,0 +1,66 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandPermissions extends AcediaConfig
perobjectconfig
config(AcediaCommands)
abstract;
var public config array<string> forbiddenSubCommands;
protected function HashTable ToData() {
local int i;
local HashTable data;
local ArrayList forbiddenList;
data = _.collections.EmptyHashTable();
forbiddenList = _.collections.EmptyArrayList();
for (i = 0; i < forbiddenSubCommands.length; i += 1) {
forbiddenList.AddString(Locs(forbiddenSubCommands[i]));
}
data.SetItem(P("forbiddenSubCommands"), forbiddenList);
_.memory.Free(forbiddenList);
return data;
}
protected function FromData(HashTable source) {
local int i;
local ArrayList forbiddenList;
if (source == none) return;
forbiddenList = source.GetArrayList(P("forbiddenSubCommands"));
if (forbiddenList == none) return;
forbiddenSubCommands.length = 0;
for (i = 0; i < forbiddenList.GetLength(); i += 1) {
forbiddenSubCommands[i] = forbiddenList.GetString(i);
}
_.memory.Free(forbiddenList);
}
protected function DefaultIt() {
forbiddenSubCommands.length = 0;
}
defaultproperties {
configName = "AcediaCommands"
supportsDataConversion = true
}

View File

@ -0,0 +1,81 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandRegistrationJob extends SchedulerJob
dependson(CommandAPI);
var private CommandAPI.AsyncTask nextItem;
// Expecting 300 units of work, this gives us registering 20 commands per tick
const ADDING_COMMAND_COST = 15;
// Adding voting option is approximately the same as adding a command's
// single sub-command - we'll estimate it as 1/3rd of the full value
const ADDING_VOTING_COST = 5;
// Authorizing is relatively cheap, whether it's commands or voting
const AUTHORIZING_COST = 1;
protected function Constructor() {
nextItem = _.commands._popPending();
}
protected function Finalizer() {
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
nextItem.entityClass = none;
nextItem.entityName = none;
nextItem.userGroup = none;
nextItem.configName = none;
}
public function bool IsCompleted() {
return (nextItem.entityName == none);
}
public function DoWork(int allottedWorkUnits) {
while (allottedWorkUnits > 0 && nextItem.entityName != none) {
if (nextItem.type == CAJT_AddCommand) {
allottedWorkUnits -= ADDING_COMMAND_COST;
_.commands.AddCommand(class<Command>(nextItem.entityClass), nextItem.entityName);
_.memory.Free(nextItem.entityName);
} else if (nextItem.type == CAJT_AddVoting) {
allottedWorkUnits -= ADDING_VOTING_COST;
_.commands.AddVoting(class<Voting>(nextItem.entityClass), nextItem.entityName);
_.memory.Free(nextItem.entityName);
} else if (nextItem.type == CAJT_AuthorizeCommand) {
allottedWorkUnits -= AUTHORIZING_COST;
_.commands.AuthorizeCommandUsage(
nextItem.entityName,
nextItem.userGroup,
nextItem.configName);
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
} else /*if (nextItem.type == CAJT_AuthorizeVoting)*/ {
allottedWorkUnits -= AUTHORIZING_COST;
_.commands.AuthorizeVotingUsage(
nextItem.entityName,
nextItem.userGroup,
nextItem.configName);
_.memory.Free3(nextItem.entityName, nextItem.userGroup, nextItem.configName);
}
nextItem = _.commands._popPending();
}
}
defaultproperties {
}

View File

@ -0,0 +1,230 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Commands extends FeatureConfig
perobjectconfig
config(AcediaCommands);
/// Auxiliary struct for describing adding a particular command set to
/// a particular group of users.
struct CommandSetGroupPair {
/// Name of the command set to add
var public string name;
/// Name of the group, for which to add this set
var public string for;
};
/// Auxiliary struct for describing a rule to rename a particular command for
/// compatibility reasons.
struct RenamingRulePair {
/// Command class to rename
var public class<AcediaObject> rename;
/// Name to use for that class
var public string to;
};
/// Setting this to `true` enables players to input commands with "mutate"
/// console command.
/// Default is `true`.
var public config bool useMutateInput;
/// Setting this to `true` enables players to input commands right in the chat
/// by prepending them with [`chatCommandPrefix`].
/// Default is `true`.
var public config bool useChatInput;
/// Chat messages, prepended by this prefix will be treated as commands.
/// Default is "!". Empty values are also treated as "!".
var public config string chatCommandPrefix;
/// Allows to specify which user groups are used in determining command/votings
/// permission.
/// They must be specified in the order of importance: from the group with
/// highest level of permissions to the lowest. When determining player's
/// permission to use a certain command/voting, his group with the highest
/// available permissions will be used.
var public config array<string> commandGroup;
/// Add a specified `CommandList` to the specified user group
var public config array<CommandSetGroupPair> addCommandList;
/// Allows to specify a name for a certain command class
///
/// NOTE:By default command choses that name by itself and its not recommended
/// to override it. You should only use this setting in case there is naming
/// conflict between commands from different packages.
var public config array<RenamingRulePair> renamingRule;
/// Allows to specify a name for a certain voting class
///
/// NOTE:By default voting choses that name by itself and its not recommended
/// to override it. You should only use this setting in case there is naming
/// conflict between votings from different packages.
var public config array<RenamingRulePair> votingRenamingRule;
protected function HashTable ToData() {
local int i;
local HashTable data;
local ArrayList innerList;
local HashTable innerPair;
data = __().collections.EmptyHashTable();
data.SetBool(P("useChatInput"), useChatInput, true);
data.SetBool(P("useMutateInput"), useMutateInput, true);
data.SetString(P("chatCommandPrefix"), chatCommandPrefix);
// Serialize `commandGroup`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < commandGroup.length; i += 1) {
innerList.AddString(commandGroup[i]);
}
data.SetItem(P("commandGroups"), innerList);
_.memory.Free(innerList);
// Serialize `addCommandSet`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < addCommandList.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("name"), addCommandList[i].name);
innerPair.SetString(P("for"), addCommandList[i].for);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("commandSets"), innerList);
_.memory.Free(innerList);
// Serialize `renamingRule`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < renamingRule.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("rename"), string(renamingRule[i].rename));
innerPair.SetString(P("to"), renamingRule[i].to);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("renamingRules"), innerList);
_.memory.Free(innerList);
// Serialize `votingRenamingRule`
innerList = _.collections.EmptyArrayList();
for (i = 0; i < votingRenamingRule.length; i += 1) {
innerPair = _.collections.EmptyHashTable();
innerPair.SetString(P("rename"), string(votingRenamingRule[i].rename));
innerPair.SetString(P("to"), votingRenamingRule[i].to);
innerList.AddItem(innerPair);
_.memory.Free(innerPair);
}
data.SetItem(P("votingRenamingRules"), innerList);
_.memory.Free(innerList);
return data;
}
protected function FromData(HashTable source) {
local int i;
local ArrayList innerList;
local HashTable innerPair;
local CommandSetGroupPair nextCommandSetGroupPair;
local RenamingRulePair nextRenamingRule;
local class<AcediaObject> nextClass;
if (source == none) {
return;
}
useChatInput = source.GetBool(P("useChatInput"));
useMutateInput = source.GetBool(P("useMutateInput"));
chatCommandPrefix = source.GetString(P("chatCommandPrefix"), "!");
// De-serialize `commandGroup`
commandGroup.length = 0;
innerList = source.GetArrayList(P("commandGroups"));
if (innerList != none) {
for (i = 0; i < commandGroup.length; i += 1) {
commandGroup[i] = innerList.GetString(i);
}
_.memory.Free(innerList);
}
// De-serialize `addCommandSet`
addCommandList.length = 0;
innerList = source.GetArrayList(P("commandSets"));
if (innerList != none) {
for (i = 0; i < addCommandList.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextCommandSetGroupPair.name = innerPair.GetString(P("name"));
nextCommandSetGroupPair.for = innerPair.GetString(P("for"));
addCommandList[addCommandList.length] = nextCommandSetGroupPair;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
// De-serialize `renamingRule`
renamingRule.length = 0;
innerList = source.GetArrayList(P("renamingRules"));
if (innerList != none) {
for (i = 0; i < renamingRule.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextClass =
class<AcediaObject>(_.memory.LoadClass_S(innerPair.GetString(P("rename"))));
nextRenamingRule.rename = nextClass;
nextRenamingRule.to = innerPair.GetString(P("to"));
renamingRule[renamingRule.length] = nextRenamingRule;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
// De-serialize `votingRenamingRule`
votingRenamingRule.length = 0;
innerList = source.GetArrayList(P("votingRenamingRules"));
if (innerList != none) {
for (i = 0; i < votingRenamingRule.length; i += 1) {
innerPair = innerList.GetHashTable(i);
if (innerPair != none) {
nextClass =
class<AcediaObject>(_.memory.LoadClass_S(innerPair.GetString(P("rename"))));
nextRenamingRule.rename = nextClass;
nextRenamingRule.to = innerPair.GetString(P("to"));
votingRenamingRule[votingRenamingRule.length] = nextRenamingRule;
_.memory.Free(innerPair);
}
}
_.memory.Free(innerList);
}
}
protected function DefaultIt() {
local CommandSetGroupPair defaultPair;
useChatInput = true;
useMutateInput = true;
chatCommandPrefix = "!";
commandGroup[0] = "admin";
commandGroup[1] = "moderator";
commandGroup[2] = "trusted";
addCommandList.length = 0;
defaultPair.name = "default";
defaultPair.for = "all";
addCommandList[0] = defaultPair;
renamingRule.length = 0;
}
defaultproperties {
configName = "AcediaCommands"
}

View File

@ -0,0 +1,39 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnCommandAdded_Signal extends Signal;
public final function bool Emit(class<Command> addedClass, Text usedName) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnCommandAdded_Slot(nextSlot).connect(addedClass, usedName);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnCommandAdded_Slot'
}

View File

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnCommandAdded_Slot extends Slot;
delegate connect(class<Command> addedClass, Text usedName) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

View File

@ -0,0 +1,39 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnCommandRemoved_Signal extends Signal;
public final function bool Emit(class<Command> removedClass) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnCommandRemoved_Slot(nextSlot).connect(removedClass);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnCommandRemoved_Slot'
}

View File

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnCommandRemoved_Slot extends Slot;
delegate connect(class<Command> removedClass) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

View File

@ -0,0 +1,39 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingAdded_Signal extends Signal;
public final function bool Emit(class<Voting> addedClass, Text usedName) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnVotingAdded_Slot(nextSlot).connect(addedClass, usedName);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnVotingAdded_Slot'
}

View File

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingAdded_Slot extends Slot;
delegate connect(class<Voting> addedClass, Text usedName) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

View File

@ -0,0 +1,39 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingEnded_Signal extends Signal;
public final function bool Emit(bool success, HashTable arguments) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnVotingEnded_Slot(nextSlot).connect(success, arguments);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnVotingEnded_Slot'
}

View File

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingEnded_Slot extends Slot;
delegate connect(bool success, HashTable arguments) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

View File

@ -0,0 +1,39 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingRemoved_Signal extends Signal;
public final function bool Emit(class<Voting> removedClass) {
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none) {
CommandsAPI_OnVotingRemoved_Slot(nextSlot).connect(removedClass);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
return true;
}
defaultproperties {
relatedSlotClass = class'CommandsAPI_OnVotingRemoved_Slot'
}

View File

@ -0,0 +1,38 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsAPI_OnVotingRemoved_Slot extends Slot;
delegate connect(class<Voting> removedClass) {
DummyCall();
}
protected function Constructor() {
connect = none;
}
protected function Finalizer() {
super.Finalizer();
connect = none;
}
defaultproperties {
}

View File

@ -0,0 +1,142 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CommandsTool extends CmdItemsTool;
//! This is a base class for auxiliary objects that will be used for storing
//! named [`Command`] instances.
//!
//! This storage class allows for efficient manipulation and retrieval of
//! [`Command`]s, along with information about what use groups were authorized
//! to use them.
//!
//! Additionally, this tool allows for efficient fetching of commands that
//! belong to a particular *command group*.
/// [`HashTable`] that maps a command group name to a set of command names that
/// belong to it.
var private HashTable groupedCommands;
protected function Constructor() {
super.Constructor();
groupedCommands = _.collections.EmptyHashTable();
}
protected function Finalizer() {
super.Finalizer();
_.memory.Free(groupedCommands);
groupedCommands = none;
}
/// Returns all known command groups' names.
public final function array<Text> GetGroupsNames() {
local array<Text> emptyResult;
if (groupedCommands != none) {
return groupedCommands.GetTextKeys();
}
return emptyResult;
}
/// Returns array of names of all available commands belonging to the specified
/// group.
public final function array<Text> GetCommandNamesInGroup(BaseText groupName) {
local int i;
local ArrayList commandNamesArray;
local array<Text> result;
if (groupedCommands == none) return result;
commandNamesArray = groupedCommands.GetArrayList(groupName);
if (commandNamesArray == none) return result;
for (i = 0; i < commandNamesArray.GetLength(); i += 1) {
result[result.length] = commandNamesArray.GetText(i);
}
_.memory.Free(commandNamesArray);
return result;
}
protected function ItemCard MakeCard(class<AcediaObject> commandClass, BaseText itemName) {
local Command newCommandInstance;
local ItemCard newCard;
local Text commandGroup;
if (class<Command>(commandClass) != none) {
newCommandInstance = Command(_.memory.Allocate(commandClass, true));
newCommandInstance.Initialize(itemName);
newCard = ItemCard(_.memory.Allocate(class'ItemCard'));
newCard.InitializeWithInstance(newCommandInstance);
// Guaranteed to be lower case (keys of [`HashTable`])
if (itemName != none) {
itemName = itemName.LowerCopy();
} else {
itemName = newCommandInstance.GetPreferredName();
}
commandGroup = newCommandInstance.GetGroupName();
AssociateGroupAndName(commandGroup, itemName);
_.memory.Free3(newCommandInstance, itemName, commandGroup);
}
return newCard;
}
protected function DiscardCard(ItemCard toDiscard) {
local Text groupKey, commandName;
local Command storedCommand;
local ArrayList listOfCommands;
if (toDiscard == none) return;
// Guaranteed to store a [`Command`]
storedCommand = Command(toDiscard.GetItem());
if (storedCommand == none) return;
// Guaranteed to be stored in a lower case
commandName = storedCommand.GetName();
listOfCommands = groupedCommands.GetArrayList(groupKey);
if (listOfCommands != none && commandName != none) {
listOfCommands.RemoveItem(commandName);
}
_.memory.Free2(commandName, storedCommand);
}
// Expect both arguments to be not `none`.
// Expect both arguments to be lower-case.
private final function AssociateGroupAndName(BaseText groupKey, BaseText commandName) {
local ArrayList listOfCommands;
if (groupedCommands != none) {
listOfCommands = groupedCommands.GetArrayList(groupKey);
if (listOfCommands == none) {
listOfCommands = _.collections.EmptyArrayList();
}
if (listOfCommands.Find(commandName) < 0) {
// `< 0` means not found
listOfCommands.AddItem(commandName);
}
// Set `listOfCommands` in case we've just created that array.
// Won't do anything if it is already recorded there.
groupedCommands.SetItem(groupKey, listOfCommands);
}
}
defaultproperties {
ruleBaseClass = class'Command';
}

View File

@ -0,0 +1,708 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Commands_Feature extends Feature
dependson(CommandAPI)
dependson(Commands);
//! This feature manages commands that automatically parse their arguments into standard Acedia
//! collections.
//!
//! # Implementation
//!
//! Implementation is simple: calling a method `RegisterCommand()` adds
//! command into two caches `registeredCommands` for obtaining registered
//! commands by name and `groupedCommands` for obtaining arrays of commands by
//! their group name. These arrays are used for providing methods for fetching
//! arrays of commands and obtaining pre-allocated `Command` instances by their
//! name.
//! Depending on settings, this feature also connects to corresponding
//! signals for catching "mutate"/chat input, then it checks user-specified name
//! for being an alias and picks correct command from `registeredCommands`.
//! Emergency enabling this feature sets `emergencyEnabledMutate` flag that
//! enforces connecting to the "mutate" input.
/// Auxiliary struct for passing name of the command to call with pre-specified
/// sub-command name.
///
/// Normally sub-command name is parsed by the command itself, however command
/// aliases can try to enforce one.
struct CommandCallPair {
var MutableText commandName;
/// Not `none` in case it is enforced by an alias
var MutableText subCommandName;
};
/// Auxiliary struct that stores all the information needed to load
/// a certain command
struct EntityLoadInfo {
/// Command class to load.
var public class<AcediaObject> entityClass;
/// Name to load that command class under.
var public Text name;
/// Groups that are authorized to use that command.
var public array<Text> authorizedGroups;
/// Groups that are authorized to use that command.
var public array<Text> groupsConfig;
};
/// Auxiliary struct for describing adding a particular command set to
/// a particular group of users.
struct CommandListGroupPair {
/// Name of the command set to add
var public Text commandListName;
/// Name of the group, for which to add this set
var public Text permissionGroup;
};
/// Auxiliary struct for describing a rule to rename a particular command for
/// compatibility reasons.
struct RenamingRulePair {
/// Command class to rename
var public class<AcediaObject> class;
/// Name to use for that class
var public Text newName;
};
/// Tools that provide functionality of managing registered commands and votings
var private CommandAPI.CommandFeatureTools tools;
/// Delimiters that always separate command name from it's parameters
var private array<Text> commandDelimiters;
/// When this flag is set to true, mutate input becomes available despite
/// [`useMutateInput`] flag to allow to unlock server in case of an error
var private bool emergencyEnabledMutate;
var private /*config*/ bool useChatInput;
var private /*config*/ bool useMutateInput;
var private /*config*/ Text chatCommandPrefix;
var public /*config*/ array<string> commandGroup;
var public /*config*/ array<Commands.CommandSetGroupPair> addCommandSet;
var public /*config*/ array<Commands.RenamingRulePair> renamingRule;
var public /*config*/ array<Commands.RenamingRulePair> votingRenamingRule;
// Converted version of `commandGroup`
var private array<Text> permissionGroupOrder;
/// Converted version of `addCommandSet`
var private array<CommandListGroupPair> usedCommandLists;
/// Converted version of `renamingRule` and `votingRenamingRule`
var private array<RenamingRulePair> commandRenamingRules;
var private array<RenamingRulePair> votingRenamingRules;
// Name, under which `ACommandHelp` is registered
var private Text helpCommandName;
var LoggerAPI.Definition errServerAPIUnavailable, warnDuplicateRenaming, warnNoCommandList;
var LoggerAPI.Definition infoCommandAdded, infoVotingAdded;
protected function OnEnabled() {
helpCommandName = P("help");
// Macro selector
commandDelimiters[0] = _.text.FromString("@");
// Key selector
commandDelimiters[1] = _.text.FromString("#");
// Player array (possibly JSON array)
commandDelimiters[2] = _.text.FromString("[");
// Negation of the selector
// NOT the same thing as default command prefix in chat
commandDelimiters[3] = _.text.FromString("!");
if (useChatInput) {
_.chat.OnMessage(self).connect = HandleCommands;
}
else {
_.chat.OnMessage(self).Disconnect();
}
if (useMutateInput || emergencyEnabledMutate) {
if (__server() != none) {
__server().unreal.mutator.OnMutate(self).connect = HandleMutate;
} else {
_.logger.Auto(errServerAPIUnavailable);
}
}
LoadConfigArrays();
// `SetPermissionGroupOrder()` must be called *after* loading configs
tools.commands = CommandsTool(_.memory.Allocate(class'CommandsTool'));
tools.votings = VotingsTool(_.memory.Allocate(class'VotingsTool'));
tools.commands.SetPermissionGroupOrder(permissionGroupOrder);
tools.votings.SetPermissionGroupOrder(permissionGroupOrder);
_.commands._reloadFeature();
// Uses `CommandAPI`, so must be done after `_reloadFeature()` call
LoadCommands();
LoadVotings();
}
protected function OnDisabled() {
if (useChatInput) {
_.chat.OnMessage(self).Disconnect();
}
if (useMutateInput && __server() != none) {
__server().unreal.mutator.OnMutate(self).Disconnect();
}
useChatInput = false;
useMutateInput = false;
_.memory.Free3(tools.commands, tools.votings, chatCommandPrefix);
tools.commands = none;
tools.votings = none;
chatCommandPrefix = none;
_.memory.FreeMany(commandDelimiters);
commandDelimiters.length = 0;
_.memory.FreeMany(permissionGroupOrder);
permissionGroupOrder.length = 0;
FreeUsedCommandSets();
FreeRenamingRules();
_.commands._reloadFeature();
}
protected function SwapConfig(FeatureConfig config) {
local Commands newConfig;
newConfig = Commands(config);
if (newConfig == none) {
return;
}
_.memory.Free(chatCommandPrefix);
chatCommandPrefix = _.text.FromString(newConfig.chatCommandPrefix);
useChatInput = newConfig.useChatInput;
useMutateInput = newConfig.useMutateInput;
commandGroup = newConfig.commandGroup;
addCommandSet = newConfig.addCommandList;
renamingRule = newConfig.renamingRule;
votingRenamingRule = newConfig.votingRenamingRule;
}
/// This method allows to forcefully enable `Command_Feature` along with
/// "mutate" input in case something goes wrong.
///
/// `Command_Feature` is a critical command to have running on your server and,
/// if disabled by accident, there will be no way of starting it again without
/// restarting the level or even editing configs.
public final static function EmergencyEnable() {
local bool noWayToInputCommands;
local Text autoConfig;
local Commands_Feature feature;
if (!IsEnabled()) {
autoConfig = GetAutoEnabledConfig();
EnableMe(autoConfig);
__().memory.Free(autoConfig);
}
feature = Commands_Feature(GetEnabledInstance());
noWayToInputCommands = !feature.emergencyEnabledMutate
&& !feature.IsUsingMutateInput()
&& !feature.IsUsingChatInput();
if (noWayToInputCommands) {
default.emergencyEnabledMutate = true;
feature.emergencyEnabledMutate = true;
if (__server() != none) {
__server().unreal.mutator.OnMutate(feature).connect = HandleMutate;
} else {
__().logger.Auto(default.errServerAPIUnavailable);
}
}
}
/// Checks if `Commands_Feature` currently uses chat as input.
///
/// If `Commands_Feature` is not enabled, then it does not use anything
/// as input.
public final static function bool IsUsingChatInput() {
local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance());
if (instance != none) {
return instance.useChatInput;
}
return false;
}
/// Checks if `Commands_Feature` currently uses mutate command as input.
///
/// If `Commands_Feature` is not enabled, then it does not use anything
/// as input.
public final static function bool IsUsingMutateInput() {
local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance());
if (instance != none) {
return instance.useMutateInput;
}
return false;
}
/// Returns prefix that will indicate that chat message is intended to be
/// a command. By default "!".
///
/// If `Commands_Feature` is disabled, always returns `none`.
public final static function Text GetChatPrefix() {
local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance());
if (instance != none && instance.chatCommandPrefix != none) {
return instance.chatCommandPrefix.Copy();
}
return none;
}
/// Returns name, under which [`ACommandHelp`] is registered.
///
/// If `Commands_Feature` is disabled, always returns `none`.
public final static function Text GetHelpCommandName() {
local Commands_Feature instance;
instance = Commands_Feature(GetEnabledInstance());
if (instance != none && instance.helpCommandName != none) {
return instance.helpCommandName.Copy();
}
return none;
}
/// Executes command based on the input.
///
/// Takes [`commandLine`] as input with command's call, finds appropriate
/// registered command instance and executes it with parameters specified in
/// the [`commandLine`].
///
/// [`callerPlayer`] has to be specified and represents instigator of this
/// command that will receive appropriate result/error messages.
///
/// Returns `true` iff command was successfully executed.
///
/// # Errors
///
/// Doesn't log any errors, but can complain about errors in name or parameters
/// to the [`callerPlayer`]
public final function bool HandleInput(BaseText input, EPlayer callerPlayer) {
local bool result;
local Parser wrapper;
if (input == none) {
return false;
}
wrapper = input.Parse();
result = HandleInputWith(wrapper, callerPlayer);
wrapper.FreeSelf();
return result;
}
/// Executes command based on the input.
///
/// Takes [`commandLine`] as input with command's call, finds appropriate
/// registered command instance and executes it with parameters specified in
/// the [`commandLine`].
///
/// [`callerPlayer`] has to be specified and represents instigator of this
/// command that will receive appropriate result/error messages.
///
/// Returns `true` iff command was successfully executed.
///
/// # Errors
///
/// Doesn't log any errors, but can complain about errors in name or parameters
/// to the [`callerPlayer`]
public final function bool HandleInputWith(Parser parser, EPlayer callerPlayer) {
local bool errorOccured;
local User identity;
local CommandAPI.CommandConfigInfo commandPair;
local Command.CallData callData;
local CommandCallPair callPair;
if (parser == none) return false;
if (callerPlayer == none) return false;
if (!parser.Ok()) return false;
identity = callerPlayer.GetIdentity();
callPair = ParseCommandCallPairWith(parser);
commandPair = _.commands.ResolveCommandForUser(callPair.commandName, identity);
if (commandPair.instance == none || commandPair.usageForbidden) {
if (callerPlayer != none && callerPlayer.IsExistent()) {
callerPlayer
.BorrowConsole()
.Flush()
.Say(F("{$TextFailure Command not found!}"));
}
}
if (parser.Ok() && commandPair.instance != none && !commandPair.usageForbidden) {
callData =
commandPair.instance.ParseInputWith(parser, callerPlayer, callPair.subCommandName);
errorOccured = commandPair.instance.Execute(callData, callerPlayer, commandPair.config);
commandPair.instance.DeallocateCallData(callData);
}
_.memory.Free4(commandPair.instance, callPair.commandName, callPair.subCommandName, identity);
return errorOccured;
}
// Parses command's name into `CommandCallPair` - sub-command is filled in case
// specified name is an alias with specified sub-command name.
private final function CommandCallPair ParseCommandCallPairWith(Parser parser) {
local Text resolvedValue;
local MutableText userSpecifiedName;
local CommandCallPair result;
local Text.Character dotCharacter;
if (parser == none) return result;
if (!parser.Ok()) return result;
parser.MUntilMany(userSpecifiedName, commandDelimiters, true, true);
resolvedValue = _.alias.ResolveCommand(userSpecifiedName);
// This isn't an alias
if (resolvedValue == none) {
result.commandName = userSpecifiedName;
return result;
}
// It is an alias - parse it
dotCharacter = _.text.GetCharacter(".");
resolvedValue.Parse()
.MUntil(result.commandName, dotCharacter)
.MatchS(".")
.MUntil(result.subCommandName, dotCharacter)
.FreeSelf();
if (result.subCommandName.IsEmpty()) {
result.subCommandName.FreeSelf();
result.subCommandName = none;
}
resolvedValue.FreeSelf();
return result;
}
private function bool HandleCommands(EPlayer sender, MutableText message, bool teamMessage) {
local Parser parser;
// We are only interested in messages that start with `chatCommandPrefix`
parser = _.text.Parse(message);
if (!parser.Match(chatCommandPrefix).Ok()) {
parser.FreeSelf();
return true;
}
// Pass input to command feature
HandleInputWith(parser, sender);
parser.FreeSelf();
return false;
}
private function HandleMutate(string command, PlayerController sendingPlayer) {
local Parser parser;
local EPlayer sender;
// A lot of other mutators use these commands
if (command ~= "help") return;
if (command ~= "version") return;
if (command ~= "status") return;
if (command ~= "credits") return;
parser = _.text.ParseString(command);
sender = _.players.FromController(sendingPlayer);
HandleInputWith(parser, sender);
sender.FreeSelf();
parser.FreeSelf();
}
private final function LoadConfigArrays() {
local int i;
local CommandListGroupPair nextCommandSetGroupPair;
for (i = 0; i < commandGroup.length; i += 1) {
permissionGroupOrder[i] = _.text.FromString(commandGroup[i]);
}
for (i = 0; i < addCommandSet.length; i += 1) {
nextCommandSetGroupPair.commandListName = _.text.FromString(addCommandSet[i].name);
nextCommandSetGroupPair.permissionGroup = _.text.FromString(addCommandSet[i].for);
usedCommandLists[i] = nextCommandSetGroupPair;
}
FreeRenamingRules();
commandRenamingRules = LoadRenamingRules(renamingRule);
votingRenamingRules = LoadRenamingRules(votingRenamingRule);
}
private final function array<RenamingRulePair> LoadRenamingRules(
array<Commands.RenamingRulePair> inputRules) {
local int i, j;
local RenamingRulePair nextRule;
local array<RenamingRulePair> result;
// Clear away duplicates
for (i = 0; i < inputRules.length; i += 1) {
j = i + 1;
while (j < inputRules.length) {
if (inputRules[i].rename == inputRules[j].rename) {
_.logger.Auto(warnDuplicateRenaming)
.ArgClass(inputRules[i].rename)
.Arg(_.text.FromString(inputRules[i].to))
.Arg(_.text.FromString(inputRules[j].to));
inputRules.Remove(j, 1);
} else {
j += 1;
}
}
}
// Translate rules
for (i = 0; i < inputRules.length; i += 1) {
nextRule.class = inputRules[i].rename;
nextRule.newName = _.text.FromString(inputRules[i].to);
if (nextRule.class == class'ACommandHelp') {
_.memory.Free(helpCommandName);
helpCommandName = nextRule.newName.Copy();
}
result[result.length] = nextRule;
}
return result;
}
private final function LoadCommands() {
local int i, j;
local Text nextName;
local array<EntityLoadInfo> commandClassesToLoad;
commandClassesToLoad = CollectAllCommandClasses();
// Load command names to use, according to preferred names and name rules
for (i = 0; i < commandClassesToLoad.length; i += 1) {
nextName = none;
for (j = 0; j < commandRenamingRules.length; j += 1) {
if (commandClassesToLoad[i].entityClass == commandRenamingRules[j].class) {
nextName = commandRenamingRules[j].newName.Copy();
break;
}
}
if (nextName == none) {
nextName = class<Command>(commandClassesToLoad[i].entityClass)
.static.GetPreferredName();
}
commandClassesToLoad[i].name = nextName;
}
// Actually load commands
for (i = 0; i < commandClassesToLoad.length; i += 1) {
_.commands.AddCommandAsync(
class<Command>(commandClassesToLoad[i].entityClass),
commandClassesToLoad[i].name);
for (j = 0; j < commandClassesToLoad[i].authorizedGroups.length; j += 1) {
_.commands.AuthorizeCommandUsageAsync(
commandClassesToLoad[i].name,
commandClassesToLoad[i].authorizedGroups[j],
commandClassesToLoad[i].groupsConfig[j]);
}
_.logger.Auto(infoCommandAdded)
.ArgClass(commandClassesToLoad[i].entityClass)
.Arg(/*take*/ commandClassesToLoad[i].name);
}
for (i = 0; i < commandClassesToLoad.length; i += 1) {
// `name` field was already released through `Arg()` logger function
_.memory.FreeMany(commandClassesToLoad[i].authorizedGroups);
_.memory.FreeMany(commandClassesToLoad[i].groupsConfig);
}
}
private final function LoadVotings() {
local int i, j;
local Text nextName;
local array<EntityLoadInfo> votingClassesToLoad;
votingClassesToLoad = CollectAllVotingClasses();
// Load voting names to use, according to preferred names and name rules
for (i = 0; i < votingClassesToLoad.length; i += 1) {
nextName = none;
for (j = 0; j < votingRenamingRules.length; j += 1) {
if (votingClassesToLoad[i].entityClass == votingRenamingRules[j].class) {
nextName = votingRenamingRules[j].newName.Copy();
break;
}
}
if (nextName == none) {
nextName = class<Voting>(votingClassesToLoad[i].entityClass)
.static.GetPreferredName();
}
votingClassesToLoad[i].name = nextName;
}
// Actually load votings
for (i = 0; i < votingClassesToLoad.length; i += 1) {
_.commands.AddVotingAsync(
class<Voting>(votingClassesToLoad[i].entityClass),
votingClassesToLoad[i].name);
for (j = 0; j < votingClassesToLoad[i].authorizedGroups.length; j += 1) {
_.commands.AuthorizeVotingUsageAsync(
votingClassesToLoad[i].name,
votingClassesToLoad[i].authorizedGroups[j],
votingClassesToLoad[i].groupsConfig[j]);
}
_.logger.Auto(infoVotingAdded)
.ArgClass(votingClassesToLoad[i].entityClass)
.Arg(/*take*/ votingClassesToLoad[i].name);
}
for (i = 0; i < votingClassesToLoad.length; i += 1) {
// `name` field was already released through `Arg()` logger function
_.memory.FreeMany(votingClassesToLoad[i].authorizedGroups);
_.memory.FreeMany(votingClassesToLoad[i].groupsConfig);
}
}
// Guaranteed to not return `none` items in the array
private final function array<EntityLoadInfo> CollectAllCommandClasses() {
local int i;
local bool debugging;
local CommandList nextList;
local array<EntityLoadInfo> result;
debugging = _.environment.IsDebugging();
class'CommandList'.static.Initialize();
for (i = 0; i < usedCommandLists.length; i += 1) {
nextList = CommandList(class'CommandList'.static
.GetConfigInstance(usedCommandLists[i].commandListName));
if (nextList != none) {
if (!debugging && nextList.debugOnly) {
continue;
}
MergeEntityClassArrays(
result,
/*take*/ nextList.GetCommandData(),
usedCommandLists[i].permissionGroup);
} else {
_.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy());
}
}
return result;
}
// Guaranteed to not return `none` items in the array
private final function array<EntityLoadInfo> CollectAllVotingClasses() {
local int i;
local bool debugging;
local CommandList nextList;
local array<EntityLoadInfo> result;
debugging = _.environment.IsDebugging();
class'CommandList'.static.Initialize();
for (i = 0; i < usedCommandLists.length; i += 1) {
nextList = CommandList(class'CommandList'.static
.GetConfigInstance(usedCommandLists[i].commandListName));
if (nextList != none) {
if (!debugging && nextList.debugOnly) {
continue;
}
MergeEntityClassArrays(
result,
/*take*/ nextList.GetVotingData(),
usedCommandLists[i].permissionGroup);
} else {
_.logger.Auto(warnNoCommandList).Arg(usedCommandLists[i].commandListName.Copy());
}
}
return result;
}
// Adds `newCommands` into `infoArray`, adding `commandsGroup` to
// their array `authorizedGroups`
//
// Assumes that all arguments aren't `none`.
//
// Guaranteed to not add `none` commands from `newCommands`.
//
// Assumes that items from `infoArray` all have `name` field set to `none`,
// will also leave them as `none`.
private final function MergeEntityClassArrays(
out array<EntityLoadInfo> infoArray,
/*take*/ array<CommandList.EntityConfigPair> newCommands,
Text commandsGroup
) {
local int i, infoToEditIndex;
local EntityLoadInfo infoToEdit;
local array<Text> editedArray;
for (i = 0; i < newCommands.length; i += 1) {
if (newCommands[i].class == none) {
continue;
}
// Search for an existing record of class `newCommands[i]` in
// `infoArray`. If found, copy to `infoToEdit` and index into
// `infoToEditIndex`, else `infoToEditIndex` will hold the next unused
// index in `infoArray`.
infoToEditIndex = 0;
while (infoToEditIndex < infoArray.length) {
if (infoArray[infoToEditIndex].entityClass == newCommands[i].class) {
infoToEdit = infoArray[infoToEditIndex];
break;
}
infoToEditIndex += 1;
}
// Update data inside `infoToEdit`.
infoToEdit.entityClass = newCommands[i].class;
editedArray = infoToEdit.authorizedGroups;
editedArray[editedArray.length] = commandsGroup.Copy();
infoToEdit.authorizedGroups = editedArray;
editedArray = infoToEdit.groupsConfig;
editedArray[editedArray.length] = newCommands[i].config; // moving here
infoToEdit.groupsConfig = editedArray;
// Update `infoArray` with the modified record.
infoArray[infoToEditIndex] = infoToEdit;
// Forget about data `authorizedGroups` and `groupsConfig`:
//
// 1. Their references have already been moved into `infoArray` and
// don't need to be released;
// 2. If we don't find corresponding struct inside `infoArray` on
// the next iteration, we'll override `commandClass`,
// but not `authorizedGroups`/`groupsConfig`, so we'll just reset them
// to empty now.
// (`name` field is expected to be `none` during this method)
infoToEdit.authorizedGroups.length = 0;
infoToEdit.groupsConfig.length = 0;
}
}
private final function FreeUsedCommandSets() {
local int i;
for (i = 0; i < usedCommandLists.length; i += 1) {
_.memory.Free(usedCommandLists[i].commandListName);
_.memory.Free(usedCommandLists[i].permissionGroup);
}
usedCommandLists.length = 0;
}
private final function FreeRenamingRules() {
local int i;
for (i = 0; i < commandRenamingRules.length; i += 1) {
_.memory.Free(commandRenamingRules[i].newName);
}
commandRenamingRules.length = 0;
for (i = 0; i < votingRenamingRules.length; i += 1) {
_.memory.Free(votingRenamingRules[i].newName);
}
votingRenamingRules.length = 0;
}
public final function CommandAPI.CommandFeatureTools _borrowTools() {
return tools;
}
defaultproperties {
configClass = class'Commands'
errServerAPIUnavailable = (l=LOG_Error,m="Server API is currently unavailable. Input methods through chat and mutate command will not be available.")
warnDuplicateRenaming = (l=LOG_Warning,m="Class `%1` has duplicate renaming rules: \"%2\" and \"%3\". Latter will be discarded. It is recommended that you fix your `AcediaCommands` configuration file.")
warnNoCommandList = (l=LOG_Warning,m="Command list \"%1\" not found.")
infoCommandAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load command `%1` as \"%2\".")
infoVotingAdded = (l=LOG_Info,m="`Commands_Feature` has resolved to load voting `%1` as \"%2\".")
}

View File

@ -0,0 +1,77 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2024 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 Component extends AcediaObject
abstract;
//! A base class to implement components from, supposed to function as a special data storage
//! for in-game [`Entity`]s.
//!
//! On the current stage of implementation doesn't do anything by itself.
/// Creates an independent copy of the component.
///
/// This function duplicates the caller component, copying all data in an identical
/// fashion, including references to other entities (stored as [`EntityRef`]).
/// The result is a new component that is a direct copy of the original, maintaining
/// all internal states and links.
///
/// To make a new component using this one like a template use [`Component::TemplateCopy()`].
public final function Component Copy() {
return _copy(false);
}
/// Generates a new component instance using the caller as a template.
///
/// This function uses the caller component as a template to create a new component.
/// Unlike the [`Component::Copy()`] method, references to other entities (stored as [`EntityRef`])
/// are not directly copied.
/// Instead, they are dropped, and new references are created based on the template names provided
/// by [`EntityRef`] if available.
/// This method is intended for scenarios where a new, similar instance is needed without retaining
/// direct links to the entities referenced by the original component.
public final function Component TemplateCopy() {
return _copy(true);
}
/// Internal component copy function to avoid code duplication for copying logic in
/// [`Component::Copy()`] and [`Component::TemplateCopy()`].
///
/// This protected function serves as the core implementation for copying component instances.
/// It can operate in two modes depending on the `actAsTemplate` flag:
/// 1. When `actAsTemplate` is false, it performs a direct copy of the component,
/// replicating all data and entity references exactly as in the original component.
/// 2. When `actAsTemplate` is true, it creates a new component based on the template
/// characteristics of the caller.
/// In this mode, entity references (stored as [`EntityRef`]) are not directly copied.
/// Instead, these references are dropped, and new instances are created based on the template
/// names provided by the [`EntityRef`] if available, ensuring that the new component does not
/// retain direct links to the entities of the original.
///
/// This method should not be called directly; use `Copy()` or `TemplateCopy()` as
/// public interfaces.
/// They correspond to modes 1 and 2 respectively.
protected function Component _copy(bool actAsTemplate) {
return none;
}
defaultproperties {
}

View File

@ -0,0 +1,260 @@
/**
* 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 AcediaObject
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 does 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 new `ConsoleWriter` instance that will write into
* consoles of all players.
*
* @return ConsoleWriter New `ConsoleWriter` instance, configured to
* write into consoles of all players.
* Guaranteed to not be `none`.
*/
public final function ConsoleWriter ForAll()
{
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 given player.
*
* @param targetPlayer 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 console of `targetPlayer`.
* Guaranteed to not be `none`.
*/
public final function ConsoleWriter For(EPlayer targetPlayer)
{
local ConsoleDisplaySettings globalSettings;
globalSettings.defaultColor = defaultColor;
globalSettings.maxTotalLineWidth = maxTotalLineWidth;
globalSettings.maxVisibleLineWidth = maxVisibleLineWidth;
return ConsoleWriter(_.memory.Allocate(class'ConsoleWriter'))
.Initialize(globalSettings).ForPlayer(targetPlayer);
}
/**
* Returns new `ConsoleWriter` instance that will write into
* console of the given player.
*
* @param targetPlayer 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 console of `targetPlayer`.
* Guaranteed to not be `none`.
*/
public final function ConsoleWriter ForController(PlayerController targetPlayer)
{
local EPlayer wrapper;
local ConsoleWriter result;
wrapper = _.players.FromController(targetPlayer);
result = For(wrapper);
_.memory.Free(wrapper);
return result;
}
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

@ -0,0 +1,417 @@
/**
* 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(BaseText)
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).
*
* It is implemented making heavier use of `string`s instead of `Text`.
* This is because:
* 1. `string`s that are passed to console are broken into lines,
* that need to be specifically prepared anyway;
* 2. It was coded before I the switch to mostly using `Text`,
* when a lot of methods were also accepting `string`s
* and `array<Character>` types as parameters.
* And i really do not want to have to reimplement it.
*/
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, as a colored `string`.
// Not a `Text`, because it has to be prepared exactly how we want it.
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.
// We are using array of `Character`s instead of `MutableText` since
// we want to have a more directly control over how it is converted into
// a colored string anyway and otherwise only need an ability to
// append `Character`s to it.
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 `Insert()`. 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 `Text` to be added to the current line in caller
* `ConsoleBuffer`. Does nothing if passed `none`.
* @param inputType How to treat given `string` regarding coloring.
* @return Returns caller `ConsoleBuffer` to allow method chaining.
*/
public final function ConsoleBuffer Insert(BaseText input)
{
local int inputConsumed;
local BaseText.Character nextCharacter;
if (input == none) {
return self;
}
// Regular symbols and whitespaces are treated differently when
// breaking input into lines, so alternate between adding them,
// switching the logic appropriately
while (inputConsumed < input.GetLength())
{
while (inputConsumed < input.GetLength())
{
nextCharacter = input.GetCharacter(inputConsumed);
if (_.text.IsWhitespace(nextCharacter)) {
break;
}
InsertIntoWordBuffer(input.GetCharacter(inputConsumed));
inputConsumed += 1;
}
// If we didn't encounter any whitespace symbols - bail
if (inputConsumed >= input.GetLength()) {
return self;
}
FlushWordBuffer();
// Dump whitespaces into lines
while (inputConsumed < input.GetLength())
{
nextCharacter = input.GetCharacter(inputConsumed);
if (!_.text.IsWhitespace(nextCharacter)) {
break;
}
AppendWhitespaceToCurrentLine(nextCharacter);
inputConsumed += 1;
}
}
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(BaseText.Character newCharacter)
{
local int newCharacterIndex;
local BaseText.Formatting newFormatting;
local Color oldColor, newColor;
// Fix text color in the buffer to remember default color, if we use it.
newFormatting = _.text.GetCharacterFormatting(newCharacter);
newFormatting.color =
_.text.GetCharacterColor(newCharacter, displaySettings.defaultColor);
newFormatting.isColored = true;
newCharacter = _.text.SetFormatting(newCharacter, newFormatting);
// Add new character and check if color swapped
newCharacterIndex = wordBuffer.length;
wordBuffer[newCharacterIndex] = newCharacter;
if (newCharacterIndex <= 0) {
return;
}
newColor = newFormatting.color;
oldColor = _.text.GetCharacterColor(wordBuffer[newCharacterIndex - 1]);
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 = _.text.GetCharacterColor(wordBuffer[i]);
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(
BaseText.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(_.text.GetCharacterColor(nextCharacter))) {
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(
BaseText.Character whitespace)
{
if (_.text.IsCodePoint(whitespace, CODEPOINT_NEWLINE)) {
BreakLine(false);
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(_.text.GetCharacterColor(wordBuffer[0]))) {
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

@ -0,0 +1,668 @@
/**
* 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;
var private string INDENTATION;
/**
* Describes current output target of the `ConsoleWriter`.
*/
enum ConsoleWriterTarget
{
// No one. Can happed if our target disconnects.
CWTARGET_None,
// A certain set of players.
CWTARGET_Players,
// All players.
CWTARGET_All
};
var private ConsoleWriterTarget targetType;
// Players that will receive output passed to this `ConsoleWriter`.
// Only used when `targetType == CWTARGET_Players`
// Cannot be allowed to contain `none` values.
var private array<EPlayer> outputTargets;
var private ConsoleBuffer outputBuffer;
var private bool needToResetColor;
var private ConsoleAPI.ConsoleDisplaySettings displaySettings;
// Sometimes we want to output a certain part of text with a color
// different from the default one. However this requires to remember current
// default in additional variable, set new color and then return the old one.
// To slightly simplify this process we use pair of `UseColor()`/`ResetColor()`
// methods that use this variable to remember "real" default color and allowing
// us to quickly reset back to it.
// This also means that `displaySettings` can sometimes store "default"
// color information instead.
var private Color defaultColor;
protected function Finalizer()
{
_.memory.FreeMany(outputTargets);
_.memory.Free(outputBuffer);
outputTargets.length = 0;
outputBuffer = none;
}
public final function ConsoleWriter Initialize(
ConsoleAPI.ConsoleDisplaySettings newDisplaySettings)
{
defaultColor = newDisplaySettings.defaultColor;
displaySettings = newDisplaySettings;
if (outputBuffer == none) {
outputBuffer = ConsoleBuffer(_.memory.Allocate(class'ConsoleBuffer'));
}
else {
outputBuffer.Clear();
}
outputBuffer.SetSettings(displaySettings);
return self;
}
/**
* Return default color setting for caller `ConsoleWriter`. It can be
* temporarily overwritten by `UseColor()` method.
*
* This method returns default color setting, i.e. color that will be used
* if no other is specified by text you're outputting and if it was not
* overwritten with `UseColor()` method.
* To get color currently used for outputting text, see `GetColor()`
* method.
*
* Do note that `ConsoleWriter` can have two "default" colors: a "real"
* default and a "temporary" default: "temporary" one can be set with
* `UseColor()` or `UseColorOnce()` method calls to temporarily color certain
* part of the output and then revert to the "real" default color.
* This method always returns "real" default color.
*
* 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 (the one that will be used to color output
* text after `ResetColor()` method call).
*/
public final function Color GetDefaultColor()
{
return defaultColor;
}
/**
* Return currently used 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 is ignored.
* See also `GetDefaultColor()`.
*
* Do note that `ConsoleWriter` can have two "default" colors: a "real"
* default and a "temporary" default: "temporary" one can be set with
* `UseColor()` or `UseColorOnce()` method calls to temporarily color certain
* part of the output and then revert to the "real" default color.
* This method always return the color that will be actually used to
* output text, so "temporary" default if it's set and "real" default
* otherwise.
*
* @return Current default color (currently used to output text information).
*/
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 "temporary" color. If color is specified, this value will
* be ignored.
*
* If you only want to quickly color certain part of output, it is better to
* use `UseColor()` or `UseColorOnce()` methods that temporarily changes used
* default color and allow to return to actual default color with
* `ResetColor()` method.
*
* 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)
{
defaultColor = newDefaultColor;
displaySettings.defaultColor = newDefaultColor;
if (outputBuffer != none) {
outputBuffer.SetSettings(displaySettings);
}
return self;
}
/**
* Sets "temporary" default color that can be reverted to "real" default color
* with `ResetColor()` method.
*
* For quickly coloring certain parts of output:
* `console.UseColor(_.color.blue).Write(blueMessage)
* .Write(moreBlueMessage).ResetColor()`.
* Also see `UseColorOnce()` method.
*
* Consecutive calls do not "stack up" colors - only last one is remembered:
* `console.UseColor(_.color.blue).UseColor(_.color.green)` is the same as
* `console.UseColor(_.color.green)`.
*
* Use `SetColor()` to set both "real" and "temporary" color.
*
* @param temporaryColor Color to use as default one in the next console
* output calls until the `ResetColor()` method call.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter UseColor(Color temporaryColor)
{
needToResetColor = false;
displaySettings.defaultColor = temporaryColor;
if (outputBuffer != none) {
outputBuffer.SetSettings(displaySettings);
}
return self;
}
/**
* Sets "temporary" default color that will be automatically reverted to "real"
* default color after any "writing" method (i.e. `Write()`, `WriteLine()`,
* `WriteBlock()` or `Say()`) or when `ResetColor()` method is called.
*
* For quickly coloring certain parts of output:
* `console.UseColorOnce(_.color.blue).Write(blueMessage)`.
*
* Consecutive calls do not "stack up" colors - only last one is remembered:
* `console.UseColorOnce(_.color.blue).UseColorOnce(_.color.green)`
* is the same as `console.UseColorOnce(_.color.green)`.
*
* Use `SetColor()` to set both "real" and "temporary" color.
*
* @param temporaryColor Color to use as default one in the next console
* output calls until any "writing" method (or `ResetColor()` method)
* is called.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter UseColorOnce(Color temporaryColor)
{
needToResetColor = true;
displaySettings.defaultColor = temporaryColor;
if (outputBuffer != none) {
outputBuffer.SetSettings(displaySettings);
}
return self;
}
/**
* Resets "temporary" default text color to "real" default color.
* See `UseColor()` and `UseColorOnce()` for details.
*
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter ResetColor()
{
needToResetColor = false;
displaySettings.defaultColor = defaultColor;
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 if target actually has to switch
* (before the switch occurs).
*
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter ForAll()
{
if (targetType != CWTARGET_All) {
Flush();
}
targetType = CWTARGET_All;
return self;
}
/**
* Configures caller `ConsoleWriter` to output only to the given player.
* `Flush()` will be automatically called if target actually has to switch
* (before the switch occurs).
*
* @param targetPlayer Player, to whom console we want to write.
* If `none` - caller `ConsoleWriter` would be configured to
* throw messages away.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter ForPlayer(EPlayer targetPlayer)
{
if (targetPlayer == none)
{
Flush();
targetType = CWTARGET_None;
return self;
}
if (targetType != CWTARGET_Players) {
Flush();
}
outputTargets.length = 0;
targetType = CWTARGET_Players;
outputTargets[0] = targetPlayer;
targetPlayer.NewRef();
return self;
}
/**
* Configures caller `ConsoleWriter` to output to one more player,
* given by `targetPlayer`.
* `Flush()` will be automatically called if target actually has to switch
* (before the switch occurs).
*
* @param targetPlayer Player, to whom console we want to write.
* If `none` - this method will do nothing.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter AndPlayer(EPlayer targetPlayer)
{
local int i;
if (targetPlayer == none) return self;
if (!targetPlayer.IsExistent()) return self;
if (targetType != CWTARGET_Players)
{
Flush();
_.memory.FreeMany(outputTargets);
if (targetType == CWTARGET_None) {
outputTargets.length = 0;
}
else {
outputTargets = _.players.GetAll();
}
}
targetType = CWTARGET_Players;
for (i = 0; i < outputTargets.length; i += 1)
{
if (targetPlayer.SameAs(outputTargets[i])) {
return self;
}
}
Flush();
outputTargets[outputTargets.length] = targetPlayer;
targetPlayer.NewRef();
return self;
}
/**
* Configures caller `ConsoleWriter` to output to one less player,
* given by `targetPlayer`.
* `Flush()` will be automatically called if target actually has to switch
* (before the switch occurs).
*
* @param targetPlayer Player, to whom console we no longer want to write.
* If `none` - this method will do nothing.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter ButPlayer(EPlayer playerToRemove)
{
local int i;
if (targetType == CWTARGET_None) return self;
if (playerToRemove == none) return self;
if (!playerToRemove.IsExistent()) return self;
if (targetType == CWTARGET_All)
{
Flush();
_.memory.FreeMany(outputTargets);
outputTargets = _.players.GetAll();
}
targetType = CWTARGET_Players;
while (i < outputTargets.length)
{
if (playerToRemove.SameAs(outputTargets[i]))
{
Flush();
_.memory.Free(outputTargets[i]);
outputTargets.Remove(i, 1);
break;
}
i += 1;
}
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_Players && outputTargets.length == 0) {
targetType = CWTARGET_None;
}
return targetType;
}
/**
* Returns `EPlayer` to whom console caller `ConsoleWriter` is
* outputting messages.
* If caller `ConsoleWriter` is setup to message several different players,
* returns an arbitrary one of them.
*
* @return Player (`EPlayer` class) to whom console caller `ConsoleWriter` is
* outputting messages. Returns `none` iff it currently outputs to
* every player or to no one.
*/
public final function EPlayer GetTargetPlayer()
{
if (targetType == CWTARGET_All) return none;
if (outputTargets.length <= 0) return none;
outputTargets[0].NewRef();
return outputTargets[0];
}
/**
* Returns array of `EPlayer`s, to whom console caller `ConsoleWriter` is
* outputting messages.
*
* @return Player (`EPlayer` class) to whom console caller `ConsoleWriter` is
* outputting messages. Returned array is guaranteed to not contain `none`s
* or `EPlayer` interfaces with non-existent status.
*/
public final function array<EPlayer> GetTargetPlayers()
{
local int i;
local array<EPlayer> result;
if (targetType == CWTARGET_None) return result;
if (targetType == CWTARGET_All) return _.players.GetAll();
for (i = 0; i < outputTargets.length; i += 1)
{
if (outputTargets[i].IsExistent()) {
result[result.length] = outputTargets[i];
outputTargets[i].NewRef();
}
}
return result;
}
/**
* 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 text's contents into console.
*
* Does not trigger console output, for that use `WriteLine()` or `Flush()`.
*
* @param message `Text` to output.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter Write(optional BaseText message)
{
outputBuffer.Insert(message);
if (needToResetColor) {
ResetColor();
}
return self;
}
/**
* Writes text's contents into console.
* Result will be output immediately, starts a new line.
*
* @param message `Text` to output.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter WriteLine(optional BaseText message)
{
return Write(message).Flush();
}
/**
* Writes text's indented contents into console.
*
* Acts like a `WriteLine()` call, except all output contents will be
* additionally indented by four whitespace symbols
* (including lines after line breaks).
*
* Result will be output immediately, starts a new line.
*
* @param message `Text` to output.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter WriteBlock(optional BaseText message)
{
outputBuffer.Insert(message).Flush();
SendBuffer(true);
if (needToResetColor) {
ResetColor();
}
return self;
}
/**
* Writes text's contents into console as a player's chat message, causing them
* to appear on screen in vanilla UI.
*
* All the buffer stored in caller `ConsoleWriter` so far will be flushed.
* Result will be output immediately. Starts a new line.
*
* @param message `Text` to output.
* @return Returns caller `ConsoleWriter` to allow for method chaining.
*/
public final function ConsoleWriter Say(optional BaseText message)
{
outputBuffer.Insert(message).Flush();
SendBuffer(, true);
if (needToResetColor) {
ResetColor();
}
return self;
}
// Send all completed lines from an `outputBuffer`.
// Setting `indented` to `true` will cause additional four whitespaces to
// be added to the output.
private final function SendBuffer(optional bool asIndented, optional bool asSay)
{
local string prefix;
local ConsoleBuffer.LineRecord nextLineRecord;
local array<PlayerController> recipients;
recipients = GetRecipientsControllers();
while (outputBuffer.HasCompletedLines())
{
nextLineRecord = outputBuffer.PopNextLine();
if (nextLineRecord.wrappedLine) {
prefix = NEWLINE_PREFIX;
}
else {
prefix = BROKENLINE_PREFIX;
}
if (asIndented) {
prefix $= INDENTATION;
}
SendConsoleMessage(recipients, prefix $ nextLineRecord.contents, asSay);
}
}
// Assumes `connectionService != none`, caller function must ensure that.
private final function SendConsoleMessage(
array<PlayerController> recipients,
string message,
bool asSay)
{
local int i;
for (i = 0; i < recipients.length; i += 1)
{
if (recipients[i] != none)
{
if (asSay) {
recipients[i].ClientMessage(message);
}
else {
recipients[i].TeamMessage(none, message, 'AcediaConsole');
}
}
}
}
// Method for retrieving `PlayerController`s of recipients at the moment
// of the call
private final function array<PlayerController> GetRecipientsControllers()
{
local int i;
local PlayerController nextRecipient;
local ConnectionService connectionService;
local array<PlayerController> recipients;
local array<ConnectionService.Connection> connections;
// No targets
if (targetType == CWTARGET_None) {
return recipients;
}
// Selected targets case
if (targetType != CWTARGET_All)
{
for (i = 0; i < outputTargets.length; i += 1)
{
nextRecipient = outputTargets[i].GetController();
if (nextRecipient != none) {
recipients[recipients.length] = nextRecipient;
}
}
return recipients;
}
// All players target case
connectionService =
ConnectionService(class'ConnectionService'.static.Require());
if (connectionService == none) {
return recipients;
}
connections = connectionService.GetActiveConnections();
for (i = 0; i < connections.length; i += 1)
{
if (connections[i].controllerReference != none) {
recipients[recipients.length] = connections[i].controllerReference;
}
}
return recipients;
}
defaultproperties
{
NEWLINE_PREFIX = "| "
BROKENLINE_PREFIX = " "
INDENTATION = " "
}

View File

@ -0,0 +1,126 @@
/**
* Base class for objects that will provide an access to a Acedia's client- and
* server-specific functionality by giving a reference to this object to all
* Acedia's objects and actors, emulating a global API namespace.
* Copyright 2022-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class CoreGlobal extends Object;
var protected bool initialized;
var protected class<AcediaAdapter> adapterClass;
var public SideEffectAPI sideEffects;
var public TimeAPI time;
var public DBAPI db;
var private LoggerAPI.Definition fatNoAdapterClass;
/**
* Accessor to the generic `UnrealAPI`.
*/
public function UnrealAPI unreal_api()
{
return none;
}
public final static function LevelCore GetLevelCore()
{
local LevelCore availableCore;
availableCore = class'ServerLevelCore'.static.GetInstance();
if (availableCore != none) {
return availableCore;
}
availableCore = class'ClientLevelCore'.static.GetInstance();
return availableCore;
}
public final static function CoreGlobal GetGenericInstance()
{
local ServerGlobal serverAPI;
local ClientGlobal clientAPI;
serverAPI = class'ServerGlobal'.static.GetInstance();
if (serverAPI != none && serverAPI.IsAvailable()) {
return serverAPI;
}
clientAPI = class'ClientGlobal'.static.GetInstance();
if (clientAPI != none && clientAPI.IsAvailable()) {
return clientAPI;
}
return none;
}
/**
* This method must perform initialization of the caller `...Global` instance.
*
* It must only be executed once (execution should be marked using
* `initialized` flag).
*/
protected function Initialize()
{
local MemoryAPI api;
if (initialized) {
return;
}
initialized = true;
if (adapterClass == none)
{
class'Global'.static.GetInstance().logger
.Auto(fatNoAdapterClass)
.ArgClass(self.class);
return;
}
api = class'Global'.static.GetInstance().memory;
time = TimeAPI(api.Allocate(adapterClass.default.timeAPIClass));
db = DBAPI(api.Allocate(adapterClass.default.dbAPIClass));
}
/**
* Checks is caller `CoreGlobal` is available to be used.
*
* Server and client `CoreGlobal` instances are always created, so that they
* can be added to `AcediaObject`s and `AcediaActor`s at any time, even before
* they were initialized (whether they ever will be or not). This method
* allows one to check whether they were already initialized and can be used.
*
* @return `true` if caller `CoreGlobal` can be used and `false` otherwise.
*/
public function bool IsAvailable()
{
return initialized;
}
/**
* Changes adapter class for the caller `...Global` instance.
*
* Must not do anything when caller `...Global` instance was already
* initialized or when passed adapter class is `none`.
*
* @param newAdapter New adapter class to use in the caller `...Global`
* instance.
* @return `true` if new adapter was set and `false` otherwise.
*/
public function bool SetAdapter(class<AcediaAdapter> newAdapter);
defaultproperties
{
adapterClass = class'AcediaAdapter'
fatNoAdapterClass = (l=LOG_Fatal,m="`none` specified as an adapter for `%1` level core class. This should not have happened. AcediaCore cannot properly function.")
}

View File

@ -0,0 +1,409 @@
/**
* API that provides methods for creating/destroying and managing available
* databases.
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBAPI extends AcediaObject;
var private const class<Database> localDBClass;
// Store all already loaded databases to make sure we do not create two
// different `LocalDatabaseInstance` that are trying to make changes
// separately.
var private HashTable loadedLocalDatabases;
var private LoggerAPI.Definition infoLocalDatabaseCreated;
var private LoggerAPI.Definition infoLocalDatabaseDeleted;
var private LoggerAPI.Definition infoLocalDatabaseLoaded;
private final function CreateLocalDBMapIfMissing()
{
if (loadedLocalDatabases == none) {
loadedLocalDatabases = __().collections.EmptyHashTable();
}
}
/**
* Loads database based on the link.
*
* Links have the form of "<db_name>:" (or, optionally, "[<type>]<db_name>:"),
* followed by the JSON pointer (possibly empty one) to the object inside it.
* "<type>" can be either "local" or "remote" and is necessary only when both
* local and remote database have the same name (which should be avoided).
* "<db_name>" refers to the database that we are expected
* to load, it has to consist of numbers and latin letters only.
*
* @param databaseLink Link from which to extract database's name.
* @return Database named "<db_name>" of type "<type>" from the `databaseLink`.
*/
public final function Database Load(BaseText databaseLink)
{
local Parser parser;
local Database result;
local MutableText databaseName;
if (databaseLink == none) {
return none;
}
parser = _.text.Parse(databaseLink);
// Only local DBs are supported for now!
// So just consume this prefix, if it's present.
parser.Match(P("[local]")).Confirm();
parser.R().MUntil(databaseName, _.text.GetCharacter(":")).MatchS(":");
if (!parser.Ok())
{
parser.FreeSelf();
return none;
}
result = LoadLocal(databaseName);
parser.FreeSelf();
databaseName.FreeSelf();
return result;
}
/**
* Extracts `MutableJSONPointer` from the database path, given by `databaseLink`.
*
* Links have the form of "<db_name>:" (or, optionally, "[<type>]<db_name>:"),
* followed by the JSON pointer (possibly empty one) to the object inside it.
* "<type>" can be either "local" or "remote" and is necessary only when both
* local and remote database have the same name (which should be avoided).
* "<db_name>" refers to the database that we are expected
* to load, it has to consist of numbers and latin letters only.
* This method returns `MutableJSONPointer` that comes after type-name pair.
*
* @param Link from which to extract `MutableJSONPointer`.
* @return `MutableJSONPointer` from the database link.
* Guaranteed to not be `none` if provided argument `databaseLink`
* is not `none`.
*/
public final function MutableJsonPointer GetMutablePointer(BaseText databaseLink)
{
local int slashIndex;
local Text textPointer;
local MutableJsonPointer result;
if (databaseLink == none) {
return none;
}
slashIndex = databaseLink.IndexOf(P(":"));
if (slashIndex < 0) {
return MutableJsonPointer(_.memory.Allocate(class'MutableJsonPointer'));
}
textPointer = databaseLink.Copy(slashIndex + 1);
result = _.json.MutablePointer(textPointer);
textPointer.FreeSelf();
return result;
}
/**
* Extracts `JSONPointer` from the database path, given by `databaseLink`.
*
* Links have the form of "<db_name>:" (or, optionally, "[<type>]<db_name>:"),
* followed by the JSON pointer (possibly empty one) to the object inside it.
* "<type>" can be either "local" or "remote" and is necessary only when both
* local and remote database have the same name (which should be avoided).
* "<db_name>" refers to the database that we are expected
* to load, it has to consist of numbers and latin letters only.
* This method returns `JSONPointer` that comes after type-name pair.
*
* @param Link from which to extract `JSONPointer`.
* @return `JSONPointer` from the database link.
* Guaranteed to not be `none` if provided argument `databaseLink`
* is not `none`.
*/
public final function JSONPointer GetPointer(BaseText databaseLink)
{
local int slashIndex;
local Text textPointer;
local JSONPointer result;
if (databaseLink == none) {
return none;
}
slashIndex = databaseLink.IndexOf(P(":"));
if (slashIndex < 0) {
return JSONPointer(_.memory.Allocate(class'JSONPointer'));
}
textPointer = databaseLink.Copy(slashIndex + 1);
result = _.json.Pointer(textPointer);
textPointer.FreeSelf();
return result;
}
/**
* Opens a new `DBConnection` to the data referred to by the database link.
*
* Opened `DBConnection` doesn't automatically start a connection, so you
* need to call its `Connect()` method.
*
* @param databaseLink Database link to the data we want to connect to.
* @return Initialized `DBConnection` in case given link is valid and `none`
* otherwise.
*/
public final function DBConnection OpenConnection(BaseText databaseLink)
{
local DBConnection result;
local Parser parser;
local Database databaseToConnect;
local JSONPointer locationToConnect;
local MutableText databaseName, textPointer;
if (databaseLink == none) {
return none;
}
parser = _.text.Parse(databaseLink);
// Only local DBs are supported for now!
// So just consume this prefix, if it's present.
parser.Match(P("[local]")).Confirm();
textPointer = parser
.R()
.MUntil(databaseName, _.text.GetCharacter(":"))
.MatchS(":")
.GetRemainderM();
if (parser.Ok())
{
databaseToConnect = LoadLocal(databaseName);
locationToConnect = _.json.Pointer(textPointer);
result = DBConnection(_.memory.Allocate(class'DBConnection'));
result.Initialize(databaseToConnect, locationToConnect);
_.memory.Free(databaseToConnect);
_.memory.Free(locationToConnect);
}
parser.FreeSelf();
_.memory.Free(databaseName);
_.memory.Free(textPointer);
if (result != none && !result.IsInitialized())
{
result.FreeSelf();
result = none;
}
return result;
}
/**
* Creates new local database with name `databaseName`.
*
* This method will fail if:
* 1. `databaseName` is `none` or empty;
* 2. Local database with name `databaseName` already exists.
*
* @param databaseName Name for the new database.
* @return Reference to created database. Returns `none` iff method failed.
*/
public final function LocalDatabaseInstance NewLocal(BaseText databaseName)
{
local DBRecord rootRecord;
local Text rootRecordName;
local Text databaseNameCopy;
local LocalDatabase newConfig;
local LocalDatabaseInstance newLocalDBInstance;
CreateLocalDBMapIfMissing();
if (databaseName == none) return none;
if (!databaseName.IsValidName()) return none;
newConfig = class'LocalDatabase'.static.Load(databaseName);
if (newConfig == none) return none;
if (newConfig.HasDefinedRoot()) return none;
if (loadedLocalDatabases.HasKey(databaseName)) return none;
newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass));
databaseNameCopy = databaseName.Copy();
loadedLocalDatabases.SetItem(databaseNameCopy, newLocalDBInstance);
rootRecord = class'DBRecord'.static.NewRecord(databaseName);
rootRecordName = _.text.FromString(string(rootRecord.name));
newConfig.SetRootName(rootRecordName);
newConfig.Save();
newLocalDBInstance.Initialize(newConfig, rootRecord);
_.logger.Auto(infoLocalDatabaseCreated).Arg(databaseNameCopy);
_.memory.Free(rootRecordName);
return newLocalDBInstance;
}
/**
* Loads and returns local database with the name `databaseName`.
*
* If specified database is already loaded - simply returns it's reference
* (consequent calls to `LoadLocal()` will keep returning the same reference,
* unless database is deleted).
*
* @param databaseName Name of the database to load.
* @return Loaded local database. `none` if it does not exist.
*/
public final function LocalDatabaseInstance LoadLocal(BaseText databaseName)
{
local DBRecord rootRecord;
local Text rootRecordName;
local LocalDatabase newConfig;
local LocalDatabaseInstance newLocalDBInstance, result;
local Text dbKey;
if (databaseName == none) {
return none;
}
CreateLocalDBMapIfMissing();
dbKey = databaseName.Copy();
if (loadedLocalDatabases.HasKey(dbKey))
{
result = LocalDatabaseInstance(loadedLocalDatabases.GetItem(dbKey));
_.memory.Free(dbKey);
return result;
}
// No need to check `dbKey` for being valid,
// since `Load()` will just return `none` if it is not.
newConfig = class'LocalDatabase'.static.Load(dbKey);
if (newConfig == none) {
_.memory.Free(dbKey);
return none;
}
if (!newConfig.HasDefinedRoot() && !newConfig.ShouldCreateIfMissing()) {
_.memory.Free(dbKey);
return none;
}
newLocalDBInstance = LocalDatabaseInstance(_.memory.Allocate(localDBClass));
loadedLocalDatabases.SetItem(dbKey, newLocalDBInstance);
dbKey.FreeSelf();
if (newConfig.HasDefinedRoot())
{
rootRecordName = newConfig.GetRootName();
rootRecord = class'DBRecord'.static.LoadRecord(rootRecordName, dbKey);
}
else
{
rootRecord = class'DBRecord'.static.NewRecord(dbKey);
rootRecordName = _.text.FromString(string(rootRecord.name));
newConfig.SetRootName(rootRecordName);
newConfig.Save();
}
newLocalDBInstance.Initialize(newConfig, rootRecord);
_.logger.Auto(infoLocalDatabaseLoaded).Arg(dbKey);
_.memory.Free(rootRecordName);
return newLocalDBInstance;
}
/**
* Checks if local database with the name `databaseName` already exists.
*
* @param databaseName Name of the database to check.
* @return `true` if database with specified name exists and `false` otherwise.
*/
public final function bool ExistsLocal(BaseText databaseName)
{
local bool result;
local LocalDatabaseInstance instance;
instance = LoadLocal(databaseName);
result = (instance != none);
_.memory.Free(instance);
return result;
}
/**
* Deletes local database with name `databaseName`.
*
* @param databaseName Name of the database to delete.
* @return `true` if database with specified name existed and was deleted and
* `false` otherwise.
*/
public final function bool DeleteLocal(BaseText databaseName)
{
local LocalDatabase localDatabaseConfig;
local LocalDatabaseInstance localDatabase;
local HashTable.Entry dbEntry;
if (databaseName == none) {
return false;
}
CreateLocalDBMapIfMissing();
// To delete database we first need to load it
localDatabase = LoadLocal(databaseName);
if (localDatabase != none)
{
localDatabaseConfig = localDatabase.GetConfig();
localDatabase.WriteToDisk();
_.memory.Free(localDatabase);
}
dbEntry = loadedLocalDatabases.TakeEntry(databaseName);
// Delete `LocalDatabaseInstance` before erasing the package,
// to allow it to clean up safely
_.memory.Free(dbEntry.key);
_.memory.Free(dbEntry.value);
if (localDatabaseConfig != none)
{
EraseAllPackageData(localDatabaseConfig.GetPackageName());
localDatabaseConfig.DeleteSelf();
_.logger.Auto(infoLocalDatabaseDeleted).Arg(databaseName.Copy());
return true;
}
return false;
}
private function EraseAllPackageData(BaseText packageToErase)
{
local int i;
local string packageName;
local GameInfo game;
local DBRecord nextRecord;
local array<DBRecord> allRecords;
packageName = _.text.IntoString(packageToErase);
if (packageName == "") {
return;
}
game = __level().unreal_api().GetGameType();
game.DeletePackage(packageName);
// Delete any leftover objects. This has to be done *after*
// `DeletePackage()` call, otherwise removed garbage can reappear.
// No clear idea why it works this way.
foreach game.AllDataObjects(class'DBRecord', nextRecord, packageName) {
allRecords[allRecords.length] = nextRecord;
}
for (i = 0; i < allRecords.length; i += 1)
{
game.DeleteDataObject( class'DBRecord', string(allRecords[i].name),
packageName);
}
}
/**
* Returns array of names of all available local databases.
*
* @return List of names of all local databases.
*/
public final function array<Text> ListLocal()
{
local int i;
local array<Text> dbNames;
local array<string> dbNamesAsStrings;
dbNamesAsStrings = GetPerObjectNames( "AcediaDB",
string(class'LocalDatabase'.name),
MaxInt);
for (i = 0; i < dbNamesAsStrings.length; i += 1) {
dbNames[dbNames.length] = _.text.FromString(dbNamesAsStrings[i]);
}
return dbNames;
}
defaultproperties
{
localDBClass = class'LocalDatabaseInstance'
infoLocalDatabaseCreated = (l=LOG_Info,m="Local database \"%1\" was created.")
infoLocalDatabaseLoaded = (l=LOG_Info,m="Local database \"%1\" was loaded.")
infoLocalDatabaseDeleted = (l=LOG_Info,m="Local database \"%1\" was deleted.")
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
/**
* Variant of `DBTask` for `CheckDataType()` query.
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBCheckTask extends DBTask;
var private Database.DataType queryTypeResponse;
delegate connect(
Database.DBQueryResult result,
Database.DataType type,
Database source,
int requestID) {}
protected function Finalizer()
{
super.Finalizer();
queryTypeResponse = JSON_Undefined;
connect = none;
}
public function SetDataType(Database.DataType type)
{
queryTypeResponse = type;
}
protected function CompleteSelf(Database source)
{
connect(GetResult(), queryTypeResponse, source, GetRequestID());
}
defaultproperties
{
}

View File

@ -0,0 +1,794 @@
/**
* Auxiliary object for simplifying working with databases.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection extends AcediaObject
dependson(Database)
dependson(DBCache);
/**
* # `DBConnection`
*
* Auxiliary object for simplifying working with databases.
* `Database` class has a rather simple interface and there are several issues
* that constantly arise when trying to use it:
*
* 1. If one tries to read/write data from/to specific location in
* the database, then `JSONPointer` has to be kept in addition to
* the `Database` reference at all times;
* 2. One has to perform initial checks about whether database can even be
* connected to, if at desired location there is a proper data
* structure, etc.. If one also wants to start using data before
* database's response or after its failure, then the same work of
* duplication that data locally must be performed.
* 3. Instead of immediate operations, database operations are delayed and
* user has to handle their results asynchronously in separate methods.
*
* `DBConnection` takes care of these issues by providing you synchronous
* methods for accessing cached version of the data at the given location that
* is duplicated to the database as soon as possible (and even if its no longer
* possible in the case of a failure).
* `DBConnection` makes immediate changes on the local cache and reports
* about possible failures with database later through signals `OnEditResult()`
* (reports about success of writing operations) and `OnStateChanged()`
* (reports about state changes of connected database, including complete
* failures).
* The only reading of database's values occurs at the moment of connecting
* to it, after that all the data is read from the local cache.
* Possible `DBConnection` states include:
*
* * `DBCS_Idle` - database was created, but not yet connected;
* * `DBCS_Connecting` - `Connect()` method was successfully called, but
* its result is still unknown;
* * `DBCS_Connected` - database is connected and properly working;
* * `DBCS_Disconnected` - database was manually disconnected and now
* operates solely on the local cache. Once disconnected `DBConnection`
* cannot be reconnected - create a new one instead.
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason
* can be found out with `GetError()` method:
*
* * `FDE_None` - no error has yet occurred;
* * `FDE_CannotReadRootData` - root data couldn't be read from
* the database, most likely because of the invalid `JSONPointer`
* for the root value;
* * `FDE_UnexpectedRootData` - root data was read from the database,
* but has an unexpected format. `DBConnection` expects either
* JSON object or array (can be specified which one) and this error
* occurs if its not found at specified database's location;
* * `FDE_Unknown` - database returned `DBR_InvalidDatabase` result
* for one of the queries. This is likely to happen when database
* is damaged. More precise details depend on the implementation,
* but result boils down to database being unusable.
*
* ## Usage
*
* Usage is straightforward:
*
* 1. Initialize with appropriate database by calling `Initialize()`;
* 2. Start connecting to it by calling `Connect()`;
* 3. Use `ReadDataByJSON()`/`WriteDataByJSON()` to read/write into
* the connected database;
*
* You can use it transparently even if database connection fails, but if you
* need to handle such failure - connect to the `OnStateChanged()` signal for
* tracking `DBCS_FaultyDatabase` state and to `OnEditResult()` for tracking
* success of writing operations.
*
* ## Implementation
*
* The brunt of work is done by `DBCache` and most of the logic in this
* class is for tracking state of the connection to the database and then
* reporting these changes through its own signals.
* The most notable hidden functionality is tracking requests by ID -
* `DBConnection` makes each request with unique ID and then stores them inside
* `requestIDs` (and in case of write requests - along with corresponding
* `JSONPointer` inside `queuedPointers`). This is necessary because:
*
* 1. Even if `DBConnection` gets reallocated - as far as UnrealScript is
* concerned it is still the same object, so the responses we no longer
* care about will still arrive. Keeping track of the IDs that interest
* us inside `requestIDs` allows us to filter out responses we no
* longer care about;
* 2. Tracking corresponding (to the IDs) `queuedPointers` also allow us
* to know responses to which writing requests we've received.
*
* ## Remarks
*
* Currently `DBConnection` doesn't support important feature of *incrementing*
* data that allows several sources to safely change the same value
* asynchronously. We're skipping on it right now to save time as its not
* really currently needed, however it will be added in the future.
*/
enum DBConnectionState
{
// `DBConnection` was created, but didn't yet attempt to connect
// to database
DBCS_Idle,
// `DBConnection` is currently connecting
DBCS_Connecting,
// `DBConnection` has already connected without errors
DBCS_Connected,
// `DBConnection` was manually disconnected
DBCS_Disconnected,
// `DBConnection` was disconnected because of the database error,
// @see `FaultyDatabaseError` for more.
DBCS_FaultyDatabase
};
// Current connection state
var private DBConnectionState currentState;
enum FaultyDatabaseError
{
// No error has occurred yet
FDE_None,
// Root data isn't available
FDE_CannotReadRootData,
// Root data has incorrect format
FDE_UnexpectedRootData,
// Some internal error in database has occurred
FDE_Unknown
};
// Reason for why current state is in `DBCS_FaultyDatabase` state;
// `FDE_None` if `DBConnection` is in any other state.
var private FaultyDatabaseError dbFailureReason;
// Keeps track whether root value read from the database was of the correct
// type. Only relevant after database tried connecting (i.e. it is in states
// `DBCS_Connected`, `DBCS_Disconnected` or `DBCS_FaultyDatabase`).
// This variable helps us determine whether error should be
// `FDE_CannotReadRootData` or `FDE_UnexpectedRootData`.
var private bool rootIsOfExpectedType;
// `Database` + `JSONPointer` combo that point at the data we want to
// connect to
var private Database dbInstance;
var private JSONPointer rootPointer;
// Local, cached version of that data
var private DBCache localCache;
// This is basically an array of (`int`, `JSONPointer`) pairs for tracking
// database requests of interest
var private array<int> requestIDs;
var private array<JSONPointer> queuedPointers;
// Next usable ID. `DBConnection` is expected to always use unique IDs.
var private int nextRequestID;
var private DBConnection_StateChanged_Signal onStateChangedSignal;
var private DBConnection_EditResult_Signal onEditResultSignal;
var private LoggerAPI.Definition errDoubleInitialization;
/**
* Signal that will be emitted whenever `DBConnection` changes state.
* List of the available states:
*
* * `DBCS_Idle` - database was created, but not yet connected;
* * `DBCS_Connecting` - `Connect()` method was successfully called, but
* its result is still unknown;
* * `DBCS_Connected` - database is connected and properly working;
* * `DBCS_Disconnected` - database was manually disconnected and now
* operates solely on the local cache. Once disconnected `DBConnection`
* cannot be reconnected - create a new one instead.
* * `DBCS_FaultyDatabase` - database is somehow faulty. Precise reason
* can be found out with `GetError()` method.
*
* This method *is not* called when `DBConnection` is deallocated.
*
* [Signature]
* void <slot>(
* DBConnection instance,
* DBConnectionState oldState,
* DBConnectionState newState)
*
* @param instance Instance of the `DBConnection` that has changed state.
* @param oldState State it was previously in.
* @param oldState New state.
*/
/* SIGNAL */
public final function DBConnection_StateChanged_Slot OnStateChanged(
AcediaObject receiver)
{
return DBConnection_StateChanged_Slot(onStateChangedSignal
.NewSlot(receiver));
}
/**
* Signal that will be emitted whenever `DBConnection` receives response from
* connected database about success of writing operation.
*
* Responses to old requests can still be received even if database got
* disconnected.
*
* Any emissions of this signal when the database's state is `DBCS_Connecting`
* correspond to reapplying edits made prior connection was established.
*
* [Signature]
* void <slot>(JSONPointer editLocation, bool isSuccessful)
*
* @param editLocation Location of the writing operation this is
* a response to.
* @param isSuccessful Whether writing operation ended in the success.
*/
/* SIGNAL */
public final function DBConnection_EditResult_Slot OnEditResult(
AcediaObject receiver)
{
return DBConnection_EditResult_Slot(onEditResultSignal.NewSlot(receiver));
}
protected function Constructor()
{
localCache = DBCache(_.memory.Allocate(class'DBCache'));
onStateChangedSignal = DBConnection_StateChanged_Signal(
_.memory.Allocate(class'DBConnection_StateChanged_Signal'));
onEditResultSignal = DBConnection_EditResult_Signal(
_.memory.Allocate(class'DBConnection_EditResult_Signal'));
}
protected function Finalizer()
{
rootIsOfExpectedType = false;
currentState = DBCS_Idle;
_.memory.Free(dbInstance);
_.memory.Free(rootPointer);
_.memory.Free(localCache);
dbInstance = none;
rootPointer = none;
localCache = none;
_.memory.FreeMany(queuedPointers);
queuedPointers.length = 0;
requestIDs.length = 0;
// Free signals
_.memory.Free(onStateChangedSignal);
_.memory.Free(onEditResultSignal);
onStateChangedSignal = none;
onEditResultSignal = none;
}
/**
* Initializes `DBConnection` with database and location to which it must be
* connected.
*
* For the initialization to be successful `DBConnection` must not yet be
* initialized and `initDatabase` be not `none`.
*
* To check whether caller `DBConnection` is initialized
* @see `IsInitialized()`.
*
* @param initDatabase Database with data we want to connect to.
* @param initRootPointer Location of said data in the given database.
* If `none` is specified, uses root object of the database.
* @return `true` if initialization was successful and `false` otherwise.
*/
public final function bool Initialize(
Database initDatabase,
optional BaseJSONPointer initRootPointer)
{
if (IsInitialized()) return false;
if (initDatabase == none) return false;
if (!initDatabase.IsAllocated()) return false;
dbInstance = initDatabase;
dbInstance.NewRef();
if (initRootPointer != none) {
rootPointer = initRootPointer.Copy();
}
else {
rootPointer = _.json.Pointer();
}
return true;
}
/**
* Reads data from the `DBConnection` at the location defined by the given
* `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual returned data's location in the database is defined by appending
* given `pointer` to that root pointer.
*
* Data is actually always read from the local cache and, therefore, we can
* read data we've written via `DBConnection` even without actually connecting
* to the database.
*
* @param pointer Location from which to read the data.
* @return Data recorded for the given `JSONPointer`. `none` if it is missing.
*/
public final function AcediaObject ReadDataByJSON(BaseJSONPointer pointer)
{
return localCache.Read(pointer);
}
/**
* Writes given data into the `DBConnection` at the location defined by
* the given `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual location for writing data in the database is defined by appending
* given `pointer` to that root pointer.
*
* Data is actually always also written into the local cache, even when
* there is no connection to the database. Once connection is made - all valid
* changes will be duplicated into it.
* Success of failure of actually making changes into the database can be
* tracked with `OnEditResult()` signal.
*
* This operation also returns immediate indication of whether it has
* failed *locally*. This can happen when trying to perform operation
* impossible for the local cache. For example, we cannot write any data at
* location "/a/b/c" for the JSON object "{"a":45.6}".
* If operation ended in failure locally, then change to database won't
* even be attempted.
*
* @param pointer Location into which to write the data.
* @param data Data to write into the connection.
* @return `true` on success and `false` on failure. `true` is required for
* the writing database request to be made.
*/
public final function bool WriteDataByJSON(
BaseJSONPointer pointer,
AcediaObject data)
{
if (pointer == none) {
return false;
}
if (localCache.Write(pointer, data))
{
ModifyDataInDatabase(pointer, data, false);
return true;
}
return false;
}
/**
* Increments given data into the `DBConnection` at the location defined by
* the given `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual location for incrementing data in the database is defined by
* appending given `pointer` to that root pointer.
*
* Data is actually always also incremented into the local cache, even when
* there is no connection to the database. Once connection is made - all valid
* changes will be duplicated into it.
* Success of failure of actually making changes into the database can be
* tracked with `OnEditResult()` signal.
*
* This operation also returns immediate indication of whether it has
* failed *locally*. This can happen when trying to perform operation
* impossible for the local cache. For example, we cannot increment any data at
* location "/a/b/c" for the JSON object "{"a":45.6}".
* If operation ended in failure locally, then change to database won't
* even be attempted.
*
* @param pointer Location at which to increment the data.
* @param data Data with which to increment value inside the connection.
* @return `true` on success and `false` on failure. `true` is required for
* the incrementing database request to be made.
*/
public final function bool IncrementDataByJSON(
BaseJSONPointer pointer,
AcediaObject data)
{
if (pointer == none) {
return false;
}
if (localCache.Increment(pointer, data))
{
ModifyDataInDatabase(pointer, data, true);
return true;
}
return false;
}
/**
* Removes data from the `DBConnection` at the location defined by the given
* `JSONPointer`.
*
* If data was initialized with non-empty location for the root data, then
* actual location at which to remove data in the database is defined by
* appending given `pointer` to that root pointer.
*
* Data is actually always also removed from the local cache, even when
* there is no connection to the database. Once connection is made - all valid
* changes will be duplicated into it.
* Success of failure of actually making changes into the database can be
* tracked with `OnEditResult()` signal.
*
* This operation also returns immediate indication of whether it has
* failed *locally*.
* If operation ended in failure locally, then change to database won't
* even be attempted.
*
* @param pointer Location at which to remove data.
* @return `true` on success and `false` on failure. `true` is required for
* the removal database request to be made.
*/
public final function bool RemoveDataByJSON(BaseJSONPointer pointer)
{
if (pointer == none) {
return false;
}
if (localCache.Remove(pointer))
{
RemoveDataInDatabase(pointer);
return true;
}
return false;
}
private final function ModifyDataInDatabase(
BaseJSONPointer pointer,
AcediaObject data,
bool increment)
{
local MutableJSONPointer dataPointer;
if (currentState != DBCS_Connected) {
return;
}
dataPointer = rootPointer.MutableCopy();
dataPointer.Append(pointer);
// `dataPointer` is consumed by `RegisterNextRequestID()` method
if (increment)
{
dbInstance
.IncrementData(
dataPointer,
data,
RegisterNextRequestID(/*take*/ dataPointer.Copy()))
.connect = EditDataHandler;
_.memory.Free(dataPointer);
}
else
{
dbInstance
.WriteData(dataPointer, data, RegisterNextRequestID(/*take*/ dataPointer.Copy()))
.connect = EditDataHandler;
_.memory.Free(dataPointer);
}
}
private final function RemoveDataInDatabase(BaseJSONPointer pointer)
{
local MutableJSONPointer dataPointer;
if (currentState != DBCS_Connected) {
return;
}
dataPointer = rootPointer.MutableCopy();
dataPointer.Append(pointer);
// `dataPointer` is consumed by `RegisterNextRequestID()` method
dbInstance
.RemoveData(dataPointer, RegisterNextRequestID(/*take*/ dataPointer.Copy()))
.connect = EditDataHandler;
_.memory.Free(dataPointer);
}
/**
* Checks caller `DBConnection` was successfully initialized.
*
* @return `true` if caller `DBConnection` was initialized and `false`
* otherwise.
*/
public final function bool IsInitialized()
{
return (dbInstance != none);
}
/**
* Returns current state of the connection of `DBConnection` to the database
* it was initialized with.
*
* @see `OnStateChanged()` for more information about connection states.
* @return Current connection state.
*/
public final function DBConnectionState GetConnectionState()
{
return currentState;
}
/**
* Checks whether caller `DBConnection` is currently connected without errors
* to the database it was initialized with.
*
* @return `true` if caller `DBConnection` is connected to the database and
* `false` otherwise.
*/
public final function bool IsConnected()
{
return (currentState == DBCS_Connected);
}
/**
* Checks whether an error has occurred with connection to the database.
*
* `DBConnection` can get disconnected from database manually and without
* any errors, so, if you simply want to check whether connection exists,
* @see `IsConnected()` or @see `GetConnectionState()`.
* To obtain more detailed information @see `GetError()`.
*
* @return `true` if there were no error thus far and `false` otherwise.
*/
public final function bool IsOk()
{
return (dbFailureReason == FDE_None);
}
/**
* Returns error that has occurred during connection.
*
* @return Error that has occurred during connection to the database,
* `FDE_None` if there was no errors.
*/
public final function FaultyDatabaseError GetError()
{
return dbFailureReason;
}
private final function ChangeState(DBConnectionState newState)
{
local DBConnectionState oldState;
oldState = currentState;
currentState = newState;
onStateChangedSignal.Emit(self, oldState, newState);
}
/**
* Attempts connection to the database caller `DBConnection` was initialized
* with. Result isn't immediate and can be tracked with `OnStateChanged()`
* signal.
*
* Connection checks whether data by the initialization address can be read and
* has proper type (by default JSON object, but JSON array can be used
* instead).
*
* Whether connection is successfully established isn't known at the moment
* this function returns. User `OnStateChanged()` to track that.
*
* @param expectArray Set this to `true` if the expected root value is
* JSON array.
*/
public final function Connect(optional bool expectArray)
{
local Collection incrementObject;
if (!IsInitialized()) return;
if (currentState != DBCS_Idle) return;
if (expectArray) {
incrementObject = _.collections.EmptyArrayList();
}
else {
incrementObject = _.collections.EmptyHashTable();
}
dbInstance.IncrementData(
rootPointer,
incrementObject,
RegisterNextRequestID()).connect = IncrementCheckHandler;
incrementObject.FreeSelf();
// Copy of the `rootPointer` is consumed by `RegisterNextRequestID()`
// method
dbInstance.ReadData(rootPointer,, RegisterNextRequestID(rootPointer.Copy()))
.connect = InitialLoadingHandler;
ChangeState(DBCS_Connecting);
}
/**
* Disconnects `DBConnection` from its database, preventing its further
* updates.
*
* Database can only be disconnected if connection was at least initialized
* (state isn't `DBCS_Idle`) and no error has yet occurred (state isn't
* `DBCS_FaultyDatabase`).
*
* @return `true` if `DBConnection` was disconnected from the database and
* `false` otherwise (including if it already was disconnected).
*/
public final function bool Disconnect()
{
if ( currentState != DBCS_FaultyDatabase
&& currentState != DBCS_Idle
&& currentState != DBCS_Disconnected)
{
ChangeState(DBCS_Disconnected);
return true;
}
return false;
}
private final function int RegisterNextRequestID(
optional /*take*/ JSONPointer relativePointer)
{
if (relativePointer != none) {
queuedPointers[queuedPointers.length] = relativePointer;
}
else {
queuedPointers[queuedPointers.length] = _.json.Pointer();
}
requestIDs[requestIDs.length] = nextRequestID;
nextRequestID += 1;
return (nextRequestID - 1);
}
private final function JSONPointer FetchRequestPointer(int requestID)
{
local int i;
local JSONPointer result;
while (i < requestIDs.length)
{
if (requestIDs[i] < requestID)
{
// We receive all requests in order, so if `requestID` is higher
// than IDs of some other requests - it means that they are older,
// lost requests
_.memory.Free(queuedPointers[i]);
queuedPointers.Remove(i, 1);
requestIDs.Remove(i, 1);
}
if (requestIDs[i] == requestID)
{
result = queuedPointers[i];
queuedPointers.Remove(i, 1);
requestIDs.Remove(i, 1);
return result;
}
i += 1;
}
return none;
}
private final function bool FetchIfRequestStillValid(int requestID)
{
local JSONPointer result;
result = FetchRequestPointer(requestID);
if (result != none)
{
_.memory.Free(result);
return true;
}
return false;
}
private final function IncrementCheckHandler(
Database.DBQueryResult result,
Database source,
int requestID)
{
if (!FetchIfRequestStillValid(requestID)) {
return;
}
// If we could successfully increment value with appropriate JSON value,
// then its type is correct
rootIsOfExpectedType = (result == DBR_Success);
}
private final function InitialLoadingHandler(
Database.DBQueryResult result,
/*take*/ AcediaObject data,
Database source,
int requestID)
{
local int i;
local array<DBCache.PendingEdit> completedEdits;
if (!FetchIfRequestStillValid(requestID))
{
_.memory.Free(data);
return;
}
if (HandleInitializationError(result))
{
_.memory.Free(data);
return;
}
completedEdits = localCache.SetRealData(data);
for (i = 0; i < completedEdits.length; i += 1)
{
if (completedEdits[i].successful)
{
if (completedEdits[i].type == DBCET_Remove) {
RemoveDataInDatabase(completedEdits[i].location);
}
else
{
ModifyDataInDatabase(
completedEdits[i].location,
completedEdits[i].data,
completedEdits[i].type == DBCET_Increment);
}
}
else {
onEditResultSignal.Emit(completedEdits[i].location, false);
}
_.memory.Free(completedEdits[i].location);
_.memory.Free(completedEdits[i].data);
}
_.memory.Free(data);
ChangeState(DBCS_Connected);
}
// Return `true` if further initialization must be stopped.
private final function bool HandleInitializationError(
Database.DBQueryResult result)
{
// Get disconnected before even response has even arrived
if (currentState == DBCS_Disconnected) {
return true;
}
if (currentState == DBCS_Connected)
{
_.logger.Auto(errDoubleInitialization).Arg(rootPointer.ToText());
return true;
}
if (result == DBR_InvalidDatabase)
{
dbFailureReason = FDE_Unknown;
ChangeState(DBCS_FaultyDatabase);
return true;
}
if (result != DBR_Success)
{
dbFailureReason = FDE_CannotReadRootData;
ChangeState(DBCS_FaultyDatabase);
return true;
}
if (!rootIsOfExpectedType)
{
dbFailureReason = FDE_UnexpectedRootData;
ChangeState(DBCS_FaultyDatabase);
return true;
}
return false;
}
private final function EditDataHandler(
Database.DBQueryResult result,
Database source,
int requestID)
{
local JSONPointer relatedPointer;
relatedPointer = FetchRequestPointer(requestID);
if (relatedPointer == none) {
return;
}
if (result == DBR_InvalidDatabase)
{
dbFailureReason = FDE_Unknown;
ChangeState(DBCS_FaultyDatabase);
relatedPointer.FreeSelf();
return;
}
if (result == DBR_Success) {
onEditResultSignal.Emit(relatedPointer, true);
}
else {
onEditResultSignal.Emit(relatedPointer, false);
}
relatedPointer.FreeSelf();
}
defaultproperties
{
errDoubleInitialization = (l=LOG_Error,m="`DBConnection` connected to \"%1\" was double-initialized. This SHOULD NOT happen. Please report this bug.")
}

View File

@ -0,0 +1,40 @@
/**
* Signal class for `DBConnections`'s `OnEditResult()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_EditResult_Signal extends Signal
dependson(DBConnection);
public final function Emit(JSONPointer editLocation, bool isSuccessful)
{
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
DBConnection_EditResult_Slot(nextSlot)
.connect(editLocation, isSuccessful);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
relatedSlotClass = class'DBConnection_EditResult_Slot'
}

View File

@ -0,0 +1,41 @@
/**
* Slot class for `DBConnections`'s `OnEditResult()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_EditResult_Slot extends Slot
dependson(DBConnection);
delegate connect(JSONPointer editLocation, bool isSuccessful)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

View File

@ -0,0 +1,43 @@
/**
* Signal class for `DBConnections`'s `OnStatusChanged()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_StateChanged_Signal extends Signal
dependson(DBConnection);
public final function Emit(
DBConnection instance,
DBConnection.DBConnectionState oldState,
DBConnection.DBConnectionState newState)
{
local Slot nextSlot;
StartIterating();
nextSlot = GetNextSlot();
while (nextSlot != none)
{
DBConnection_StateChanged_Slot(nextSlot)
.connect(instance, oldState, newState);
nextSlot = GetNextSlot();
}
CleanEmptySlots();
}
defaultproperties
{
relatedSlotClass = class'DBConnection_StateChanged_Slot'
}

View File

@ -0,0 +1,44 @@
/**
* Slot class for `DBConnections`'s `OnStatusChanged()` signal.
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBConnection_StateChanged_Slot extends Slot
dependson(DBConnection);
delegate connect(
DBConnection instance,
DBConnection.DBConnectionState oldState,
DBConnection.DBConnectionState newState)
{
DummyCall();
}
protected function Constructor()
{
connect = none;
}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
defaultproperties
{
}

View File

@ -0,0 +1,40 @@
/**
* Variant of `DBTask` for `IncrementData()` query.
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBIncrementTask extends DBTask;
delegate connect(
Database.DBQueryResult result,
Database source,
int requestID) {}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
protected function CompleteSelf(Database source)
{
connect(GetResult(), source, GetRequestID());
}
defaultproperties
{
}

View File

@ -0,0 +1,49 @@
/**
* Variant of `DBTask` for `GetDataKeys()` query.
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBKeysTask extends DBTask;
var private ArrayList queryKeysResponse;
delegate connect(
Database.DBQueryResult result,
/*take*/ ArrayList keys,
Database source,
int requestID) {}
protected function Finalizer()
{
super.Finalizer();
queryKeysResponse = none;
connect = none;
}
public function SetDataKeys(/* take */ ArrayList keys)
{
queryKeysResponse = keys;
}
protected function CompleteSelf(Database source)
{
connect(GetResult(), queryKeysResponse, source, GetRequestID());
}
defaultproperties
{
}

View File

@ -0,0 +1,49 @@
/**
* Variant of `DBTask` for `ReadData()` query.
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBReadTask extends DBTask;
var private AcediaObject queryDataResponse;
delegate connect(
Database.DBQueryResult result,
/*take*/ AcediaObject data,
Database source,
int requestID) {}
protected function Finalizer()
{
super.Finalizer();
queryDataResponse = none;
connect = none;
}
public function SetReadData(AcediaObject data)
{
queryDataResponse = data;
}
protected function CompleteSelf(Database source)
{
connect(GetResult(), queryDataResponse, source, GetRequestID());
}
defaultproperties
{
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
/**
* Variant of `DBTask` for `RemoveData()` query.
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBRemoveTask extends DBTask;
delegate connect(
Database.DBQueryResult result,
Database source,
int requestID) {}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
protected function CompleteSelf(Database source)
{
connect(GetResult(), source, GetRequestID());
}
defaultproperties
{
}

View File

@ -0,0 +1,49 @@
/**
* Variant of `DBTask` for `GetDataSize()` query.
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBSizeTask extends DBTask;
var private int querySizeResponse;
delegate connect(
Database.DBQueryResult result,
int size,
Database source,
int requestID) {}
protected function Finalizer()
{
super.Finalizer();
querySizeResponse = 0;
connect = none;
}
public function SetDataSize(int size)
{
querySizeResponse = size;
}
protected function CompleteSelf(Database source)
{
connect(GetResult(), querySizeResponse, source, GetRequestID());
}
defaultproperties
{
}

View File

@ -0,0 +1,210 @@
/**
* This should be considered an internal class and a detail of
* implementation.
* An object that is created when user tries to query database.
* It contains a delegate `connect()` that will be called when query is
* completed and will self-destruct afterwards. Concrete delegates are
* declared in child classes of this `DBTask`, since they can have different
* signatures, depending on the query.
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBTask extends AcediaObject
dependson(Database)
abstract;
/**
* Life of instances of this class is supposed to go like so:
* 1. Get created and returned to the user that made database query so
* that he can setup a delegate that will receive the result;
* 2. Wait until database query result is ready AND all previous tasks
* have completed;
* 3. Call it's `connect()` delegate with query results;
* 4. Deallocate itself.
*
* Task is determined ready when it's `DBQueryResult` variable was set.
*
* This class IS NOT supposed to be accessed by user at all - this is simply
* an auxiliary construction that allows us to make calls to the database
* like so: `db.ReadData(...).connect = handler;`.
*
* Since every query can have it's own set of returning parameters -
* signature of `connect()` method can vary from task to task.
* For this reason we define it in child classes of `BDTask` that specialize in
* particular query.
*/
var private DBTask previousTask;
// These allows us to detect when previous task got completed (deallocated)
var private int previousTaskLifeVersion;
var private Database.DBQueryResult taskResult;
var private bool isReadyToComplete;
var private int requestID;
var private LoggerAPI.Definition errLoopInTaskChain;
protected function Finalizer()
{
if (previousTask != none) {
previousTask.FreeSelf(previousTaskLifeVersion);
}
previousTask = none;
previousTaskLifeVersion = -1;
isReadyToComplete = false;
requestID = 0;
}
/**
* Returns ID of the request set inside `SetResult()` for the caller `DBTask`.
*
* @return ID of the request set inside `SetResult()` for the caller `DBTask`.
* If `SetResult()` wasn't yet called returns `0`.
*/
protected function int GetRequestID()
{
return requestID;
}
/**
* Sets `DBQueryResult` for the caller task.
*
* Having previous task assigned is not required for the caller task to
* be completed, since it can be the first task.
*
* @param task Task that has to be completed before this one can.
*/
public final function SetPreviousTask(DBTask task)
{
previousTask = task;
if (previousTask != none) {
previousTaskLifeVersion = previousTask.GetLifeVersion();
}
}
/**
* Returns `DBQueryResult` assigned to the caller `DBTask`.
*
* This method should only be called after `SetResult()`, otherwise it's
* behavior and return result should be considered undefined.
*
* @return `DBQueryResult` assigned to the caller `DBTask`.
*/
public final function Database.DBQueryResult GetResult()
{
return taskResult;
}
/**
* Assigns `DBQueryResult` for the caller task.
*
* Every single task has to be assigned one and cannot be completed before
* it does.
*
* This value can be assigned several times and the last assigned value will
* be used.
*
* @param result Result of the query, relevant to the caller task.
* @param requestID ID of the request this task is responding to, specified
* at the time request was made.
*/
public final function SetResult(
Database.DBQueryResult result,
optional int completedRequestID)
{
taskResult = result;
isReadyToComplete = true;
requestID = completedRequestID;
}
/**
* Override this to call `connect()` delegate declared in child classes.
* Since this base class does not itself have `connect()` delegate declared -
* this method cannot be implemented here.
*/
protected function CompleteSelf(Database source) {}
/**
* Attempts to complete this task.
* Can only succeed iff caller task both has necessary data to complete it's
* query and all previous tasks have completed.
*
* @param source Database that will be passed to `DBTask`'s delegate as
* its cause.
*/
public final function TryCompleting(optional Database source)
{
local int i;
local array<DBTask> tasksQueue;
tasksQueue = BuildRequiredTasksQueue();
// Queue is built backwards: tasks that have to be completed first are
// at the end of the array
for (i = tasksQueue.length - 1; i >= 0; i -= 1)
{
if (tasksQueue[i].isReadyToComplete)
{
tasksQueue[i].CompleteSelf(source);
_.memory.Free(tasksQueue[i]);
}
else {
break;
}
}
}
// We do not know how deep `previousTask`-based chain will go, so we
// will store tasks that have to complete last earlier in the array.
private final function array<DBTask> BuildRequiredTasksQueue()
{
local int i;
local int expectedLifeVersion;
local bool loopDetected;
local DBTask nextRequiredTask;
local array<DBTask> tasksQueue;
nextRequiredTask = self;
tasksQueue[0] = nextRequiredTask;
while (nextRequiredTask.previousTask != none)
{
expectedLifeVersion = nextRequiredTask.previousTaskLifeVersion;
nextRequiredTask = nextRequiredTask.previousTask;
if (nextRequiredTask.GetLifeVersion() != expectedLifeVersion) {
break;
}
for (i = 0; i < tasksQueue.length; i += 1)
{
if (nextRequiredTask == tasksQueue[i])
{
loopDetected = true;
break;
}
}
if (!loopDetected) {
tasksQueue[tasksQueue.length] = nextRequiredTask;
}
else
{
_.logger.Auto(errLoopInTaskChain).ArgClass(nextRequiredTask.class);
break;
}
}
return tasksQueue;
}
defaultproperties
{
errLoopInTaskChain = (l=LOG_Error,m="`DBTask` of class `%1` required itself to complete. This might cause database to get damaged unexpectedly. Please report this to the developer.")
}

View File

@ -0,0 +1,40 @@
/**
* Variant of `DBTask` for `WriteData()` query.
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class DBWriteTask extends DBTask;
delegate connect(
Database.DBQueryResult result,
Database source,
int requestID) {}
protected function Finalizer()
{
super.Finalizer();
connect = none;
}
protected function CompleteSelf(Database source)
{
connect(GetResult(), source, GetRequestID());
}
defaultproperties
{
}

View File

@ -0,0 +1,390 @@
/**
* Interface database class that provides all Acedia's functionality for
* querying databases. For most of the cases, this is a class you are expected
* to work with and providing appropriate implementation is Acedia's `DBAPI`
* responsibility. Choice of the implementation is done based on user's
* config files.
* All of the methods are asynchronous - they do not return requested
* values immediately and instead require user to provide a handler function
* that will be called once operation is completed.
* Copyright 2021-2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class Database extends AcediaObject
abstract;
/**
* Describes possible data types that can be stored in Acedia's databases.
* Lists consists of all possible JSON values types (with self-explanatory
* names) plus technical `JSON_Undefined` type that is used to indicate that
* a particular value does not exist.
*/
enum DataType
{
JSON_Undefined,
JSON_Null,
JSON_Boolean,
JSON_Number,
JSON_String,
JSON_Array,
JSON_Object
};
/**
* Possible outcomes of any query: success (only `DBR_Success`) or
* some kind of failure (any other value).
* This type is common for all queries, however reasons as to why
* a particular result value was obtained can differ from one to another.
*/
enum DBQueryResult
{
// Means query has succeeded;
DBR_Success,
// Query was provided with an invalid JSON pointer
// (`none` or somehow otherwise unfit to be used with a particular query);
DBR_InvalidPointer,
// Operation could not finish because database is damaged and unusable;
DBR_InvalidDatabase,
// Means that data (provided for the query) is somehow invalid.
DBR_InvalidData
};
/**
* Schedules reading data, located at the given `pointer` in
* the caller database.
*
* @param pointerToData JSON pointer to the value in database to read.
* `none` is always treated as an invalid JSON pointer.
* @param makeMutable Setting this to `false` (default) will force method
* to load data as immutable Acedia's types and `true` will make it load
* data as mutable types. This setting does not affect `Collection`s into
* which JSON arrays and objects are converted - they are always mutable.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `ReadData()` call.
* * Guaranteed to be not `none`;
* * Use it to connect a handler for when reading task is complete:
* `ReadData(...).connect = handler`,
* where `handler` must have the following signature:
* ```
* connect(
* DBQueryResult result,
* take AcediaObject data,
* Database source,
* int requestID)`;
* ```
* * Ownership of `data` object returned in the `connect()` is considered
* to be transferred to whoever handled result of this query.
* It must be deallocated once no longer needed.
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer` and `DBR_InvalidDatabase`;
* * `data` is guaranteed to be `none` if `result != DBR_Success`;
* * `DBR_InvalidPointer` can be produced if either `pointer == none` or
* it does not point at any existing value inside the caller database.
*/
public function DBReadTask ReadData(
BaseJSONPointer pointer,
optional bool makeMutable,
optional int requestID)
{
return none;
}
/**
* Schedules writing `data` at the location inside the caller database,
* given by the `pointer`.
*
* Only `HashTable` (that represents JSON object) can be recorded as
* a database's root value (referred to by an empty JSON pointer "").
*
* @param pointer JSON pointer to the location in the database, where `data`
* should be written (as a JSON value).
* This JSON pointer can make use of "-" index for JSON arrays that allows
* appending data at their end.
* `none` is always treated as an invalid JSON pointer.
* @param data Data that needs to be written at the specified location
* inside the database. For method to succeed this object needs to have
* JSON-compatible type (see `_.json.IsCompatible()` for more details).
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `WriteData()` call.
* * Guaranteed to be not `none`;
* * Use it to connect a handler for when writing task is complete:
* `WriteData(...).connect = handler`,
* where `handler` must have the following signature:
* `connect(DBQueryResult result, Database source, int requestID)`;
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer`, `DBR_InvalidDatabase` and `DBR_InvalidData`;
* * Data is actually written inside the database iff
* `result == DBR_Success`;
* * `result == DBR_InvalidData` iff either given `data`'s type is not
* JSON-compatible or a non-`HashTable` was attempted to be
* recorded as caller database's root value;
* * `DBR_InvalidPointer` can be produced if either `pointer == none` or
* container of the value `pointer` points at does not exist.
* Example: writing data at "/sub-object/valueA" will always fail if
* "sub-object" does not exist.
*/
public function DBWriteTask WriteData(
BaseJSONPointer pointer,
AcediaObject data,
optional int requestID)
{
return none;
}
/**
* Schedules removing data at the location inside the caller database,
* given by the `pointer`.
*
* "Removing" root object results in simply erasing all of it's stored data.
*
* @param pointer JSON pointer to the location of the data to remove from
* database. `none` is always treated as an invalid JSON pointer.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `RemoveData()` call.
* * Guaranteed to be not `none`;
* * Use it to connect a handler for when writing task is complete:
* `RemoveData(...).connect = handler`,
* where `handler` must have the following signature:
* `connect(DBQueryResult result, Database source, int requestID)`.
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer` and `DBR_InvalidDatabase`;
* * Data is actually removed from the database iff
* `result == DBR_Success`.
* * `DBR_InvalidPointer` can be produced if either `pointer == none` or
* it does not point at any existing value inside the caller database.
*/
public function DBRemoveTask RemoveData(
BaseJSONPointer pointer,
optional int requestID)
{
return none;
}
/**
* Schedules checking type of data at the location inside the caller database,
* given by the `pointer`.
*
* @param pointer JSON pointer to the location of the data for which type
* needs to be checked.
* `none` is always treated as an invalid JSON pointer.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `CheckDataType()` call.
* * Guaranteed to be not `none`;
* * Use it to connect a handler for when reading task is complete:
* `CheckDataType(...).connect = handler`,
* where `handler` must have the following signature:
* ```
* connect(
* DBQueryResult result,
* Database.DataType type,
* Database source,
* int requestID)
* ```
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer` and `DBR_InvalidDatabase`;
* * This task can only fail if either caller database is broken
* (task will produce `DBR_InvalidDatabase` result) or given `pointer`
* is `none` (task will produce `DBR_InvalidPointer` result).
* Otherwise the result will be `DBR_Success`.
* * Data is actually removed from the database iff
* `result == DBR_Success`.
*/
public function DBCheckTask CheckDataType(
BaseJSONPointer pointer,
optional int requestID)
{
return none;
}
/**
* Schedules obtaining "size": amount of elements stored inside
* either JSON object or JSON array, which location inside the caller database
* is given by provided `pointer`.
*
* For every JSON value that is neither object or array size is
* defined as `-1`.
*
* @param pointer JSON pointer to the location of the JSON object or array
* for which size needs to be obtained.
* `none` is always treated as an invalid JSON pointer.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `GetDataSize()` call.
* * Guaranteed to be not `none`;
* * Use it to connect a handler for when reading task is complete:
* `GetDataSize(...).connect = handler`,
* where `handler` must have the following signature:
* ```
* connect(
* DBQueryResult result,
* int size,
* Database source,
* int requestID)
* ```
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer` and `DBR_InvalidDatabase`;
* * Returned `size` value is actually a size of referred
* JSON object/array inside the database iff `result == DBR_Success`;
* * `DBR_InvalidPointer` can be produced if either `pointer == none` or
* it does not point at a JSON object or array inside the
* caller database.
*/
public function DBSizeTask GetDataSize(
BaseJSONPointer pointer,
optional int requestID)
{
return none;
}
/**
* Schedules obtaining set of keys inside the JSON object, which location in
* the caller database is given by provided `pointer`.
*
* Only JSON objects have (and will return) keys (names of their sub-values).
*
* @param pointer JSON pointer to the location of the JSON object for which
* keys need to be obtained.
* `none` is always treated as an invalid JSON pointer.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `GetDataKeys()` call.
* * Guaranteed to be not `none`;
* * Use it to connect a handler for when reading task is complete:
* `GetDataKeys(...).connect = handler`,
* where `handler` must have the following signature:
* ```
* connect(
* DBQueryResult result,
* take ArrayList keys,
* Database source,
* int requestID)
* ```
* * Ownership of `keys` array returned in the `connect()` is considered
* to be transferred to whoever handled result of this query.
* It must be deallocated once no longer needed.
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer`, `DBR_InvalidData` and `DBR_InvalidDatabase`;
* * Returned `keys` will be non-`none` and contain keys of the referred
* JSON object inside the database iff `result == DBR_Success`;
* * `DBR_InvalidPointer` can be produced iff `pointer == none`;
* * `result == DBR_InvalidData` iff `pointer != none`, but does not
* point at a JSON object inside caller database
* (value can either not exist at all or have some other type).
*/
public function DBKeysTask GetDataKeys(
BaseJSONPointer pointer,
optional int requestID)
{
return none;
}
/**
* Schedules "incrementing" data, located at the given `pointer` in
* the caller database.
*
* "Incrementing" is an operation that is safe from the point of view of
* simultaneous access. What "incrementing" actually does depends on
* the passed JSON value (`increment` parameter):
*
* (0. ...unless `pointer` points at the JSON null or missing value (within
* existing container - then "increment" acts as a `WriteData()` method
* regardless of `increment`'s value;)
* 1. JSON null: it never modifies existing value and reports an error if
* existing value was not itself JSON null;
* 2. JSON bool: if combined with stored JSON bool value -
* performs logical "or" operation. Otherwise fails;
* 3. JSON number: if combined with stored JSON numeric value -
* adds values together. Otherwise fails.
* 4. JSON string: if combined with stored JSON string value -
* concatenates itself at the end. Otherwise fails.
* 5. JSON array: if combined with stored JSON array value -
* concatenates itself at the end. Otherwise fails.
* 6. JSON object: if combined with stored JSON object value -
* `increment` adds it's own values with new keys into the stored
* JSON object. Does not override old values.
* Fails when combined with any other type.
*
* @param pointer JSON pointer to the location in the database, where
* data should be incremented (by `increment`).
* `none` is always treated as an invalid JSON pointer.
* This JSON pointer can make use of "-" index for JSON arrays that allows
* to add `none` value at the end of that array and then "increment" it
* with `increment` parameter.
* @param increment JSON-compatible value to be used as an increment for
* the data at the specified location inside the database.
* @param requestID ID of this request. It will be reported when
* database's task is completed. Can be used to correspond database's
* responses with particular requests.
* @return Task object that corresponds to this `IncrementData()` call.
* * Guaranteed to be not `none`;
* * Use it to connect a handler for when reading task is complete:
* `IncrementData(...).connect = handler`,
* where `handler` must have the following signature:
* `connect(DBQueryResult result, Database source, int requestID)`.
* * `source` provides reference to the database, whose data was
* requested, `requestID` provides the same number as `requestID`
* parameter of this method.
* * Possible `DBQueryResult` types are `DBR_Success`,
* `DBR_InvalidPointer`, `DBR_InvalidData` and `DBR_InvalidDatabase`;
* * Data is actually incremented iff `result == DBR_Success`;
* * `DBR_InvalidPointer` can be produced if either `pointer == none` or
* container of the value `pointer` points at does not exist.
* Example: incrementing data at "/sub-object/valueA" will always fail
* if "sub-object" does not exist.
* * `result == DBR_InvalidData` iff `pointer != none`, but does not
* point at a JSON value compatible (in the sense of "increment"
* operation) with `increment` parameter.
*/
public function DBIncrementTask IncrementData(
BaseJSONPointer pointer,
AcediaObject increment,
optional int requestID)
{
return none;
}
defaultproperties
{
}

View File

@ -0,0 +1,56 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2022-2024 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 EAmmo extends EItem
abstract;
//! Abstract interface that represents ammunition of a certain type.
/// Checks whether the owner of the referred ammo item also has a weapon that
/// can be loaded with this ammo.
public function bool HasWeapon() {
return false;
}
/// Returns an array of all weapons in the owner's inventory that can be loaded
/// with the specified ammo.
///
/// Ensures that the array contains no `none` values or references to
/// non-existent entities.
public function array<EWeapon> GetCompatibleWeapons() {
local array<EWeapon> emptyArray;
return emptyArray;
}
/// Maxes out amount of ammo of the referred ammunition item.
///
/// Does nothing if current ammo is already at (or higher) than maximum value
/// (see [`GetMaxAmount()`]).
public function Fill() {
if (GetMaxStackSize() < 0) return;
if (GetStackSize() >= GetMaxStackSize()) return;
SetStackSize(GetMaxStackSize());
}
defaultproperties {
}

View File

@ -0,0 +1,104 @@
/**
* Base class for all entity interfaces. Entity interface is a reference to
* an entity inside the game world that provides a specific API for
* that entity.
* A single entity is not bound to a single `EInterface` class,
* e.g. a weapon can provide `EWeapon` and `ESellable` interfaces.
* An entity can also have several `EInterface` instances reference it at
* once (including those of the same type). Deallocating one such reference
* should not affect referred entity in any way and should be treated as simply
* getting rid of one of the references.
* Copyright 2021 - 2022 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class EInterface extends AcediaObject
abstract;
/**
* Checks if entity, referred to by the caller `EInterface` supports
* `newInterfaceClass` interface class.
*
* @param newInterfaceClass Class of the `EInterface`, for which method
* should check support by entity, referred to by the caller `EInterface`.
* @return `true` if referred entity supports `newInterfaceClass` and
* `false` otherwise.
*/
public function bool Supports(class<EInterface> newInterfaceClass)
{
return false;
}
/**
* Provides `EInterface` reference of given class `newInterfaceClass` to
* the entity, referred to by the caller `EInterface` (if supported).
*
* Can be used to access entity's other API.
*
* @param newInterfaceClass Class of the new `EInterface` for the entity,
* referred to by the caller `EInterface`.
* @return `EInterface` of the given class `newInterfaceClass` that refers to
* the caller `EInterface`'s entity.
* Can only be `none` if either caller `EInterface`'s entity does not
* support `EInterface` of the specified class or caller `EInterface`
* no longer exists (`self.IsExistent() == false`).
*/
public function EInterface As(class<EInterface> newInterfaceClass)
{
return none;
}
/**
* Checks whether caller `EInterface` refers to the entity that still exists
* in the game world.
*
* Once destroyed, same entity will not come into existence again (but can be
* replaced by its exact copy), so once `EInterface`'s `IsExistent()` call
* returns `false` it will never return `true` again.
*
* `EInterface`'s entity being gone is not the same as that `EInterface` being
* deallocated - deallocation of such `EInterface` still has to be manually
* done.
*
* @return `true` if caller `EInterface` refers to the entity that exists in
* the game world and `false` otherwise.
*/
public function bool IsExistent()
{
return false;
}
/**
* Checks whether caller interface refers to the same entity as
* the `other` argument.
*
* If two `EInterface`s referred to the same entity
* (`SameAs()` returned `true`), but that entity got destroyed,
* these `EInterface`s will not longer be considered "same"
* (`SameAs()` will return false).
*
* @param other `EInterface` to check for referring to the same entity.
* @return `true` if `other` refers to the same entity as the caller
* `EInterface` and `false` otherwise.
*/
public function bool SameAs(EInterface other)
{
return false;
}
defaultproperties
{
}

View File

@ -0,0 +1,514 @@
/**
* Author: dkanus
* Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore
* License: GPL
* Copyright 2021-2024 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 EInventory extends EInterface
abstract;
//! Abstract interface that represents inventory system.
//!
//! The inventory system is designed to handle items in a player's inventory.
//! In Killing Floor, it is a simple set of items with total weight limitations.
//! Any other kind of inventory can be implemented as long as it follows
//! the limitations of this interface.
/// Describes mapping between equipped item and slots that it is equipped in.
struct ItemSlotsPair {
/// Item for which we describe a set of slots.
var public EItem equippedItem;
/// Case-insensitive names of the slots in which item is equipped.
var public array<Text> slotNames;
};
/// Initializes `EInventory` for a given `player`.
///
/// This method should not be called manually, unless you implement your own
/// game interface.
///
/// It cannot fail for any connected player and assumes it will not be called for
/// disconnected players.
public function Initialize(EPlayer player);
/// Adds the given [`EItem`] to the inventory system and returns a reference to the added item.
///
///
/// If adding [`newItem`] is not possible, the inventory system may refuse it.
/// The [`newItem`] may be destroyed as a result of this call.
/// For example, it could be merged with another item in the inventory.
/// In such cases, the method returns the resulting [`EItem`] in the inventory.
/// Example: adding a single pistol when the inventory already contains a pistol
/// of the same type may result in merging them.
///
/// Returns `none` if and only if the inventory failed to add [`newItem`].
///
/// Parameter [`forceAddition`] is relevant when [`newItem`] cannot be added
/// under normal circumstances.
/// Setting this to `true` allows the inventory system to "bend the rules" to
/// add the item.
/// The inventory system may remove conflicting items to make room for
/// [`newItem`].
/// Removal of items is only allowed if it enables the addition of [`newItem`],
/// and the removal process is implementation-specific.
public function EItem Add(EItem newItem, optional bool forceAddition) {
return none;
}
/// Adds an [`EItem`] described by the template [`newItemTemplate`] to
/// the inventory system and returns a reference to the added item.
///
/// If adding the new item is not possible, the inventory system may refuse it.
/// The newly created item may be destroyed as a result of this call.
/// For example, it could be merged with another item in the inventory.
/// In such cases, the method returns the resulting [`EItem`] in the inventory.
/// Example: adding a single pistol when the inventory already contains a pistol
/// of the same type may result in merging them.
///
/// Returns `none` if and only if the inventory failed to add the new item.
///
/// **Parameters:**
/// Parameter [`forceAddition`] is relevant when the new item cannot be added
/// under normal circumstances.
/// Setting this to `true` allows the inventory system to "bend the rules" to
/// add the item.
/// The inventory system may remove conflicting items to make room for
/// the new item.
/// Removal of items is only allowed if it enables the addition of the new item,
/// and the removal process is implementation-specific.
public function EItem AddItemFromTemplate(BaseText newItemTemplate, optional bool forceAddition) {
return none;
}
/// Checks if the given [`itemToCheck`] can be added to the inventory.
///
/// This method determines whether the specified [`EItem`] can be added to
/// the caller inventory system, considering the [`forceAddition`] flag:
/// if it is set to `true`, the inventory will attempt to "bend the rules" to
/// add the item.
/// Defaults to `false`.
///
/// Returns `true` if [`itemToCheck`] can be added to the inventory with
/// the given `forceAddition` flag and `false` otherwise.
/// If you'd like more detailed error report, use method
/// [`EInventory::CanAddExplain()`].
public final function bool CanAdd(EItem itemToCheck, optional bool forceAddition) {
local bool success;
local AcediaError explanation;
explanation = CanAddExplain(itemToCheck, forceAddition);
success = (explanation == none);
_.memory.Free(explanation);
return success;
}
/// Checks if an item created from the given template can be added to
/// the inventory.
///
/// This method determines whether an item instantiated from template
/// [`itemTemplateToCheck`] can be added to the caller inventory system,
/// considering the [`forceAddition`] flag:
/// if it is set to `true`, the inventory will attempt to "bend the rules" to
/// add the item.
/// Defaults to `false`.
///
/// Returns `true` if an item from [`itemTemplateToCheck`] can be added to
/// the inventory with the given [`forceAddition`] flag and `false` otherwise.
/// If you'd like more detailed error report, use method
/// [`EInventory::CanAddItemFromTemplateExplain()`].
public function bool CanAddItemFromTemplate(
BaseText itemTemplateToCheck,
optional bool forceAddition
) {
local bool success;
local AcediaError explanation;
explanation = CanAddItemFromTemplateExplain(itemTemplateToCheck, forceAddition);
success = (explanation == none);
_.memory.Free(explanation);
return success;
}
/// Checks if the given [`itemToCheck`] can be added to the inventory and
/// provides an explanation if it cannot.
///
/// This method determines whether the specified [`EItem`] can be added to
/// the caller inventory system, considering the [`forceAddition`] flag:
/// if `true`, the inventory will attempt to "bend the rules" to add the item.
/// Defaults to `false`.
///
/// If the item cannot be added, it returns an [`AcediaError`] that contains
/// appropriate details; otherwise, it returns `none`.
///
/// See also [`EInventory::CanAdd()`] method that returns a simple boolean
/// indicating if the item can be added.
public function AcediaError CanAddExplain(EItem itemToCheck, optional bool forceAddition) {
return none;
}
/// Checks if an item created from the given template can be added to
/// the inventory and provides an explanation if it cannot.
///
/// This method determines whether an item instantiated from template
/// [`itemTemplateToCheck`] can be added to the caller inventory system,
/// considering the [`forceAddition`] flag:
/// if `true`, the inventory will attempt to "bend the rules" to add the item.
/// Defaults to `false`.
///
/// If the item cannot be added, it returns an [`AcediaError`] that contains
/// appropriate details; otherwise, it returns `none`.
///
/// See also [`EInventory::CanAddTemplate()`] method that returns a simple
/// boolean indicating if the item can be added.
public function AcediaError CanAddItemFromTemplateExplain(
BaseText itemTemplateToCheck,
optional bool forceAddition
) {
return none;
}
/// Removes the specified [`itemToRemove`] from the inventory.
///
/// This method attempts to remove the given [`EItem`] from the caller
/// inventory system.
/// The inventory may refuse to remove items where [`EItem::IsRemovable()`]
/// returns `false`, unless [`forceRemoval`] is set to `true`.
///
/// Argument [`keepItem`] determines whether removed item should be
/// destroyed.
/// If `false` (default), the removed item is destroyed.
/// If `true`, the inventory will attempt to preserve the item in some way
/// (e.g., dropping it in the world).
///
/// Method returns `true` if the item was removed and `false` if the item
/// was not removed (including if it was not in the inventory).
public function bool Remove(
EItem itemToRemove,
optional bool keepItem,
optional bool forceRemoval
) {
return false;
}
/// Removes item(s) of the specified type from the inventory.
///
/// This method attempts to remove item(s) that have [`itemTypeToRemove`] type
/// from the caller inventory system.
/// By default, it removes one arbitrary item, but setting [`removeAll`] to
/// `true` will remove all items of that type.
/// The inventory may refuse to remove items where [`EItem::IsRemovable()`]
/// returns `false`, unless [`forceRemoval`] is set to `true`.
///
/// Argument [`keepItem`] determines whether removed items should be
/// destroyed.
/// If `false` (default), the removed items are destroyed.
/// If `true`, the inventory will attempt to preserve items in some way
/// (e.g., dropping them in the world).
///
/// Returns `true` if any item(s) were removed and `false` if no item(s) were
/// removed (including if no items of the given type were in the inventory).
public function bool RemoveItemOfType(
BaseText itemTypeToRemove,
optional bool keepItem,
optional bool forceRemoval,
optional bool removeAll
) {
return false;
}
/// Removes all items from the inventory.
///
/// By default, this method removes only items with the "visible" tag
/// (i.e., items the player can see and interact with).
/// Setting [`includeHidden`] to `true` will also remove items without
/// the "visible" tag.
///
/// The inventory may refuse to remove items where [`EItem::IsRemovable()`]
/// returns `false`, unless `forceRemoval` is set to `true`.
///
/// Argument [`keepItem`] determines whether removed items should be
/// destroyed.
/// If `false` (default), the removed items are destroyed.
/// If `true`, the inventory will attempt to preserve items in some way
/// (e.g., dropping them in the world).
///
/// Returns `true` if any items were removed and `false` if no items were
/// removed (including if the inventory was already empty).
public function bool RemoveAll(
optional bool keepItems,
optional bool forceRemoval,
optional bool includeHidden
) {
return false;
}
/// Checks if the inventory contains the specified [`itemToCheck`].
///
/// Returns `true` if [`itemToCheck`] is in the inventory and `false` otherwise.
public function bool Contains(EItem itemToCheck) {
return false;
}
/// Checks if the inventory contains any item of the specified type.
///
/// Returns `true` if an item created from [`typeToCheck`] is in
/// the inventory and `false` otherwise.
public function bool HasItemOfType(BaseText typeToCheck) {
return false;
}
/// Retrieves all items currently in the inventory.
///
/// The returned array is guaranteed not to contain `none` references or
/// interfaces to nonexistent entities.
public function array<EItem> GetAllItems() {
local array<EItem> emptyArray;
return emptyArray;
}
/// Retrieves all items in the inventory that support a specified interface.
///
/// The returned array is guaranteed not to contain `none` references or
/// interfaces to nonexistent entities.
public function array<EItem> GetItemsSupporting(class<EItem> interfaceClass) {
local array<EItem> emptyArray;
return emptyArray;
}
/// Retrieves all items in the inventory that have the specified tag.
///
/// The returned array is guaranteed not to contain `none` references or
/// interfaces to nonexistent entities.
public function array<EItem> GetTagItems(BaseText tag) {
local array<EItem> emptyArray;
return emptyArray;
}
/// Retrieves one item from the inventory that has the specified tag.
///
/// If multiple items have the specified tag, the inventory system may select
/// one arbitrarily.
/// The selected item may vary between calls.
///
/// Returns `none` if there is no item has specified tag.
/// Otherwise the returned value is guaranteed not to be `none` or refer to
/// a nonexistent entity.
public function EItem GetTagItem(BaseText tag) {
return none;
}
/// Retrieves all items in the inventory that have a specified type.
///
/// The returned array is guaranteed not to contain `none` references or
/// interfaces to nonexistent entities.
public function array<EItem> GetItemsOfType(BaseText template) {
local array<EItem> emptyArray;
return emptyArray;
}
/// Retrieves one item from the inventory that has a specified type.
///
/// If multiple items originated from the specified template, the inventory
/// system may select one arbitrarily. The selected item may vary between calls.
///
/// Returns `none` if there is no item of the given type.
/// Otherwise the returned value is guaranteed not to be `none` or refer to
/// a nonexistent entity.
public function EItem GetItemOfType(BaseText template) {
return none;
}
/// Returns array with all available slot names.
///
/// Slot is a designated position in the inventory for a specific type of item,
/// such as "weapon," "armor," or "boots".
/// Slot names are case-insensitive.
/// Each slot can hold only one item, though some items can occupy multiple
/// slots simultaneously.
///
/// The returned array is guaranteed to not contain `none`.
public function array<Text> GetSlots() {
local array<Text> emptyArray;
return emptyArray;
}
/// Retrieves all items in the inventory that are currently equipped by
/// its owner.
///
/// Returns an array of [`EItem`]s that are equipped by the inventory's owner.
/// The returned array is guaranteed not to contain `none` references or
/// interfaces to nonexistent entities.
public function array<EItem> GetEquippedItems() {
local array<EItem> emptyArray;
return emptyArray;
}
/// Retrieves all equipped items in the inventory along with their corresponding
/// slots.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
///
/// Returns an array of [`ItemSlotsPair`] structs, each mapping an equipped item
/// to its slots.
/// This array contains no `none` references or interfaces to nonexistent
/// entities and ensures that each slot is uniquely listed in only one
/// [`ItemSlotsPair`] struct.
public function array<ItemSlotsPair> GetEquippedItemsWithSlots() {
local array<ItemSlotsPair> emptyArray;
return emptyArray;
}
/// Returns the "main" equipped item.
///
/// This method primarily summarizes the player's current state by identifying
/// the main equipped item, which varies based on specific gameplay
/// considerations.
/// For instance, in Killing Floor, this would typically be the currently
/// equipped weapon.
///
/// Returns `none` if no main item is equipped.
/// Guaranteed to not return reference to a non-existent entity.
public function EItem GetEquippedItem() {
return none;
}
/// Returns the item equipped in the specified slot.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
///
/// If you want to find a slot that is occupied by a specific item,
/// see [`EInventory::FindItemSlots()`].
///
/// Returns `none` if no item is equipped in the specified slot.
/// Guaranteed to not return reference to a non-existent entity.
public function EItem GetItemInSlot(Text slotName) {
return none;
}
/// Returns all slots, occupied by the specified item.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
///
/// If you want to find an item that occupies a specific slot,
/// see [`EInventory::GetItemInSlot()`].
///
/// The returned array is guaranteed to not contain `none`s or references to
/// a non-existent entities.
public function array<Text> FindItemSlots(EItem item) {
local array<Text> emptyArray;
return emptyArray;
}
/// Equips given item into a specified slot.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
public function AcediaError EquipItem(EItem itemToEquip, Text slotName) {
return none;
}
/// Checks if given item can be equipped into a specified slot.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
public final function bool CanEquipItem(EItem itemToEquip, Text slotName) {
local bool success;
local AcediaError explanation;
explanation = CanEquipItemExplain(itemToEquip, slotName);
success = (explanation == none);
_.memory.Free(explanation);
return success;
}
/// Checks if given item can be equipped into a specified slot and provides
/// an explanation if it cannot.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
public function AcediaError CanEquipItemExplain(EItem itemToEquip, Text slotName) {
return none;
}
/// Unequips given item.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
public function AcediaError UnequipItem(EItem itemToUnequip) {
return none;
}
/// Checks if given item can be unequipped.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
public final function bool CanUnequipItem(EItem itemToUnequip) {
local bool success;
local AcediaError explanation;
explanation = CanUnequipItemExplain(itemToUnequip);
success = (explanation == none);
_.memory.Free(explanation);
return success;
}
/// Checks if given item can be unequipped and provides an explanation if
/// it cannot.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
public function AcediaError CanUnequipItemExplain(EItem itemToUnequip) {
return none;
}
/// Unequips an item from the specified slot.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
public function AcediaError UnequipFromSlot(BaseText slotToUnequipFrom) {
return none;
}
/// Checks if an item can be unequipped from the specified slot.
///
/// Returns false if the slot is empty.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
public final function bool CanUnequipFromSlot(EItem slotToUnequipFrom) {
local bool success;
local AcediaError explanation;
explanation = CanUnequipFromSlotExplain(slotToUnequipFrom);
success = (explanation == none);
_.memory.Free(explanation);
return success;
}
/// Checks if an item can be unequipped from the specified slot and provides
/// an explanation if it cannot.
///
/// If a slot is empty, then item cannot be unequipped from it.
///
/// See [`EInventory::GetSlots()`] for the description of what a slot is.
public function AcediaError CanUnequipFromSlotExplain(EItem slotToUnequipFrom) {
return none;
}
defaultproperties {
}

Some files were not shown because too many files have changed in this diff Show More