diff --git a/config/AcediaGameModes.ini b/config/AcediaGameModes.ini index 951715e..c71b0fe 100644 --- a/config/AcediaGameModes.ini +++ b/config/AcediaGameModes.ini @@ -1,7 +1,48 @@ [hard GameMode] -title={$green Hard difficulty} +;= Add a section like this one for every voting option +;= +;= `title` will be displayed in a drop-down list in the voting dialog +title={$green Hard} +;= `difficulty` determines... difficulty and for vanilla Killing Floor can be one of +;= the following values: +;= +;= * Beginner: "easy", "beginner" +;= * Normal: "normal", "default", "regular" +;= * Hard: "harder" +;= * Suicidal: "suicidal" +;= * Hell On Earth: "hellonearth", "hell on earth", "hoe" +;= +;= Any prefixes will also work, e.g. "sui", "hard", etc. difficulty=hard +;= How long should game last? For vanilla Killing Floor its "short", +;= "normal" (also "default" and "regular"), "long". +length=long +;= A short game mode name that will be displayed after map vote has finished +acronym={$green hard} +;= Use this to add map lists (from "AcediaMaps.ini") to this game mode +;= NOTE: map lists, NOT maps +includeMaps=default +;= Use this to add mutators to this game mode, one mutator per line +;includeMutator= +;= Use this do add one of Acedia's Features into this game mode with "default" config +;includeFeature= +;= Use this to add feature with specified config +;includeFeatureAs=(feature=,config=) +;= Use this to disable one of the Acedia's features for this game mode. +;= This overrides all other settings. +;= Purpose of this setting is to disable auto-enabled features. +;excludeFeature= + +[sui GameMode] +title={$orange Suicidal} +difficulty=suicidal +length=long +acronym={$orange sui} +includeMaps=default [hell GameMode] title={$crimson Hell On Earth} difficulty=hoe +length=long +acronym={$crimson hoe} +includeMaps=default \ No newline at end of file diff --git a/config/AcediaMaps.ini b/config/AcediaMaps.ini new file mode 100644 index 0000000..9d1d898 --- /dev/null +++ b/config/AcediaMaps.ini @@ -0,0 +1,39 @@ +[default MapList] +map=KF-AbusementPark +map=KF-Aperture +map=KF-Bedlam +map=KF-Biohazard +map=KF-BioticsLab +map=KF-Clandestine +map=KF-Crash +map=KF-Departed +map=KF-EvilSantasLair +map=KF-Farm +map=KF-FilthsCross +map=KF-Forgotten +map=KF-Foundry +map=KF-FrightYard +map=KF-Hell +map=KF-Hellride +map=KF-HillbillyHorror +map=KF-Hospitalhorrors +map=KF-Icebreaker +map=KF-IceCave +map=KF-Manor +map=KF-MoonBase +map=KF-MountainPass +map=KF-Offices +map=KF-SirensBelch +map=KF-Steamland +map=KF-Stronghold +map=KF-Suburbia +map=KF-ThrillsChills +map=KF-Transit +map=KF-Waterworks +map=KF-WestLondon +map=KF-Wyre + +[objective MapList] +map=KFO-FrightYard +map=KFO-Steamland +map=KFO-Transit \ No newline at end of file diff --git a/sources/AcediaLauncherMut.uc b/sources/AcediaLauncherMut.uc index 82ee6ca..05c71d5 100644 --- a/sources/AcediaLauncherMut.uc +++ b/sources/AcediaLauncherMut.uc @@ -1,7 +1,8 @@ /** * Main and only Acedia mutator. Used for providing access to mutator * events' calls and detecting server travel. - * Copyright 2020-2022 Anton Tarasenko + * Copyright 2020-2023 Anton Tarasenko + * 2023 Shtoyan *------------------------------------------------------------------------------ * This file is part of Acedia. * diff --git a/sources/GameModes/BaseGameMode.uc b/sources/GameModes/BaseGameMode.uc index 3b7a017..144dede 100644 --- a/sources/GameModes/BaseGameMode.uc +++ b/sources/GameModes/BaseGameMode.uc @@ -10,7 +10,7 @@ * be used based on game info's settings; * 3. `Report...()` methods that perform various validation checks * (and log them) on config data. - * Copyright 2021-2022 Anton Tarasenko + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -44,6 +44,8 @@ var protected config array includeFeature; // `Feature`s to exclude from game mode, regardless of other settings // (this one has highest priority) var protected config array excludeFeature; +// Lists of maps to include for this game mode +var protected config array includeMaps; struct FeatureConfigPair { @@ -75,17 +77,23 @@ protected function HashTable ToData() _.memory.Free(nextArray); nextArray = _.collections.EmptyArrayList(); for (i = 0; i < excludeFeature.length; i += 1) { - nextArray.AddItem(_.text.FromString(excludeFeature[i])); + nextArray.AddString(excludeFeature[i]); } result.SetItem(P("excludeFeature"), nextArray); _.memory.Free(nextArray); nextArray = _.collections.EmptyArrayList(); for (i = 0; i < includeMutator.length; i += 1) { - nextArray.AddItem(_.text.FromString(includeFeature[i])); + nextArray.AddString(includeFeature[i]); } result.SetItem(P("includeMutator"), nextArray); _.memory.Free(nextArray); nextArray = _.collections.EmptyArrayList(); + for (i = 0; i < includeMaps.length; i += 1) { + nextArray.AddString(includeMaps[i]); + } + result.SetItem(P("includeMaps"), nextArray); + _.memory.Free(nextArray); + nextArray = _.collections.EmptyArrayList(); for (i = 0; i < includeFeatureAs.length; i += 1) { nextPair = _.collections.EmptyHashTable(); @@ -120,6 +128,9 @@ protected function FromData(HashTable source) nextArray = source.GetArrayList(P("includeMutator")); includeMutator = DynamicIntoStringArray(nextArray); _.memory.Free(nextArray); + nextArray = source.GetArrayList(P("includeMaps")); + includeMaps = DynamicIntoStringArray(nextArray); + _.memory.Free(nextArray); nextArray = source.GetArrayList(P("includeFeatureAs")); if (nextArray == none) { return; @@ -420,6 +431,16 @@ public function array GetIncludedMutators() return StringToTextArray(validatedMutators); } +public function array GetIncludedMapLists() +{ + return StringToTextArray(includeMaps); +} + +public function array GetIncludedMapLists_S() +{ + return includeMaps; +} + defaultproperties { configName = "AcediaGameModes" diff --git a/sources/GameModes/GameMode.uc b/sources/GameModes/GameMode.uc index 13589bd..faab86a 100644 --- a/sources/GameModes/GameMode.uc +++ b/sources/GameModes/GameMode.uc @@ -1,7 +1,7 @@ /** * The only implementation for `BaseGameMode` suitable for standard * killing floor game types. - * Copyright 2021-2022 Anton Tarasenko + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -35,9 +35,6 @@ 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; // Aliases are an unnecessary overkill for difficulty names, so just define // them in special `string` arrays. @@ -57,7 +54,6 @@ protected function DefaultIt() difficulty = "Hell On Earth"; gameTypeClass = "KFMod.KFGameType"; acronym = ""; - mapPrefix = "KF"; includeFeature.length = 0; excludeFeature.length = 0; includeMutator.length = 0; @@ -76,7 +72,7 @@ protected function HashTable ToData() } result.SetString(P("gameTypeClass"), gameTypeClass); result.SetString(P("acronym"), acronym); - result.SetString(P("mapPrefix"), mapPrefix); + nextArray = _.collections.EmptyArrayList(); for (i = 0; i < option.length; i += 1) { @@ -103,9 +99,9 @@ protected function FromData(HashTable source) return; } gameTypeClass = source.GetString(P("gameTypeClass")); - acronym = source.GetString(P("acronym")); - mapPrefix = source.GetString(P("mapPrefix")); - nextArray = source.GetArrayList(P("option")); + acronym = source.GetString(P("acronym")); + + nextArray = source.GetArrayList(P("option")); if (nextArray == none) { return; } @@ -144,16 +140,6 @@ public function Text GetAcronym() } } -public function Text GetMapPrefix() -{ - if (mapPrefix == "") { - return _.text.FromString("KF-"); - } - else { - return _.text.FromString(mapPrefix); - } -} - /** * Checks option-related settings (`option`) for correctness and reports * any issues. diff --git a/sources/MapList/MapList.uc b/sources/MapList/MapList.uc new file mode 100644 index 0000000..6ff5a19 --- /dev/null +++ b/sources/MapList/MapList.uc @@ -0,0 +1,99 @@ +/** + * Config class for storing map lists. + * Copyright 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 . + */ +class MapList extends AcediaConfig + perObjectConfig + config(AcediaMaps); + +var public config array map; + +protected function HashTable ToData() { + local int i; + local ArrayList mapArray; + local HashTable result; + + result = _.collections.EmptyHashTable(); + mapArray = _.collections.EmptyArrayList(); + for (i = 0; i < map.length; i += 1) { + mapArray.AddString(map[i]); + } + result.SetItem(P("maps"), mapArray); + _.memory.Free(mapArray); + return result; +} + +protected function FromData(HashTable source) { + local int i; + local ArrayList mapArray; + + if (source == none) { + return; + } + mapArray = source.GetArrayList(P("maps")); + if (mapArray == none) { + return; + } + map.length = 0; + for (i = 0; i < mapArray.GetLength(); i += 1) { + map[map.length] = mapArray.GetString(i); + } + _.memory.Free(mapArray); +} + +protected function DefaultIt() { + map.length = 0; + map[0] = "KF-AbusementPark"; + map[1] = "KF-Aperture"; + map[2] = "KF-Bedlam"; + map[3] = "KF-Biohazard"; + map[4] = "KF-BioticsLab"; + map[5] = "KF-Clandestine"; + map[6] = "KF-Crash"; + map[7] = "KF-Departed"; + map[8] = "KF-EvilSantasLair"; + map[9] = "KF-Farm"; + map[10] = "KF-FilthsCross"; + map[11] = "KF-Forgotten"; + map[12] = "KF-Foundry"; + map[13] = "KF-FrightYard"; + map[14] = "KF-Hell"; + map[15] = "KF-Hellride"; + map[16] = "KF-HillbillyHorror"; + map[17] = "KF-Hospitalhorrors"; + map[18] = "KF-Icebreaker"; + map[19] = "KF-IceCave"; + map[20] = "KF-Manor"; + map[21] = "KF-MoonBase"; + map[22] = "KF-MountainPass"; + map[23] = "KF-Offices"; + map[24] = "KF-SirensBelch"; + map[25] = "KF-Steamland"; + map[26] = "KF-Stronghold"; + map[27] = "KF-Suburbia"; + map[28] = "KF-ThrillsChills"; + map[29] = "KF-Transit"; + map[30] = "KF-Waterworks"; + map[31] = "KF-WestLondon"; + map[32] = "KF-Wyre"; +} + +defaultproperties { + configName = "AcediaMaps" +} \ No newline at end of file diff --git a/sources/MapList/MapTool.uc b/sources/MapList/MapTool.uc new file mode 100644 index 0000000..b736b70 --- /dev/null +++ b/sources/MapList/MapTool.uc @@ -0,0 +1,346 @@ +/** + * Author: Anton Tarasenko + * Home repo: https://insultplayers.ru/git/AcediaFramework/AcediaCore/ + * License: GPL + * Copyright 2023 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class MapTool extends AcediaObject; + +//! Tool for adapting AcediaLauncher's map lists for [`xVotingHandler`]. +//! +//! This class is responsible for making [`xVotingHandler`] use specific maps (defined in +//! AcediaLauncer's configs) for specific game modes. +//! To achieve that it abuses [`xVotingHandler`]'s ability to filter maps by +//! game mode-specific prefix. +//! Normally prefix filtering for [`xVotingHandler`] is of limited use, because most Killing Floor +//! maps start with the same prefix `KF-` (and some objective ones starting with `KFO-`). +//! However we swap that prefix for something unique for each game mode: `MapSet0-`, `MapSet1-`, +//! etc, allowing us to pick the precise map set we want. +//! +//! There's two main challenges: +//! +//! 1. *Altered map names break voting* - since [`xVotingHandler`] expects to be provided real map +//! names and our mangled ones. We deal with it by catching a map change message broadcasted +//! right before actual map change occurs and swap our names with real ones. +//! 2. *Increased amount of maps to replicate* - if we implement this name mangling by using +//! a naive approach, in which we separately add maps for every game mode, then it will lead to +//! drastic increase in replication time of the complete map list to players. +//! Consider, for example, that you have 10 different game modes with exactly the same maps: +//! we will be needlessly replicating the exact same thing 10 times! +//! To solve this issue we specifically track map lists other game modes use, along with +//! prefixes assigned to them, and reuse already added maps in case two game modes are defined +//! to use the exactly same ones. + +/// For storing which map sequences have which prefixes. Storage order is important. +struct MapSequenceRecord { + var public array sequence; + var public string prefix; +}; + +// To avoid doing excesive work when injecting maps for a second time +var private bool injectedMaps; + +// Finding voting handler is not cheap, so only do it once and then store it. +var private NativeActorRef votingHandlerReference; +// Resulting full map list with pseudonim (with replaced prefixes) and real names of maps. +var private array pseudonimMapList; +var private array realMapList; + +// Map sequences used by game modes we've seen so far. +var private array usedMapSequences; + +// To more easily detect broadcasted message about map change we replace it with our own that is +// both unlikely to occur and is easy to get voted map name from. +var private string backupMessageMapWon; +var private string backupMessageAdminMapChange; +var private const string ACEDIA_ADMIN_MAP_CHANGE_COMMAND; +var private const string ACEDIA_MAP_WON_COMMAND; + +var private LoggerAPI.Definition fatVotingHandlerMissing, warnMissingMapList; + +protected function Finalizer() { + _server.unreal.broadcasts.OnHandleText(self).Disconnect(); + _.memory.Free(votingHandlerReference); + votingHandlerReference = none; + pseudonimMapList.length = 0; + realMapList.length = 0; + usedMapSequences.length = 0; + injectedMaps = false; +} + +/// Initializes [`MapTool`] by associating it with an [`XVotingHandler`]. +/// +/// Initialization fails if [`initVotingHandlerReference`] doesn't provide reference to +/// [`XVotingHandler`] or caller [`MapTool`] already was initialized. +/// Returns `true` iff initialization was successful. +public final function bool Initialize(NativeActorRef initVotingHandlerReference) { + if (initVotingHandlerReference == none) return false; + if (XVotingHandler(initVotingHandlerReference.Get()) == none) return false; + + initVotingHandlerReference.NewRef(); + votingHandlerReference = initVotingHandlerReference; + return true; +} + +/// Adds map information from the new game mode. +/// +/// Returns prefix that given game mode must use to display maps configured for it. +public final function string AddGameMode(GameMode gameMode) { + local XVotingHandler votingHandler; + local string gameModePrefix; + + votingHandler = GetVotingHandler(); + if (votingHandler == none) { + _.logger.Auto(fatVotingHandlerMissing); + return "KF"; + } + if (CheckNeedToLoadMaps(gameMode, gameModePrefix)) { + LoadGameModeMaps(gameMode, gameModePrefix, votingHandler); + } + return gameModePrefix; +} + +/// Injects final map list into [`XVotingHandler`]. +/// +/// Call this after all game modes have been added. +/// Shouldn't be called more than once. +public final function Inject() { + local XVotingHandler votingHandler; + + votingHandler = GetVotingHandler(); + if (votingHandler == none) { + _.logger.Auto(fatVotingHandlerMissing); + return; + } + votingHandler.mapList = pseudonimMapList; + votingHandler.mapCount = pseudonimMapList.length; + // Replace map change messages with our commands and make sure it is done only once, + // in case we mess up somewhere else and call this method second time + if (!injectedMaps) { + backupMessageMapWon = votingHandler.lmsgMapWon; + backupMessageAdminMapChange = votingHandler.lmsgAdminMapChange; + votingHandler.lmsgMapWon = ACEDIA_MAP_WON_COMMAND $ "::%mapname%"; + votingHandler.lmsgAdminMapChange = ACEDIA_ADMIN_MAP_CHANGE_COMMAND $ "::%mapname%"; + _server.unreal.broadcasts.OnHandleText(self).connect = HandleMapChange; + } + injectedMaps = true; +} + +/// Builds arrays of [`VotingHandler::MapVoteMapList`] (each such item describes a map + +/// its meta data in a way [`XVotingHandler`] understands). +private function string LoadGameModeMaps( + GameMode gameMode, + string gameModePrefix, + XVotingHandler votingHandler +) { + local int i; + local ArrayList gameModeMaps; + local Text mapNameReal, mapNamePseudonim; + local VotingHandler.MapHistoryInfo nextMapInfo; + local VotingHandler.MapVoteMapList nextRecord; + local array newMapsPseudonim, newMapReal; + + nextRecord.bEnabled = true; + gameModeMaps = GetAllGameModeMaps(gameMode); + for (i = 0; i < gameModeMaps.GetLength(); i += 1) { + mapNameReal = gameModeMaps.GetText(i); + mapNamePseudonim = MakeMapPseudonim(mapNameReal, gameModePrefix); + if (votingHandler.history != none) { + nextMapInfo = votingHandler.history.GetMapHistory(mapNameReal.ToString()); + nextRecord.playCount = nextMapInfo.p; + nextRecord.sequence = nextMapInfo.s; + } + nextRecord.mapName = _.text.IntoString(mapNamePseudonim); + newMapsPseudonim[newMapsPseudonim.length] = nextRecord; + nextRecord.mapName = _.text.IntoString(mapNameReal); + newMapReal[newMapReal.length] = nextRecord; + } + AppendMapsIntoVotingHandler(newMapsPseudonim, newMapReal); + _.memory.Free(gameModeMaps); + return gameModePrefix; +} + +private function bool CheckNeedToLoadMaps(GameMode gameMode, out string prefix) { + local int mapSequenceIndex, mapListIndex; + local bool sameMapList, foundMatch; + local array existingMapSequence, newMapSequence; + local MapSequenceRecord newRecord; + + // We don't need to load maps for the `gameMode` only when we've already added the exactly same + // map sequence, order being important + newMapSequence = gameMode.GetIncludedMapLists_S(); + for (mapSequenceIndex = 0; mapSequenceIndex < usedMapSequences.length; mapSequenceIndex += 1) { + existingMapSequence = usedMapSequences[mapSequenceIndex].sequence; + if (existingMapSequence.length != newMapSequence.length) { + continue; + } + foundMatch = true; + for (mapListIndex = 0; mapListIndex < newMapSequence.length; mapListIndex += 1) { + // Map lists are ASCII config names, so we can compare them with case-ignoring + // built-in `~=` operator works (it can only handle properly ASCII input) + sameMapList = (existingMapSequence[mapListIndex] ~= newMapSequence[mapListIndex]); + if (!sameMapList) { + foundMatch = false; + break; + } + } + if (foundMatch) { + prefix = usedMapSequences[mapSequenceIndex].prefix; + return false; + } + } + newRecord.sequence = newMapSequence; + newRecord.prefix = "MapSet" $ usedMapSequences.length; + usedMapSequences[usedMapSequences.length] = newRecord; + prefix = newRecord.prefix; + return true; +} + +// Replaces prefixes like "KF-", "KFO-" or "KFS-" with "{gameModePrefix}-". +private function Text MakeMapPseudonim(Text realName, string gameModePrefix) { + local Parser parser; + local MutableText prefix, nameBody; + local MutableText result; + + result = _.text.FromStringM(gameModePrefix); + result.Append(P("-")); + parser = realName.Parse(); + parser.MUntil(prefix, _.text.GetCharacter("-")); + parser.Match(P("-")); + if (parser.Ok()) { + nameBody = parser.GetRemainderM(); + result.Append(nameBody); + } + else { + result.Append(realName); + } + _.memory.Free(nameBody); + _.memory.Free(prefix); + _.memory.Free(parser); + return result.IntoText(); +} + +private function ArrayList GetAllGameModeMaps(GameMode gameMode) { + local int i, j; + local HashTable uniqueMapSet; + local ArrayList result; + local array usedMapLists; + local array nextMapArray; + local Text nextMapName, lowerMapName; + + uniqueMapSet = _.collections.EmptyHashTable(); // for testing map name uniqueness + result = _.collections.EmptyArrayList(); + usedMapLists = gameMode.GetIncludedMapLists(); + for (i = 0; i < usedMapLists.length; i += 1) { + nextMapArray = GetMapNameFromConfig(usedMapLists[i]); + for (j = 0; j < nextMapArray.length; j += 1) { + nextMapName = _.text.FromString(nextMapArray[j]); + // Use lower case version of map name for uniqueness testing to ignore characters' case + lowerMapName = nextMapName.LowerCopy(); + if (!uniqueMapSet.HasKey(lowerMapName)) { + uniqueMapSet.SetItem(lowerMapName, none); + result.AddItem(nextMapName); + } + _.memory.Free(lowerMapName); + _.memory.Free(nextMapName); + } + } + _.memory.Free(uniqueMapSet); + _.memory.FreeMany(usedMapLists); + return result; +} + +private function array GetMapNameFromConfig(Text configName) { + local MapList mapConfig; + local array result; + + mapConfig = MapList(class'MapList'.static.GetConfigInstance(configName)); + if (mapConfig == none) { + _.logger.Auto(warnMissingMapList).Arg(configName.Copy()); + } else { + result = mapConfig.map; + _.memory.Free(mapConfig); + } + return result; +} + +private function AppendMapsIntoVotingHandler( + array newMapsPseudonim, + array newMapsReal) { + local int i; + local XVotingHandler votingHandler; + + votingHandler = GetVotingHandler(); + if (votingHandler == none) { + _.logger.Auto(fatVotingHandlerMissing); + return; + } + for (i = 0; i < newMapsPseudonim.length; i += 1) { + pseudonimMapList[pseudonimMapList.length] = newMapsPseudonim[i]; + } + for (i = 0; i < newMapsReal.length; i += 1) { + realMapList[realMapList.length] = newMapsReal[i]; + } +} + +private function bool HandleMapChange( + Actor sender, + out string message, + name type, + bool teamMessage +) { + local Parser parser; + local XVotingHandler votingHandler; + + votingHandler = GetVotingHandler(); + if (sender == none) return true; + if (votingHandler != sender) return true; + + parser = _.text.ParseString(message); + parser.Match(P(ACEDIA_MAP_WON_COMMAND)); + parser.Match(P("::")); + if (parser.Ok()) { + message = Repl(backupMessageMapWon, "%mapname%", parser.GetRemainderS()); + } else { + parser.Match(P(ACEDIA_ADMIN_MAP_CHANGE_COMMAND)); + parser.Match(P("::")); + if (parser.Ok()) { + message = Repl(backupMessageAdminMapChange, "%mapname%", parser.GetRemainderS()); + } + } + if (parser.Ok()) { + votingHandler.mapList = realMapList; + votingHandler.mapCount = realMapList.length; + } + _.memory.Free(parser); + return true; +} + +private function XVotingHandler GetVotingHandler() { + if (votingHandlerReference != none) { + return XVotingHandler(votingHandlerReference.Get()); + } + return none; +} + +defaultproperties { + ACEDIA_ADMIN_MAP_CHANGE_COMMAND = "ACEDIA_LAUNCHER:ADMIN_MAP_CHANGE:DEADBEEF" + ACEDIA_MAP_WON_COMMAND = "ACEDIA_LAUNCHER:MAP_WON:DEADBEEF" + fatVotingHandlerMissing = (l=LOG_Fatal,m="No voting `XVotingHandler` available. This is unexpected at this stage. Report this issue.") + warnMissingMapList = (l=LOG_Warning,m="Cannot find map list `%1`.") +} \ No newline at end of file diff --git a/sources/StartUp.uc b/sources/StartUp.uc index 478883d..2210404 100644 --- a/sources/StartUp.uc +++ b/sources/StartUp.uc @@ -1,6 +1,7 @@ /** * This actor's role is to perform Acedia's server startup. - * Copyright 2019-2022 Anton Tarasenko + * Copyright 2019-2023 Anton Tarasenko + * 2023 Shtoyan *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -60,6 +61,7 @@ private function InitializeServer() _ = class'Global'.static.GetInstance(); _server = class'ServerGlobal'.static.GetInstance(); class'ServerLevelCore'.static.CreateLevelCore(self); + class'MapList'.static.Initialize(); for (i = 0; i < class'Packages'.default.package.length; i += 1) { _.environment.RegisterPackage_S(class'Packages'.default.package[i]); } diff --git a/sources/VotingHandlerAdapter.uc b/sources/VotingHandlerAdapter.uc index d0538d1..fcd8a3f 100644 --- a/sources/VotingHandlerAdapter.uc +++ b/sources/VotingHandlerAdapter.uc @@ -5,7 +5,8 @@ * 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-2022 Anton Tarasenko + * Copyright 2021-2023 Anton Tarasenko + * 2023 Shtoyan *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -59,25 +60,28 @@ class VotingHandlerAdapter extends AcediaObject // the same index. var private array availableGameModes; -// Finding voting handler is not cheap, so only do it once and then store it. -var private NativeActorRef votingHandlerReference; -// Save `VotingHandler`'s config to restore it before server travel - -// otherwise Acedia will alter its config -var private array backupVotingHandlerConfig; +// Finding voting handler is not cheap, so only do it once and then store it. +var private NativeActorRef votingHandlerReference; +// Save `VotingHandler`'s config to restore it before server travel - otherwise Acedia will alter +// its config +var private array backupVotingHandlerConfig; +// 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; +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; +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; +var private config int storedGameLength; + // Aliases are an unnecessary overkill for difficulty names, so just define // them in special `string` arrays. @@ -87,14 +91,19 @@ var private const array normalSynonyms; var private const array longSynonyms; var private LoggerAPI.Definition fatNoXVotingHandler, fatBadGameConfigIndexVH; -var private LoggerAPI.Definition fatBadGameConfigIndexAdapter; +var private LoggerAPI.Definition fatBadGameConfigIndexAdapter, warnMissingMapList; -protected function Finalizer() -{ +protected function Constructor() { + mapTool = MapTool(_.memory.Allocate(class'MapTool')); +} + +protected function Finalizer() { + _.memory.Free(mapTool); _.memory.Free(votingHandlerReference); _.memory.FreeMany(availableGameModes); - votingHandlerReference = none; - availableGameModes.length = 0; + mapTool = none; + votingHandlerReference = none; + availableGameModes.length = 0; } /** @@ -104,28 +113,31 @@ protected function Finalizer() */ public final function InjectIntoVotingHandler() { - local int i; - local GameMode nextGameMode; - local XVotingHandler votingHandler; - local array newVotingHandlerConfig; + local int i; + local string nextGameModePrefix; + local GameMode nextGameMode; + local XVotingHandler votingHandler; + local array 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) - { + _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])); - newVotingHandlerConfig[i] = BuildVotingHandlerConfig(nextGameMode); + 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; @@ -133,19 +145,22 @@ public final function InjectIntoVotingHandler() // Report omitted mutators / server options nextGameMode.ReportBadMutatorNames(); nextGameMode.ReportBadOptions(); + _.memory.Free(nextGameMode); } - backupVotingHandlerConfig = votingHandler.gameConfig; - votingHandler.gameConfig = newVotingHandlerConfig; + backupVotingHandlerConfig = votingHandler.gameConfig; + votingHandler.gameConfig = newVotingHandlerConfig; + mapTool.Inject(); } private function VotingHandler.MapVoteGameConfig BuildVotingHandlerConfig( - GameMode gameMode) + GameMode gameMode, + string gameModePrefix) { local MutableText nextColoredName; local VotingHandler.MapVoteGameConfig result; result.gameClass = _.text.IntoString(gameMode.GetGameTypeClass()); - result.prefix = _.text.IntoString(gameMode.GetMapPrefix()); + result.prefix = gameModePrefix $ "-"; nextColoredName = gameMode .GetTitle() .IntoMutableText()