Anton Tarasenko
3 years ago
7 changed files with 1025 additions and 187 deletions
@ -1,3 +1,4 @@
|
||||
[Acedia.Packages] |
||||
useGameModes=false |
||||
corePackage="AcediaCore_0_2" |
||||
package="AcediaFixes" |
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
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<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 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<string> DynamicIntoStringArray(DynamicArray 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.ToPlainString(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
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 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<CoreService.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(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<CoreService.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.") |
||||
} |
@ -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 <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; |
||||
|
||||
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.") |
||||
} |
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
class TestingListener_AcediaLauncher extends TestingListenerBase |
||||
abstract; |
||||
|
||||
static function TestingEnded( |
||||
array< class<TestCase> > testQueue, |
||||
array<TestCaseSummary> results) |
||||
{ |
||||
local int i; |
||||
local MutableText nextLine; |
||||
local array<string> 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' |
||||
} |
@ -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 <https://www.gnu.org/licenses/>. |
||||
*/ |
||||
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<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; |
||||
|
||||
// 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 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<VotingHandler.MapVoteGameConfig> 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<Text> 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<GameInfo> 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<GameInfo>(_.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.") |
||||
} |
Reference in new issue