|
|
|
/**
|
|
|
|
* 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 <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
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<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;
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
// 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_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<VotingHandler.MapVoteMapList> 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<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;
|
|
|
|
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<Text> usedMapLists;
|
|
|
|
local array<string> 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<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;
|
|
|
|
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`.")
|
|
|
|
}
|