/** * 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 // '_' }