using System; using System.Collections.Generic; using System.IO; using Cthangover.BaseMenu; using Cthangover.Core.Bootstrap; using Cthangover.Core.Mods; using Cthangover.Core.Settings; using Cthangover.Core.Utils; using Cthangover.Mods.Interface.View; using Godot; namespace Mods.DisplaySettingsTest { /// /// Display tab of the settings menu driven like a player, one step per tick /// so the window settles: windowed mode with a picked size gives a window of /// that size centred on the monitor; borderless fullscreen disables the size /// list; the frame rate cap and V-Sync are applied; every change is in the /// settings file at once; a "restart" (the file read anew and applied to a /// window put in another state) restores it all; reset returns the game /// defaults. The player's settings file is restored at the end. With a shots /// directory the main menu and every settings tab are saved as PNG. Prints /// [TEST_RESULT] name=display_settings. /// public partial class DisplaySettingsTestDriverNode : Node { private int readinessToken = int.MinValue; private ulong readinessStarted; private ulong readinessFrame; private const string Display = "Window/Column/Body/Pages/Display/"; private const float TimeoutSeconds = 60f; private const string ResetButton = "Window/Column/Footer/Row/ResetButton"; private static readonly Vector2I WindowSize = new(1280, 720); private readonly List failures = new(); private readonly string settingsPath = ProjectSettings.GlobalizePath("user://settings.json"); private readonly string backup; private readonly string shotsDir; private SettingsMenu menu; private int passed; private int step; private int shotTab; private double elapsed; private bool done; public DisplaySettingsTestDriverNode(string shotsDir) { this.shotsDir = shotsDir; backup = File.Exists(settingsPath) ? File.ReadAllText(settingsPath) : null; } /// Advances when observable scene/UI readiness has held for two real frames; the watchdog remains active. public override void _Process(double delta) { if (done) return; elapsed += delta; if (elapsed > TimeoutSeconds) { failures.Add($"timeout at step {step}"); Finish(); return; } try { RunStep(); } catch (Exception ex) { failures.Add($"step {step}: {ex.GetType().Name}: {ex.Message}"); Finish(); } } // Deferred UI/scene work must finish before the original assertions run. private bool WaitForReadyFrames(int token, bool ready, double timeoutSeconds = 30) { var now = global::Godot.Time.GetTicksMsec(); if (readinessToken != token) { readinessToken = token; readinessStarted = now; readinessFrame = 0; } if (now - readinessStarted > timeoutSeconds * 1000) throw new global::System.TimeoutException($"readiness at step {token}: scene/UI did not become ready"); if (!ready) { readinessFrame = 0; return false; } if (readinessFrame == 0) readinessFrame = global::Godot.Engine.GetProcessFrames(); return global::Godot.Engine.GetProcessFrames() - readinessFrame >= 2; } private bool DisplayReady() { if (step == 0) return true; if (menu == null || !menu.Visible) return false; return step switch { 2 => DisplayServer.WindowGetMode() == DisplayServer.WindowMode.Windowed && DisplayServer.WindowGetSize() == WindowSize, 3 or 6 => DisplayServer.WindowGetMode() == DisplayServer.WindowMode.Fullscreen, 5 => DisplayServer.WindowGetMode() == DisplayServer.WindowMode.Windowed, _ => true }; } private void RunStep() { if (!WaitForReadyFrames(step * 10 + shotTab, Boot.IsReady && GetTree().CurrentScene != null && DisplayReady(), 30)) return; switch (step) { case 0: if (!Boot.IsReady) return; if (GetTree().CurrentScene?.FindChild("LauncherPlay", true, false) is Button launcherPlay) { launcherPlay.EmitSignal(BaseButton.SignalName.Pressed); return; } if (GetTree().CurrentScene is not MainMenu main) return; menu = main.GetNodeOrNull("SettingsPanel"); Check("settings panel exists", menu != null); if (menu == null) { Finish(); return; } Shot("main_menu"); menu.Open(); shotTab = 0; step = 100; return; case 100: // One tick per settings tab for the screenshots, then the Display tab checks. if (shotTab > 0) Shot("settings_" + shotTab); if (shotTab < 4 && !string.IsNullOrEmpty(shotsDir)) { menu.ShowTab(shotTab++); return; } menu.ShowTab(2); Check("the Display tab shows its page and the display reset", menu.GetNode("Window/Column/Body/Pages/Display").Visible && !menu.GetNode("Window/Column/Body/Pages/Audio").Visible && menu.GetNode