Settings menu: display settings, language codes, style guide window
Display tab: window mode (windowed/maximized/borderless/exclusive), monitor, window size per monitor (centred, disabled in fullscreen), V-Sync, frame rate cap, reset; applied and saved at once through the core DisplaySettingsService. Language is read through LanguageCode (en-en/en_US = English). The window follows the style guide shell: title and tabs in the header, close on the right, footer hint and per-tab reset. The main menu hides every session panel of UiHost (pause menu stayed above it). New suite display_settings (+ --shots); collection_edit leaves the game through the pause menu and checks it is closed.
This commit is contained in:
1 parent
b3ac8aec8f
commit
9bd0483af9
10 files changed
+940
-446
No files matched your search
@@ -23,6 +23,8 @@ namespace Mods.CollectionEditTest
|
||||
/// <item>The collection gets the developer toolset (<c>dev_tools</c>: editors,
|
||||
/// the scenario studio, …), saved the way the collection editor saves it (a
|
||||
/// user package with the same id): the running game is now outdated.</item>
|
||||
/// <item>The pause menu's "Main menu" button leaves the game like a player
|
||||
/// does, and the pause menu does not stay above the title screen.</item>
|
||||
/// <item>Menu → Play → New game of the same game switches the mod set in
|
||||
/// place: the editors and the studio are loaded, the studio's button is on
|
||||
/// the game's toolbar and the game matches its collection again.</item>
|
||||
@@ -38,6 +40,7 @@ namespace Mods.CollectionEditTest
|
||||
private const string AddedToolset = "dev_tools";
|
||||
private const string AddedMod = "editor_tools";
|
||||
private const string StudioMod = "scenario_studio";
|
||||
private const string GameMenuId = "game_menu";
|
||||
private const float TimeoutSeconds = 240f;
|
||||
|
||||
private readonly List<string> failures = new();
|
||||
@@ -115,21 +118,42 @@ namespace Mods.CollectionEditTest
|
||||
|
||||
AddEditorsToCollection();
|
||||
Check("the edited collection makes the running game outdated", GameCatalog.IsActiveOutdated());
|
||||
GetTree().Root.GetNode<GodotSceneService>("GodotSceneService").SwitchToMenu();
|
||||
AppServices.Current.Ui.ToggleEntry(GameMenuId);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
{
|
||||
var open = UiHost.Instance.TryGetInstance(GameMenuId, out var menu) && menu.Visible;
|
||||
Check("the pause menu is open", open);
|
||||
if (menu?.FindChild("MainMenuBtn", true, false) is not Button mainMenu)
|
||||
{
|
||||
failures.Add("the pause menu has no Main menu button");
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
mainMenu.EmitSignal(BaseButton.SignalName.Pressed);
|
||||
break;
|
||||
}
|
||||
|
||||
case 2:
|
||||
if (Busy || GetTree().CurrentScene is not Cthangover.BaseMenu.MainMenu)
|
||||
return;
|
||||
Check("the pause menu is closed on the title screen",
|
||||
!UiHost.Instance.TryGetInstance(GameMenuId, out var left) || !left.IsVisibleInTree());
|
||||
break;
|
||||
|
||||
case 3:
|
||||
if (Busy || !PressMenuPlay())
|
||||
return;
|
||||
enteredScenes.Clear();
|
||||
break;
|
||||
|
||||
case 2:
|
||||
case 4:
|
||||
if (!StartFromPicker())
|
||||
return;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
case 5:
|
||||
{
|
||||
if (Busy || ActiveGame.Id != GameId || enteredScenes.Count == 0)
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "Display Settings Test",
|
||||
"description": "Integration test of the Display tab of the settings menu: window mode, window size centred on the monitor, borderless fullscreen, frame rate cap, V-Sync, the restore of the saved file on start and the reset; the player's settings file is restored at the end. Activated only by --itest=display_settings (with --frontend, windowed).",
|
||||
"sources": [
|
||||
"src/*.cs"
|
||||
],
|
||||
"depends": [
|
||||
"menu_base>=1"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
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 Godot;
|
||||
|
||||
namespace Mods.DisplaySettingsTest
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <c>[TEST_RESULT] name=display_settings</c>.
|
||||
/// </summary>
|
||||
public partial class DisplaySettingsTestDriverNode : Node
|
||||
{
|
||||
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<string> 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 double stepAt;
|
||||
private bool done;
|
||||
|
||||
public DisplaySettingsTestDriverNode(string shotsDir)
|
||||
{
|
||||
this.shotsDir = shotsDir;
|
||||
backup = File.Exists(settingsPath) ? File.ReadAllText(settingsPath) : null;
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (done)
|
||||
return;
|
||||
|
||||
elapsed += delta;
|
||||
if (elapsed > TimeoutSeconds)
|
||||
{
|
||||
failures.Add($"timeout at step {step}");
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (elapsed - stepAt < 0.5)
|
||||
return;
|
||||
stepAt = elapsed;
|
||||
|
||||
try
|
||||
{
|
||||
RunStep();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failures.Add($"step {step}: {ex.GetType().Name}: {ex.Message}");
|
||||
Finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void RunStep()
|
||||
{
|
||||
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<SettingsMenu>("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<Control>("Window/Column/Body/Pages/Display").Visible && !menu.GetNode<Control>("Window/Column/Body/Pages/Audio").Visible
|
||||
&& menu.GetNode<Button>(ResetButton).Visible);
|
||||
var monitor = menu.GetNode<OptionButton>(Display + "MonitorRow/MonitorSelect");
|
||||
var primary = DisplayServer.GetPrimaryScreen();
|
||||
var screen = DisplayServer.ScreenGetSize(primary);
|
||||
Check("monitors are listed, the primary one marked",
|
||||
monitor.ItemCount == DisplayServer.GetScreenCount()
|
||||
&& monitor.GetItemText(primary) == string.Format(TranslationServer.Translate("settings/monitor_screen_primary"), primary + 1, screen.X, screen.Y));
|
||||
Pick("ModeRow/ModeSelect", 0);
|
||||
step = 1;
|
||||
return;
|
||||
|
||||
case 1:
|
||||
var sizes = menu.GetNode<OptionButton>(Display + "ResRow/ResSelect");
|
||||
Check("windowed mode enables the size list and hides the hint", !sizes.Disabled && !menu.GetNode<Label>(Display + "ResHint").Visible);
|
||||
var index = ItemIndex(sizes, $"{WindowSize.X}×{WindowSize.Y}");
|
||||
Check("size list offers 1280×720", index >= 0);
|
||||
Pick("ResRow/ResSelect", index);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
{
|
||||
var area = DisplayServer.ScreenGetUsableRect(DisplayServer.WindowGetCurrentScreen());
|
||||
Check($"window is windowed ({DisplayServer.WindowGetMode()})", DisplayServer.WindowGetMode() == DisplayServer.WindowMode.Windowed);
|
||||
Check($"window has the picked size ({DisplayServer.WindowGetSize()})", DisplayServer.WindowGetSize() == WindowSize);
|
||||
Check($"window is centred ({DisplayServer.WindowGetPosition()} in {area})", DisplayServer.WindowGetPosition() == area.Position + (area.Size - WindowSize) / 2);
|
||||
var saved = Saved();
|
||||
Check("windowed mode and size are saved",
|
||||
saved.WindowMode == (int)DisplayServer.WindowMode.Windowed && saved.ResolutionWidth == WindowSize.X && saved.ResolutionHeight == WindowSize.Y);
|
||||
Pick("ModeRow/ModeSelect", DisplayOptions.WindowModeIndex((int)DisplayServer.WindowMode.Fullscreen));
|
||||
break;
|
||||
}
|
||||
|
||||
case 3:
|
||||
Check($"borderless fullscreen is on ({DisplayServer.WindowGetMode()})", DisplayServer.WindowGetMode() == DisplayServer.WindowMode.Fullscreen);
|
||||
Check("fullscreen disables the size list and shows the hint",
|
||||
menu.GetNode<OptionButton>(Display + "ResRow/ResSelect").Disabled && menu.GetNode<Label>(Display + "ResHint").Visible);
|
||||
Check("fullscreen mode is saved, the window size kept", Saved().WindowMode == (int)DisplayServer.WindowMode.Fullscreen && Saved().ResolutionWidth == WindowSize.X);
|
||||
Pick("FpsRow/FpsSelect", DisplayOptions.FrameRateIndex(60));
|
||||
var vsync = menu.GetNode<CheckButton>(Display + "VSyncRow/VSyncToggle");
|
||||
vsync.ButtonPressed = !vsync.ButtonPressed;
|
||||
break;
|
||||
|
||||
case 4:
|
||||
{
|
||||
var vsyncOn = menu.GetNode<CheckButton>(Display + "VSyncRow/VSyncToggle").ButtonPressed;
|
||||
Check($"frame rate cap applied ({Godot.Engine.MaxFps})", Godot.Engine.MaxFps == 60);
|
||||
var saved = Saved();
|
||||
Check("frame rate cap and V-Sync are saved", saved.MaxFps == 60 && saved.VsyncEnabled == vsyncOn);
|
||||
// "Restart": the window in another state (settled for a tick), then the start-up restore from the file.
|
||||
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed);
|
||||
DisplayServer.WindowSetSize(new Vector2I(800, 600));
|
||||
Godot.Engine.MaxFps = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
case 5:
|
||||
Check($"window left fullscreen before the restore ({DisplayServer.WindowGetMode()})", DisplayServer.WindowGetMode() == DisplayServer.WindowMode.Windowed);
|
||||
DisplaySettingsService.Apply(Saved());
|
||||
break;
|
||||
|
||||
case 6:
|
||||
Check($"restore from the file brings fullscreen back ({DisplayServer.WindowGetMode()})", DisplayServer.WindowGetMode() == DisplayServer.WindowMode.Fullscreen);
|
||||
Check("restore from the file brings the frame rate cap back", Godot.Engine.MaxFps == 60);
|
||||
menu.GetNode<Button>(ResetButton).EmitSignal(BaseButton.SignalName.Pressed);
|
||||
break;
|
||||
|
||||
case 7:
|
||||
{
|
||||
var defaults = new SettingsData();
|
||||
var saved = Saved();
|
||||
Check("reset saves the game defaults",
|
||||
saved.WindowMode == defaults.WindowMode && saved.ResolutionWidth == defaults.ResolutionWidth
|
||||
&& saved.ResolutionHeight == defaults.ResolutionHeight && saved.MaxFps == defaults.MaxFps && saved.VsyncEnabled == defaults.VsyncEnabled);
|
||||
Check("reset shows the default mode",
|
||||
menu.GetNode<OptionButton>(Display + "ModeRow/ModeSelect").Selected == DisplayOptions.WindowModeIndex(defaults.WindowMode));
|
||||
Check($"reset applies the default frame rate cap ({Godot.Engine.MaxFps})", Godot.Engine.MaxFps == defaults.MaxFps);
|
||||
Finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
step++;
|
||||
}
|
||||
|
||||
private void Shot(string name)
|
||||
{
|
||||
if (string.IsNullOrEmpty(shotsDir) || DisplayServer.GetName() == "headless")
|
||||
return;
|
||||
var dir = ProjectSettings.GlobalizePath(shotsDir);
|
||||
Directory.CreateDirectory(dir);
|
||||
var path = Path.Combine(dir, name + ".png");
|
||||
GetViewport().GetTexture().GetImage().SavePng(path);
|
||||
GameLogger.Log("TEST", $"DisplaySettingsTest: screenshot {path}");
|
||||
}
|
||||
|
||||
/// <summary>Selects item <paramref name="index"/> of a Display tab list the way a click does.</summary>
|
||||
private void Pick(string path, int index)
|
||||
{
|
||||
var option = menu.GetNode<OptionButton>(Display + path);
|
||||
option.Select(index);
|
||||
option.EmitSignal(OptionButton.SignalName.ItemSelected, (long)index);
|
||||
}
|
||||
|
||||
private static int ItemIndex(OptionButton option, string text)
|
||||
{
|
||||
for (var i = 0; i < option.ItemCount; i++)
|
||||
{
|
||||
if (option.GetItemText(i) == text)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>The settings as the next start reads them from the file.</summary>
|
||||
private static SettingsData Saved()
|
||||
{
|
||||
var settings = new SettingsData();
|
||||
settings.Load();
|
||||
return settings;
|
||||
}
|
||||
|
||||
private void Check(string name, bool ok)
|
||||
{
|
||||
if (ok)
|
||||
{
|
||||
passed++;
|
||||
GameLogger.Log("TEST", $"DisplaySettingsTest: [PASS] {name}");
|
||||
}
|
||||
else
|
||||
{
|
||||
failures.Add(name);
|
||||
}
|
||||
}
|
||||
|
||||
private void Finish()
|
||||
{
|
||||
if (done)
|
||||
return;
|
||||
done = true;
|
||||
|
||||
if (backup != null)
|
||||
File.WriteAllText(settingsPath, backup);
|
||||
else if (File.Exists(settingsPath))
|
||||
File.Delete(settingsPath);
|
||||
GameData.Instance?.Settings?.Load();
|
||||
|
||||
foreach (var failure in failures)
|
||||
GameLogger.Log("TEST", $"DisplaySettingsTest: failed: {failure}");
|
||||
GameLogger.Log("TEST", $"[TEST_RESULT] name=display_settings passed={passed} failed={failures.Count}");
|
||||
GetTree().Quit(failures.Count == 0 ? 0 : 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.DisplaySettingsTest
|
||||
{
|
||||
/// <summary>Queues <see cref="DisplaySettingsTestDriverNode"/> only for <c>--itest=display_settings</c>; <c>--shots=<dir></c> makes it save screenshots of the menu and every settings tab.</summary>
|
||||
public class DisplaySettingsTestInitializer : IModInitializer
|
||||
{
|
||||
private static bool queued;
|
||||
|
||||
public void OnModLoaded(string modId) { }
|
||||
|
||||
public void OnModResourcesReady()
|
||||
{
|
||||
var args = OS.GetCmdlineUserArgs();
|
||||
if (queued || !args.Contains("--itest=display_settings"))
|
||||
return;
|
||||
|
||||
string shots = null;
|
||||
foreach (var arg in args)
|
||||
{
|
||||
if (arg.StartsWith("--shots="))
|
||||
shots = arg.Substring("--shots=".Length);
|
||||
}
|
||||
|
||||
queued = true;
|
||||
GameLogger.Log("TEST", "DisplaySettingsTest: activating");
|
||||
(Godot.Engine.GetMainLoop() as SceneTree)?.Root?.CallDeferred("add_child", new DisplaySettingsTestDriverNode(shots));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user