From bd9147e1b6e248106c4ac1dc756e547f2e5693ac Mon Sep 17 00:00:00 2001 From: iisaev Date: Sun, 18 Oct 2026 21:03:41 +0200 Subject: [PATCH] Play-test from a running game returns into the game Stop brings the player back to the scene they were in instead of the main menu, and a scene switch inside the played scenario ends the play-test in place. New GUI test suite ingame_play covers both. --- manifest.json | 3 +- src/ScenarioEditor/ScenarioEditorWindow.cs | 23 ++- test_suites/ingame_play/manifest.json | 10 ++ .../src/InGamePlayTestDriverNode.cs | 163 ++++++++++++++++++ .../src/InGamePlayTestInitializer.cs | 31 ++++ 5 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 test_suites/ingame_play/manifest.json create mode 100644 test_suites/ingame_play/src/InGamePlayTestDriverNode.cs create mode 100644 test_suites/ingame_play/src/InGamePlayTestInitializer.cs diff --git a/manifest.json b/manifest.json index 9ecf5da..087ce15 100644 --- a/manifest.json +++ b/manifest.json @@ -10,6 +10,7 @@ ], "depends": ["dialog_base>=1", "interactive_base>=1", "time_base>=1", "map_base>=1", "ui_base>=1"], "test_suites": [ - {"id": "windows", "activate": ["--itest=tools_windows"]} + {"id": "windows", "activate": ["--itest=tools_windows"]}, + {"id": "ingame_play", "activate": ["--itest=ingame_play"]} ] } diff --git a/src/ScenarioEditor/ScenarioEditorWindow.cs b/src/ScenarioEditor/ScenarioEditorWindow.cs index c17fb14..1d7c87a 100644 --- a/src/ScenarioEditor/ScenarioEditorWindow.cs +++ b/src/ScenarioEditor/ScenarioEditorWindow.cs @@ -37,6 +37,7 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor private bool _playActive; private string _returnRoute; + private string _returnScene; private CanvasLayer _stopOverlay; public ScenarioEditorWindow() : base("tools/scenario_editor/title") @@ -302,11 +303,19 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor var current = sceneService?.CurrentGodotSceneName; _returnRoute = !string.IsNullOrEmpty(current) && SceneRouteRegistry.TryGet(current, out _) ? current : null; + // Opened inside a running game: come back into the scene the + // player was in, not to the main menu (there is no way back to the + // editor from there). + var sceneManager = GetNodeOrNull("/root/SceneManager"); + var gameScene = sceneManager?.CurrentSceneName; + _returnScene = _returnRoute == null && current != "mainmenu" && !string.IsNullOrEmpty(gameScene) && sceneManager.HasScene(gameScene) + ? gameScene + : null; + _stopOverlay = CreateStopOverlay(); var tree = Godot.Engine.GetMainLoop() as SceneTree; tree?.Root.AddChild(_stopOverlay); - var sceneManager = GetNodeOrNull("/root/SceneManager"); if (sceneManager != null) { sceneManager.Initialize(); @@ -336,11 +345,23 @@ namespace Cthangover.Core.UI.Tool.ScenarioEditor } var sceneService = GetNodeOrNull("/root/GodotSceneService"); + var sceneManager = GetNodeOrNull("/root/SceneManager"); if (_returnRoute != null && SceneRouteRegistry.TryGet(_returnRoute, out var route)) + { sceneService?.SwitchRoute(route); + } + else if (_returnScene != null && sceneManager != null) + { + sceneManager.PendingSceneName = _returnScene; + sceneService?.LoadShell(); + } else + { sceneService?.SwitchToMenu(); + } + _returnRoute = null; + _returnScene = null; SetStatus(ToolUi.Tr("tools/scenario_editor/play_stopped")); } diff --git a/test_suites/ingame_play/manifest.json b/test_suites/ingame_play/manifest.json new file mode 100644 index 0000000..3886d51 --- /dev/null +++ b/test_suites/ingame_play/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "In-game Play-test Test", + "description": "Integration test of the scenario editor opened inside a running game (the novel template with the editors added): plays a scenario from the editor and checks the play-test stays in the game until Stop, then Stop returns to the game with the editor visible — never to the main menu. Activated by --itest=ingame_play.", + "sources": [ + "src/*.cs" + ], + "depends": [ + "my_novel_game>=1" + ] +} diff --git a/test_suites/ingame_play/src/InGamePlayTestDriverNode.cs b/test_suites/ingame_play/src/InGamePlayTestDriverNode.cs new file mode 100644 index 0000000..0902394 --- /dev/null +++ b/test_suites/ingame_play/src/InGamePlayTestDriverNode.cs @@ -0,0 +1,163 @@ +using System.Linq; +using Cthangover.Core.Bootstrap; +using Cthangover.Core.Games; +using Cthangover.Core.Scenes; +using Cthangover.Core.UI.Tool.ScenarioEditor; +using Cthangover.Core.Utils; +using Cthangover.Mods.Interface.Tool; +using Cthangover.Tools.Common; +using Cthangover.Tools.Services; +using Godot; + +namespace Cthangover.Tools.Tests.InGamePlay +{ + /// + /// The player's path that used to throw them out to the main menu: inside + /// a running game (its dialog on screen) open the scenario editor, play a + /// scenario, and expect the play-test to keep running — the teardown of the + /// game's own dialog must not count as the end of the played scenario. + /// Stop, and the played scenario leaving its scene (an option's + /// switch_scene), must both come back into the game with the editor + /// visible. Ends with + /// [TEST_RESULT] name=ingame_play passed=N failed=M and the exit code. + /// + public partial class InGamePlayTestDriverNode : Node + { + private const string ResultName = "ingame_play"; + private const string GameModId = "my_novel_game"; + private const string ScenarioPath = "scenarios/my_novel_home.scenario"; + private const float TickSeconds = 0.5f; + private const float TimeoutSeconds = 90f; + private const int GameSettleTicks = 6; + private const int PlaySettleTicks = 6; + + private int _passed; + private int _failed; + private int _step; + private int _settle; + private double _elapsed; + private bool _finished; + private ScenarioEditorWindow _window; + + /// Creates the driver ticking on its own timer. + public InGamePlayTestDriverNode() + { + var timer = new Timer { WaitTime = TickSeconds, Autostart = true }; + AddChild(timer); + timer.Timeout += Tick; + } + + private void Tick() + { + if (_finished) + return; + + _elapsed += TickSeconds; + if (_elapsed >= TimeoutSeconds) + { + Check(false, $"timeout ({TimeoutSeconds:0}s) at step {_step}"); + Finish(); + return; + } + + var tree = GetTree(); + var scenes = tree.Root.GetNodeOrNull("GodotSceneService"); + switch (_step) + { + case 0: + if (!Boot.IsReady || !ActiveGame.IsActive || InMainMenu(scenes) || GodotSceneService.IsTransitioning) + return; + if (++_settle < GameSettleTicks) + return; + Check(SceneManager.GetCurrentSceneName() == "my_novel_town", $"game entered its town (scene '{SceneManager.GetCurrentSceneName()}')"); + _window = (ScenarioEditorWindow)ToolWindow.ShowWindow(new ScenarioEditorWindow()); + var tabs = _window.FindChildren("*", "", true, false).OfType().FirstOrDefault(); + tabs?.OpenFile(ScenarioFileService.MakeKey(GameModId, ScenarioPath)); + Check(tabs?.HasActiveFile == true, "scenario editor opened the home scenario"); + break; + + case 1: + PressButton(_window, "tools/scenario_editor/play"); + _settle = 0; + break; + + case 2: + if (GodotSceneService.IsTransitioning) + return; + if (InMainMenu(scenes)) + { + Check(false, "play-test threw the player out to the main menu"); + Finish(); + return; + } + + if (++_settle < PlaySettleTicks) + return; + Check(SceneManager.IsTestPlayActive, "play-test is still running while its dialog waits for the player"); + Check(!_window.Visible, "the editor is hidden during the play-test"); + Check(SceneManager.GetCurrentSceneName() == "my_novel_home", $"play-test entered the scenario's scene (scene '{SceneManager.GetCurrentSceneName()}')"); + PressButton(tree.Root, "tools/scenario_editor/stop"); + _settle = 0; + break; + + case 3: + if (GodotSceneService.IsTransitioning || ++_settle < PlaySettleTicks) + return; + Check(!InMainMenu(scenes), "Stop returns into the game, not to the main menu"); + Check(IsInstanceValid(_window) && _window.Visible, "the editor is visible again after Stop"); + Check(!SceneManager.IsTestPlayActive, "Stop ends the play-test"); + Check(SceneManager.GetCurrentSceneName() == "my_novel_town", $"Stop returns to the scene the player was in (scene '{SceneManager.GetCurrentSceneName()}')"); + break; + + case 4: + PressButton(_window, "tools/scenario_editor/play"); + _settle = 0; + break; + + case 5: + if (GodotSceneService.IsTransitioning || ++_settle < PlaySettleTicks) + return; + // The played scenario's option "Back to the town" does exactly this. + tree.Root.GetNodeOrNull("SceneManager")?.SwitchScene("my_novel_town"); + _settle = 0; + break; + + case 6: + if (GodotSceneService.IsTransitioning || ++_settle < PlaySettleTicks) + return; + Check(!InMainMenu(scenes), "a scene switch in the played scenario returns into the game, not to the main menu"); + Check(!SceneManager.IsTestPlayActive, "leaving the played scene ends the play-test"); + Check(IsInstanceValid(_window) && _window.Visible, "the editor is visible again after the played scenario left its scene"); + Finish(); + return; + } + + _step++; + } + + private static bool InMainMenu(GodotSceneService scenes) => scenes?.CurrentGodotSceneName == "mainmenu"; + + private static void PressButton(Node root, string textKey) + { + var text = ToolUi.Tr(textKey); + var button = root.FindChildren("*", nameof(Button), true, false).OfType