Prepare fixtures
This commit is contained in:
parent
797e5ea192
commit
9c94356263
@ -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>"));
|
||||
}
|
||||
}
|
||||
413
kf_sources/Acedia/Classes/BaseGameMode.uc
Normal file
413
kf_sources/Acedia/Classes/BaseGameMode.uc
Normal 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.")
|
||||
}
|
||||
280
kf_sources/Acedia/Classes/GameMode.uc
Normal file
280
kf_sources/Acedia/Classes/GameMode.uc
Normal 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.")
|
||||
}
|
||||
274
kf_sources/Acedia/Classes/Packages.uc
Normal file
274
kf_sources/Acedia/Classes/Packages.uc
Normal 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.")
|
||||
}
|
||||
36
kf_sources/Acedia/Classes/StartUp.uc
Normal file
36
kf_sources/Acedia/Classes/StartUp.uc
Normal 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
|
||||
}
|
||||
364
kf_sources/Acedia/Classes/VotingHandlerAdapter.uc
Normal file
364
kf_sources/Acedia/Classes/VotingHandlerAdapter.uc
Normal 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.")
|
||||
}
|
||||
11
kf_sources/Acedia/Classes/index.html
Normal file
11
kf_sources/Acedia/Classes/index.html
Normal 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>
|
||||
7
kf_sources/Acedia/index.html
Normal file
7
kf_sources/Acedia/index.html
Normal 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>
|
||||
163
kf_sources/AcediaCore/Classes/ACommandFakers.uc
Normal file
163
kf_sources/AcediaCore/Classes/ACommandFakers.uc
Normal 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"
|
||||
}
|
||||
731
kf_sources/AcediaCore/Classes/ACommandHelp.uc
Normal file
731
kf_sources/AcediaCore/Classes/ACommandHelp.uc
Normal 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"
|
||||
}
|
||||
69
kf_sources/AcediaCore/Classes/ACommandNotify.uc
Normal file
69
kf_sources/AcediaCore/Classes/ACommandNotify.uc
Normal 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"
|
||||
}
|
||||
197
kf_sources/AcediaCore/Classes/ACommandSideEffects.uc
Normal file
197
kf_sources/AcediaCore/Classes/ACommandSideEffects.uc
Normal 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"
|
||||
}
|
||||
220
kf_sources/AcediaCore/Classes/ACommandVote.uc
Normal file
220
kf_sources/AcediaCore/Classes/ACommandVote.uc
Normal 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"
|
||||
}
|
||||
116
kf_sources/AcediaCore/Classes/ASquad.uc
Normal file
116
kf_sources/AcediaCore/Classes/ASquad.uc
Normal 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
|
||||
{
|
||||
}
|
||||
101
kf_sources/AcediaCore/Classes/AWorldComponent.uc
Normal file
101
kf_sources/AcediaCore/Classes/AWorldComponent.uc
Normal 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
|
||||
{
|
||||
}
|
||||
49
kf_sources/AcediaCore/Classes/AcediaAdapter.uc
Normal file
49
kf_sources/AcediaCore/Classes/AcediaAdapter.uc
Normal 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
|
||||
{
|
||||
}/*
|
||||
|
||||
|
||||
365
kf_sources/AcediaCore/Classes/AcediaConfig.uc
Normal file
365
kf_sources/AcediaCore/Classes/AcediaConfig.uc
Normal 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
|
||||
}
|
||||
518
kf_sources/AcediaCore/Classes/AcediaEnvironment.uc
Normal file
518
kf_sources/AcediaCore/Classes/AcediaEnvironment.uc
Normal 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`.")
|
||||
}
|
||||
166
kf_sources/AcediaCore/Classes/AcediaObjectPool.uc
Normal file
166
kf_sources/AcediaCore/Classes/AcediaObjectPool.uc
Normal 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 {
|
||||
}
|
||||
40
kf_sources/AcediaCore/Classes/AcediaReplicationInfo.uc
Normal file
40
kf_sources/AcediaCore/Classes/AcediaReplicationInfo.uc
Normal 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
|
||||
}
|
||||
294
kf_sources/AcediaCore/Classes/ActorService.uc
Normal file
294
kf_sources/AcediaCore/Classes/ActorService.uc
Normal 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
|
||||
{
|
||||
}
|
||||
385
kf_sources/AcediaCore/Classes/AliasSource.uc
Normal file
385
kf_sources/AcediaCore/Classes/AliasSource.uc
Normal 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.")
|
||||
}
|
||||
141
kf_sources/AcediaCore/Classes/Aliases.uc
Normal file
141
kf_sources/AcediaCore/Classes/Aliases.uc
Normal 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'
|
||||
}
|
||||
420
kf_sources/AcediaCore/Classes/AliasesAPI.uc
Normal file
420
kf_sources/AcediaCore/Classes/AliasesAPI.uc
Normal 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
|
||||
{
|
||||
}
|
||||
205
kf_sources/AcediaCore/Classes/AliasesStorage.uc
Normal file
205
kf_sources/AcediaCore/Classes/AliasesStorage.uc
Normal 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"
|
||||
}
|
||||
263
kf_sources/AcediaCore/Classes/Aliases_Feature.uc
Normal file
263
kf_sources/AcediaCore/Classes/Aliases_Feature.uc
Normal 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.")
|
||||
}
|
||||
1245
kf_sources/AcediaCore/Classes/ArrayList.uc
Normal file
1245
kf_sources/AcediaCore/Classes/ArrayList.uc
Normal file
File diff suppressed because it is too large
Load Diff
86
kf_sources/AcediaCore/Classes/ArrayListIterator.uc
Normal file
86
kf_sources/AcediaCore/Classes/ArrayListIterator.uc
Normal 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
|
||||
{
|
||||
}
|
||||
110
kf_sources/AcediaCore/Classes/Avarice.uc
Normal file
110
kf_sources/AcediaCore/Classes/Avarice.uc
Normal 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"
|
||||
}
|
||||
77
kf_sources/AcediaCore/Classes/AvariceAPI.uc
Normal file
77
kf_sources/AcediaCore/Classes/AvariceAPI.uc
Normal 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
|
||||
{
|
||||
}
|
||||
443
kf_sources/AcediaCore/Classes/AvariceLink.uc
Normal file
443
kf_sources/AcediaCore/Classes/AvariceLink.uc
Normal 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")
|
||||
}
|
||||
97
kf_sources/AcediaCore/Classes/AvariceMessage.uc
Normal file
97
kf_sources/AcediaCore/Classes/AvariceMessage.uc
Normal 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"
|
||||
}
|
||||
149
kf_sources/AcediaCore/Classes/AvariceStreamReader.uc
Normal file
149
kf_sources/AcediaCore/Classes/AvariceStreamReader.uc
Normal 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
|
||||
}
|
||||
387
kf_sources/AcediaCore/Classes/AvariceTcpStream.uc
Normal file
387
kf_sources/AcediaCore/Classes/AvariceTcpStream.uc
Normal 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")
|
||||
}
|
||||
168
kf_sources/AcediaCore/Classes/Avarice_Feature.uc
Normal file
168
kf_sources/AcediaCore/Classes/Avarice_Feature.uc
Normal 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\"")
|
||||
}
|
||||
38
kf_sources/AcediaCore/Classes/Avarice_OnMessage_Signal.uc
Normal file
38
kf_sources/AcediaCore/Classes/Avarice_OnMessage_Signal.uc
Normal 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'
|
||||
}
|
||||
40
kf_sources/AcediaCore/Classes/Avarice_OnMessage_Slot.uc
Normal file
40
kf_sources/AcediaCore/Classes/Avarice_OnMessage_Slot.uc
Normal 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
|
||||
{
|
||||
}
|
||||
311
kf_sources/AcediaCore/Classes/BaseAliasSource.uc
Normal file
311
kf_sources/AcediaCore/Classes/BaseAliasSource.uc
Normal 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
|
||||
{
|
||||
}
|
||||
654
kf_sources/AcediaCore/Classes/BigInt.uc
Normal file
654
kf_sources/AcediaCore/Classes/BigInt.uc
Normal 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 {
|
||||
}
|
||||
415
kf_sources/AcediaCore/Classes/ChatAPI.uc
Normal file
415
kf_sources/AcediaCore/Classes/ChatAPI.uc
Normal 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
|
||||
}
|
||||
49
kf_sources/AcediaCore/Classes/ChatAPI_OnMessageFor_Signal.uc
Normal file
49
kf_sources/AcediaCore/Classes/ChatAPI_OnMessageFor_Signal.uc
Normal 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'
|
||||
}
|
||||
41
kf_sources/AcediaCore/Classes/ChatAPI_OnMessageFor_Slot.uc
Normal file
41
kf_sources/AcediaCore/Classes/ChatAPI_OnMessageFor_Slot.uc
Normal 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
|
||||
{
|
||||
}
|
||||
49
kf_sources/AcediaCore/Classes/ChatAPI_OnMessage_Signal.uc
Normal file
49
kf_sources/AcediaCore/Classes/ChatAPI_OnMessage_Signal.uc
Normal 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'
|
||||
}
|
||||
44
kf_sources/AcediaCore/Classes/ChatAPI_OnMessage_Slot.uc
Normal file
44
kf_sources/AcediaCore/Classes/ChatAPI_OnMessage_Slot.uc
Normal 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
|
||||
{
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
41
kf_sources/AcediaCore/Classes/ChatAPI_OnVoiceMessage_Slot.uc
Normal file
41
kf_sources/AcediaCore/Classes/ChatAPI_OnVoiceMessage_Slot.uc
Normal 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 {
|
||||
}
|
||||
306
kf_sources/AcediaCore/Classes/CmdItemsTool.uc
Normal file
306
kf_sources/AcediaCore/Classes/CmdItemsTool.uc
Normal 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.")
|
||||
}
|
||||
1045
kf_sources/AcediaCore/Classes/Collection.uc
Normal file
1045
kf_sources/AcediaCore/Classes/Collection.uc
Normal file
File diff suppressed because it is too large
Load Diff
61
kf_sources/AcediaCore/Classes/CollectionIterator.uc
Normal file
61
kf_sources/AcediaCore/Classes/CollectionIterator.uc
Normal 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
|
||||
{
|
||||
}
|
||||
96
kf_sources/AcediaCore/Classes/CollectionsAPI.uc
Normal file
96
kf_sources/AcediaCore/Classes/CollectionsAPI.uc
Normal 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
|
||||
{
|
||||
}
|
||||
1539
kf_sources/AcediaCore/Classes/ColorAPI.uc
Normal file
1539
kf_sources/AcediaCore/Classes/ColorAPI.uc
Normal file
File diff suppressed because it is too large
Load Diff
26
kf_sources/AcediaCore/Classes/ColorAliasSource.uc
Normal file
26
kf_sources/AcediaCore/Classes/ColorAliasSource.uc
Normal 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'
|
||||
}
|
||||
28
kf_sources/AcediaCore/Classes/ColorAliases.uc
Normal file
28
kf_sources/AcediaCore/Classes/ColorAliases.uc
Normal 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'
|
||||
}
|
||||
805
kf_sources/AcediaCore/Classes/Command.uc
Normal file
805
kf_sources/AcediaCore/Classes/Command.uc
Normal 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
|
||||
}
|
||||
1578
kf_sources/AcediaCore/Classes/CommandAPI.uc
Normal file
1578
kf_sources/AcediaCore/Classes/CommandAPI.uc
Normal file
File diff suppressed because it is too large
Load Diff
26
kf_sources/AcediaCore/Classes/CommandAliasSource.uc
Normal file
26
kf_sources/AcediaCore/Classes/CommandAliasSource.uc
Normal 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'
|
||||
}
|
||||
28
kf_sources/AcediaCore/Classes/CommandAliases.uc
Normal file
28
kf_sources/AcediaCore/Classes/CommandAliases.uc
Normal 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'
|
||||
}
|
||||
939
kf_sources/AcediaCore/Classes/CommandDataBuilder.uc
Normal file
939
kf_sources/AcediaCore/Classes/CommandDataBuilder.uc
Normal 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.")
|
||||
}
|
||||
249
kf_sources/AcediaCore/Classes/CommandList.uc
Normal file
249
kf_sources/AcediaCore/Classes/CommandList.uc
Normal 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'
|
||||
}
|
||||
983
kf_sources/AcediaCore/Classes/CommandParser.uc
Normal file
983
kf_sources/AcediaCore/Classes/CommandParser.uc
Normal 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.")
|
||||
}
|
||||
66
kf_sources/AcediaCore/Classes/CommandPermissions.uc
Normal file
66
kf_sources/AcediaCore/Classes/CommandPermissions.uc
Normal 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
|
||||
}
|
||||
81
kf_sources/AcediaCore/Classes/CommandRegistrationJob.uc
Normal file
81
kf_sources/AcediaCore/Classes/CommandRegistrationJob.uc
Normal 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 {
|
||||
}
|
||||
230
kf_sources/AcediaCore/Classes/Commands.uc
Normal file
230
kf_sources/AcediaCore/Classes/Commands.uc
Normal 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"
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
@ -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'
|
||||
}
|
||||
@ -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 {
|
||||
}
|
||||
142
kf_sources/AcediaCore/Classes/CommandsTool.uc
Normal file
142
kf_sources/AcediaCore/Classes/CommandsTool.uc
Normal 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';
|
||||
}
|
||||
708
kf_sources/AcediaCore/Classes/Commands_Feature.uc
Normal file
708
kf_sources/AcediaCore/Classes/Commands_Feature.uc
Normal 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\".")
|
||||
}
|
||||
77
kf_sources/AcediaCore/Classes/Component.uc
Normal file
77
kf_sources/AcediaCore/Classes/Component.uc
Normal 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 {
|
||||
}
|
||||
260
kf_sources/AcediaCore/Classes/ConsoleAPI.uc
Normal file
260
kf_sources/AcediaCore/Classes/ConsoleAPI.uc
Normal 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
|
||||
}
|
||||
417
kf_sources/AcediaCore/Classes/ConsoleBuffer.uc
Normal file
417
kf_sources/AcediaCore/Classes/ConsoleBuffer.uc
Normal 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
|
||||
}
|
||||
668
kf_sources/AcediaCore/Classes/ConsoleWriter.uc
Normal file
668
kf_sources/AcediaCore/Classes/ConsoleWriter.uc
Normal 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 = " "
|
||||
}
|
||||
126
kf_sources/AcediaCore/Classes/CoreGlobal.uc
Normal file
126
kf_sources/AcediaCore/Classes/CoreGlobal.uc
Normal 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.")
|
||||
}
|
||||
409
kf_sources/AcediaCore/Classes/DBAPI.uc
Normal file
409
kf_sources/AcediaCore/Classes/DBAPI.uc
Normal 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.")
|
||||
}
|
||||
1092
kf_sources/AcediaCore/Classes/DBCache.uc
Normal file
1092
kf_sources/AcediaCore/Classes/DBCache.uc
Normal file
File diff suppressed because it is too large
Load Diff
49
kf_sources/AcediaCore/Classes/DBCheckTask.uc
Normal file
49
kf_sources/AcediaCore/Classes/DBCheckTask.uc
Normal 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
|
||||
{
|
||||
}
|
||||
794
kf_sources/AcediaCore/Classes/DBConnection.uc
Normal file
794
kf_sources/AcediaCore/Classes/DBConnection.uc
Normal 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.")
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Signal class for `DBConnections`'s `OnEditResult()` signal.
|
||||
* Copyright 2023 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class DBConnection_EditResult_Signal extends Signal
|
||||
dependson(DBConnection);
|
||||
|
||||
public final function Emit(JSONPointer editLocation, bool isSuccessful)
|
||||
{
|
||||
local Slot nextSlot;
|
||||
StartIterating();
|
||||
nextSlot = GetNextSlot();
|
||||
while (nextSlot != none)
|
||||
{
|
||||
DBConnection_EditResult_Slot(nextSlot)
|
||||
.connect(editLocation, isSuccessful);
|
||||
nextSlot = GetNextSlot();
|
||||
}
|
||||
CleanEmptySlots();
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedSlotClass = class'DBConnection_EditResult_Slot'
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Slot class for `DBConnections`'s `OnEditResult()` signal.
|
||||
* Copyright 2023 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class DBConnection_EditResult_Slot extends Slot
|
||||
dependson(DBConnection);
|
||||
|
||||
delegate connect(JSONPointer editLocation, bool isSuccessful)
|
||||
{
|
||||
DummyCall();
|
||||
}
|
||||
|
||||
protected function Constructor()
|
||||
{
|
||||
connect = none;
|
||||
}
|
||||
|
||||
protected function Finalizer()
|
||||
{
|
||||
super.Finalizer();
|
||||
connect = none;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Signal class for `DBConnections`'s `OnStatusChanged()` signal.
|
||||
* Copyright 2023 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class DBConnection_StateChanged_Signal extends Signal
|
||||
dependson(DBConnection);
|
||||
|
||||
public final function Emit(
|
||||
DBConnection instance,
|
||||
DBConnection.DBConnectionState oldState,
|
||||
DBConnection.DBConnectionState newState)
|
||||
{
|
||||
local Slot nextSlot;
|
||||
StartIterating();
|
||||
nextSlot = GetNextSlot();
|
||||
while (nextSlot != none)
|
||||
{
|
||||
DBConnection_StateChanged_Slot(nextSlot)
|
||||
.connect(instance, oldState, newState);
|
||||
nextSlot = GetNextSlot();
|
||||
}
|
||||
CleanEmptySlots();
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
relatedSlotClass = class'DBConnection_StateChanged_Slot'
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Slot class for `DBConnections`'s `OnStatusChanged()` signal.
|
||||
* Copyright 2023 Anton Tarasenko
|
||||
*------------------------------------------------------------------------------
|
||||
* This file is part of Acedia.
|
||||
*
|
||||
* Acedia is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Acedia is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
class DBConnection_StateChanged_Slot extends Slot
|
||||
dependson(DBConnection);
|
||||
|
||||
delegate connect(
|
||||
DBConnection instance,
|
||||
DBConnection.DBConnectionState oldState,
|
||||
DBConnection.DBConnectionState newState)
|
||||
{
|
||||
DummyCall();
|
||||
}
|
||||
|
||||
protected function Constructor()
|
||||
{
|
||||
connect = none;
|
||||
}
|
||||
|
||||
protected function Finalizer()
|
||||
{
|
||||
super.Finalizer();
|
||||
connect = none;
|
||||
}
|
||||
|
||||
defaultproperties
|
||||
{
|
||||
}
|
||||
40
kf_sources/AcediaCore/Classes/DBIncrementTask.uc
Normal file
40
kf_sources/AcediaCore/Classes/DBIncrementTask.uc
Normal 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
|
||||
{
|
||||
}
|
||||
49
kf_sources/AcediaCore/Classes/DBKeysTask.uc
Normal file
49
kf_sources/AcediaCore/Classes/DBKeysTask.uc
Normal 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
|
||||
{
|
||||
}
|
||||
49
kf_sources/AcediaCore/Classes/DBReadTask.uc
Normal file
49
kf_sources/AcediaCore/Classes/DBReadTask.uc
Normal 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
|
||||
{
|
||||
}
|
||||
1199
kf_sources/AcediaCore/Classes/DBRecord.uc
Normal file
1199
kf_sources/AcediaCore/Classes/DBRecord.uc
Normal file
File diff suppressed because it is too large
Load Diff
40
kf_sources/AcediaCore/Classes/DBRemoveTask.uc
Normal file
40
kf_sources/AcediaCore/Classes/DBRemoveTask.uc
Normal 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
|
||||
{
|
||||
}
|
||||
49
kf_sources/AcediaCore/Classes/DBSizeTask.uc
Normal file
49
kf_sources/AcediaCore/Classes/DBSizeTask.uc
Normal 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
|
||||
{
|
||||
}
|
||||
210
kf_sources/AcediaCore/Classes/DBTask.uc
Normal file
210
kf_sources/AcediaCore/Classes/DBTask.uc
Normal 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.")
|
||||
}
|
||||
40
kf_sources/AcediaCore/Classes/DBWriteTask.uc
Normal file
40
kf_sources/AcediaCore/Classes/DBWriteTask.uc
Normal 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
|
||||
{
|
||||
}
|
||||
390
kf_sources/AcediaCore/Classes/Database.uc
Normal file
390
kf_sources/AcediaCore/Classes/Database.uc
Normal 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
|
||||
{
|
||||
}
|
||||
56
kf_sources/AcediaCore/Classes/EAmmo.uc
Normal file
56
kf_sources/AcediaCore/Classes/EAmmo.uc
Normal 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 {
|
||||
}
|
||||
104
kf_sources/AcediaCore/Classes/EInterface.uc
Normal file
104
kf_sources/AcediaCore/Classes/EInterface.uc
Normal 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
|
||||
{
|
||||
}
|
||||
514
kf_sources/AcediaCore/Classes/EInventory.uc
Normal file
514
kf_sources/AcediaCore/Classes/EInventory.uc
Normal 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
Loading…
Reference in New Issue
Block a user