Anton Tarasenko
3 years ago
7 changed files with 1025 additions and 187 deletions
@ -1,3 +1,4 @@ |
|||||||
[Acedia.Packages] |
[Acedia.Packages] |
||||||
|
useGameModes=false |
||||||
corePackage="AcediaCore_0_2" |
corePackage="AcediaCore_0_2" |
||||||
package="AcediaFixes" |
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