Browse Source

Add `FutileNicknames` feature

feature_improvement
Anton Tarasenko 3 years ago
parent
commit
89a376b0cb
  1. 51
      config/FutilityNicknames.ini
  2. 179
      sources/Features/FutileNickames/FutileNickames.uc
  3. 340
      sources/Features/FutileNickames/FutileNickames_Feature.uc
  4. 1
      sources/Manifest.uc

51
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"

179
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 <https://www.gnu.org/licenses/>.
*/
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<string> 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"
}

340
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 <https://www.gnu.org/licenses/>.
*/
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<Text> 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<Text> 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<EPlayer> 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 // '_'
}

1
sources/Manifest.uc

@ -23,4 +23,5 @@
defaultproperties
{
features(0) = class'Futility_Feature'
features(1) = class'FutileNickames_Feature'
}
Loading…
Cancel
Save