/** * Author: dkanus * Home repo: https://www.insultplayers.ru/git/AcediaFramework/AcediaCore * License: GPL * Copyright 2023 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 PlayerNotificationQueue extends AcediaObject config(AcediaSystem); /// Manages queue of notifications that should be displayed for a certain player. /// Describes a single notification: title (optional, can be `none`) + message body and timeout struct Notification { var Text title; var Text body; var float duration; }; var private array notificationQueue; /// Reference to the `PlayerController` for the player that owns this queue var private NativeActorRef playerControllerRef; /// Timer until next notification can be displayed /// /// `none` if queue currently doesn't display any notifications var private Timer nextNotificationTimer; /// Maximum time that a notification is allowed to be displayed on the player's screen var private const config float maximumNotifyTime; var private const int CODEPOINT_NEWLINE; protected function Finalizer() { local int i; for (i = 0; i < notificationQueue.length; i += 1) { _.memory.Free(notificationQueue[i].title); _.memory.Free(notificationQueue[i].body); } notificationQueue.length = 0; _.memory.Free(nextNotificationTimer); _.memory.Free(playerControllerRef); nextNotificationTimer = none; playerControllerRef = none; } /// Set owner `PlayerController` for this queue public final /*native*/ function SetupController(NativeActorRef newPlayerControllerRef) { local PlayerController oldController, newController; if (playerControllerRef != none) { oldController = PlayerController(playerControllerRef.Get()); } if (newPlayerControllerRef != none) { newController = PlayerController(newPlayerControllerRef.Get()); } if (oldController != newController) { InterruptScheduling(); _.memory.Free(playerControllerRef); if (newPlayerControllerRef != none) { newPlayerControllerRef.NewRef(); playerControllerRef = newPlayerControllerRef; SetupNextNotification(none); } else { playerControllerRef = none; } } } /// Add new notification to the queue public final function AddNotification(BaseText title, BaseText body, float duration) { local Notification newNotification; if (body == none) { return; } if (title != none) { newNotification.title = title.Copy(); } newNotification.body = body.Copy(); newNotification.duration = duration; notificationQueue[notificationQueue.length] = newNotification; if (!IsUpdateScheduled()) { SetupNextNotification(none); } } // Sets up [`SetupNextNotification()`] to be called after [`timeUntilNextUpdate`] private function ScheduleUpdate(float timeUntilNextUpdate) { if (nextNotificationTimer == none) { nextNotificationTimer = __level().time.StartRealTimer(timeUntilNextUpdate, false); nextNotificationTimer.OnElapsed(nextNotificationTimer).connect = SetupNextNotification; } else { nextNotificationTimer.SetInterval(timeUntilNextUpdate); nextNotificationTimer.Start(); } } private function bool IsUpdateScheduled() { return (nextNotificationTimer != none); } // Prevents scheduled [`SetupNextNotification()`] call from going off private function InterruptScheduling() { _.memory.Free(nextNotificationTimer); nextNotificationTimer = none; } // Properly translates [`contents`] into a colored [`string`] before setting it up as // a [`PlayerController`]'s progress message at the given line index. private function SetupProgressLine( int lineIndex, BaseText contents, PlayerController playerController ) { local string contentsAsString; local BaseText.Formatting startingFormatting; if (contents == none) return; if (playerController == none) return; // Drop first colored tag, since we'll set first color through `playerController.progressColor` contentsAsString = Mid(contents.ToColoredString(,, _.color.white), 4); startingFormatting = contents.GetFormatting(0); if (startingFormatting.isColored) { playerController.SetProgressMessage(lineIndex, contentsAsString, startingFormatting.color); } else { playerController.SetProgressMessage(lineIndex, contentsAsString, _.color.white); } } // Prints [`notification`] on given lines, respecting line breaks inside it as much as possible // (creates up to [`maxLines`], replacing the rest of line breaks with whitespaces) private function PrintNotifcationAt( PlayerController playerController, BaseText notification, int startingLine, int maxLines ) { local int i, j; local MutableText lastLine; local array lines; if (notification == none) return; if (startingLine < 0) return; if (startingLine > 3) return; lines = notification.SplitByCharacter(_.text.CharacterFromCodePoint(CODEPOINT_NEWLINE),, true); for (i = 0; i < lines.length; i += 1) { if (i + 1 < maxLines) { SetupProgressLine(i + startingLine, lines[i], playerController); } else { lastLine = lines[i].MutableCopy(); for (j = i + 1; j < lines.length; j += 1) { lastLine.Append(P(" ")); lastLine.Append(lines[j]); } SetupProgressLine(startingLine + maxLines - 1, lastLine, playerController); _.memory.Free(lastLine); break; } } } private function SetupNextNotification(Timer callerInstance) { local int titleShift; local MutableText upperCaseTitle; local Notification nextNotification; local PlayerController playerController; // Get appropriate [`PlayerController`] and next notification playerController = PlayerController(playerControllerRef.Get()); 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; } // And print playerController.ClearProgressMessages(); playerController.SetProgressTime(nextNotification.duration); if (nextNotification.title != none) { upperCaseTitle = nextNotification.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); } defaultproperties { CODEPOINT_NEWLINE = 10 maximumNotifyTime = 20.0 }