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;
}