diff --git a/config/Acedia.ini b/config/Acedia.ini
index fecbb06..8e1b9d2 100644
--- a/config/Acedia.ini
+++ b/config/Acedia.ini
@@ -1,3 +1,4 @@
[Acedia.Packages]
+useGameModes=false
corePackage="AcediaCore_0_2"
package="AcediaFixes"
\ No newline at end of file
diff --git a/sources/GameModes/BaseGameMode.uc b/sources/GameModes/BaseGameMode.uc
new file mode 100644
index 0000000..b7141c0
--- /dev/null
+++ b/sources/GameModes/BaseGameMode.uc
@@ -0,0 +1,386 @@
+/**
+ * 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 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class BaseGameMode extends AcediaConfig
+ dependson(CoreService)
+ abstract;
+
+// Name of the game mode players will see in voting (formatted string)
+var protected config string title;
+// Preferable difficulty level (plain string)
+var protected config string difficulty;
+// `Mutator`s to add with this game mode
+var protected config array includeMutator;
+// `Feature`s to include (with "default" config)
+var protected config array includeFeature;
+// `Feature`s to exclude from game mode, regardless of other settings
+// (this one has highest priority)
+var protected config array 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 includeFeatureAs;
+
+var private LoggerAPI.Definition warnBadMutatorName, warnBadFeatureName;
+
+protected function AssociativeArray ToData()
+{
+ local int i;
+ local AssociativeArray result;
+ local AssociativeArray nextPair;
+ local DynamicArray nextArray;
+ result = _.collections.EmptyAssociativeArray();
+ result.SetItem(P("title"), _.text.FromFormattedString(title));
+ result.SetItem(P("difficulty"), _.text.FromString(difficulty));
+ nextArray = _.collections.EmptyDynamicArray();
+ for (i = 0; i < includeFeature.length; i += 1) {
+ nextArray.AddItem(_.text.FromString(includeFeature[i]));
+ }
+ result.SetItem(P("includeFeature"), nextArray);
+ nextArray = _.collections.EmptyDynamicArray();
+ for (i = 0; i < excludeFeature.length; i += 1) {
+ nextArray.AddItem(_.text.FromString(excludeFeature[i]));
+ }
+ result.SetItem(P("excludeFeature"), nextArray);
+ nextArray = _.collections.EmptyDynamicArray();
+ for (i = 0; i < includeMutator.length; i += 1) {
+ nextArray.AddItem(_.text.FromString(includeFeature[i]));
+ }
+ result.SetItem(P("includeMutator"), nextArray);
+ nextArray = _.collections.EmptyDynamicArray();
+ for (i = 0; i < includeFeatureAs.length; i += 1)
+ {
+ nextPair = _.collections.EmptyAssociativeArray();
+ nextPair.SetItem(P("feature"),
+ _.text.FromString(includeFeatureAs[i].feature));
+ nextPair.SetItem(P("config"),
+ _.text.FromString(includeFeatureAs[i].config));
+ nextArray.AddItem(nextPair);
+ }
+ result.SetItem(P("includeFeatureAs"), nextArray);
+ return result;
+}
+
+protected function FromData(AssociativeArray source)
+{
+ local int i;
+ local Text nextText;
+ local DynamicArray includeFeatureAsSource;
+ if (source == none) {
+ return;
+ }
+ nextText = source.GetText(P("title"));
+ if (nextText != none) {
+ title = nextText.ToFormattedString();
+ }
+ nextText = source.GetText(P("difficulty"));
+ if (nextText != none) {
+ difficulty = nextText.ToPlainString();
+ }
+ includeFeature =
+ DynamicIntoStringArray(source.GetDynamicArray(P("includeFeature")));
+ excludeFeature =
+ DynamicIntoStringArray(source.GetDynamicArray(P("excludeFeature")));
+ includeMutator =
+ DynamicIntoStringArray(source.GetDynamicArray(P("includeMutator")));
+ includeFeatureAsSource = source.GetDynamicArray(P("includeFeatureAs"));
+ if (includeFeatureAsSource == none) {
+ return;
+ }
+ includeFeatureAs.length = 0;
+ for (i = 0; i < includeFeatureAsSource.GetLength(); i += 1)
+ {
+ includeFeatureAs[i] = AssociativeArrayIntoPair(
+ includeFeatureAsSource.GetAssociativeArray(i));
+ }
+}
+
+private final function FeatureConfigPair AssociativeArrayIntoPair(
+ AssociativeArray source)
+{
+ local Text nextText;
+ local FeatureConfigPair result;
+ if (source == none) {
+ return result;
+ }
+ nextText = source.GetText(P("feature"));
+ if (nextText != none) {
+ result.feature = nextText.ToPlainString();
+ }
+ nextText = source.GetText(P("config"));
+ if (nextText != none) {
+ result.config = nextText.ToPlainString();
+ }
+ return result;
+}
+
+private final function array DynamicIntoStringArray(DynamicArray source)
+{
+ local int i;
+ local Text nextText;
+ local array result;
+ if (source == none) {
+ return result;
+ }
+ for (i = 0; i < source.GetLength(); i += 1)
+ {
+ nextText = source.GetText(i);
+ if (nextText != none) {
+ includeFeature[i] = nextText.ToPlainString();
+ }
+ }
+}
+
+protected function array StringToTextArray(array input)
+{
+ local int i;
+ local array 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 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 featuresToEnable)
+{
+ local int i;
+ local array 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 subset,
+ array 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(warnBadMutatorName)
+ .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 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 GetIncludedMutators()
+{
+ local int i;
+ local array 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.")
+}
\ No newline at end of file
diff --git a/sources/GameModes/GameMode.uc b/sources/GameModes/GameMode.uc
new file mode 100644
index 0000000..4c1960f
--- /dev/null
+++ b/sources/GameModes/GameMode.uc
@@ -0,0 +1,201 @@
+/**
+ * The only implementation for `BaseGameMode` suitable for standard
+ * killing floor game types.
+ * 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 .
+ */
+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 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;
+
+var private LoggerAPI.Definition warnBadOption;
+
+protected function DefaultIt()
+{
+ title = "Acedia game mode";
+ difficulty = "Hell On Earth";
+ gameTypeClass = "KFMod.KFGameType";
+ acronym = "";
+ mapPrefix = "KF";
+ includeFeature.length = 0;
+ excludeFeature.length = 0;
+ includeMutator.length = 0;
+ option.length = 0;
+}
+
+protected function AssociativeArray ToData()
+{
+ local int i;
+ local AssociativeArray result;
+ local AssociativeArray nextPair;
+ local DynamicArray nextArray;
+ result = super.ToData();
+ if (result == none) {
+ return none;
+ }
+ result.SetItem(P("gameTypeClass"), _.text.FromString(gameTypeClass));
+ result.SetItem(P("acronym"), _.text.FromString(acronym));
+ result.SetItem(P("mapPrefix"), _.text.FromString(mapPrefix));
+ nextArray = _.collections.EmptyDynamicArray();
+ for (i = 0; i < option.length; i += 1)
+ {
+ nextPair = _.collections.EmptyAssociativeArray();
+ nextPair.SetItem(P("key"), _.text.FromString(option[i].key));
+ nextPair.SetItem(P("value"), _.text.FromString(option[i].value));
+ nextArray.AddItem(nextPair);
+ }
+ result.SetItem(P("option"), nextArray);
+ return result;
+}
+
+protected function FromData(AssociativeArray source)
+{
+ local int i;
+ local Text nextText;
+ local GameOption nextPair;
+ local DynamicArray nextArray;
+ super.FromData(source);
+ if (source == none) {
+ return;
+ }
+ nextText = source.GetText(P("gameTypeClass"));
+ if (nextText != none) {
+ gameTypeClass = nextText.ToPlainString();
+ }
+ nextText = source.GetText(P("acronym"));
+ if (nextText != none) {
+ acronym = nextText.ToPlainString();
+ }
+ nextText = source.GetText(P("mapPrefix"));
+ if (nextText != none) {
+ mapPrefix = nextText.ToPlainString();
+ }
+ nextArray = source.GetDynamicArray(P("option"));
+ if (nextArray == none) {
+ return;
+ }
+ option.length = 0;
+ for (i = 0; i < nextArray.GetLength(); i += 1)
+ {
+ nextPair.key = "";
+ nextPair.value = "";
+ nextText = source.GetText(P("key"));
+ if (nextText != none) {
+ nextPair.key = nextText.ToPlainString();
+ }
+ nextText = source.GetText(P("value"));
+ if (nextText != none) {
+ nextPair.value = nextText.ToPlainString();
+ }
+ option[option.length] = nextPair;
+ }
+}
+
+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.FromString(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 `AssociativeArray`.
+ */
+public function AssociativeArray GetOptions()
+{
+ local int i;
+ local AssociativeArray result;
+ result = _.collections.EmptyAssociativeArray();
+ for (i = 0; i < option.length; i += 1)
+ {
+ if (!ValidateServerURLName(option[i].key)) continue;
+ if (!ValidateServerURLName(option[i].value)) continue;
+ result.SetItem( _.text.FromString(option[i].key),
+ _.text.FromString(option[i].value));
+ }
+ return result;
+}
+
+defaultproperties
+{
+ configName = "AcediaGameModes"
+ 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.")
+}
\ No newline at end of file
diff --git a/sources/Packages.uc b/sources/Packages.uc
index fb0a796..05a2cb6 100644
--- a/sources/Packages.uc
+++ b/sources/Packages.uc
@@ -2,7 +2,7 @@
* Main and only Acedia mutator used for loading Acedia packages
* and providing access to mutator events' calls.
* Name is chosen to make config files more readable.
- * Copyright 2020 Anton Tarasenko
+ * Copyright 2020-2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@@ -20,6 +20,7 @@
* along with Acedia. If not, see .
*/
class Packages extends Mutator
+ dependson(CoreService)
config(Acedia);
// Default value of this variable will be used to store
@@ -32,23 +33,29 @@ var private Packages selfReference;
// Acedia's reference to a `Global` object.
var private Global _;
-// Package's manifest is supposed to always have a name of
-// ".Manifest", this variable stores the ".Manifest" part
-var private const string manifestSuffix;
-
// Array of predefined services that must be started along with Acedia mutator.
var private config array 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_OnCheckReplacement_Signal onCheckReplacementSignal;
-// AcediaCore package that this launcher is build for
-var private config const string corePackage;
+var private LoggerAPI.Definition infoFeatureEnabled;
static public final function Packages GetInstance()
{
return default.selfReference;
}
+// "Constructor"
event PreBeginPlay()
{
+ local GameMode currentGameMode;
+ local array availableFeatures;
+ CheckForGarbage();
// Enforce one copy rule and remember a reference to that copy
if (default.selfReference != none)
{
@@ -56,179 +63,123 @@ event PreBeginPlay()
return;
}
default.selfReference = self;
- BootUp();
- if (class'TestingService'.default.runTestsOnStartUp) {
- RunStartUpTests();
- }
-}
-
-private final function BootUp()
-{
- local int i;
- local class<_manifest> nextManifest;
- // Load core
- Spawn(class'CoreService');
+ // Launch and setup core Acedia
+ class'CoreService'.static.LaunchAcedia(self, package);
_ = class'Global'.static.GetInstance();
- nextManifest = LoadManifestClass(corePackage);
- if (nextManifest == none)
- {
- /*_.logger.Fatal("Cannot load required AcediaCore package \""
- $ corePackage $ "\". Acedia will shut down.");*/
- Destroy();
- return;
- }
- LoadManifest(nextManifest);
- // Load packages
- for (i = 0; i < package.length; i += 1)
+ SetupMutatorSignals();
+ // Determine required features and launch them
+ availableFeatures = CoreService(class'CoreService'.static.GetInstance())
+ .GetAutoConfigurationInfo();
+ if (useGameModes)
{
- nextManifest = LoadManifestClass(package[i]);
- if (nextManifest == none)
- {
- /*_.logger.Failure("Cannot load `Manifest` for package \""
- $ package[i] $ "\". Check if it's missing or"
- @ "if it's name is spelled incorrectly.");*/
- continue;
+ votingAdapter = VotingHandlerAdapter(
+ _.memory.Allocate(class'VotingHandlerAdapter'));
+ votingAdapter.InjectIntoVotingHandler();
+ currentGameMode = votingAdapter.SetupGameModeAfterTravel();
+ if (currentGameMode != none) {
+ currentGameMode.UpdateFeatureArray(availableFeatures);
}
- LoadManifest(nextManifest);
}
- // Inject broadcast handler
- InjectBroadcastHandler();
+ EnableFeatures(availableFeatures);
}
-private final function RunStartUpTests()
+// "Finalizer"
+function ServerTraveling(string URL, bool bItems)
{
- 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())
+ if (votingAdapter != none)
{
- // This listener will output test results into server's console
- class'TestingListener_AcediaLauncher'.static.SetActive(true);
+ votingAdapter.PrepareForServerTravel();
+ votingAdapter.RestoreVotingHandlerConfigBackup();
+ _.memory.Free(votingAdapter);
+ votingAdapter = none;
}
- else
- {
- //_.logger.Failure("Could not launch Acedia's start up testing process.");
+ default.selfReference = none;
+ CoreService(class'CoreService'.static.GetInstance()).ShutdownAcedia();
+ if (nextMutator != none) {
+ nextMutator.ServerTraveling(URL, bItems);
}
+ Destroy();
}
-private final function class<_manifest> LoadManifestClass(string packageName)
+// 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()
{
- return class<_manifest>(DynamicLoadObject( packageName $ manifestSuffix,
- class'Class', true));
-}
-
-private final function LoadManifest(class<_manifest> manifestClass)
-{
- local int i;
- for (i = 0; i < manifestClass.default.aliasSources.length; i += 1)
- {
- if (manifestClass.default.aliasSources[i] == none) continue;
- //Spawn(manifestClass.default.aliasSources[i]);
- _.memory.Allocate(manifestClass.default.aliasSources[i]);
+ local int leftoverObjectAmount, leftoverActorAmount, leftoverDBRAmount;
+ local AcediaObject nextObject;
+ local AcediaActor nextActor;
+ local DBRecord nextRecord;
+ foreach AllObjects(class'AcediaObject', nextObject) {
+ leftoverObjectAmount += 1;
}
- LaunchServicesAndFeatures(manifestClass);
- if (class'Commands_Feature'.static.IsEnabled()) {
- RegisterCommands(manifestClass);
+ foreach AllActors(class'AcediaActor', nextActor) {
+ leftoverActorAmount += 1;
}
- for (i = 0; i < manifestClass.default.testCases.length; i += 1)
- {
- class'TestingService'.static
- .RegisterTestCase(manifestClass.default.testCases[i]);
+ foreach AllObjects(class'DBRecord', nextRecord) {
+ leftoverDBRAmount += 1;
}
-}
-
-private final function RegisterCommands(class<_manifest> manifestClass)
-{
- local int i;
- local Commands_Feature commandsFeature;
- commandsFeature =
- Commands_Feature(class'Commands_Feature'.static.GetInstance());
- for (i = 0; i < manifestClass.default.commands.length; i += 1)
+ if ( leftoverObjectAmount == 0 && leftoverActorAmount == 0
+ && leftoverDBRAmount == 0)
{
- if (manifestClass.default.commands[i] == none) continue;
- commandsFeature.RegisterCommand(manifestClass.default.commands[i]);
+ Log("Acedia garbage check: nothing was found.");
}
-}
-
-private final function LaunchServicesAndFeatures(class<_manifest> manifestClass)
-{
- local int i;
- local Text autoConfigName;
- // Services
- for (i = 0; i < manifestClass.default.services.length; i += 1)
+ else
{
- if (manifestClass.default.services[i] == none) continue;
- manifestClass.default.services[i].static.Require();
- }
- // Features
- for (i = 0; i < manifestClass.default.features.length; i += 1)
- {
- if (manifestClass.default.features[i] == none) continue;
- manifestClass.default.features[i].static.LoadConfigs();
- autoConfigName =
- manifestClass.default.features[i].static.GetAutoEnabledConfig();
- if (autoConfigName != none) {
- manifestClass.default.features[i].static.EnableMe(autoConfigName);
- }
- _.memory.Free(autoConfigName);
+ 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);
}
}
-private final function InjectBroadcastHandler()
+private function EnableFeatures(array features)
{
- local BroadcastEventsObserver ourBroadcastHandler;
- local BroadcastEventsObserver.InjectionLevel injectionLevel;
- injectionLevel = class'BroadcastEventsObserver'.default.usedInjectionLevel;
- if (level == none || level.game == none) return;
- if (injectionLevel == BHIJ_None) return;
-
- ourBroadcastHandler = Spawn(class'BroadcastEventsObserver');
- if (injectionLevel == BHIJ_Registered)
+ local int i;
+ for (i = 0; i < features.length; i += 1)
{
- level.game.broadcastHandler
- .RegisterBroadcastHandler(ourBroadcastHandler);
- return;
+ 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`
}
- // Here `injectionLevel == BHIJ_Root` holds.
- // Swap out level's first handler with ours
- // (needs to be done for both actor reference and it's class)
- ourBroadcastHandler.nextBroadcastHandler = level.game.broadcastHandler;
- ourBroadcastHandler.nextBroadcastHandlerClass = level.game.broadcastClass;
- level.game.broadcastHandler = ourBroadcastHandler;
- level.game.broadcastClass = class'BroadcastEventsObserver';
}
-// Acedia is only able to run in a server mode right now,
-// so this function is just a stub.
-public final function bool IsServerOnly()
+// Fetches and sets up signals that `Mutator` needs to provide
+private function SetupMutatorSignals()
{
- return true;
+ local UnrealService service;
+ service = UnrealService(class'UnrealService'.static.Require());
+ onMutateSignal = Mutator_OnMutate_Signal(
+ service.GetSignal(class'Mutator_OnMutate_Signal'));
+ onCheckReplacementSignal = Mutator_OnCheckReplacement_Signal(
+ service.GetSignal(class'Mutator_OnCheckReplacement_Signal'));
}
-// Provide a way to handle CheckReplacement event
+/**
+ * Below `Mutator` events are redirected into appropriate signals.
+ */
function bool CheckReplacement(Actor other, out byte isSuperRelevant)
{
- return class'MutatorEvents'.static.
- CallCheckReplacement(other, isSuperRelevant);
+ if (onCheckReplacementSignal != none) {
+ return onCheckReplacementSignal.Emit(other, isSuperRelevant);
+ }
+ return true;
}
function Mutate(string command, PlayerController sendingController)
{
- if (class'MutatorEvents'.static.CallMutate(command, sendingController)) {
- super.Mutate(command, sendingController);
+ if (onMutateSignal != none) {
+ onMutateSignal.Emit(command, sendingController);
}
+ super.Mutate(command, sendingController);
}
defaultproperties
{
- corePackage = "AcediaCore_0_2"
- manifestSuffix = ".Manifest"
+ useGameModes = false
// This is a server-only mutator
remoteRole = ROLE_None
bAlwaysRelevant = true
@@ -236,4 +187,5 @@ defaultproperties
GroupName = "Package loader"
FriendlyName = "Acedia loader"
Description = "Launcher for Acedia packages"
+ infoFeatureEnabled = (l=LOG_Info,m="Feature `%1` enabled with config \"%2\".")
}
\ No newline at end of file
diff --git a/sources/StartUp.uc b/sources/StartUp.uc
index 7d4ac49..ac40796 100644
--- a/sources/StartUp.uc
+++ b/sources/StartUp.uc
@@ -23,8 +23,7 @@ class StartUp extends Actor;
function PreBeginPlay()
{
super.PreBeginPlay();
- if (level != none && level.game != none)
- {
+ if (level != none && level.game != none) {
level.game.AddMutator(string(class'Packages'));
}
Destroy();
diff --git a/sources/TestingListener_AcediaLauncher.uc b/sources/TestingListener_AcediaLauncher.uc
deleted file mode 100644
index ac2283c..0000000
--- a/sources/TestingListener_AcediaLauncher.uc
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * Overloaded testing events listener to catch when tests that we run during
- * server loading finish.
- * Copyright 2020 Anton Tarasenko
- *------------------------------------------------------------------------------
- * This file is part of Acedia.
- *
- * Acedia is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, version 3 of the License, or
- * (at your option) any later version.
- *
- * Acedia is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with Acedia. If not, see .
- */
-class TestingListener_AcediaLauncher extends TestingListenerBase
- abstract;
-
-static function TestingEnded(
- array< class > testQueue,
- array results)
-{
- local int i;
- local MutableText nextLine;
- local array textSummary;
- nextLine = __().text.Empty();
- textSummary = class'TestCaseSummary'.static.GenerateStringSummary(results);
- for (i = 0; i < textSummary.length; i += 1)
- {
- nextLine.Clear();
- nextLine.AppendFormattedString(textSummary[i]);
- Log(nextLine.ToPlainString());
- }
- // No longer need to listen to testing events
- SetActive(false);
-}
-
-defaultproperties
-{
- relatedEvents = class'TestingEvents'
-}
\ No newline at end of file
diff --git a/sources/VotingHandlerAdapter.uc b/sources/VotingHandlerAdapter.uc
new file mode 100644
index 0000000..18d76ba
--- /dev/null
+++ b/sources/VotingHandlerAdapter.uc
@@ -0,0 +1,345 @@
+/**
+ * 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 Anton Tarasenko
+ *------------------------------------------------------------------------------
+ * This file is part of Acedia.
+ *
+ * Acedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Acedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Acedia. If not, see .
+ */
+class VotingHandlerAdapter extends AcediaObject
+ dependson(VotingHandler);
+
+/**
+ * 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.
+ */
+
+// Aliases are an unnecessary overkill for difficulty names, so just define
+// them in special `string` arrays.
+// We accept detect not just these exact words, but any of their prefixes.
+var private const array beginnerSynonyms;
+var private const array normalSynonyms;
+var private const array hardSynonyms;
+var private const array suicidalSynonyms;
+var private const array hoeSynonyms;
+
+// 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 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 backupVotingHandlerConfig;
+
+// Setting default 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 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 default variable before switching maps.
+var private 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 default variable for that.
+var private float storedGameDifficulty;
+
+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 newVotingHandlerConfig;
+ if (votingHandlerReference != none) {
+ return;
+ }
+ votingHandler = XVotingHandler(_.unreal.FindActorInstance(
+ _.unreal.GetGameType().VotingHandlerClass));
+ if (votingHandler == none)
+ {
+ _.logger.Auto(fatNoXVotingHandler);
+ return;
+ }
+ votingHandlerReference = _.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 VotingHandler.MapVoteGameConfig result;
+ result.gameClass = _.text.ToString(gameMode.GetGameTypeClass());
+ result.gameName = _.text.ToColoredString(gameMode.GetTitle());
+ result.prefix = _.text.ToString(gameMode.GetMapPrefix());
+ result.acronym = _.text.ToString(gameMode.GetAcronym());
+ result.mutators = BuildMutatorString(gameMode);
+ result.options = BuildOptionsString(gameMode);
+ return result;
+}
+
+private function string BuildMutatorString(GameMode gameMode)
+{
+ local int i;
+ local string result;
+ local array usedMutators;
+ usedMutators = gameMode.GetIncludedMutators();
+ for (i = 0; i < usedMutators.length; i += 1)
+ {
+ if (i > 0) {
+ result $= ",";
+ }
+ result $= _.text.ToString(usedMutators[i]);
+ }
+ return result;
+}
+
+private function string BuildOptionsString(GameMode gameMode)
+{
+ local bool optionWasAdded;
+ local string result;
+ local string nextKey, nextValue;
+ local Iter iter;
+ local AssociativeArray options;
+ options = gameMode.GetOptions();
+ for (iter = options.Iterate(); !iter.HasFinished(); iter.Next())
+ {
+ nextKey = Text(iter.GetKey()).ToPlainString();
+ nextValue = Text(iter.Get()).ToPlainString();
+ if (optionWasAdded) {
+ result $= "?";
+ }
+ result $= (nextKey $ "=" $ nextValue);
+ optionWasAdded = true;
+ }
+ options.Empty(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 nextGameClass;
+ 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(_.unreal.GetGameType().class) ~= nextGameClassName) {
+ nextGameClass = _.unreal.GetGameType().class;
+ }
+ else {
+ nextGameClass = class(_.memory.LoadClassS(nextGameClassName));
+ }
+ default.isServerTraveling = true;
+ default.targetGameMode = availableGameModes[pickedVHConfig].ToPlainString();
+ nextGameMode = GetConfigFromString(default.targetGameMode);
+ default.storedGameDifficulty = nextGameClass.default.gameDifficulty;
+ nextGameClass.default.gameDifficulty = GetNumericDifficulty(nextGameMode);
+}
+
+/**
+ * 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()
+{
+ if (!default.isServerTraveling) {
+ return none;
+ }
+ _.unreal.GetGameType().default.gameDifficulty = default.storedGameDifficulty;
+ default.isServerTraveling = false;
+ 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 GetNumericDifficulty(GameMode gameMode)
+{
+ local int i;
+ local string difficulty;
+ difficulty = Locs(_.text.ToString(gameMode.GetDifficulty()));
+ for (i = 0; i < default.beginnerSynonyms.length; i += 1)
+ {
+ if (IsPrefixOf(difficulty, default.beginnerSynonyms[i])) {
+ return 1;
+ }
+ }
+ for (i = 0; i < default.normalSynonyms.length; i += 1)
+ {
+ if (IsPrefixOf(difficulty, default.normalSynonyms[i])) {
+ return 2;
+ }
+ }
+ for (i = 0; i < default.hardSynonyms.length; i += 1)
+ {
+ if (IsPrefixOf(difficulty, default.hardSynonyms[i])) {
+ return 4;
+ }
+ }
+ for (i = 0; i < default.suicidalSynonyms.length; i += 1)
+ {
+ if (IsPrefixOf(difficulty, default.suicidalSynonyms[i])) {
+ return 5;
+ }
+ }
+ for (i = 0; i < default.hoeSynonyms.length; i += 1)
+ {
+ if (IsPrefixOf(difficulty, default.hoeSynonyms[i])) {
+ return 7;
+ }
+ }
+ return int(difficulty);
+}
+
+protected final static function bool IsPrefixOf(string prefix, string value)
+{
+ return (InStr(value, prefix) == 0);
+}
+
+defaultproperties
+{
+ 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"
+ 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.")
+}
\ No newline at end of file