diff --git a/sources/Players/EPlayer.uc b/sources/Players/EPlayer.uc index ad58c7e..7b0217d 100644 --- a/sources/Players/EPlayer.uc +++ b/sources/Players/EPlayer.uc @@ -30,12 +30,9 @@ var private int consoleLifeVersion; // `PlayerController` reference var private NativeActorRef controller; -// These variables record name of this player; -// `hashedName` is used to track outside changes that bypass our getter/setter. -var private Text textName; -var private string hashedName; - -// Describes the player's admin status (as defined by standard KF classes) +/** + * Describes the player's admin status (as defined by standard KF classes) + */ enum AdminStatus { // Not an admin @@ -46,6 +43,16 @@ enum AdminStatus AS_SilentAdmin }; +// Stores all the types of signals `EPlayer` might emit +struct PlayerSignals +{ + var public PlayerAPI_OnPlayerNameChanging_Signal onNameChanging; + var public PlayerAPI_OnPlayerNameChanged_Signal onNameChanged; +}; +// We do not own objects in this structure, but it is created and managed by +// `PlayersAPI` and is expected to be allocated during the whole Acedia run. +var protected PlayerSignals signalsReferences; + protected function Finalizer() { _.memory.Free(controller); @@ -74,10 +81,10 @@ protected function Finalizer() * @return `true` if initialization was successful and `false` otherwise. */ public final /* unreal */ function bool Initialize( - PlayerController initController) + PlayerController initController, + PlayerSignals playerSignals) { - local Text idHash; - local PlayerReplicationInfo myReplicationInfo; + local Text idHash; if (controller != none) return false; // Already initialized! if (initController == none) return false; @@ -91,14 +98,8 @@ public final /* unreal */ function bool Initialize( idHash.FreeSelf(); idHash = none; } - controller = _.unreal.ActorRef(initController); - myReplicationInfo = initController.playerReplicationInfo; - // Hash current name - if (myReplicationInfo != none) - { - hashedName = myReplicationInfo.playerName; - textName = _.text.FromColoredString(hashedName); - } + signalsReferences = playerSignals; + controller = _.unreal.ActorRef(initController); return true; } @@ -121,7 +122,8 @@ public function EInterface Copy() return playerCopy; } playerCopy.identity = identity; - playerCopy.Initialize(PlayerController(controller.Get())); + playerCopy.Initialize( PlayerController(controller.Get()), + signalsReferences); return playerCopy; } @@ -216,16 +218,10 @@ public final function Text GetName() { local PlayerReplicationInfo myReplicationInfo; myReplicationInfo = GetRI(); - if (myReplicationInfo == none) { - return P("").Copy(); + if (myReplicationInfo != none) { + return _.text.FromColoredString(myReplicationInfo.playerName); } - if (textName != none && myReplicationInfo.playerName == hashedName) { - return textName.Copy(); - } - _.memory.Free(textName); - hashedName = myReplicationInfo.playerName; - textName = _.text.FromColoredString(hashedName); - return textName.Copy(); + return P("").Copy(); } /** @@ -236,40 +232,73 @@ public final function Text GetName() */ public final function SetName(Text newPlayerName) { - local Text.Formatting endingFormatting; - local PlayerReplicationInfo myReplicationInfo; - myReplicationInfo = GetRI(); - if (myReplicationInfo == none) return; - - _.memory.Free(textName); - // Filter both `none` and empty `newPlayerName`, so that we can - // later rely on it having at least one character - if (newPlayerName == none || newPlayerName.IsEmpty()) { - textName = P("").Copy(); + local Text oldPlayerName; + local PlayerReplicationInfo replicationInfo; + replicationInfo = GetRI(); + if (replicationInfo == none) { + return; + } + if (ConvertTextNameIntoString(newPlayerName) == replicationInfo.playerName) + { + return; } - else { - textName = newPlayerName.Copy(); + oldPlayerName = _.text.FromFormattedString(replicationInfo.playerName); + replicationInfo.playerName = CensorPlayerName(oldPlayerName, newPlayerName); + _.memory.Free(oldPlayerName); +} + +// Converts `Text` nickname into a suitable `string` representation. +private final function string ConvertTextNameIntoString(Text playerName) +{ + local string newPlayerNameAsString; + local Text.Formatting endingFormatting; + if (playerName == none) { + return ""; } - hashedName = textName.ToColoredString(,, _.color.white); + newPlayerNameAsString = playerName.ToColoredString(,, _.color.white); // To correctly display nicknames we want to drop default color tag // at the beginning (the one `ToColoredString()` adds if first character // has no defined color). // This is a compatibility consideration with vanilla UIs that use // color codes from `myReplicationInfo.playerName` for displaying nicknames // and whose expected behavior can get broken by default color tag. - if (!textName.GetFormatting(0).isColored) { - hashedName = Mid(hashedName, 4); + if (!playerName.GetFormatting(0).isColored) { + newPlayerNameAsString = Mid(newPlayerNameAsString, 4); } // This is another compatibility consideration with vanilla UIs: unless // we restore color to neutral white, Killing Floor will paint any chat // messages we send in the color our nickname ended with. - endingFormatting = textName.GetFormatting(textName.GetLength() - 1); + endingFormatting = playerName.GetFormatting(playerName.GetLength() - 1); if ( endingFormatting.isColored && !_.color.AreEqual(endingFormatting.color, _.color.white, true)) { - hashedName $= _.color.GetColorTag(_.color.white); + newPlayerNameAsString $= _.color.GetColorTag(_.color.white); + } + return newPlayerNameAsString; +} + +// Calls appropriate events to let them modify / "censor" player's new name. +private final function string CensorPlayerName( + Text oldPlayerName, + Text newPlayerName) +{ + local string result; + local Text censoredName; + local MutableText mutablePlayerName; + if (newPlayerName == none) { + return ""; } - myReplicationInfo.playerName = hashedName; + mutablePlayerName = newPlayerName.MutableCopy(); + // Let signal handlers alter the name + signalsReferences.onNameChanging + .Emit(self, oldPlayerName, mutablePlayerName); + censoredName = mutablePlayerName.Copy(); + signalsReferences.onNameChanged.Emit(self, oldPlayerName, censoredName); + // Returns "censored" result + result = ConvertTextNameIntoString(censoredName); + _.memory.Free(mutablePlayerName); + _.memory.Free(censoredName); + return result; } // TODO: replace this, it has no place here diff --git a/sources/Players/Events/PlayerAPI_OnPlayerNameChanged_Signal.uc b/sources/Players/Events/PlayerAPI_OnPlayerNameChanged_Signal.uc new file mode 100644 index 0000000..f78f69b --- /dev/null +++ b/sources/Players/Events/PlayerAPI_OnPlayerNameChanged_Signal.uc @@ -0,0 +1,39 @@ +/** + * Signal class implementation for `PlayerAPI`'s `OnPlayerNameChanged` signal. + * 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 PlayerAPI_OnPlayerNameChanged_Signal extends Signal; + +public final function Emit(EPlayer player, Text oldName, Text newName) +{ + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + PlayerAPI_OnPlayerNameChanged_Slot(nextSlot) + .connect(player, oldName, newName); + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); +} + +defaultproperties +{ + relatedSlotClass = class'PlayerAPI_OnPlayerNameChanged_Slot' +} \ No newline at end of file diff --git a/sources/Players/Events/PlayerAPI_OnPlayerNameChanged_Slot.uc b/sources/Players/Events/PlayerAPI_OnPlayerNameChanged_Slot.uc new file mode 100644 index 0000000..74488ba --- /dev/null +++ b/sources/Players/Events/PlayerAPI_OnPlayerNameChanged_Slot.uc @@ -0,0 +1,40 @@ +/** + * Slot class implementation for `PlayerAPI`'s `OnPlayerNameChanged` signal. + * 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 PlayerAPI_OnPlayerNameChanged_Slot extends Slot; + +delegate connect(EPlayer player, Text oldName, Text newName) +{ + DummyCall(); +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Players/Events/PlayerAPI_OnPlayerNameChanging_Signal.uc b/sources/Players/Events/PlayerAPI_OnPlayerNameChanging_Signal.uc new file mode 100644 index 0000000..bafb91f --- /dev/null +++ b/sources/Players/Events/PlayerAPI_OnPlayerNameChanging_Signal.uc @@ -0,0 +1,39 @@ +/** + * Signal class implementation for `PlayerAPI`'s `OnPlayerNameChanging` signal. + * 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 PlayerAPI_OnPlayerNameChanging_Signal extends Signal; + +public final function Emit(EPlayer player, Text oldName, MutableText newName) +{ + local Slot nextSlot; + StartIterating(); + nextSlot = GetNextSlot(); + while (nextSlot != none) + { + PlayerAPI_OnPlayerNameChanging_Slot(nextSlot) + .connect(player, oldName, newName); + nextSlot = GetNextSlot(); + } + CleanEmptySlots(); +} + +defaultproperties +{ + relatedSlotClass = class'PlayerAPI_OnPlayerNameChanging_Slot' +} \ No newline at end of file diff --git a/sources/Players/Events/PlayerAPI_OnPlayerNameChanging_Slot.uc b/sources/Players/Events/PlayerAPI_OnPlayerNameChanging_Slot.uc new file mode 100644 index 0000000..4be3110 --- /dev/null +++ b/sources/Players/Events/PlayerAPI_OnPlayerNameChanging_Slot.uc @@ -0,0 +1,40 @@ +/** + * Slot class implementation for `PlayerAPI`'s `OnPlayerNameChanging` signal. + * 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 PlayerAPI_OnPlayerNameChanging_Slot extends Slot; + +delegate connect(EPlayer player, Text oldName, MutableText newName) +{ + DummyCall(); +} + +protected function Constructor() +{ + connect = none; +} + +protected function Finalizer() +{ + super.Finalizer(); + connect = none; +} + +defaultproperties +{ +} \ No newline at end of file diff --git a/sources/Players/PlayersAPI.uc b/sources/Players/PlayersAPI.uc index fae24cc..2fa7f18 100644 --- a/sources/Players/PlayersAPI.uc +++ b/sources/Players/PlayersAPI.uc @@ -1,6 +1,6 @@ /** * API that provides functions for working player references (`EPlayer`). - * Copyright 2021 Anton Tarasenko + * Copyright 2021 - 2022 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -19,7 +19,8 @@ */ class PlayersAPI extends AcediaObject dependson(ConnectionService) - dependson(Text); + dependson(Text) + dependson(EPlayer); // Writer that can be used to write into this player's console var private ConsoleWriter consoleInstance; @@ -30,12 +31,18 @@ var protected bool connectedToConnectionServer; var protected PlayerAPI_OnNewPlayer_Signal onNewPlayerSignal; var protected PlayerAPI_OnLostPlayer_Signal onLostPlayerSignal; +var private EPlayer.PlayerSignals playerSignals; + protected function Constructor() { onNewPlayerSignal = PlayerAPI_OnNewPlayer_Signal( _.memory.Allocate(class'PlayerAPI_OnNewPlayer_Signal')); onLostPlayerSignal = PlayerAPI_OnLostPlayer_Signal( _.memory.Allocate(class'PlayerAPI_OnLostPlayer_Signal')); + playerSignals.onNameChanging = PlayerAPI_OnPlayerNameChanging_Signal( + _.memory.Allocate(class'PlayerAPI_OnPlayerNameChanging_Signal')); + playerSignals.onNameChanged = PlayerAPI_OnPlayerNameChanged_Signal( + _.memory.Allocate(class'PlayerAPI_OnPlayerNameChanged_Signal')); } protected function Finalizer() @@ -50,8 +57,12 @@ protected function Finalizer() } _.memory.Free(onNewPlayerSignal); _.memory.Free(onLostPlayerSignal); - onNewPlayerSignal = none; - onLostPlayerSignal = none; + _.memory.Free(playerSignals.onNameChanging); + _.memory.Free(playerSignals.onNameChanged); + onNewPlayerSignal = none; + onLostPlayerSignal = none; + playerSignals.onNameChanging = none; + playerSignals.onNameChanged = none; } /** @@ -89,6 +100,52 @@ public function PlayerAPI_OnLostPlayer_Slot OnLostPlayerHandle( return PlayerAPI_OnLostPlayer_Slot(onLostPlayerSignal.NewSlot(receiver)); } +/** + * Signal that will be emitted once player's name attempt to change. + * + * This signal gives all handlers a change to modify mutable `newName`, + * so the one you are given as a parameter might not be final, since other + * handlers can modify it after you. + * If you simply need to see the final version of the changed name - + * use `OnPlayerNameChanged` instead. + * + * [Signature] + * void (EPlayer affectedPlayer, Text oldName, MutableText newName) + * + * @param affectedPlayer Player, whos name got changed. + * @param oldName Player's old name. + * @param newName Player's new name. Can be modified, if you want + * to make corrections. + */ +/* SIGNAL */ +public function PlayerAPI_OnPlayerNameChanging_Slot OnPlayerNameChanging( + AcediaObject receiver) +{ + return PlayerAPI_OnPlayerNameChanging_Slot(playerSignals.onNameChanging + .NewSlot(receiver)); +} + +/** + * Signal that will be emitted once player's name is changed. + * + * This signal simply notifies you of the changed name, if you wish to alter it + * after change caused by someone else, use `OnPlayerNameChanging` instead. + * + * [Signature] + * void (EPlayer affectedPlayer, Text oldName, Text newName) + * + * @param affectedPlayer Player, whos name got changed. + * @param oldName Player's old name. + * @param newName Player's new name. + */ +/* SIGNAL */ +public function PlayerAPI_OnPlayerNameChanged_Slot OnPlayerNameChanged( + AcediaObject receiver) +{ + return PlayerAPI_OnPlayerNameChanged_Slot(playerSignals.onNameChanged + .NewSlot(receiver)); +} + /** * Return `ConsoleWriter` that can be used to write into every player's * console. @@ -163,7 +220,7 @@ public final /* unreal */ function EPlayer FromController( { local EPlayer result; result = EPlayer(_.memory.Allocate(class'EPlayer')); - result.Initialize(controller); + result.Initialize(controller, playerSignals); return result; }