Browse Source

Add optimizations for shared map sequences

Fixes an issue where map list could become bloated,
even if every game mode uses the same map set
pull/2/head
Anton Tarasenko 2 years ago
parent
commit
e8cc1421c7
  1. 7
      sources/GameModes/BaseGameMode.uc
  2. 18
      sources/GameModes/GameMode.uc
  3. 232
      sources/MapList/MapTool.uc
  4. 69
      sources/VotingHandlerAdapter.uc

7
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.
*
@ -436,6 +436,11 @@ public function array<Text> GetIncludedMapLists()
return StringToTextArray(includeMaps);
}
public function array<string> GetIncludedMapLists_S()
{
return includeMaps;
}
defaultproperties
{
configName = "AcediaGameModes"

18
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,6 @@ 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)
@ -105,7 +100,6 @@ protected function FromData(HashTable source)
}
gameTypeClass = source.GetString(P("gameTypeClass"));
acronym = source.GetString(P("acronym"));
mapPrefix = source.GetString(P("mapPrefix"));
nextArray = source.GetArrayList(P("option"));
if (nextArray == none) {
@ -146,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.

232
sources/MapList/MapTool.uc

@ -1,5 +1,8 @@
/**
* Copyright 2023 Anton Tarasenko
* Author: Anton Tarasenko
* Home repo: https://insultplayers.ru/git/AcediaFramework/AcediaCore/
* License: GPL
* Copyright 2023 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
@ -18,35 +21,74 @@
*/
class MapTool extends AcediaObject;
// Finding voting handler is not cheap, so only do it once and then store it.
//! 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<string> 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;
// Maps map pseudonims we've used in voting handler to real map names
var private HashTable pseudonimToMap;
// Resulting full map list with pseudonim (with replaced prefixes) and real names of maps.
var private array<VotingHandler.MapVoteMapList> pseudonimMapList;
var private array<VotingHandler.MapVoteMapList> realMapList;
var private int gameModesSeen;
// Map sequences used by game modes we've seen so far.
var private array<MapSequenceRecord> 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_MAP_FORCED_COMMAND;
var private const string ACEDIA_ADMIN_MAP_CHANGE_COMMAND;
var private const string ACEDIA_MAP_WON_COMMAND;
protected function Constructor() {
pseudonimToMap = _.collections.EmptyHashTable();
}
var private LoggerAPI.Definition fatVotingHandlerMissing, warnMissingMapList;
protected function Finalizer() {
_server.unreal.broadcasts.OnHandleText(self).Disconnect();
_.memory.Free(votingHandlerReference);
_.memory.Free(pseudonimToMap);
votingHandlerReference = none;
pseudonimToMap = none;
pseudonimMapList.length = 0;
realMapList.length = 0;
gameModesSeen = 0;
usedMapSequences.length = 0;
injectedMaps = false;
}
public function bool Initialize(NativeActorRef initVotingHandlerReference) {
/// 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;
@ -55,47 +97,121 @@ public function bool Initialize(NativeActorRef initVotingHandlerReference) {
return true;
}
public function string LoadGameModeMaps(GameMode gameMode) {
local int i;
/// 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<VotingHandler.MapVoteMapList> newMapsPseudonim, newMapReal;
local string gameModePrefix;
votingHandler = GetVotingHandler();
if (votingHandler == none) {
return "!!!";
}
gameModePrefix = ("KF" $ gameModesSeen);
nextRecord.bEnabled = true;
gameModeMaps = GetAllGameModeMaps(gameMode);
for (i = 0; i < gameModeMaps.GetLength(); i += 1) {
// Make a pseudonim to map connection
mapNameReal = gameModeMaps.GetText(i);
mapNamePseudonim = MakeMapPseudonim(mapNameReal, gameModePrefix);
pseudonimToMap.SetItem(mapNamePseudonim, mapNameReal);
// Setup `VotingHandler.MapVoteMapList` struct for next map
if (votingHandler.history != none) {
nextMapInfo = votingHandler.history.GetMapHistory(mapNameReal.ToString());
nextRecord.playCount = nextMapInfo.p;
nextRecord.sequence = nextMapInfo.s;
} else {
nextRecord.playCount = 0;
nextRecord.sequence = 0;
}
nextRecord.mapName = mapNamePseudonim.ToString();
nextRecord.mapName = _.text.IntoString(mapNamePseudonim);
newMapsPseudonim[newMapsPseudonim.length] = nextRecord;
nextRecord.mapName = mapNameReal.ToString();
nextRecord.mapName = _.text.IntoString(mapNameReal);
newMapReal[newMapReal.length] = nextRecord;
}
AppendMaps(newMapsPseudonim, newMapReal);
gameModesSeen += 1;
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<string> 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;
@ -124,26 +240,17 @@ private function ArrayList GetAllGameModeMaps(GameMode gameMode) {
local HashTable uniqueMapSet;
local ArrayList result;
local array<Text> usedMapLists;
local MapList nextMapConfig;
local array<string> nextMapArray;
local Text nextMapName, lowerMapName;
uniqueMapSet = _.collections.EmptyHashTable(); // to quickly make sure we add each map only once
uniqueMapSet = _.collections.EmptyHashTable(); // for testing map name uniqueness
result = _.collections.EmptyArrayList();
usedMapLists = gameMode.GetIncludedMapLists();
for (i = 0; i < usedMapLists.length; i += 1) {
// Get maps from `MapList` config
nextMapConfig = MapList(class'MapList'.static.GetConfigInstance(usedMapLists[i]));
nextMapArray.length = 0;
if (nextMapConfig != none) {
nextMapArray = nextMapConfig.map;
} else {
//_.logger.Auto(warnMissingMapList).Arg(usedMapLists[i].Copy());
}
_.memory.Free(nextMapConfig);
// Add maps we haven't yet added from other lists
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);
@ -158,7 +265,21 @@ private function ArrayList GetAllGameModeMaps(GameMode gameMode) {
return result;
}
private function AppendMaps(
private function array<string> GetMapNameFromConfig(Text configName) {
local MapList mapConfig;
local array<string> 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<VotingHandler.MapVoteMapList> newMapsPseudonim,
array<VotingHandler.MapVoteMapList> newMapsReal) {
local int i;
@ -166,7 +287,7 @@ private function AppendMaps(
votingHandler = GetVotingHandler();
if (votingHandler == none) {
warn("votingHandler is none!");
_.logger.Auto(fatVotingHandlerMissing);
return;
}
for (i = 0; i < newMapsPseudonim.length; i += 1) {
@ -177,21 +298,6 @@ private function AppendMaps(
}
}
public final function InjectMaps() {
local XVotingHandler votingHandler;
votingHandler = GetVotingHandler();
if (votingHandler != none) {
votingHandler.mapList = pseudonimMapList;
votingHandler.mapCount = pseudonimMapList.length;
backupMessageMapWon = votingHandler.lmsgMapWon;
backupMessageAdminMapChange = votingHandler.lmsgAdminMapChange;
votingHandler.lmsgMapWon = ACEDIA_MAP_WON_COMMAND $ "::%mapname%";
votingHandler.lmsgAdminMapChange = ACEDIA_MAP_FORCED_COMMAND $ "::%mapname%";
_server.unreal.broadcasts.OnHandleText(self).connect = HandleMapChange;
}
}
private function bool HandleMapChange(
Actor sender,
out string message,
@ -211,7 +317,7 @@ private function bool HandleMapChange(
if (parser.Ok()) {
message = Repl(backupMessageMapWon, "%mapname%", parser.GetRemainderS());
} else {
parser.Match(P(ACEDIA_MAP_FORCED_COMMAND));
parser.Match(P(ACEDIA_ADMIN_MAP_CHANGE_COMMAND));
parser.Match(P("::"));
if (parser.Ok()) {
message = Repl(backupMessageAdminMapChange, "%mapname%", parser.GetRemainderS());
@ -233,6 +339,8 @@ private function XVotingHandler GetVotingHandler() {
}
defaultproperties {
ACEDIA_MAP_FORCED_COMMAND = "ACEDIA_LAUNCHER:MAP_FORCED:DEADBEEF"
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`.")
}

69
sources/VotingHandlerAdapter.uc

@ -60,25 +60,28 @@ class VotingHandlerAdapter extends AcediaObject
// 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;
// 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;
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.
@ -90,12 +93,17 @@ var private const array<string> longSynonyms;
var private LoggerAPI.Definition fatNoXVotingHandler, fatBadGameConfigIndexVH;
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;
}
/**
@ -105,33 +113,30 @@ protected function Finalizer()
*/
public final function InjectIntoVotingHandler()
{
local int i;
local string nextGameModePrefix;
local GameMode nextGameMode;
local XVotingHandler votingHandler;
local array<VotingHandler.MapVoteGameConfig> newVotingHandlerConfig;
local MapTool mapTool;
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)
{
_server.unreal.GetGameType().votingHandlerClass));
if (votingHandler == none) {
_.logger.Auto(fatNoXVotingHandler);
return;
}
votingHandlerReference = _server.unreal.ActorRef(votingHandler);
mapTool = MapTool(_.memory.Allocate(class'MapTool'));
mapTool.Initialize(votingHandlerReference); //TODO check return value
// 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();
votingHandler.mapCount = 0;
votingHandler.mapList.length = 0;
for (i = 0; i < availableGameModes.length; i += 1) {
nextGameMode = GameMode(class'GameMode'.static
.GetConfigInstance(availableGameModes[i]));
nextGameModePrefix = mapTool.LoadGameModeMaps(nextGameMode);
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) {
@ -142,10 +147,9 @@ public final function InjectIntoVotingHandler()
nextGameMode.ReportBadOptions();
_.memory.Free(nextGameMode);
}
backupVotingHandlerConfig = votingHandler.gameConfig;
votingHandler.gameConfig = newVotingHandlerConfig;
mapTool.InjectMaps();
//_.memory.Free(mapTool);
backupVotingHandlerConfig = votingHandler.gameConfig;
votingHandler.gameConfig = newVotingHandlerConfig;
mapTool.Inject();
}
private function VotingHandler.MapVoteGameConfig BuildVotingHandlerConfig(
@ -370,7 +374,6 @@ defaultproperties
normalSynonyms(1) = "medium"
normalSynonyms(2) = "regular"
longSynonyms(0) = "long"
warnMissingMapList = (l=LOG_Warning,m="Cannot find map list `%1`.")
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.")

Loading…
Cancel
Save