UnrealScript library and basis for all Acedia Framework mods
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

220 lines
7.7 KiB

/**
* 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 <https://www.gnu.org/licenses/>.
*/
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<Notification> 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<BaseText> 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
}