fix: retain notifications while the scene background is absent

Buffer reward messages until the shell returns without allocating detached UI nodes. Register mod tests that verify notification ordering and prevent duplicates.
This commit is contained in:
cthangover committed 2026-11-04 08:41:19 +01:00
1 parent 69d3241a33
commit 51350bf476
3 files changed
+71 -14

No files matched your search

+3
View File
@@ -9,6 +9,9 @@
"sources": [
"src/**/*.cs"
],
"test_sources": [
"tests/*.cs"
],
"ui": [
{
"id": "game_menu",
+17 -14
View File
@@ -12,7 +12,8 @@ namespace Cthangover.Mods.Interface.Messages
/// pickup notifications). Manages a cycling position index of 0-5 so
/// consecutive messages stack vertically without overlapping. Each message
/// is instantiated from message.tscn and parented to the Background node
/// (found via scene tree search). Messages auto-fade and self-destruct
/// (found via scene tree search). Without that shell, only the text is buffered for its return;
/// no detached native controls are allocated. Visible messages auto-fade and self-destruct
/// via UiMessage's OnUpdate.
/// </summary>
public static class MessagesHelper
@@ -85,6 +86,7 @@ namespace Cthangover.Mods.Interface.Messages
/// <summary>
/// Instantiates a <see cref="UiMessage"/> from <c>message.tscn</c>, parents
/// it to the Background node, and configures it for display and auto-fade.
/// A scene without a background queues the text until the shell returns.
/// </summary>
/// <param name="position">Y-offset in pixels for this message.</param>
/// <param name="text">The message text to display.</param>
@@ -92,22 +94,23 @@ namespace Cthangover.Mods.Interface.Messages
/// <param name="speed">Fade speed multiplier (default 1).</param>
public static void AddMessage(float position, string text, Color color, float speed = 1f)
{
if (prefab == null)
prefab = new ModScenePrefab("scenes/ui/message.tscn");
var instance = prefab.Instantiate<UiMessage>();
var background = SceneContextNode.FindNode<Control>("Background");
// Battle and menu scenes may have no shell background. Resolve the owner before allocating native nodes.
var tree = SceneContextNode.Instance?.GetTree();
var background = tree?.Root.FindChild("Background", true, false) as Control
?? tree?.GetFirstNodeInGroup("Background") as Control;
if (background == null)
{
background = SceneContextNode.Instance?.GetTree()?.GetFirstNodeInGroup("Background") as Control;
if (background == null)
{
GameLogger.Log("UI", "Background node not found for messages", LogLevel.Error);
return;
}
// Keep battle rewards until the shell returns, without creating a detached message tree.
NotificationBuffer.Add(text);
return;
}
prefab ??= new ModScenePrefab("scenes/ui/message.tscn");
var instance = prefab.Instantiate<UiMessage>();
if (instance != null)
{
background.AddChild(instance);
instance.Setup(position, text, speed, color);
}
background.AddChild(instance);
instance.Setup(position, text, speed, color);
ModEventBus.Publish(new NotificationRequestedEvent
{
+51
View File
@@ -0,0 +1,51 @@
namespace Cthangover.Mods.Interface.Messages.Tests
{
using System.Collections.Generic;
using Cthangover.Core.Mods.Testing;
using Cthangover.Core.Scenes;
using Cthangover.Core.UI.Messages;
using Godot;
/// <summary>Battle/menu notifications must survive a missing shell without allocating orphan UI trees.</summary>
public class MessagesHelperTests
{
[ModTest("messages/without_background_preserves_each_notification")]
static void WithoutBackground(ModTestContext ctx)
{
var root = SceneContextNode.Instance.GetTree().Root;
var background = root.FindChild("Background", true, false) as Control;
var originalName = background?.Name ?? default;
var grouped = background?.IsInGroup("Background") == true;
var previous = new List<string>();
while (NotificationBuffer.TryDequeue(out var pendingText)) previous.Add(pendingText);
try
{
// The shared test scene has a background; model a battle without detaching or freeing borrowed UI.
if (background != null)
{
background.Name = "MessagesContractBackground";
if (grouped) background.RemoveFromGroup("Background");
}
ModAssert.True(root.FindChild("Background", true, false) == null, "fixture has no shell background");
for (var i = 0; i < 40; i++) MessagesHelper.AddMessage("reward/" + i);
ModAssert.Equal(40, NotificationBuffer.PendingCount, "every late reward is buffered exactly once");
for (var i = 0; i < 40; i++)
{
ModAssert.True(NotificationBuffer.TryDequeue(out var actual), "notification is retained");
ModAssert.Equal("reward/" + i, actual, "text and order survive until the shell returns");
}
ModAssert.Equal(0, NotificationBuffer.PendingCount, "no duplicate banners remain");
}
finally
{
if (background != null)
{
background.Name = originalName;
if (grouped) background.AddToGroup("Background");
}
NotificationBuffer.Clear();
foreach (var previousText in previous) NotificationBuffer.Add(previousText);
}
}
}
}