/** * This feature allows to configure nickname limitations for the server. * It allows you to customize vanilla limitations for nickname length and * color with those of your own design. Enabling this feature overwrites * default behaviour. * Copyright 2022 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; * 3. When censoring is re-activated. * In case all censoring options of this feature are disabled * (checked by `IsAnyCensoringEnabled()`) - we do not attempt to * catch any events or do anything at all. * If settings change mid-execution, this feature might need to * enable or disable censoring on-the-fly. To accomplish that we * remember current status inside `censoringNicknames` boolean * variable and enable/disable events if required by settings. * So whenever we re-activate censoring we also need to update * ("censor") current players' nicknames - a third occasion to * call `CensorNickname()`, implemented inside * `CensorCurrentPlayersNicknames()`. */ // How to treat whitespace characters inside players' nicknames. // * `NSA_DoNothing` - does nothing, leaving whitespaces as they are; // * `NSA_Trim` - removes leading and trailing whitespaces for nicknames; // * `NSA_Simplify` - removes leading and trailing whitespaces // for nicknames, also reducing a sequence of whitespaces inside // nickname to a single space, e.g. "my nick" becomes "my nick". // Default is `NSA_DoNothing`, same as on vanilla. var private /*config*/ FutilityNicknames.NicknameSpacesAction spacesAction; // How to treat colored nicknames. // * `NCP_ForbidColor` - completely strips down any color from nicknames; // * `NCP_ForceTeamColor` - forces all nicknames to have player's current // team's color; // * `NCP_ForceSingleColor` - allows nickname to be painted with a single // color (sets nickname's color to that of the first character); // * `NCP_AllowAnyColor` - allows nickname to be colored in any way player // wants. // Default is `NCP_ForbidColor`, same as on vanilla. var private /*config*/ FutilityNicknames.NicknameColorPermissions colorPermissions; // Set this to `true` if you wish to replace all whitespace characters with // underscores and `false` to leave them as is. // Default is `true`, same as on vanilla. However there is one difference: // Futility replaces all whitespace characters (including tabulations, // non-breaking spaces, etc.) instead of only ' '. var private /*config*/ bool replaceSpacesWithUnderscores; // Set this to `true` to remove single 'quotation marks' and `false` to // leave them. Default is `false`, same as on vanilla. var private /*config*/ bool removeSingleQuotationMarks; // Set this to `true` to remove dobule 'quotation marks' and `false` to // leave them. Default is `true`, same as on vanilla. var private /*config*/ bool removeDoubleQuotationMarks; // Max allowed nickname length. Negative values disable any length limits. // // NOTE #1: `0` resets all nicknames to be empty and, // if `correctEmptyNicknames` is set to `true`, they will be replaced with // one of the fallback nicknames // (see `correctEmptyNicknames` and `fallbackNickname`). // NOTE #2: Because of how color swapping in vanilla Killing Floor works, // every color swap makes text count as being about 4 characters longer. // So if one uses too many colors in the nickname, for drawing functions // it will appear to be longer than it actually is and it *will* mess up // UI. Unless you are using custom HUD it is recommended to keep this value // at default `20` and forbid colored nicknames // (by setting `colorPermissions=NCP_ForbidColor`). Or to allow only one // color (by setting `colorPermissions=NCP_ForceSingleColor` or // `colorPermissions=NCP_ForceTeamColor`) and reducing `maxNicknameLength` // to `16` (20 characters - 4 for color swap). // If you want to increase the limit above that, you can also do your // own research by testing nicknames of various length on // screen resolutions you care about. var private /*config*/ int maxNicknameLength; // Should we replace empty player nicknames with a random fallback nickname // (defined in `fallbackNickname` array)? var private /*config*/ bool correctEmptyNicknames; // Array of fallback nicknames that will be used to replace any empty nicknames // if `correctEmptyNicknames` is set to `true`. 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; // Are we currently censoring nicknames? // Set to `false` if none of the feature's options require // any action (censoring) and, therefore, we do not listen to any signals. var private bool censoringNicknames; var private const int CODEPOINT_UNDERSCORE; protected function OnDisabled() { _.memory.FreeMany(fallbackNickname); // Free this `Text` data - it will be refilled with `SwapConfig()` // if this feature is ever reenabled if (fallbackNickname.length > 0) { _.memory.FreeMany(fallbackNickname); fallbackNickname.length = 0; unusedNicknames.length = 0; } if (censoringNicknames) { censoringNicknames = false; _.players.OnPlayerNameChanging(self).Disconnect(); _.players.OnNewPlayer(self).Disconnect(); } } protected function SwapConfig(FeatureConfig config) { local bool configRequiresCensoring; 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; configRequiresCensoring = IsAnyCensoringEnabled(); // Enable or disable censoring if `IsAnyCensoringEnabled()`'s response // has changed. if (!censoringNicknames && configRequiresCensoring) { censoringNicknames = true; // Do this before adding event handler to // avoid censoring nicknames second time CensorCurrentPlayersNicknames(); _.players.OnPlayerNameChanging(self).connect = HandleNicknameChange; _.players.OnNewPlayer(self).connect = CensorOriginalNickname; } if (censoringNicknames && !configRequiresCensoring) { censoringNicknames = false; _.players.OnPlayerNameChanging(self).Disconnect(); _.players.OnNewPlayer(self).Disconnect(); } SwapFallbackNicknames(newConfig); } 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; } protected function SwapFallbackNicknames(FutilityNicknames newConfig) { local int i; _.memory.FreeMany(fallbackNickname); if (fallbackNickname.length > 0) { fallbackNickname.length = 0; } for (i = 0; i < newConfig.fallbackNickname.length; i += 1) { fallbackNickname[i] = _.text.FromFormattedString(newConfig.fallbackNickname[i]); } unusedNicknames = fallbackNickname; } 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, Text 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 Text.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 Text.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 // '_' }