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