diff --git a/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc index d992449..c24e018 100644 --- a/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc +++ b/sources/BaseAPI/API/Commands/BuiltInCommands/ACommandNotify.uc @@ -34,6 +34,12 @@ protected function BuildData(CommandDataBuilder builder) { builder.Option(P("title")); builder.Describe(P("Specify the optional title of the notification.")); builder.ParamText(P("title")); + + builder.Option(P("channel")); + builder.Describe(P("Specify the optional channel. A channel is a grouping mechanism used to" + @ "control the display of related notifications. Only last message from the same channel is" + @ "stored in queue.")); + builder.ParamText(P("channel_name")); } protected function ExecutedFor(EPlayer target, CallData arguments, EPlayer instigator) { @@ -45,7 +51,11 @@ protected function ExecutedFor(EPlayer target, CallData arguments, EPlayer insti } title = _.text.FromFormatted(plainTitle); message = _.text.FromFormatted(plainMessage); - target.Notify(title, message, arguments.parameters.GetFloat(P("duration"))); + target.Notify( + title, + message, + arguments.parameters.GetFloat(P("duration")), + arguments.options.GetTextBy(P("/channel/channel_name"))); _.memory.Free4(title, message, plainTitle, plainMessage); } diff --git a/sources/BaseAPI/API/Commands/Voting/Voting.uc b/sources/BaseAPI/API/Commands/Voting/Voting.uc index 9c0b6e8..0393430 100644 --- a/sources/BaseAPI/API/Commands/Voting/Voting.uc +++ b/sources/BaseAPI/API/Commands/Voting/Voting.uc @@ -141,7 +141,7 @@ public final function Start(BaseText votingConfigName) { voters = UpdateVoters(); howToVoteHint = MakeHowToVoteHint(); for (i = 0; i < voters.length; i += 1) { - voters[i].Notify(F(votingStartedLine), howToVoteHint); + voters[i].Notify(F(votingStartedLine), howToVoteHint,, P("voting")); voters[i].BorrowConsole().WriteLine(F(votingStartedLine)); voters[i].BorrowConsole().WriteLine(howToVoteHint); } @@ -420,7 +420,7 @@ private final function AnnounceOutcome(BaseText outcomeMessage) { for (i = 0; i < currentPlayers.length; i += 1) { currentPlayers[i].BorrowConsole().WriteLine(outcomeMessage); currentPlayers[i].BorrowConsole().WriteLine(summaryLine); - currentPlayers[i].Notify(outcomeMessage, summaryLine); + currentPlayers[i].Notify(outcomeMessage, summaryLine,, P("voting")); } _.memory.FreeMany(currentPlayers); _.memory.Free(summaryLine); diff --git a/sources/Players/EPlayer.uc b/sources/Players/EPlayer.uc index 1dc7fef..20dd00e 100644 --- a/sources/Players/EPlayer.uc +++ b/sources/Players/EPlayer.uc @@ -504,12 +504,28 @@ public final function /* borrow */ ConsoleWriter BorrowConsole() return consoleInstance.ForPlayer(self); } -/// Notifies player about something with a text message. +/// Sends a text message to notify the player about a particular event or situation, displayed +/// as a notification. /// -/// Header is allowed to be `none`, but it is recommended to set it to an actual value. -/// Duration is more of a suggestion and might be clamped to some reasonable value that depends on -/// implementation/server settings. -public final function Notify(BaseText header, BaseText body, optional float duration) { +/// While the header parameter is optional, it is recommended to provide a meaningful header +/// to give the player context about the notification. The duration parameter suggests the time +/// that the message should be displayed for, but the actual duration may be limited by the +/// implementation or server settings. +/// +/// The [`channel`] parameter (case-sensitive) allows you to group related notifications together by +/// assigning them to a specific channel. +/// Each channel can only have one message in the queue at any given time. +/// For instance, when conducting a vote, the old message about the vote's start can be quickly +/// replaced with a newer message about the vote's end. +/// Notifications without a channel `none` can have any number of messages queued up. +/// +/// Acedia uses "voting" channel for voting. +public final function Notify( + BaseText header, + BaseText body, + optional float duration, + optional BaseText channel +) { local HashTable sessionData; local PlayerNotificationQueue messageQueue; @@ -524,7 +540,7 @@ public final function Notify(BaseText header, BaseText body, optional float dura sessionData.SetItem(P("MessageQueue"), messageQueue); } messageQueue.SetupController(controller); - messageQueue.AddNotification(header, body, duration); + messageQueue.AddNotification(header, body, duration, channel); _.memory.Free2(sessionData, messageQueue); } diff --git a/sources/Players/PlayerNotificationQueue.uc b/sources/Players/PlayerNotificationQueue.uc index 5880e1f..db4c68b 100644 --- a/sources/Players/PlayerNotificationQueue.uc +++ b/sources/Players/PlayerNotificationQueue.uc @@ -22,15 +22,32 @@ class PlayerNotificationQueue extends AcediaObject config(AcediaSystem); -/// Manages queue of notifications that should be displayed for a certain player. +//! Manages queue of notifications that should be displayed for a certain player. +//! +//! Pushes messages one-by-one, once its their time to be displayed and making sure that only up to +//! one message per channel is ever in queue. +//! +//! A channel is a way to group related notifications together. +//! Its purpose is to control the display of notifications in a more organized and efficient manner. +//! Each channel can only have one message in the queue at a time. +//! This ensures that only the most relevant or up-to-date message is displayed to the player. -/// Describes a single notification: title (optional, can be `none`) + message body and timeout +/// Describes a single notification: title (optional, can be `none`) + message body, channel +/// and timeout. struct Notification { var Text title; var Text body; var float duration; + var Text channel; }; var private array notificationQueue; + +/// We need this variable to keep track of which channel the currently displayed message belongs to, +/// so that we can ensure that only one message is displayed at a time for each channel. +/// This variable allows us to easily check if a new message belongs to the same channel as +/// the currently displayed message, and if so, replace the currently displayed message with +/// the new one. +var private Text currentChannel; /// Reference to the `PlayerController` for the player that owns this queue var private NativeActorRef playerControllerRef; /// Timer until next notification can be displayed @@ -80,8 +97,29 @@ public final /*native*/ function SetupController(NativeActorRef newPlayerControl } } -/// Add new notification to the queue -public final function AddNotification(BaseText title, BaseText body, float duration) { +/// Sends a text message to notify the player about a particular event or situation, displayed +/// as a notification. +/// +/// While the header parameter is optional, it is recommended to provide a meaningful header +/// to give the player context about the notification. The duration parameter suggests the time +/// that the message should be displayed for, but the actual duration may be limited by the +/// implementation or server settings. +/// +/// The [`channel`] parameter (case-sensitive) allows you to group related notifications together by +/// assigning them to a specific channel. +/// Each channel can only have one message in the queue at any given time. +/// For instance, when conducting a vote, the old message about the vote's start can be quickly +/// replaced with a newer message about the vote's end. +/// Notifications without a channel `none` can have any number of messages queued up. +/// +/// Acedia uses "voting" channel for voting. +public final function AddNotification( + BaseText title, + BaseText body, + float duration, + BaseText channel +) { + local int i, newNotificationIndex; local Notification newNotification; if (body == none) { @@ -90,9 +128,30 @@ public final function AddNotification(BaseText title, BaseText body, float durat if (title != none) { newNotification.title = title.Copy(); } + if (channel != none) { + newNotification.channel = channel.Copy(); + } newNotification.body = body.Copy(); newNotification.duration = duration; - notificationQueue[notificationQueue.length] = newNotification; + newNotificationIndex = notificationQueue.length; + // Make sure there is only one message per channel by replacing the old one + if (currentChannel != none && currentChannel.Compare(channel)) { + SetupNotification(/*take*/ newNotification); + return; + } + if (channel != none) { + for (i = 0; i < notificationQueue.length; i += 1) { + if (channel.Compare(notificationQueue[i].channel)) { + _.memory.Free3( + notificationQueue[i].title, + notificationQueue[i].body, + notificationQueue[i].channel); + newNotificationIndex = i; + break; + } + } + } + notificationQueue[newNotificationIndex] = newNotification; if (!IsUpdateScheduled()) { SetupNextNotification(none); } @@ -177,9 +236,20 @@ private function PrintNotifcationAt( } private function SetupNextNotification(Timer callerInstance) { + local Notification nextNotification; + + if (notificationQueue.length <= 0) { + InterruptScheduling(); + return; + } + nextNotification = notificationQueue[0]; + notificationQueue.Remove(0, 1); + SetupNotification(/*take*/ nextNotification); +} + +private function SetupNotification(/*take*/ Notification notification) { local int titleShift; local MutableText upperCaseTitle; - local Notification nextNotification; local PlayerController playerController; // Get appropriate [`PlayerController`] and next notification @@ -187,31 +257,30 @@ private function SetupNextNotification(Timer callerInstance) { if (playerController == none) { _.memory.Free(playerControllerRef); playerControllerRef = none; - } - if (notificationQueue.length <= 0 || playerController == none) { InterruptScheduling(); return; } - nextNotification = notificationQueue[0]; - notificationQueue.Remove(0, 1); - nextNotification.duration = FMin(nextNotification.duration, maximumNotifyTime); - if (nextNotification.duration <= 0) { - nextNotification.duration = 10.0; + _.memory.Free(currentChannel); + currentChannel = notification.channel; + notification.duration = FMin(notification.duration, maximumNotifyTime); + if (notification.duration <= 0) { + notification.duration = 10.0; } // And print playerController.ClearProgressMessages(); - playerController.SetProgressTime(nextNotification.duration); - if (nextNotification.title != none) { - upperCaseTitle = nextNotification.title.UpperMutableCopy(); + playerController.SetProgressTime(notification.duration); + if (notification.title != none) { + upperCaseTitle = notification.title.UpperMutableCopy(); upperCaseTitle.ChangeDefaultColor(_.color.TextHeader); PrintNotifcationAt(playerController, upperCaseTitle, 0, 1); titleShift = 1; _.memory.Free(upperCaseTitle); } - PrintNotifcationAt(playerController, nextNotification.body, titleShift, 4 - titleShift); - ScheduleUpdate(nextNotification.duration); - _.memory.Free2(nextNotification.title, nextNotification.body); + PrintNotifcationAt(playerController, notification.body, titleShift, 4 - titleShift); + ScheduleUpdate(notification.duration); + // We moved channel's reference into `currentChannel` + _.memory.Free2(notification.title, notification.body); } defaultproperties {