diff --git a/manifest.json b/manifest.json index 74d66ed..c7fba93 100644 --- a/manifest.json +++ b/manifest.json @@ -9,6 +9,9 @@ "sources": [ "src/**/*.cs" ], + "test_sources": [ + "tests/*.cs" + ], "ui": [ { "id": "game_menu", diff --git a/src/Messages/MessagesHelper.cs b/src/Messages/MessagesHelper.cs index f2ec71c..daabb6b 100644 --- a/src/Messages/MessagesHelper.cs +++ b/src/Messages/MessagesHelper.cs @@ -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. /// public static class MessagesHelper @@ -85,6 +86,7 @@ namespace Cthangover.Mods.Interface.Messages /// /// Instantiates a from message.tscn, 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. /// /// Y-offset in pixels for this message. /// The message text to display. @@ -92,22 +94,23 @@ namespace Cthangover.Mods.Interface.Messages /// Fade speed multiplier (default 1). 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(); - var background = SceneContextNode.FindNode("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(); + 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 { diff --git a/tests/MessagesHelperTests.cs b/tests/MessagesHelperTests.cs new file mode 100644 index 0000000..8a01de5 --- /dev/null +++ b/tests/MessagesHelperTests.cs @@ -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; + + /// Battle/menu notifications must survive a missing shell without allocating orphan UI trees. + 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(); + 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); + } + } + } +}