/**
* Config class for storing map lists.
* Copyright 2022-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 FutilityNicknames_Feature extends Feature
dependson(FutilityNicknames);
//! This feature's functionality is rather simple, but we will still break up
//! what its various components are.
//!
//! Fallback nicknames are picked at random from
//! the `fallbackNicknames` array. This is done by copying that array into
//! `unusedNicknames` and then picking and removing its random elements each
//! time we need a fallback. Once `unusedNicknames` is empty - it is copied from
//! `fallbackNicknames` once again, letting already used nicknames to be reused.
//! `unusedNicknames` contains same references as `fallbackNicknames`,
//! so they need to be separately deallocated and should also be forgotten once
//! `fallbackNicknames` are deallocated`.
//! This is implemented inside `PickNextFallback()` method.
//!
//! Nickname changes are applied inside `CensorNickname()` method that uses
//! several auxiliary methods to perform different stages of "censoring".
//! Censoring is performed:
//! 1. On any player's name change
//! (using `OnPlayerNameChanging()` signal, connected to
//! `HandleNicknameChange()`);
//! 2. When new player logins (using `OnNewPlayer()` signal,
//! conneted to `CensorOriginalNickname()`) to enforce our own
//! handling of player's original nickname.
var private /*config*/ FutilityNicknames.NicknameSpacesAction spacesAction;
var private /*config*/ FutilityNicknames.NicknameColorPermissions colorPermissions;
var private /*config*/ bool replaceSpacesWithUnderscores;
var private /*config*/ bool removeSingleQuotationMarks;
var private /*config*/ bool removeDoubleQuotationMarks;
var private /*config*/ int maxNicknameLength;
var private /*config*/ bool correctEmptyNicknames;
var private /*config*/ array fallbackNickname;
/// Guaranteed order of applying changes (only chosen ones) is as following:
///
/// 1. Trim/simplify spaces;
/// 2. Remove single and double quotation marks;
/// 3. Enforce max limit of nickname's length;
/// 4. Replace empty nickname with fallback nickname (no further changes
/// will be applied to fallback nickname in that case);
/// 5. Enforce color limitation;
/// 6. Replace remaining whitespaces with underscores.
///
/// NOTE #1: as follows from the instruction described above, no changes will
/// ever be applied to fallback nicknames (unless player's nickname coincides
/// with one by pure accident).
///
/// NOTE #2: whitespaces inside steam nicknames are converted into underscores
/// before they are passed into the game and this is a change Futility
/// cannot currently abort. Therefore all changes relevant to whitespaces inside
/// nicknames will only be applied to in-game changes.
/// Nicknames from `fallbackNickname` that can still be picked in the current
/// rotation.
var private array unusedNicknames;
var private const int CODEPOINT_UNDERSCORE;
protected function OnEnabled() {
if (IsAnyCensoringEnabled()) {
// Do this before adding event handler to avoid censoring nicknames
// second time (censoring nickname will trigger `OnPlayerNameChanging()`
// signal)
CensorCurrentPlayersNicknames();
_.players.OnPlayerNameChanging(self).connect = HandleNicknameChange;
_.players.OnNewPlayer(self).connect = CensorOriginalNickname;
}
}
protected function OnDisabled() {
_.memory.FreeMany(fallbackNickname);
_.memory.FreeMany(unusedNicknames);
fallbackNickname.length = 0;
unusedNicknames.length = 0;
if (IsAnyCensoringEnabled()) {
_.players.OnPlayerNameChanging(self).Disconnect();
_.players.OnNewPlayer(self).Disconnect();
}
}
protected function SwapConfig(FeatureConfig config) {
local FutilityNicknames newConfig;
newConfig = FutilityNicknames(config);
if (newConfig == none) {
return;
}
replaceSpacesWithUnderscores = newConfig.replaceSpacesWithUnderscores;
removeSingleQuotationMarks = newConfig.removeSingleQuotationMarks;
removeDoubleQuotationMarks = newConfig.removeDoubleQuotationMarks;
correctEmptyNicknames = newConfig.correctEmptyNicknames;
spacesAction = newConfig.spacesAction;
colorPermissions = newConfig.colorPermissions;
maxNicknameLength = newConfig.maxNicknameLength;
SwapFallbackNicknames(newConfig);
}
private function SwapFallbackNicknames(FutilityNicknames newConfig) {
local int i;
_.memory.FreeMany(fallbackNickname);
fallbackNickname.length = 0;
for (i = 0; i < newConfig.fallbackNickname.length; i += 1) {
fallbackNickname[i] = _.text.FromFormattedString(newConfig.fallbackNickname[i]);
}
unusedNicknames = fallbackNickname;
}
private function Text PickNextFallback() {
local int pickedIndex;
local Text result;
if (fallbackNickname.length <= 0) {
// Just in case this feature is really misconfigured
return P("Fresh Meat").Copy();
}
if (unusedNicknames.length <= 0) {
unusedNicknames = fallbackNickname;
}
// Pick one nickname at random.
// `pickedIndex` will belong to [0; unusedNicknames.length - 1] segment.
pickedIndex = Rand(unusedNicknames.length);
result = unusedNicknames[pickedIndex].Copy();
unusedNicknames.Remove(pickedIndex, 1);
return result;
}
private function bool IsAnyCensoringEnabled() {
return ( replaceSpacesWithUnderscores
|| removeSingleQuotationMarks
|| removeDoubleQuotationMarks
|| correctEmptyNicknames
|| maxNicknameLength >= 0
|| colorPermissions != NCP_AllowAnyColor
|| spacesAction != NSA_DoNothing);
}
// For nickname changes mid-game.
private function HandleNicknameChange(
EPlayer affectedPlayer,
BaseText oldName,
MutableText newName
) {
CensorNickname(newName, affectedPlayer);
}
// For handling of player's original nicknames.
private function CensorOriginalNickname(EPlayer affectedPlayer) {
local Text originalNickname;
if (affectedPlayer == none) {
return;
}
originalNickname = affectedPlayer.GetOriginalName();
// This will automatically trigger `OnPlayerNameChanging()` signal and
// our `HandleNicknameChange()` handler.
affectedPlayer.SetName(originalNickname);
_.memory.Free(originalNickname);
}
// For handling nicknames of players after censoring is re-activated by
// config change.
private function CensorCurrentPlayersNicknames() {
local int i;
local Text nextNickname;
local MutableText alteredNickname;
local array currentPlayers;
currentPlayers = _.players.GetAll();
for (i = 0; i < currentPlayers.length; i += 1) {
nextNickname = currentPlayers[i].GetName();
alteredNickname = nextNickname.MutableCopy();
CensorNickname(alteredNickname, currentPlayers[i]);
if (!alteredNickname.Compare(nextNickname)) {
currentPlayers[i].SetName(alteredNickname);
}
_.memory.Free(alteredNickname);
_.memory.Free(nextNickname);
}
}
private function CensorNickname(MutableText nickname, EPlayer affectedPlayer) {
local Text fallback;
local BaseText.Formatting newFormatting;
if (nickname == none) return;
if (affectedPlayer == none) return;
if (spacesAction != NSA_DoNothing) {
nickname.Simplify(spacesAction == NSA_Simplify);
}
if (removeSingleQuotationMarks) {
nickname.Replace(P("'"), P(""));
}
if (removeDoubleQuotationMarks) {
nickname.Replace(P("\""), P(""));
}
if (maxNicknameLength >= 0) {
nickname.Remove(maxNicknameLength);
}
if (correctEmptyNicknames && nickname.IsEmpty()) {
fallback = PickNextFallback();
nickname.Append(fallback);
_.memory.Free(fallback);
return;
}
if (colorPermissions != NCP_AllowAnyColor) {
if (colorPermissions == NCP_ForceSingleColor) {
newFormatting = nickname.GetCharacter(0).formatting;
} else if (colorPermissions == NCP_ForceTeamColor) {
newFormatting.isColored = true;
newFormatting.color = affectedPlayer.GetTeamColor();
}
// `colorPermissions == NCP_ForbidColor`
// `newFormatting` is colorless by default
nickname.ChangeFormatting(newFormatting);
}
if (replaceSpacesWithUnderscores) {
ReplaceSpaces(nickname);
}
}
// Asusmes `nickname != none`.
private function ReplaceSpaces(MutableText nickname) {
local int i;
local MutableText nicknameCopy;
local BaseText.Character nextCharacter, underscoreCharacter;
nicknameCopy = nickname.MutableCopy();
nickname.Clear();
underscoreCharacter = _.text.CharacterFromCodePoint(CODEPOINT_UNDERSCORE);
for (i = 0; i < nicknameCopy.GetLength(); i += 1) {
nextCharacter = nicknameCopy.GetCharacter(i);
if (_.text.IsWhitespace(nextCharacter)) {
// Replace character with underscore, leaving the formatting
underscoreCharacter.formatting = nextCharacter.formatting;
nextCharacter = underscoreCharacter;
}
nickname.AppendCharacter(nextCharacter);
}
_.memory.Free(nicknameCopy);
}
defaultproperties {
configClass = class'FutilityNicknames'
CODEPOINT_UNDERSCORE = 95 // '_'
}