You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
380 lines
14 KiB
380 lines
14 KiB
/** |
|
* 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-2023 Anton Tarasenko |
|
* 2023 Shtoyan |
|
*------------------------------------------------------------------------------ |
|
* This file is part of Acedia. |
|
* |
|
* Acedia is free software: you can redistribute it and/or modify |
|
* it under the terms of the GNU General Public License as published by |
|
* the Free Software Foundation, version 3 of the License, or |
|
* (at your option) any later version. |
|
* |
|
* Acedia is distributed in the hope that it will be useful, |
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
* GNU General Public License for more details. |
|
* |
|
* You should have received a copy of the GNU General Public License |
|
* along with Acedia. If not, see <https://www.gnu.org/licenses/>. |
|
*/ |
|
class VotingHandlerAdapter extends AcediaObject |
|
dependson(VotingHandler) |
|
config(AcediaLauncherData); |
|
|
|
/** |
|
* All usage of this object should start with `InjectIntoVotingHandler()` |
|
* method that will read all the `GameMode` configs and fill voting handler's |
|
* config with their data, while making a backup of all values. |
|
* Backup can be restored with `RestoreVotingHandlerConfigBackup()` method. |
|
* How that affects the clients depends on whether restoration was done before, |
|
* during or after the replication. It is intended to be done after |
|
* server travel has started. |
|
* the process of injection is to create an ordered list of game modes |
|
* (`availableGameModes`) and generate appropriate voting handler's configs |
|
* with `BuildVotingHandlerConfig()`, saving them in the same order inside |
|
* the voting handler. Picked game mode is then determined by index of |
|
* the picked voting handler's option. |
|
* |
|
* Additionally this class has a static internal state that allows it to |
|
* transfer data along the server travel - it is used mainly to remember picked |
|
* game mode and enforce game's difficulty by altering and restoring |
|
* `GameInfo`'s variable. |
|
* To make such transfer happen one must call `PrepareForServerTravel()` before |
|
* server travel to set the internal static state and |
|
* then `SetupGameModeAfterTravel()` after travel (when the new map is loading) |
|
* to read (and forget) from internal state. |
|
*/ |
|
|
|
// All available game modes for Acedia, loaded during initialization. |
|
// This array is directly produces replacement for `XVotingHandler`'s |
|
// `gameConfig` array and records of `availableGameModes` relate to those of |
|
// `gameConfig` with the same index. |
|
// So if we know that a voting option with a certain index was chosen - |
|
// it means that user picked game mode from `availableGameModes` with |
|
// the same index. |
|
var private array<Text> availableGameModes; |
|
|
|
// Finding voting handler is not cheap, so only do it once and then store it. |
|
var private NativeActorRef votingHandlerReference; |
|
// Save `VotingHandler`'s config to restore it before server travel - otherwise Acedia will alter |
|
// its config |
|
var private array<VotingHandler.MapVoteGameConfig> backupVotingHandlerConfig; |
|
// Map list management logic |
|
var private MapTool mapTool; |
|
|
|
// Setting value of this flag to `true` indicates that map switching just |
|
// occurred and we need to recover some information from the previous map. |
|
var private config bool isServerTraveling; |
|
// We should not rely on "VotingHandler" to inform us from which game mode its |
|
// selected config option originated after server travel, so we need to |
|
// remember it in this config variable before switching maps. |
|
var private config string targetGameMode; |
|
// Acedia's game modes intend on supporting difficulty switching, but |
|
// `KFGameType` does not support appropriate flags, so we enforce default |
|
// difficulty by overwriting default value of its `gameDifficulty` variable. |
|
// But to not affect game's configs we must restore old value after new map is |
|
// loaded. Store it in config variable for that. |
|
var private config int storedGameLength; |
|
|
|
|
|
// Aliases are an unnecessary overkill for difficulty names, so just define |
|
// them in special `string` arrays. |
|
// We accept not just these exact words, but any of their prefixes. |
|
var private const array<string> shortSynonyms; |
|
var private const array<string> normalSynonyms; |
|
var private const array<string> longSynonyms; |
|
|
|
var private LoggerAPI.Definition fatNoXVotingHandler, fatBadGameConfigIndexVH; |
|
var private LoggerAPI.Definition fatBadGameConfigIndexAdapter, warnMissingMapList; |
|
|
|
protected function Constructor() { |
|
mapTool = MapTool(_.memory.Allocate(class'MapTool')); |
|
} |
|
|
|
protected function Finalizer() { |
|
_.memory.Free(mapTool); |
|
_.memory.Free(votingHandlerReference); |
|
_.memory.FreeMany(availableGameModes); |
|
mapTool = none; |
|
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 string nextGameModePrefix; |
|
local GameMode nextGameMode; |
|
local XVotingHandler votingHandler; |
|
local array<VotingHandler.MapVoteGameConfig> newVotingHandlerConfig; |
|
|
|
// `votingHandlerReference != none` means that we've already injected into voting handler |
|
if (votingHandlerReference != none) { |
|
return; |
|
} |
|
votingHandler = XVotingHandler(_server.unreal.FindActorInstance( |
|
_server.unreal.GetGameType().votingHandlerClass)); |
|
if (votingHandler == none) { |
|
_.logger.Auto(fatNoXVotingHandler); |
|
return; |
|
} |
|
votingHandlerReference = _server.unreal.ActorRef(votingHandler); |
|
// This cannot actuall fail at this point - we have valid `votingHandler` reference and |
|
// `mapTool` is only initialized here (which can be executed only once) |
|
mapTool.Initialize(votingHandlerReference); |
|
availableGameModes = class'GameMode'.static.AvailableConfigs(); |
|
for (i = 0; i < availableGameModes.length; i += 1) { |
|
nextGameMode = GameMode(class'GameMode'.static.GetConfigInstance(availableGameModes[i])); |
|
nextGameModePrefix = mapTool.AddGameMode(nextGameMode); |
|
newVotingHandlerConfig[i] = BuildVotingHandlerConfig(nextGameMode, nextGameModePrefix); |
|
// Setup proper game mode index |
|
if (availableGameModes[i].ToString() == targetGameMode) { |
|
votingHandler.currentGameConfig = i; |
|
} |
|
// Report omitted mutators / server options |
|
nextGameMode.ReportBadMutatorNames(); |
|
nextGameMode.ReportBadOptions(); |
|
_.memory.Free(nextGameMode); |
|
} |
|
backupVotingHandlerConfig = votingHandler.gameConfig; |
|
votingHandler.gameConfig = newVotingHandlerConfig; |
|
mapTool.Inject(); |
|
} |
|
|
|
private function VotingHandler.MapVoteGameConfig BuildVotingHandlerConfig( |
|
GameMode gameMode, |
|
string gameModePrefix) |
|
{ |
|
local MutableText nextColoredName; |
|
local VotingHandler.MapVoteGameConfig result; |
|
|
|
result.gameClass = _.text.IntoString(gameMode.GetGameTypeClass()); |
|
result.prefix = gameModePrefix $ "-"; |
|
nextColoredName = gameMode |
|
.GetTitle() |
|
.IntoMutableText() |
|
.ChangeDefaultColor(_.color.white); |
|
result.gameName = _.text.IntoColoredString(nextColoredName) |
|
$ _.color.GetColorTag(_.color.White); |
|
nextColoredName = gameMode |
|
.GetAcronym() |
|
.IntoMutableText() |
|
.ChangeDefaultColor(_.color.white); |
|
result.acronym = _.text.IntoColoredString(nextColoredName) |
|
$ _.color.GetColorTag(_.color.White); |
|
result.mutators = BuildMutatorString(gameMode); |
|
result.options = BuildOptionsString(gameMode); |
|
return result; |
|
} |
|
|
|
private function string BuildMutatorString(GameMode gameMode) |
|
{ |
|
local int i; |
|
local string result; |
|
local array<Text> usedMutators; |
|
|
|
usedMutators = gameMode.GetIncludedMutators(); |
|
for (i = 0; i < usedMutators.length; i += 1) |
|
{ |
|
if (i > 0) { |
|
result $= ","; |
|
} |
|
result $= _.text.IntoString(usedMutators[i]); |
|
} |
|
return result; |
|
} |
|
|
|
private function string BuildOptionsString(GameMode gameMode) |
|
{ |
|
local bool optionWasAdded; |
|
local string result; |
|
local string nextKey, nextValue; |
|
local CollectionIterator iter; |
|
local HashTable options; |
|
|
|
options = gameMode.GetOptions(); |
|
for (iter = options.Iterate(); !iter.HasFinished(); iter.Next()) |
|
{ |
|
nextKey = _.text.IntoString(Text(iter.GetKey())); |
|
nextValue = _.text.IntoString(Text(iter.Get())); |
|
if (optionWasAdded) { |
|
result $= "?"; |
|
} |
|
result $= (nextKey $ "=" $ nextValue); |
|
optionWasAdded = true; |
|
} |
|
options.FreeSelf(); |
|
iter.FreeSelf(); |
|
return result; |
|
} |
|
|
|
/** |
|
* Makes necessary preparations for the server travel. |
|
*/ |
|
public final function PrepareForServerTravel() |
|
{ |
|
local int pickedVHConfig; |
|
local GameMode nextGameMode; |
|
local string nextGameClassName; |
|
local class<GameInfo> nextGameClass; |
|
local class<KFGameType> nextKFGameType; |
|
local XVotingHandler votingHandler; |
|
|
|
if (votingHandlerReference == none) return; |
|
votingHandler = XVotingHandler(votingHandlerReference.Get()); |
|
if (votingHandler == none) return; |
|
// Server travel caused by something else than `XVotingHandler` |
|
if (!votingHandler.bLevelSwitchPending) return; |
|
|
|
pickedVHConfig = votingHandler.currentGameConfig; |
|
if (pickedVHConfig < 0 || pickedVHConfig >= votingHandler.gameConfig.length) |
|
{ |
|
_.logger.Auto(fatBadGameConfigIndexVH) |
|
.ArgInt(pickedVHConfig) |
|
.ArgInt(votingHandler.gameConfig.length); |
|
return; |
|
} |
|
if (pickedVHConfig >= availableGameModes.length) |
|
{ |
|
_.logger.Auto(fatBadGameConfigIndexAdapter) |
|
.ArgInt(pickedVHConfig) |
|
.ArgInt(availableGameModes.length); |
|
return; |
|
} |
|
nextGameClassName = votingHandler.gameConfig[pickedVHConfig].gameClass; |
|
if (string(_server.unreal.GetGameType().class) ~= nextGameClassName) { |
|
nextGameClass = _server.unreal.GetGameType().class; |
|
} |
|
else |
|
{ |
|
nextGameClass = |
|
class<GameInfo>(_.memory.LoadClass_S(nextGameClassName)); |
|
} |
|
isServerTraveling = true; |
|
targetGameMode = availableGameModes[pickedVHConfig].ToString(); |
|
nextGameMode = GetConfigFromString(targetGameMode); |
|
nextKFGameType = class<KFGameType>(nextGameClass); |
|
if (nextKFGameType != none) |
|
{ |
|
storedGameLength = nextKFGameType.default.kfGameLength; |
|
nextKFGameType.default.kfGameLength = |
|
GetNumericGameLength(nextGameMode); |
|
} |
|
nextGameClass.static.StaticSaveConfig(); |
|
SaveConfig(); |
|
} |
|
|
|
/** |
|
* Restore `GameInfo`'s settings after the server travel and |
|
* apply selected `GameMode`. |
|
* |
|
* @return `GameMode` picked before server travel |
|
* (the one that must be running now). |
|
*/ |
|
public final function GameMode SetupGameModeAfterTravel() |
|
{ |
|
local KFGameType kfGameType; |
|
|
|
if (!isServerTraveling) { |
|
return none; |
|
} |
|
kfGameType = _server.unreal.GetKFGameType(); |
|
if (kfGameType != none) { |
|
kfGameType.default.kfGameLength = storedGameLength; |
|
} |
|
isServerTraveling = false; |
|
_server.unreal.GetGameType().StaticSaveConfig(); |
|
SaveConfig(); |
|
return GetConfigFromString(targetGameMode); |
|
} |
|
|
|
/** |
|
* Restores `XVotingHandler`'s config to the values that were overridden by |
|
* `VHAdapter`'s `InjectIntoVotingHandler()` method. |
|
*/ |
|
public final function RestoreVotingHandlerConfigBackup() |
|
{ |
|
local XVotingHandler votingHandler; |
|
|
|
if (votingHandlerReference == none) return; |
|
votingHandler = XVotingHandler(votingHandlerReference.Get()); |
|
if (votingHandler == none) return; |
|
|
|
votingHandler.gameConfig = backupVotingHandlerConfig; |
|
votingHandler.default.gameConfig = backupVotingHandlerConfig; |
|
votingHandler.currentGameConfig = 0; |
|
votingHandler.SaveConfig(); |
|
} |
|
|
|
// `GameMode`'s name as a `string` -> `GameMode` instance |
|
private function GameMode GetConfigFromString(string configName) |
|
{ |
|
local GameMode result; |
|
local Text nextConfigName; |
|
nextConfigName = _.text.FromString(configName); |
|
result = GameMode(class'GameMode'.static.GetConfigInstance(nextConfigName)); |
|
_.memory.Free(nextConfigName); |
|
return result; |
|
} |
|
|
|
// Convert `GameMode`'s difficulty's textual representation into |
|
// KF's numeric one. |
|
private final function int GetNumericGameLength(BaseGameMode gameMode) |
|
{ |
|
local int i; |
|
local string length; |
|
|
|
length = Locs(_.text.IntoString(gameMode.GetLength())); |
|
// Custom game is a bad guess for empty game length setting, since it can |
|
// lead to behavior unexpecte by users |
|
if (length == "") { |
|
return 2; |
|
} |
|
for (i = 0; i < default.shortSynonyms.length; i += 1) |
|
{ |
|
if (IsPrefixOf(length, default.shortSynonyms[i])) { |
|
return 0; |
|
} |
|
} |
|
for (i = 0; i < default.normalSynonyms.length; i += 1) |
|
{ |
|
if (IsPrefixOf(length, default.normalSynonyms[i])) { |
|
return 1; |
|
} |
|
} |
|
for (i = 0; i < default.longSynonyms.length; i += 1) |
|
{ |
|
if (IsPrefixOf(length, default.longSynonyms[i])) { |
|
return 2; |
|
} |
|
} |
|
return 3; |
|
} |
|
|
|
protected final static function bool IsPrefixOf(string prefix, string value) |
|
{ |
|
return (InStr(value, prefix) == 0); |
|
} |
|
|
|
defaultproperties |
|
{ |
|
shortSynonyms(0) = "short" |
|
normalSynonyms(0) = "normal" |
|
normalSynonyms(1) = "medium" |
|
normalSynonyms(2) = "regular" |
|
longSynonyms(0) = "long" |
|
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.") |
|
} |