diff --git a/config/FutilityNicknames.ini b/config/FutilityNicknames.ini new file mode 100644 index 0000000..b7360f5 --- /dev/null +++ b/config/FutilityNicknames.ini @@ -0,0 +1,51 @@ +[default FutilityNicknames] +; 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. +autoEnable=true +; 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. +spacesAction=NSA_DoNothing +; 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. +colorPermissions=NCP_ForbidColor +; 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 ' '. +replaceSpacesWithUnderscores=true +; Max allowed nickname length. Negative values disable any length limits. +; +; NOTE: `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`). +maxNicknameLength=20 +; Should we replace empty player nicknames with a random fallback nickname +; (defined in `fallbackNickname` array)? +correctEmptyNicknames=true +; Array of fallback nicknames that will be used to replace any empty nicknames +; if `correctEmptyNicknames` is set to `true`. +fallbackNickname="Fresh Meat" +fallbackNickname="Rotten Meat" +fallbackNickname="Troll Meat" +fallbackNickname="Rat Meat" +fallbackNickname="Dog Meat" +fallbackNickname="Elk Meat" +fallbackNickname="Crab Meat" +fallbackNickname="Boar Meat" +fallbackNickname="Horker Meat" +fallbackNickname="Bug Meat" \ No newline at end of file diff --git a/sources/Features/FutileNickames/FutileNickames.uc b/sources/Features/FutileNickames/FutileNickames.uc new file mode 100644 index 0000000..a3ff4b6 --- /dev/null +++ b/sources/Features/FutileNickames/FutileNickames.uc @@ -0,0 +1,179 @@ +/** + * Config object for `FutileNickames_Feature`. + * 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 FutileNickames extends FeatureConfig + perobjectconfig + config(FutilityNicknames); + +enum NicknameSpacesAction +{ + NSA_DoNothing, + NSA_Trim, + NSA_Simplify +}; + +enum NicknameColorPermissions +{ + NCP_ForbidColor, + NCP_ForceTeamColor, + NCP_ForceSingleColor, + NCP_AllowAnyColor +}; + +var public config NicknameSpacesAction spacesAction; +var public config NicknameColorPermissions colorPermissions; +var public config bool replaceSpacesWithUnderscores; +var public config bool correctEmptyNicknames; +var public config int maxNicknameLength; +var public config array fallbackNickname; + +protected function AssociativeArray ToData() +{ + local int i; + local DynamicArray fallbackNicknamesData; + local AssociativeArray data; + data = __().collections.EmptyAssociativeArray(); + data.SetItem( P("spacesAction"), + _.text.FromString(string(spacesAction)), true); + data.SetItem( P("colorPermissions"), + _.text.FromString(string(colorPermissions)), true); + data.SetBool( P("replaceSpacesWithUnderscores"), + replaceSpacesWithUnderscores, true); + data.SetBool(P("correctEmptyNicknames"), correctEmptyNicknames, true); + data.SetInt(P("maxNicknameLength"), maxNicknameLength, true); + fallbackNicknamesData = __().collections.EmptyDynamicArray(); + for (i = 0; i < fallbackNickname.length; i += 1) + { + fallbackNicknamesData.AddItem( + __().text.FromFormattedString(fallbackNickname[i]), true); + } + data.SetItem(P("fallbackNickname"), fallbackNicknamesData, true); + return data; +} + +protected function FromData(AssociativeArray source) +{ + local int i; + local Text nextNickName; + local DynamicArray fallbackNicknamesData; + if (source == none) { + return; + } + spacesAction = SpaceActionFromText(source.GetText(P("spacesAction"))); + colorPermissions = ColorPermissionsFromText( + source.GetText(P("colorPermissions"))); + replaceSpacesWithUnderscores = + source.GetBool(P("replaceSpacesWithUnderscores"), true); + correctEmptyNicknames = source.GetBool(P("correctEmptyNicknames"), true); + maxNicknameLength = source.GetInt(P("correctEmptyNicknames"), 20); + fallbackNicknamesData = DynamicArray(source.GetItem(P("fallbackNickname"))); + if (fallbackNickname.length > 0) { + fallbackNickname.length = 0; + } + for (i = 0; i < fallbackNicknamesData.GetLength(); i += 1) + { + nextNickName = fallbackNicknamesData.GetText(i); + if (nextNickName != none) { + fallbackNickname[i] = nextNickName.ToFormattedString(); + } + else { + fallbackNickname[i] = ""; + } + } +} + +private function NicknameSpacesAction SpaceActionFromText(Text action) +{ + if (action == none) { + return NSA_DoNothing; + } + if (action.EndsWith(P("DoNothing"), SCASE_INSENSITIVE)) { + return NSA_DoNothing; + } + if (action.EndsWith(P("Trim"), SCASE_INSENSITIVE)) { + return NSA_Trim; + } + if (action.EndsWith(P("Simplify"), SCASE_INSENSITIVE)) { + return NSA_Simplify; + } + return NSA_DoNothing; +} + +private function NicknameColorPermissions ColorPermissionsFromText( + Text permissions) +{ + if (permissions == none) { + return NCP_ForbidColor; + } + if (permissions.EndsWith(P("ForbidColor"), SCASE_INSENSITIVE)) { + return NCP_ForbidColor; + } + if (permissions.EndsWith(P("TeamColor"), SCASE_INSENSITIVE)) { + return NCP_ForceTeamColor; + } + if (permissions.EndsWith(P("SingleColor"), SCASE_INSENSITIVE)) { + return NCP_ForceSingleColor; + } + if (permissions.EndsWith(P("AllowAnyColor"), SCASE_INSENSITIVE)) { + return NCP_AllowAnyColor; + } + return NCP_ForbidColor; +} + +protected function DefaultIt() +{ + spacesAction = NSA_DoNothing; + colorPermissions = NCP_ForbidColor; + replaceSpacesWithUnderscores = true; + correctEmptyNicknames = true; + maxNicknameLength = 20; + if (fallbackNickname.length > 0) { + fallbackNickname.length = 0; + } + fallbackNickname[0] = "Fresh Meat"; + fallbackNickname[1] = "Rotten Meat"; + fallbackNickname[2] = "Troll Meat"; + fallbackNickname[3] = "Rat Meat"; + fallbackNickname[4] = "Dog Meat"; + fallbackNickname[5] = "Elk Meat"; + fallbackNickname[6] = "Crab Meat"; + fallbackNickname[7] = "Boar Meat"; + fallbackNickname[8] = "Horker Meat"; + fallbackNickname[9] = "Bug Meat"; +} + +defaultproperties +{ + configName = "FutilityNicknames" + spacesAction = NSA_DoNothing + colorPermissions = NCP_ForbidColor + replaceSpacesWithUnderscores = true + correctEmptyNicknames = true + maxNicknameLength = 20 + fallbackNickname(0) = "Fresh Meat" + fallbackNickname(1) = "Rotten Meat" + fallbackNickname(2) = "Troll Meat" + fallbackNickname(3) = "Rat Meat" + fallbackNickname(4) = "Dog Meat" + fallbackNickname(5) = "Elk Meat" + fallbackNickname(6) = "Crab Meat" + fallbackNickname(7) = "Boar Meat" + fallbackNickname(8) = "Horker Meat" + fallbackNickname(9) = "Bug Meat" +} \ No newline at end of file diff --git a/sources/Features/FutileNickames/FutileNickames_Feature.uc b/sources/Features/FutileNickames/FutileNickames_Feature.uc new file mode 100644 index 0000000..b189de8 --- /dev/null +++ b/sources/Features/FutileNickames/FutileNickames_Feature.uc @@ -0,0 +1,340 @@ +/** + * 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 FutileNickames_Feature extends Feature + dependson(FutileNickames); + +/** + * 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*/ FutileNickames.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*/ FutileNickames.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; + +// Max allowed nickname length. Negative values disable any length limits. +// +// NOTE: `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`). +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. Enforce max limit of nickname's length; +// 3. Replace empty nickname with fallback nickname (no further changes +// will be applied to fallback nickname in that case); +// 4. Enforce color limitation; +// 5. 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 FutileNickames newConfig; + newConfig = FutileNickames(config); + if (newConfig == none) { + return; + } + replaceSpacesWithUnderscores = newConfig.replaceSpacesWithUnderscores; + 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(FutileNickames 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 + || 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 (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'FutileNickames' + CODEPOINT_UNDERSCORE = 95 // '_' +} \ No newline at end of file diff --git a/sources/Manifest.uc b/sources/Manifest.uc index 0fde09e..7d1d965 100644 --- a/sources/Manifest.uc +++ b/sources/Manifest.uc @@ -23,4 +23,5 @@ defaultproperties { features(0) = class'Futility_Feature' + features(1) = class'FutileNickames_Feature' } \ No newline at end of file