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:
cthangover committed 2026-10-21 19:18:34 +02:00
1 parent b3ac8aec8f
commit 9bd0483af9
10 files changed
+940 -446

No files matched your search

+17 -10
View File
@@ -44,6 +44,11 @@ settings/back = Back
settings/audio = Audio settings/audio = Audio
settings/sounds = Sounds settings/sounds = Sounds
settings/music = Music settings/music = Music
settings/ambient = Ambience
settings/interface_language = Interface language
settings/section_window = Window
settings/section_performance = Performance
settings/saved_at_once = Changes are applied and saved at once
settings/volume = Volume settings/volume = Volume
settings/language = Language settings/language = Language
settings/lang_ru = Russian settings/lang_ru = Russian
@@ -55,18 +60,20 @@ settings/tab_display = Display
settings/display = Display settings/display = Display
settings/window_mode = Window Mode settings/window_mode = Window Mode
settings/window_mode_windowed = Windowed settings/window_mode_windowed = Windowed
settings/window_mode_fullscreen = Fullscreen settings/window_mode_maximized = Maximized window
settings/window_mode_borderless = Borderless settings/window_mode_borderless = Fullscreen (borderless)
settings/resolution = Resolution settings/window_mode_fullscreen = Fullscreen (exclusive)
settings/resolution = Window size
settings/resolution_fullscreen_hint = Fullscreen modes use the monitor resolution
settings/monitor = Monitor settings/monitor = Monitor
settings/monitor_screen = Screen {0} ({1}x{2}) settings/monitor_screen = Monitor {0} ({1}×{2})
settings/monitor_screen_primary = Monitor {0} ({1}×{2}), primary
settings/vsync = V-Sync settings/vsync = V-Sync
settings/scale = Scale settings/max_fps = Frame rate limit
settings/scale_1x = 1x settings/max_fps_unlimited = Unlimited
settings/scale_2x = 2x settings/max_fps_value = {0} FPS
settings/scale_3x = 3x settings/display_reset = Reset display settings
settings/scale_4x = 4x settings/tab_mods = Mods
settings/tab_mods = Mod Settings
settings/mod_list_title = Mods settings/mod_list_title = Mods
settings/mods_no_settings = No mod settings available settings/mods_no_settings = No mod settings available
settings/mods_reset = Reset to Defaults settings/mods_reset = Reset to Defaults
+18 -11
View File
@@ -44,6 +44,11 @@ settings/back = Назад
settings/audio = Аудио settings/audio = Аудио
settings/sounds = Звуки settings/sounds = Звуки
settings/music = Музыка settings/music = Музыка
settings/ambient = Окружение
settings/interface_language = Язык интерфейса
settings/section_window = Окно
settings/section_performance = Производительность
settings/saved_at_once = Изменения применяются и сохраняются сразу
settings/volume = Громкость settings/volume = Громкость
settings/language = Язык settings/language = Язык
settings/lang_ru = Русский settings/lang_ru = Русский
@@ -54,19 +59,21 @@ settings/tab_audio = Звук
settings/tab_display = Экран settings/tab_display = Экран
settings/display = Экран settings/display = Экран
settings/window_mode = Режим окна settings/window_mode = Режим окна
settings/window_mode_windowed = Оконный settings/window_mode_windowed = В окне
settings/window_mode_fullscreen = Полный экран settings/window_mode_maximized = Окно на весь экран
settings/window_mode_borderless = Без рамки settings/window_mode_borderless = Полный экран (без рамки)
settings/resolution = Разрешение settings/window_mode_fullscreen = Полный экран (эксклюзивный)
settings/resolution = Размер окна
settings/resolution_fullscreen_hint = В полноэкранных режимах используется разрешение монитора
settings/monitor = Монитор settings/monitor = Монитор
settings/monitor_screen = Экран {0} ({1}x{2}) settings/monitor_screen = Монитор {0} ({1}×{2})
settings/monitor_screen_primary = Монитор {0} ({1}×{2}), основной
settings/vsync = Вертикальная синхронизация settings/vsync = Вертикальная синхронизация
settings/scale = Масштаб settings/max_fps = Ограничение кадров
settings/scale_1x = 1x settings/max_fps_unlimited = Без ограничения
settings/scale_2x = 2x settings/max_fps_value = {0} кадров/с
settings/scale_3x = 3x settings/display_reset = Сбросить настройки экрана
settings/scale_4x = 4x settings/tab_mods = Моды
settings/tab_mods = Настройки модов
settings/mod_list_title = Моды settings/mod_list_title = Моды
settings/mods_no_settings = Нет доступных настроек модов settings/mods_no_settings = Нет доступных настроек модов
settings/mods_reset = Сбросить настройки settings/mods_reset = Сбросить настройки
+2 -1
View File
@@ -14,6 +14,7 @@
{"id": "save_dormancy", "activate": ["--itest=save_dormancy"]}, {"id": "save_dormancy", "activate": ["--itest=save_dormancy"]},
{"id": "new_game", "activate": ["--itest=new_game"]}, {"id": "new_game", "activate": ["--itest=new_game"]},
{"id": "hot_reload", "activate": ["--itest=hot_reload"]}, {"id": "hot_reload", "activate": ["--itest=hot_reload"]},
{"id": "collection_edit", "activate": ["--itest=collection_edit"]} {"id": "collection_edit", "activate": ["--itest=collection_edit"]},
{"id": "display_settings", "activate": ["--itest=display_settings"]}
] ]
} }
+273 -240
View File
@@ -2,369 +2,402 @@
[ext_resource type="Script" path="src/SettingsMenu.cs" id="SettingsMenuScript"] [ext_resource type="Script" path="src/SettingsMenu.cs" id="SettingsMenuScript"]
[node name="SettingsMenu" type="Control" unique_id=894644832] [node name="SettingsMenu" type="Control" unique_id=1272946981]
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
mouse_filter = 1
script = ExtResource("SettingsMenuScript") script = ExtResource("SettingsMenuScript")
[node name="Bg" type="ColorRect" parent="." unique_id=21945756] [node name="Backdrop" type="ColorRect" parent="." unique_id=926490946]
layout_mode = 1 layout_mode = 1
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
color = Color(0, 0, 0, 1) color = Color(0, 0, 0, 0.9)
[node name="Panel" type="Panel" parent="." unique_id=1554260654] [node name="Window" type="PanelContainer" parent="." unique_id=1346264907]
layout_mode = 1 layout_mode = 1
anchors_preset = -1 anchors_preset = 8
anchor_left = 0.1 anchor_left = 0.5
anchor_top = 0.1 anchor_top = 0.5
anchor_right = 0.9 anchor_right = 0.5
anchor_bottom = 0.9 anchor_bottom = 0.5
offset_left = -420.0
offset_top = -310.0
offset_right = 420.0
offset_bottom = 310.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
[node name="Margin" type="MarginContainer" parent="Panel" unique_id=1929120286] [node name="Column" type="VBoxContainer" parent="Window" unique_id=1929192805]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 28
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 28
theme_override_constants/margin_bottom = 20
[node name="VBox" type="VBoxContainer" parent="Panel/Margin" unique_id=150548585]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 0
[node name="Header" type="MarginContainer" parent="Window/Column" unique_id=123233874]
layout_mode = 2
theme_override_constants/margin_left = 24
theme_override_constants/margin_top = 16
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 16
[node name="Row" type="HBoxContainer" parent="Window/Column/Header" unique_id=918557862]
layout_mode = 2
theme_override_constants/separation = 24
[node name="Title" type="Label" parent="Window/Column/Header/Row" unique_id=968405469]
layout_mode = 2
text = "Settings"
uppercase = true
[node name="Tabs" type="HBoxContainer" parent="Window/Column/Header/Row" unique_id=774582017]
layout_mode = 2
theme_override_constants/separation = 4
[node name="GeneralTab" type="Button" parent="Window/Column/Header/Row/Tabs" unique_id=1470465706]
layout_mode = 2
focus_mode = 0
toggle_mode = true
[node name="AudioTab" type="Button" parent="Window/Column/Header/Row/Tabs" unique_id=1481059729]
layout_mode = 2
focus_mode = 0
toggle_mode = true
[node name="DisplayTab" type="Button" parent="Window/Column/Header/Row/Tabs" unique_id=600818871]
layout_mode = 2
focus_mode = 0
toggle_mode = true
[node name="ModsTab" type="Button" parent="Window/Column/Header/Row/Tabs" unique_id=1134775141]
layout_mode = 2
focus_mode = 0
toggle_mode = true
[node name="Spacer" type="Control" parent="Window/Column/Header/Row" unique_id=956914814]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="CloseButton" type="Button" parent="Window/Column/Header/Row" unique_id=971181459]
layout_mode = 2
custom_minimum_size = Vector2(38, 38)
focus_mode = 0
text = "✕"
[node name="RuleHeader" type="ColorRect" parent="Window/Column" unique_id=100276952]
custom_minimum_size = Vector2(0, 1)
layout_mode = 2
[node name="Body" type="MarginContainer" parent="Window/Column" unique_id=571795343]
layout_mode = 2
size_flags_vertical = 3
theme_override_constants/margin_left = 32
theme_override_constants/margin_top = 24
theme_override_constants/margin_right = 32
theme_override_constants/margin_bottom = 24
[node name="Pages" type="Control" parent="Window/Column/Body" unique_id=138945425]
layout_mode = 2
mouse_filter = 2
[node name="General" type="VBoxContainer" parent="Window/Column/Body/Pages" unique_id=215740431]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 14 theme_override_constants/separation = 14
[node name="Title" type="Label" parent="Panel/Margin/VBox" unique_id=86160906] [node name="LangSection" type="Label" parent="Window/Column/Body/Pages/General" unique_id=1563036971]
layout_mode = 2 layout_mode = 2
theme_override_font_sizes/font_size = 26 text = "Language"
text = "Settings" uppercase = true
horizontal_alignment = 1
[node name="Tabs" type="TabContainer" parent="Panel/Margin/VBox" unique_id=1524671125] [node name="LangRow" type="HBoxContainer" parent="Window/Column/Body/Pages/General" unique_id=1441486578]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 theme_override_constants/separation = 24
tab_alignment = 1
current_tab = 0
[node name="GeneralMargin" type="MarginContainer" parent="Panel/Margin/VBox/Tabs"] [node name="LangLabel" type="Label" parent="Window/Column/Body/Pages/General/LangRow" unique_id=1359724527]
custom_minimum_size = Vector2(280, 0)
layout_mode = 2 layout_mode = 2
metadata/_tab_index = 0
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 12
[node name="General" type="VBoxContainer" parent="Panel/Margin/VBox/Tabs/GeneralMargin" unique_id=2028800899]
layout_mode = 2
theme_override_constants/separation = 12
[node name="Spacer" type="Control" parent="Panel/Margin/VBox/Tabs/GeneralMargin/General" unique_id=1203872148]
custom_minimum_size = Vector2(0, 12)
layout_mode = 2
[node name="LangBox" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/GeneralMargin/General" unique_id=684741721]
layout_mode = 2
theme_override_constants/separation = 16
[node name="LangLabel" type="Label" parent="Panel/Margin/VBox/Tabs/GeneralMargin/General/LangBox" unique_id=1707108944]
layout_mode = 2
size_flags_horizontal = 3
text = "Language" text = "Language"
[node name="LangSelect" type="OptionButton" parent="Panel/Margin/VBox/Tabs/GeneralMargin/General/LangBox" unique_id=2037857401] [node name="LangSelect" type="OptionButton" parent="Window/Column/Body/Pages/General/LangRow" unique_id=1451274578]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_vertical = 4
[node name="AudioMargin" type="MarginContainer" parent="Panel/Margin/VBox/Tabs"] [node name="Audio" type="VBoxContainer" parent="Window/Column/Body/Pages" unique_id=232944441]
layout_mode = 2 layout_mode = 1
visible = false anchors_preset = 15
metadata/_tab_index = 1 anchor_right = 1.0
theme_override_constants/margin_left = 16 anchor_bottom = 1.0
theme_override_constants/margin_top = 12 grow_horizontal = 2
theme_override_constants/margin_right = 16 grow_vertical = 2
theme_override_constants/margin_bottom = 12 theme_override_constants/separation = 14
[node name="Audio" type="VBoxContainer" parent="Panel/Margin/VBox/Tabs/AudioMargin" unique_id=564523157] [node name="VolumeSection" type="Label" parent="Window/Column/Body/Pages/Audio" unique_id=358076534]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 4 text = "Volume"
uppercase = true
[node name="SoundsBox" type="VBoxContainer" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio" unique_id=508426867] [node name="SoundsRow" type="HBoxContainer" parent="Window/Column/Body/Pages/Audio" unique_id=1091551106]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 4 theme_override_constants/separation = 24
[node name="ToggleRow" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox" unique_id=274422939] [node name="SoundsLabel" type="Label" parent="Window/Column/Body/Pages/Audio/SoundsRow" unique_id=977377427]
custom_minimum_size = Vector2(280, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 12
[node name="SoundsLabel" type="Label" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox/ToggleRow" unique_id=1483011032]
layout_mode = 2
size_flags_horizontal = 3
text = "Sounds" text = "Sounds"
[node name="SoundsToggle" type="CheckButton" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox/ToggleRow" unique_id=126270437] [node name="SoundsToggle" type="CheckButton" parent="Window/Column/Body/Pages/Audio/SoundsRow" unique_id=294349187]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 4
focus_mode = 0
[node name="VolumeRow" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox" unique_id=585263148] [node name="SoundsVolume" type="HSlider" parent="Window/Column/Body/Pages/Audio/SoundsRow" unique_id=334275673]
custom_minimum_size = Vector2(300, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 12 size_flags_vertical = 4
max_value = 100.0
step = 1.0
[node name="SoundsVolumeLabel" type="Label" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox/VolumeRow" unique_id=1327120426] [node name="SoundsValue" type="Label" parent="Window/Column/Body/Pages/Audio/SoundsRow" unique_id=761672957]
custom_minimum_size = Vector2(56, 0)
layout_mode = 2 layout_mode = 2
text = "Volume"
[node name="SoundsVolume" type="HSlider" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox/VolumeRow" unique_id=2061615116]
layout_mode = 2
size_flags_horizontal = 3
value = 100.0
[node name="SoundsVolumeValue" type="Label" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox/VolumeRow" unique_id=1079108664]
custom_minimum_size = Vector2(48, 0)
layout_mode = 2
text = "100%"
horizontal_alignment = 2 horizontal_alignment = 2
[node name="Sep" type="HSeparator" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio" unique_id=988886928] [node name="MusicRow" type="HBoxContainer" parent="Window/Column/Body/Pages/Audio" unique_id=1716861232]
custom_minimum_size = Vector2(0, 8)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 24
[node name="MusicBox" type="VBoxContainer" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio" unique_id=1316259016] [node name="MusicLabel" type="Label" parent="Window/Column/Body/Pages/Audio/MusicRow" unique_id=1921053802]
custom_minimum_size = Vector2(280, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 4
[node name="ToggleRow2" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox" unique_id=553618696]
layout_mode = 2
theme_override_constants/separation = 12
[node name="MusicLabel" type="Label" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox/ToggleRow2" unique_id=1559600377]
layout_mode = 2
size_flags_horizontal = 3
text = "Music" text = "Music"
[node name="MusicToggle" type="CheckButton" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox/ToggleRow2" unique_id=901979875] [node name="MusicToggle" type="CheckButton" parent="Window/Column/Body/Pages/Audio/MusicRow" unique_id=1546243765]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 4
focus_mode = 0
[node name="VolumeRow2" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox" unique_id=1411284591] [node name="MusicVolume" type="HSlider" parent="Window/Column/Body/Pages/Audio/MusicRow" unique_id=656456481]
custom_minimum_size = Vector2(300, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 12 size_flags_vertical = 4
max_value = 100.0
step = 1.0
[node name="MusicVolumeLabel" type="Label" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox/VolumeRow2" unique_id=1952498749] [node name="MusicValue" type="Label" parent="Window/Column/Body/Pages/Audio/MusicRow" unique_id=1811565676]
custom_minimum_size = Vector2(56, 0)
layout_mode = 2 layout_mode = 2
text = "Volume"
[node name="MusicVolume" type="HSlider" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox/VolumeRow2" unique_id=504218348]
layout_mode = 2
size_flags_horizontal = 3
value = 5.0
[node name="MusicVolumeValue" type="Label" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox/VolumeRow2" unique_id=1866810087]
custom_minimum_size = Vector2(48, 0)
layout_mode = 2
text = "5%"
horizontal_alignment = 2 horizontal_alignment = 2
[node name="Sep2" type="HSeparator" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio" unique_id=1486966851] [node name="AmbientRow" type="HBoxContainer" parent="Window/Column/Body/Pages/Audio" unique_id=483864917]
custom_minimum_size = Vector2(0, 8)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 24
[node name="AmbientBox" type="VBoxContainer" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio" unique_id=127969403] [node name="AmbientLabel" type="Label" parent="Window/Column/Body/Pages/Audio/AmbientRow" unique_id=402291449]
custom_minimum_size = Vector2(280, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 4
[node name="ToggleRow3" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox" unique_id=338401886]
layout_mode = 2
theme_override_constants/separation = 12
[node name="AmbientLabel" type="Label" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox/ToggleRow3" unique_id=872436692]
layout_mode = 2
size_flags_horizontal = 3
text = "Ambient" text = "Ambient"
[node name="AmbientToggle" type="CheckButton" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox/ToggleRow3" unique_id=1160929014] [node name="AmbientToggle" type="CheckButton" parent="Window/Column/Body/Pages/Audio/AmbientRow" unique_id=427650030]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 4
focus_mode = 0
[node name="VolumeRow3" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox" unique_id=1139865718] [node name="AmbientVolume" type="HSlider" parent="Window/Column/Body/Pages/Audio/AmbientRow" unique_id=354392891]
custom_minimum_size = Vector2(300, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 12 size_flags_vertical = 4
max_value = 100.0
step = 1.0
[node name="AmbientVolumeLabel" type="Label" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox/VolumeRow3" unique_id=1610793395] [node name="AmbientValue" type="Label" parent="Window/Column/Body/Pages/Audio/AmbientRow" unique_id=1500302873]
custom_minimum_size = Vector2(56, 0)
layout_mode = 2 layout_mode = 2
text = "Volume"
[node name="AmbientVolume" type="HSlider" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox/VolumeRow3" unique_id=1343104331]
layout_mode = 2
size_flags_horizontal = 3
value = 5.0
[node name="AmbientVolumeValue" type="Label" parent="Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox/VolumeRow3" unique_id=1909728919]
custom_minimum_size = Vector2(48, 0)
layout_mode = 2
text = "5%"
horizontal_alignment = 2 horizontal_alignment = 2
[node name="DisplayMargin" type="MarginContainer" parent="Panel/Margin/VBox/Tabs"] [node name="Display" type="VBoxContainer" parent="Window/Column/Body/Pages" unique_id=218390439]
layout_mode = 2 layout_mode = 1
visible = false anchors_preset = 15
metadata/_tab_index = 2 anchor_right = 1.0
theme_override_constants/margin_left = 16 anchor_bottom = 1.0
theme_override_constants/margin_top = 12 grow_horizontal = 2
theme_override_constants/margin_right = 16 grow_vertical = 2
theme_override_constants/margin_bottom = 12 theme_override_constants/separation = 14
[node name="Display" type="VBoxContainer" parent="Panel/Margin/VBox/Tabs/DisplayMargin" unique_id=1371435162] [node name="WindowSection" type="Label" parent="Window/Column/Body/Pages/Display" unique_id=345383153]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 10 text = "Window"
uppercase = true
[node name="Spacer2" type="Control" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display" unique_id=270871165] [node name="ModeRow" type="HBoxContainer" parent="Window/Column/Body/Pages/Display" unique_id=1616575243]
custom_minimum_size = Vector2(0, 12)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 24
[node name="ModeBox" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display" unique_id=455596205] [node name="ModeLabel" type="Label" parent="Window/Column/Body/Pages/Display/ModeRow" unique_id=207553790]
custom_minimum_size = Vector2(280, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 16 text = "Window mode"
[node name="ModeLabel" type="Label" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display/ModeBox" unique_id=1346202262] [node name="ModeSelect" type="OptionButton" parent="Window/Column/Body/Pages/Display/ModeRow" unique_id=1678024463]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_vertical = 4
text = "Window Mode"
[node name="ModeSelect" type="OptionButton" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display/ModeBox" unique_id=631713497] [node name="MonitorRow" type="HBoxContainer" parent="Window/Column/Body/Pages/Display" unique_id=643742076]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 theme_override_constants/separation = 24
[node name="ResBox" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display" unique_id=603217917] [node name="MonitorLabel" type="Label" parent="Window/Column/Body/Pages/Display/MonitorRow" unique_id=465414492]
custom_minimum_size = Vector2(280, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 16
[node name="ResLabel" type="Label" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display/ResBox" unique_id=1714673968]
layout_mode = 2
size_flags_horizontal = 3
text = "Resolution"
[node name="ResSelect" type="OptionButton" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display/ResBox" unique_id=651322592]
layout_mode = 2
size_flags_horizontal = 3
[node name="MonitorBox" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display" unique_id=284981749]
layout_mode = 2
theme_override_constants/separation = 16
[node name="MonitorLabel" type="Label" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display/MonitorBox" unique_id=1905911340]
layout_mode = 2
size_flags_horizontal = 3
text = "Monitor" text = "Monitor"
[node name="MonitorSelect" type="OptionButton" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display/MonitorBox" unique_id=1334336966] [node name="MonitorSelect" type="OptionButton" parent="Window/Column/Body/Pages/Display/MonitorRow" unique_id=868484966]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_vertical = 4
[node name="ScaleBox" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display" unique_id=930383631] [node name="ResRow" type="HBoxContainer" parent="Window/Column/Body/Pages/Display" unique_id=1701841140]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 16 theme_override_constants/separation = 24
[node name="ScaleLabel" type="Label" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display/ScaleBox" unique_id=1924008447] [node name="ResLabel" type="Label" parent="Window/Column/Body/Pages/Display/ResRow" unique_id=1649779675]
custom_minimum_size = Vector2(280, 0)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 text = "Window size"
text = "Scale"
[node name="ScaleSelect" type="OptionButton" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display/ScaleBox" unique_id=2096132155] [node name="ResSelect" type="OptionButton" parent="Window/Column/Body/Pages/Display/ResRow" unique_id=127437834]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_vertical = 4
[node name="VSyncRow" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display" unique_id=348516587] [node name="ResHint" type="Label" parent="Window/Column/Body/Pages/Display" unique_id=1953536508]
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 12 text = "Fullscreen modes use the monitor resolution"
[node name="VSyncLabel" type="Label" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display/VSyncRow" unique_id=1204827790] [node name="Gap" type="Control" parent="Window/Column/Body/Pages/Display" unique_id=1362538809]
custom_minimum_size = Vector2(0, 10)
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 mouse_filter = 2
text = "VSync"
[node name="VSyncToggle" type="CheckButton" parent="Panel/Margin/VBox/Tabs/DisplayMargin/Display/VSyncRow" unique_id=1909935663] [node name="RenderSection" type="Label" parent="Window/Column/Body/Pages/Display" unique_id=1299172762]
layout_mode = 2 layout_mode = 2
text = "Performance"
uppercase = true
[node name="ModSettingsMargin" type="MarginContainer" parent="Panel/Margin/VBox/Tabs"] [node name="VSyncRow" type="HBoxContainer" parent="Window/Column/Body/Pages/Display" unique_id=886210113]
layout_mode = 2 layout_mode = 2
visible = false theme_override_constants/separation = 24
metadata/_tab_index = 3
theme_override_constants/margin_left = 16
theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 16
theme_override_constants/margin_bottom = 12
[node name="ModSettings" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin" unique_id=2038671463] [node name="VSyncLabel" type="Label" parent="Window/Column/Body/Pages/Display/VSyncRow" unique_id=700706593]
custom_minimum_size = Vector2(280, 0)
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 text = "V-Sync"
theme_override_constants/separation = 12
[node name="LeftPanel" type="VBoxContainer" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings" unique_id=1856089080] [node name="VSyncToggle" type="CheckButton" parent="Window/Column/Body/Pages/Display/VSyncRow" unique_id=1235277494]
layout_mode = 2
size_flags_vertical = 4
focus_mode = 0
[node name="FpsRow" type="HBoxContainer" parent="Window/Column/Body/Pages/Display" unique_id=327395126]
layout_mode = 2
theme_override_constants/separation = 24
[node name="FpsLabel" type="Label" parent="Window/Column/Body/Pages/Display/FpsRow" unique_id=427174428]
custom_minimum_size = Vector2(280, 0)
layout_mode = 2
text = "Frame rate limit"
[node name="FpsSelect" type="OptionButton" parent="Window/Column/Body/Pages/Display/FpsRow" unique_id=321930729]
custom_minimum_size = Vector2(400, 0)
layout_mode = 2
size_flags_vertical = 4
[node name="Mods" type="HBoxContainer" parent="Window/Column/Body/Pages" unique_id=750857449]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 24
[node name="ModListPanel" type="PanelContainer" parent="Window/Column/Body/Pages/Mods" unique_id=1830546854]
custom_minimum_size = Vector2(240, 0) custom_minimum_size = Vector2(240, 0)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 6
[node name="ModsTitle" type="Label" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/LeftPanel" unique_id=1639725410] [node name="Column" type="VBoxContainer" parent="Window/Column/Body/Pages/Mods/ModListPanel" unique_id=1090016444]
layout_mode = 2 layout_mode = 2
theme_override_font_sizes/font_size = 15 theme_override_constants/separation = 8
text = "Mods"
horizontal_alignment = 1
[node name="ModScroll" type="ScrollContainer" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/LeftPanel" unique_id=1412822434] [node name="ModsTitle" type="Label" parent="Window/Column/Body/Pages/Mods/ModListPanel/Column" unique_id=284516881]
layout_mode = 2
text = "Mods"
uppercase = true
[node name="ModScroll" type="ScrollContainer" parent="Window/Column/Body/Pages/Mods/ModListPanel/Column" unique_id=1938378648]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
horizontal_scroll_mode = 0 horizontal_scroll_mode = 0
[node name="ModList" type="VBoxContainer" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/LeftPanel/ModScroll" unique_id=543902025] [node name="ModList" type="VBoxContainer" parent="Window/Column/Body/Pages/Mods/ModListPanel/Column/ModScroll" unique_id=316495967]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 4
[node name="Detail" type="VBoxContainer" parent="Window/Column/Body/Pages/Mods" unique_id=1311174486]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 12
[node name="SelectedModName" type="Label" parent="Window/Column/Body/Pages/Mods/Detail" unique_id=562690072]
layout_mode = 2 layout_mode = 2
[node name="Sep" type="VBoxContainer" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings" unique_id=950619413] [node name="SettingsScroll" type="ScrollContainer" parent="Window/Column/Body/Pages/Mods/Detail" unique_id=1319182580]
custom_minimum_size = Vector2(10, 0)
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="RightPanel" type="VBoxContainer" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings" unique_id=272738827] [node name="SettingsContainer" type="VBoxContainer" parent="Window/Column/Body/Pages/Mods/Detail/SettingsScroll" unique_id=951372036]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_constants/separation = 8 theme_override_constants/separation = 8
[node name="TopBar" type="HBoxContainer" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/RightPanel" unique_id=678746437] [node name="Placeholder" type="Label" parent="Window/Column/Body/Pages/Mods/Detail/SettingsScroll/SettingsContainer" unique_id=1476346408]
layout_mode = 2
text = "No mod settings available"
[node name="RuleFooter" type="ColorRect" parent="Window/Column" unique_id=304272990]
custom_minimum_size = Vector2(0, 1)
layout_mode = 2
[node name="Footer" type="MarginContainer" parent="Window/Column" unique_id=1834247695]
layout_mode = 2
theme_override_constants/margin_left = 24
theme_override_constants/margin_top = 12
theme_override_constants/margin_right = 24
theme_override_constants/margin_bottom = 12
[node name="Row" type="HBoxContainer" parent="Window/Column/Footer" unique_id=922090408]
custom_minimum_size = Vector2(0, 38)
layout_mode = 2 layout_mode = 2
theme_override_constants/separation = 12 theme_override_constants/separation = 12
[node name="SelectedModName" type="Label" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/RightPanel/TopBar" unique_id=393931159] [node name="Hint" type="Label" parent="Window/Column/Footer/Row" unique_id=1010204388]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
theme_override_font_sizes/font_size = 16 size_flags_vertical = 4
[node name="ResetBtn" type="Button" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/RightPanel/TopBar" unique_id=531057889] [node name="ResetButton" type="Button" parent="Window/Column/Footer/Row" unique_id=1841068864]
layout_mode = 2 layout_mode = 2
text = "Reset" focus_mode = 0
[node name="SettingsScroll" type="ScrollContainer" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/RightPanel" unique_id=674333894]
layout_mode = 2
size_flags_vertical = 3
horizontal_scroll_mode = 0
[node name="SettingsContainer" type="VBoxContainer" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/RightPanel/SettingsScroll" unique_id=1664189823]
layout_mode = 2
theme_override_constants/separation = 6
[node name="Placeholder" type="Label" parent="Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/RightPanel/SettingsScroll/SettingsContainer" unique_id=1501445323]
layout_mode = 2
text = "No mod settings available"
horizontal_alignment = 1
[node name="BackBtn" type="Button" parent="Panel/Margin/VBox" unique_id=249196016]
layout_mode = 2
size_flags_horizontal = 8
text = "Back"
+4
View File
@@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Cthangover.Core.Scenes; using Cthangover.Core.Scenes;
using Cthangover.Core.Settings; using Cthangover.Core.Settings;
using Cthangover.Core.UI;
using Cthangover.Core.UI.Menu; using Cthangover.Core.UI.Menu;
using Cthangover.Core.UI.Theming; using Cthangover.Core.UI.Theming;
using Cthangover.Core.UI.Tool; using Cthangover.Core.UI.Tool;
@@ -54,6 +55,9 @@ namespace Cthangover.BaseMenu
private void BuildUi() private void BuildUi()
{ {
CthangoverTheme.Apply(this); CthangoverTheme.Apply(this);
// Panels of the session (pause menu, inventory, studio...) live on the
// persistent UI layer above every scene; none of them belongs on the title screen.
UiHost.Instance?.HideAll();
_settingsMenu = new ModScenePrefab("scenes/menu/settings_menu.tscn").Instantiate<SettingsMenu>(); _settingsMenu = new ModScenePrefab("scenes/menu/settings_menu.tscn").Instantiate<SettingsMenu>();
_settingsMenu.Name = "SettingsPanel"; _settingsMenu.Name = "SettingsPanel";
+289 -181
View File
@@ -9,6 +9,7 @@ using Cthangover.Core.Utils;
using Godot; using Godot;
using Cthangover.Mods.Interface.Menu; using Cthangover.Mods.Interface.Menu;
using Cthangover.Mods.Interface.Theming;
namespace Cthangover.BaseMenu namespace Cthangover.BaseMenu
{ {
/// <summary> /// <summary>
@@ -19,8 +20,12 @@ namespace Cthangover.BaseMenu
/// immediate effect, then emit LanguageChanged so parent menus can refresh /// immediate effect, then emit LanguageChanged so parent menus can refresh
/// their translated button text. Sliders become non-editable when their /// their translated button text. Sliders become non-editable when their
/// toggle is off, enforcing the "disabled means off" semantics visually. /// toggle is off, enforcing the "disabled means off" semantics visually.
/// Display changes (window mode, resolution, monitor, V-Sync, scale) are /// Display changes (window mode, monitor, window size, V-Sync, frame rate
/// applied live via DisplayServer and ProjectSettings. /// cap) are applied live through <see cref="DisplaySettingsService"/> and saved
/// at once, so the next start restores exactly what is on screen; the lists
/// come from <see cref="DisplayOptions"/>, shared with the first-run launcher.
/// The window size list is rebuilt for the chosen monitor and is disabled in
/// the fullscreen modes, which always run at the monitor's resolution.
/// </summary> /// </summary>
public partial class SettingsMenu : Control, ISettingsPanel public partial class SettingsMenu : Control, ISettingsPanel
{ {
@@ -30,7 +35,20 @@ namespace Cthangover.BaseMenu
/// refresh their translated button text. /// refresh their translated button text.
/// </summary> /// </summary>
public event Action LanguageChanged; public event Action LanguageChanged;
private TabContainer _tabs; private const string Header = "Window/Column/Header/Row/";
private const string Pages = "Window/Column/Body/Pages/";
private const string Audio = Pages + "Audio/";
private const string Display = Pages + "Display/";
private const string Mods = Pages + "Mods/";
/// <summary>Names of <see cref="LanguageCode.Supported"/>, same order.</summary>
private static readonly string[] LanguageKeys = { "settings/lang_ru", "settings/lang_en", "settings/lang_zh" };
private static readonly string[] TabKeys = { "settings/tab_general", "settings/tab_audio", "settings/tab_display", "settings/tab_mods" };
private Button[] _tabButtons;
private Control[] _pages;
private int _tab;
private CheckButton _soundsToggle; private CheckButton _soundsToggle;
private CheckButton _musicToggle; private CheckButton _musicToggle;
private CheckButton _ambientToggle; private CheckButton _ambientToggle;
@@ -41,11 +59,11 @@ namespace Cthangover.BaseMenu
private Label _musicVolumeValue; private Label _musicVolumeValue;
private Label _ambientVolumeValue; private Label _ambientVolumeValue;
private OptionButton _langSelect; private OptionButton _langSelect;
private Button _backBtn; private Button _resetButton;
private Label _footerHint;
private VBoxContainer _modListContainer; private VBoxContainer _modListContainer;
private Label _selectedModName; private Label _selectedModName;
private Button _resetModSettingsBtn;
private VBoxContainer _settingsContainer; private VBoxContainer _settingsContainer;
private string _selectedModId; private string _selectedModId;
private readonly Dictionary<Control, string> _controlKeys = new(); private readonly Dictionary<Control, string> _controlKeys = new();
@@ -53,72 +71,105 @@ namespace Cthangover.BaseMenu
private OptionButton _windowModeSelect; private OptionButton _windowModeSelect;
private OptionButton _resolutionSelect; private OptionButton _resolutionSelect;
private OptionButton _monitorSelect; private OptionButton _monitorSelect;
private OptionButton _scaleSelect; private OptionButton _fpsSelect;
private CheckButton _vsyncToggle; private CheckButton _vsyncToggle;
private Label _resHint;
private List<Vector2I> _windowSizes = new();
private readonly Dictionary<Label, string> _labelKeys = new(); private readonly Dictionary<Label, string> _labelKeys = new();
private bool _initialized; private bool _initialized;
private static readonly Vector2I[] CommonResolutions =
{
new(1280, 720),
new(1366, 768),
new(1600, 900),
new(1920, 1080),
new(2560, 1440),
new(3840, 2160),
};
private static readonly int[] ScaleOptions = { 1, 2, 3, 4 };
public override void _Ready() public override void _Ready()
{ {
_tabs = GetNode<TabContainer>("Panel/Margin/VBox/Tabs"); CthangoverTheme.Apply(this);
GetNode<ColorRect>("Backdrop").Color = UiTokens.Backdrop;
GetNode<PanelContainer>("Window").AddThemeStyleboxOverride("panel", UiKit.Frame(UiTokens.RadiusLg, UiTokens.Surface0, UiTokens.Line));
GetNode<ColorRect>("Window/Column/RuleHeader").Color = UiTokens.Line;
GetNode<ColorRect>("Window/Column/RuleFooter").Color = UiTokens.Line;
_langSelect = GetNode<OptionButton>("Panel/Margin/VBox/Tabs/GeneralMargin/General/LangBox/LangSelect"); var title = GetNode<Label>(Header + "Title");
title.AddThemeFontSizeOverride("font_size", UiTokens.FontTitle);
title.AddThemeColorOverride("font_color", UiTokens.InkBright);
RegisterLabel(title, "menu/settings");
_soundsToggle = GetNode<CheckButton>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox/ToggleRow/SoundsToggle"); var close = GetNode<Button>(Header + "CloseButton");
_soundsVolume = GetNode<HSlider>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox/VolumeRow/SoundsVolume"); CthangoverTheme.ApplyIcon(close);
_soundsVolumeValue = GetNode<Label>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox/VolumeRow/SoundsVolumeValue"); close.Pressed += OnBackPressed;
_musicToggle = GetNode<CheckButton>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox/ToggleRow2/MusicToggle"); _tabButtons = new[]
_musicVolume = GetNode<HSlider>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox/VolumeRow2/MusicVolume"); {
_musicVolumeValue = GetNode<Label>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox/VolumeRow2/MusicVolumeValue"); GetNode<Button>(Header + "Tabs/GeneralTab"),
GetNode<Button>(Header + "Tabs/AudioTab"),
GetNode<Button>(Header + "Tabs/DisplayTab"),
GetNode<Button>(Header + "Tabs/ModsTab"),
};
_pages = new[]
{
GetNode<Control>(Pages + "General"),
GetNode<Control>(Pages + "Audio"),
GetNode<Control>(Pages + "Display"),
GetNode<Control>(Pages + "Mods"),
};
var tabGroup = new ButtonGroup();
foreach (var tab in _tabButtons)
{
CthangoverTheme.ApplyTab(tab);
tab.ButtonGroup = tabGroup;
}
tabGroup.Pressed += OnTabPressed;
_ambientToggle = GetNode<CheckButton>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox/ToggleRow3/AmbientToggle"); _langSelect = GetNode<OptionButton>(Pages + "General/LangRow/LangSelect");
_ambientVolume = GetNode<HSlider>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox/VolumeRow3/AmbientVolume");
_ambientVolumeValue = GetNode<Label>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox/VolumeRow3/AmbientVolumeValue");
_windowModeSelect = GetNode<OptionButton>("Panel/Margin/VBox/Tabs/DisplayMargin/Display/ModeBox/ModeSelect"); _soundsToggle = GetNode<CheckButton>(Audio + "SoundsRow/SoundsToggle");
_resolutionSelect = GetNode<OptionButton>("Panel/Margin/VBox/Tabs/DisplayMargin/Display/ResBox/ResSelect"); _soundsVolume = GetNode<HSlider>(Audio + "SoundsRow/SoundsVolume");
_monitorSelect = GetNode<OptionButton>("Panel/Margin/VBox/Tabs/DisplayMargin/Display/MonitorBox/MonitorSelect"); _soundsVolumeValue = GetNode<Label>(Audio + "SoundsRow/SoundsValue");
_scaleSelect = GetNode<OptionButton>("Panel/Margin/VBox/Tabs/DisplayMargin/Display/ScaleBox/ScaleSelect"); _musicToggle = GetNode<CheckButton>(Audio + "MusicRow/MusicToggle");
_vsyncToggle = GetNode<CheckButton>("Panel/Margin/VBox/Tabs/DisplayMargin/Display/VSyncRow/VSyncToggle"); _musicVolume = GetNode<HSlider>(Audio + "MusicRow/MusicVolume");
_musicVolumeValue = GetNode<Label>(Audio + "MusicRow/MusicValue");
_ambientToggle = GetNode<CheckButton>(Audio + "AmbientRow/AmbientToggle");
_ambientVolume = GetNode<HSlider>(Audio + "AmbientRow/AmbientVolume");
_ambientVolumeValue = GetNode<Label>(Audio + "AmbientRow/AmbientValue");
foreach (var value in new[] { _soundsVolumeValue, _musicVolumeValue, _ambientVolumeValue })
value.AddThemeColorOverride("font_color", UiTokens.AccentBright);
_backBtn = GetNode<Button>("Panel/Margin/VBox/BackBtn"); _windowModeSelect = GetNode<OptionButton>(Display + "ModeRow/ModeSelect");
CthangoverTheme.ApplyIcon(_backBtn); _resolutionSelect = GetNode<OptionButton>(Display + "ResRow/ResSelect");
_monitorSelect = GetNode<OptionButton>(Display + "MonitorRow/MonitorSelect");
_fpsSelect = GetNode<OptionButton>(Display + "FpsRow/FpsSelect");
_vsyncToggle = GetNode<CheckButton>(Display + "VSyncRow/VSyncToggle");
_resHint = GetNode<Label>(Display + "ResHint");
StyleHint(_resHint);
_modListContainer = GetNode<VBoxContainer>("Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/LeftPanel/ModScroll/ModList"); GetNode<PanelContainer>(Mods + "ModListPanel").AddThemeStyleboxOverride("panel", ListPanelStyle());
_selectedModName = GetNode<Label>("Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/RightPanel/TopBar/SelectedModName"); _modListContainer = GetNode<VBoxContainer>(Mods + "ModListPanel/Column/ModScroll/ModList");
_resetModSettingsBtn = GetNode<Button>("Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/RightPanel/TopBar/ResetBtn"); _selectedModName = GetNode<Label>(Mods + "Detail/SelectedModName");
CthangoverTheme.ApplyDanger(_resetModSettingsBtn); _selectedModName.AddThemeFontSizeOverride("font_size", UiTokens.FontH3);
_settingsContainer = GetNode<VBoxContainer>("Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/RightPanel/SettingsScroll/SettingsContainer"); _selectedModName.AddThemeColorOverride("font_color", UiTokens.InkBright);
_settingsContainer = GetNode<VBoxContainer>(Mods + "Detail/SettingsScroll/SettingsContainer");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Title"), "menu/settings"); _resetButton = GetNode<Button>("Window/Column/Footer/Row/ResetButton");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/GeneralMargin/General/LangBox/LangLabel"), "settings/language"); CthangoverTheme.ApplyDanger(_resetButton);
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox/ToggleRow/SoundsLabel"), "settings/sounds"); _resetButton.Pressed += OnResetPressed;
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/SoundsBox/VolumeRow/SoundsVolumeLabel"), "settings/volume"); _footerHint = GetNode<Label>("Window/Column/Footer/Row/Hint");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox/ToggleRow2/MusicLabel"), "settings/music"); StyleHint(_footerHint);
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/MusicBox/VolumeRow2/MusicVolumeLabel"), "settings/volume"); RegisterLabel(_footerHint, "settings/saved_at_once");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox/ToggleRow3/AmbientLabel"), "settings/ambient");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/AudioMargin/Audio/AmbientBox/VolumeRow3/AmbientVolumeLabel"), "settings/volume"); RegisterSection(Pages + "General/LangSection", "settings/language");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/DisplayMargin/Display/ModeBox/ModeLabel"), "settings/window_mode"); RegisterLabel(GetNode<Label>(Pages + "General/LangRow/LangLabel"), "settings/interface_language");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/DisplayMargin/Display/ResBox/ResLabel"), "settings/resolution"); RegisterSection(Audio + "VolumeSection", "settings/volume");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/DisplayMargin/Display/MonitorBox/MonitorLabel"), "settings/monitor"); RegisterLabel(GetNode<Label>(Audio + "SoundsRow/SoundsLabel"), "settings/sounds");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/DisplayMargin/Display/ScaleBox/ScaleLabel"), "settings/scale"); RegisterLabel(GetNode<Label>(Audio + "MusicRow/MusicLabel"), "settings/music");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/DisplayMargin/Display/VSyncRow/VSyncLabel"), "settings/vsync"); RegisterLabel(GetNode<Label>(Audio + "AmbientRow/AmbientLabel"), "settings/ambient");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/LeftPanel/ModsTitle"), "settings/mod_list_title"); RegisterSection(Display + "WindowSection", "settings/section_window");
RegisterLabel(GetNode<Label>("Panel/Margin/VBox/Tabs/ModSettingsMargin/ModSettings/RightPanel/SettingsScroll/SettingsContainer/Placeholder"), "settings/mods_no_settings"); RegisterLabel(GetNode<Label>(Display + "ModeRow/ModeLabel"), "settings/window_mode");
RegisterLabel(GetNode<Label>(Display + "ResRow/ResLabel"), "settings/resolution");
RegisterLabel(GetNode<Label>(Display + "MonitorRow/MonitorLabel"), "settings/monitor");
RegisterLabel(_resHint, "settings/resolution_fullscreen_hint");
RegisterSection(Display + "RenderSection", "settings/section_performance");
RegisterLabel(GetNode<Label>(Display + "VSyncRow/VSyncLabel"), "settings/vsync");
RegisterLabel(GetNode<Label>(Display + "FpsRow/FpsLabel"), "settings/max_fps");
RegisterSection(Mods + "ModListPanel/Column/ModsTitle", "settings/mod_list_title");
RegisterLabel(GetNode<Label>(Mods + "Detail/SettingsScroll/SettingsContainer/Placeholder"), "settings/mods_no_settings");
_soundsToggle.Toggled += OnSoundsToggled; _soundsToggle.Toggled += OnSoundsToggled;
_musicToggle.Toggled += OnMusicToggled; _musicToggle.Toggled += OnMusicToggled;
@@ -127,27 +178,80 @@ namespace Cthangover.BaseMenu
_musicVolume.ValueChanged += OnMusicVolumeChanged; _musicVolume.ValueChanged += OnMusicVolumeChanged;
_ambientVolume.ValueChanged += OnAmbientVolumeChanged; _ambientVolume.ValueChanged += OnAmbientVolumeChanged;
_langSelect.ItemSelected += OnLanguageSelected; _langSelect.ItemSelected += OnLanguageSelected;
_backBtn.Pressed += OnBackPressed;
_backBtn.Text = TranslationServer.Translate("settings/back");
_windowModeSelect.ItemSelected += OnDisplaySettingChanged; _windowModeSelect.ItemSelected += OnWindowSettingChanged;
_resolutionSelect.ItemSelected += OnDisplaySettingChanged; _resolutionSelect.ItemSelected += OnWindowSettingChanged;
_monitorSelect.ItemSelected += OnDisplaySettingChanged; _monitorSelect.ItemSelected += OnMonitorSelected;
_scaleSelect.ItemSelected += OnDisplaySettingChanged; _fpsSelect.ItemSelected += OnRenderingSettingChanged;
_vsyncToggle.Toggled += OnDisplayToggleChanged; _vsyncToggle.Toggled += OnVsyncToggled;
_resetModSettingsBtn.Pressed += OnResetModSettings;
LoadSettingsToUI(); LoadSettingsToUI();
BuildModList(); BuildModList();
ShowTab(0);
_initialized = true; _initialized = true;
} }
/// <summary>Index of the visible tab: 0 general, 1 sound, 2 display, 3 mod settings.</summary>
public int CurrentTab => _tab;
/// <summary>Shows tab <paramref name="index"/>: its page, the gold tab button and the footer reset that belongs to it.</summary>
public void ShowTab(int index)
{
_tab = Mathf.Clamp(index, 0, _pages.Length - 1);
for (var i = 0; i < _pages.Length; i++)
{
_pages[i].Visible = i == _tab;
_tabButtons[i].SetPressedNoSignal(i == _tab);
}
UpdateResetButton();
}
private void OnTabPressed(BaseButton button) => ShowTab(Array.IndexOf(_tabButtons, button));
/// <summary>The footer reset acts on the visible tab: display settings, or the selected mod's settings.</summary>
private void UpdateResetButton()
{
_resetButton.Visible = _tab == 2 || (_tab == 3 && !string.IsNullOrEmpty(_selectedModId));
_resetButton.Text = TranslationServer.Translate(_tab == 2 ? "settings/display_reset" : "settings/mods_reset");
}
private void OnResetPressed()
{
if (_tab == 2)
OnResetDisplay();
else if (_tab == 3)
OnResetModSettings();
}
private void RegisterLabel(Label label, string key) private void RegisterLabel(Label label, string key)
{ {
if (label != null) if (label != null)
_labelKeys[label] = key; _labelKeys[label] = key;
} }
/// <summary>Section caption: small muted caps, as section titles everywhere in the kit.</summary>
private void RegisterSection(string path, string key)
{
var label = GetNode<Label>(path);
label.AddThemeFontSizeOverride("font_size", UiTokens.FontSmall);
label.AddThemeColorOverride("font_color", UiTokens.Muted);
RegisterLabel(label, key);
}
private static void StyleHint(Label label)
{
label.AddThemeFontSizeOverride("font_size", UiTokens.FontBodySmall);
label.AddThemeColorOverride("font_color", UiTokens.Faint);
}
private static StyleBoxFlat ListPanelStyle()
{
var style = UiKit.Frame(UiTokens.RadiusMd, UiTokens.Surface2, UiTokens.Line);
style.ContentMarginLeft = style.ContentMarginRight = UiTokens.Space5;
style.ContentMarginTop = style.ContentMarginBottom = UiTokens.Space5;
return style;
}
private void RefreshUIText() private void RefreshUIText()
{ {
foreach (var kv in _labelKeys) foreach (var kv in _labelKeys)
@@ -155,12 +259,10 @@ namespace Cthangover.BaseMenu
if (IsInstanceValid(kv.Key)) if (IsInstanceValid(kv.Key))
kv.Key.Text = TranslationServer.Translate(kv.Value); kv.Key.Text = TranslationServer.Translate(kv.Value);
} }
_backBtn.Text = TranslationServer.Translate("settings/back"); for (var i = 0; i < _tabButtons.Length; i++)
_tabs.SetTabTitle(0, TranslationServer.Translate("settings/tab_general")); _tabButtons[i].Text = TranslationServer.Translate(TabKeys[i]);
_tabs.SetTabTitle(1, TranslationServer.Translate("settings/tab_audio")); GetNode<Button>(Header + "CloseButton").TooltipText = TranslationServer.Translate("settings/back");
_tabs.SetTabTitle(2, TranslationServer.Translate("settings/tab_display")); UpdateResetButton();
_tabs.SetTabTitle(3, TranslationServer.Translate("settings/tab_mods"));
_resetModSettingsBtn.Text = TranslationServer.Translate("settings/mods_reset");
PopulateLangOptions(); PopulateLangOptions();
PopulateDisplayOptions(); PopulateDisplayOptions();
} }
@@ -180,96 +282,86 @@ namespace Cthangover.BaseMenu
_musicVolumeValue.Text = $"{s.MusicsVolume}%"; _musicVolumeValue.Text = $"{s.MusicsVolume}%";
_ambientVolumeValue.Text = $"{s.AmbientVolume}%"; _ambientVolumeValue.Text = $"{s.AmbientVolume}%";
_vsyncToggle.ButtonPressed = s.VsyncEnabled;
_windowModeSelect.Selected = WindowModeToIndex(s.WindowMode);
UpdateSlidersEditable(); UpdateSlidersEditable();
PopulateLangOptions(); PopulateLangOptions();
PopulateDisplayOptions();
RefreshUIText(); RefreshUIText();
LoadDisplayToUI(s);
}
SelectCurrentResolution(s); /// <summary>Shows the saved display settings; the window size list is built for the saved monitor.</summary>
SelectCurrentMonitor(s); private void LoadDisplayToUI(SettingsData s)
SelectCurrentScale(s); {
_windowModeSelect.Selected = DisplayOptions.WindowModeIndex(s.WindowMode);
_monitorSelect.Selected = DisplaySettingsService.ClampScreen(s.CurrentScreen);
FillWindowSizes(new Vector2I(s.ResolutionWidth, s.ResolutionHeight));
_vsyncToggle.ButtonPressed = s.VsyncEnabled;
_fpsSelect.Selected = DisplayOptions.FrameRateIndex(s.MaxFps);
UpdateWindowSizeEditable();
} }
private void PopulateLangOptions() private void PopulateLangOptions()
{ {
_langSelect.Clear(); _langSelect.Clear();
_langSelect.AddItem(TranslationServer.Translate("settings/lang_ru")); foreach (var key in LanguageKeys)
_langSelect.AddItem(TranslationServer.Translate("settings/lang_en")); _langSelect.AddItem(TranslationServer.Translate(key));
_langSelect.AddItem(TranslationServer.Translate("settings/lang_zh"));
var s = GameData.Instance?.Settings; var s = GameData.Instance?.Settings;
if (s == null) return; if (s == null) return;
_langSelect.Selected = s.Language switch { "en" => 1, "zh" => 2, _ => 0 }; _langSelect.Selected = Mathf.Max(0, System.Array.IndexOf(LanguageCode.Supported, LanguageCode.Normalize(s.Language) ?? LanguageCode.Supported[0]));
} }
/// <summary>Re-fills the translated display lists, keeping the selected items.</summary>
private void PopulateDisplayOptions() private void PopulateDisplayOptions()
{ {
var previousWindow = _windowModeSelect.Selected; var mode = _windowModeSelect.Selected;
var previousRes = _resolutionSelect.Selected; var monitor = _monitorSelect.Selected;
var previousMon = _monitorSelect.Selected; var fps = _fpsSelect.Selected;
var previousScale = _scaleSelect.Selected;
_windowModeSelect.Clear(); _windowModeSelect.Clear();
_windowModeSelect.AddItem(TranslationServer.Translate("settings/window_mode_windowed")); foreach (var key in DisplayOptions.WindowModeKeys)
_windowModeSelect.AddItem(TranslationServer.Translate("settings/window_mode_borderless")); _windowModeSelect.AddItem(TranslationServer.Translate(key));
_windowModeSelect.AddItem(TranslationServer.Translate("settings/window_mode_fullscreen"));
_resolutionSelect.Clear();
foreach (var r in CommonResolutions)
_resolutionSelect.AddItem($"{r.X}×{r.Y}");
_monitorSelect.Clear(); _monitorSelect.Clear();
var count = DisplayServer.GetScreenCount(); for (var i = 0; i < DisplaySettingsService.ScreenCount; i++)
if (count <= 0) count = 1;
for (var i = 0; i < count; i++)
{ {
var size = DisplayServer.ScreenGetSize(i); var size = DisplayServer.ScreenGetSize(i);
var label = string.Format(TranslationServer.Translate("settings/monitor_screen"), i + 1, size.X, size.Y); var key = i == DisplayServer.GetPrimaryScreen() ? "settings/monitor_screen_primary" : "settings/monitor_screen";
_monitorSelect.AddItem(label); _monitorSelect.AddItem(string.Format(TranslationServer.Translate(key), i + 1, size.X, size.Y));
} }
_scaleSelect.Clear(); _fpsSelect.Clear();
foreach (var sc in ScaleOptions) foreach (var rate in DisplayOptions.FrameRates)
_scaleSelect.AddItem(TranslationServer.Translate($"settings/scale_{sc}x")); _fpsSelect.AddItem(rate == 0 ? TranslationServer.Translate("settings/max_fps_unlimited") : string.Format(TranslationServer.Translate("settings/max_fps_value"), rate));
_windowModeSelect.Selected = Mathf.Clamp(previousWindow, 0, 2); _windowModeSelect.Selected = Mathf.Clamp(mode, 0, DisplayOptions.WindowModes.Length - 1);
_resolutionSelect.Selected = Mathf.Clamp(previousRes, 0, CommonResolutions.Length - 1); _monitorSelect.Selected = Mathf.Clamp(monitor, 0, DisplaySettingsService.ScreenCount - 1);
_monitorSelect.Selected = Mathf.Clamp(previousMon, 0, count - 1); _fpsSelect.Selected = Mathf.Clamp(fps, 0, DisplayOptions.FrameRates.Length - 1);
_scaleSelect.Selected = Mathf.Clamp(previousScale, 0, ScaleOptions.Length - 1);
} }
private void SelectCurrentResolution(SettingsData s) /// <summary>Fills the window sizes of the chosen monitor, keeping <paramref name="wanted"/> (or the nearest smaller size) selected.</summary>
private void FillWindowSizes(Vector2I wanted)
{ {
for (var i = 0; i < CommonResolutions.Length; i++) _windowSizes = DisplayOptions.WindowSizes(DisplayServer.ScreenGetSize(Mathf.Max(0, _monitorSelect.Selected)), wanted);
{ _resolutionSelect.Clear();
if (CommonResolutions[i].X == s.ResolutionWidth && CommonResolutions[i].Y == s.ResolutionHeight) foreach (var size in _windowSizes)
{ _resolutionSelect.AddItem($"{size.X}×{size.Y}");
_resolutionSelect.Selected = i; _resolutionSelect.Selected = DisplayOptions.WindowSizeIndex(_windowSizes, wanted);
return;
}
}
} }
private void SelectCurrentMonitor(SettingsData s) private Vector2I SelectedWindowSize()
{ {
var count = DisplayServer.GetScreenCount(); var index = _resolutionSelect.Selected;
if (s.CurrentScreen >= 0 && s.CurrentScreen < count) return index >= 0 && index < _windowSizes.Count ? _windowSizes[index] : DisplayServer.WindowGetSize();
_monitorSelect.Selected = s.CurrentScreen;
} }
private void SelectCurrentScale(SettingsData s) private DisplayServer.WindowMode SelectedWindowMode() =>
DisplayOptions.WindowModes[Mathf.Clamp(_windowModeSelect.Selected, 0, DisplayOptions.WindowModes.Length - 1)];
private void UpdateWindowSizeEditable()
{ {
for (var i = 0; i < ScaleOptions.Length; i++) var windowed = DisplayOptions.UsesWindowSize((int)SelectedWindowMode());
{ _resolutionSelect.Disabled = !windowed;
if (ScaleOptions[i] == s.Scale) _resHint.Visible = !windowed;
{
_scaleSelect.Selected = i;
return;
}
}
} }
private void ApplyAndSave() private void ApplyAndSave()
@@ -287,40 +379,32 @@ namespace Cthangover.BaseMenu
s.Save(); s.Save();
} }
private void ApplyDisplayAndSave() /// <summary>Saves the window part (mode, monitor, size) and moves the window there.</summary>
private void ApplyWindowAndSave()
{ {
if (!_initialized) return; if (!_initialized) return;
var s = GameData.Instance?.Settings; var s = GameData.Instance?.Settings;
if (s == null) return; if (s == null) return;
var windowMode = IndexToWindowMode(_windowModeSelect.Selected); var size = SelectedWindowSize();
var resIdx = Mathf.Clamp(_resolutionSelect.Selected, 0, CommonResolutions.Length - 1); s.WindowMode = (int)SelectedWindowMode();
var resolution = CommonResolutions[resIdx]; s.CurrentScreen = Mathf.Max(0, _monitorSelect.Selected);
var monitorIdx = _monitorSelect.Selected; s.ResolutionWidth = size.X;
var scaleIdx = Mathf.Clamp(_scaleSelect.Selected, 0, ScaleOptions.Length - 1); s.ResolutionHeight = size.Y;
var scale = ScaleOptions[scaleIdx]; DisplaySettingsService.ApplyWindow(s);
var vsync = _vsyncToggle.ButtonPressed; s.Save();
}
if (monitorIdx >= 0 && monitorIdx < DisplayServer.GetScreenCount()) /// <summary>Saves V-Sync and the frame rate cap and applies them.</summary>
DisplayServer.WindowSetCurrentScreen(monitorIdx); private void ApplyRenderingAndSave()
{
if (!_initialized) return;
var s = GameData.Instance?.Settings;
if (s == null) return;
DisplayServer.WindowSetMode(windowMode); s.VsyncEnabled = _vsyncToggle.ButtonPressed;
s.MaxFps = DisplayOptions.FrameRates[Mathf.Clamp(_fpsSelect.Selected, 0, DisplayOptions.FrameRates.Length - 1)];
if (windowMode == DisplayServer.WindowMode.Windowed) DisplaySettingsService.ApplyRendering(s);
DisplayServer.WindowSetSize(resolution);
DisplayServer.WindowSetVsyncMode(vsync
? DisplayServer.VSyncMode.Enabled
: DisplayServer.VSyncMode.Disabled);
ProjectSettings.SetSetting("display/window/stretch/scale", (float)scale);
s.WindowMode = (int)windowMode;
s.ResolutionWidth = resolution.X;
s.ResolutionHeight = resolution.Y;
s.CurrentScreen = monitorIdx;
s.VsyncEnabled = vsync;
s.Scale = scale;
s.Save(); s.Save();
} }
@@ -332,8 +416,41 @@ namespace Cthangover.BaseMenu
private void OnMusicVolumeChanged(double v) { _musicVolumeValue.Text = $"{(int)v}%"; ApplyAndSave(); } private void OnMusicVolumeChanged(double v) { _musicVolumeValue.Text = $"{(int)v}%"; ApplyAndSave(); }
private void OnAmbientVolumeChanged(double v) { _ambientVolumeValue.Text = $"{(int)v}%"; ApplyAndSave(); } private void OnAmbientVolumeChanged(double v) { _ambientVolumeValue.Text = $"{(int)v}%"; ApplyAndSave(); }
private void OnDisplaySettingChanged(long idx) => ApplyDisplayAndSave(); private void OnWindowSettingChanged(long idx)
private void OnDisplayToggleChanged(bool on) => ApplyDisplayAndSave(); {
UpdateWindowSizeEditable();
ApplyWindowAndSave();
}
private void OnMonitorSelected(long idx)
{
FillWindowSizes(SelectedWindowSize());
ApplyWindowAndSave();
}
private void OnRenderingSettingChanged(long idx) => ApplyRenderingAndSave();
private void OnVsyncToggled(bool on) => ApplyRenderingAndSave();
/// <summary>Returns the display settings to the game defaults (game_config.json), applies and saves them.</summary>
private void OnResetDisplay()
{
var s = GameData.Instance?.Settings;
if (s == null) return;
var defaults = new SettingsData();
s.WindowMode = defaults.WindowMode;
s.CurrentScreen = defaults.CurrentScreen;
s.ResolutionWidth = defaults.ResolutionWidth;
s.ResolutionHeight = defaults.ResolutionHeight;
s.VsyncEnabled = defaults.VsyncEnabled;
s.MaxFps = defaults.MaxFps;
DisplaySettingsService.Apply(s);
s.Save();
_initialized = false;
LoadDisplayToUI(s);
_initialized = true;
}
private void OnLanguageSelected(long idx) private void OnLanguageSelected(long idx)
{ {
@@ -341,8 +458,8 @@ namespace Cthangover.BaseMenu
var s = GameData.Instance?.Settings; var s = GameData.Instance?.Settings;
if (s == null) return; if (s == null) return;
var lang = idx switch { 1 => "en", 2 => "zh", _ => "ru-ru" }; var lang = LanguageCode.Supported[Mathf.Clamp((int)idx, 0, LanguageCode.Supported.Length - 1)];
if (s.Language == lang) return; if (LanguageCode.Normalize(s.Language) == lang) return;
s.Language = lang; s.Language = lang;
s.Save(); s.Save();
@@ -352,7 +469,11 @@ namespace Cthangover.BaseMenu
} }
/// <inheritdoc cref="IMenuPanel" /> /// <inheritdoc cref="IMenuPanel" />
public void Open() => Visible = true; public void Open()
{
Visible = true;
ShowTab(_tab);
}
private void OnBackPressed() => Visible = false; private void OnBackPressed() => Visible = false;
@@ -363,26 +484,6 @@ namespace Cthangover.BaseMenu
_ambientVolume.Editable = _ambientToggle.ButtonPressed; _ambientVolume.Editable = _ambientToggle.ButtonPressed;
} }
private static int WindowModeToIndex(int mode)
{
return mode switch
{
3 => 1,
4 => 2,
_ => 0,
};
}
private static DisplayServer.WindowMode IndexToWindowMode(int index)
{
return index switch
{
1 => DisplayServer.WindowMode.Fullscreen,
2 => DisplayServer.WindowMode.ExclusiveFullscreen,
_ => DisplayServer.WindowMode.Windowed,
};
}
private void BuildModList() private void BuildModList()
{ {
foreach (var child in _modListContainer.GetChildren()) foreach (var child in _modListContainer.GetChildren())
@@ -393,19 +494,25 @@ namespace Cthangover.BaseMenu
if (modsWithSettings.Count == 0) if (modsWithSettings.Count == 0)
return; return;
var group = new ButtonGroup();
foreach (var modId in modsWithSettings) foreach (var modId in modsWithSettings)
{ {
var btn = new Button var btn = new Button
{ {
Text = ModSettingsRegistry.GetDisplayTitle(modId), Text = ModSettingsRegistry.GetDisplayTitle(modId),
SizeFlagsHorizontal = Control.SizeFlags.ExpandFill, SizeFlagsHorizontal = Control.SizeFlags.ExpandFill,
Alignment = HorizontalAlignment.Left,
ToggleMode = true,
ButtonGroup = group,
FocusMode = FocusModeEnum.None,
}; };
CthangoverTheme.ApplyTab(btn);
btn.Pressed += () => OnModSelected(modId); btn.Pressed += () => OnModSelected(modId);
_modListContainer.AddChild(btn); _modListContainer.AddChild(btn);
} }
var firstMod = modsWithSettings[0]; ((Button)_modListContainer.GetChild(0)).SetPressedNoSignal(true);
OnModSelected(firstMod); OnModSelected(modsWithSettings[0]);
} }
private void OnModSelected(string modId) private void OnModSelected(string modId)
@@ -413,6 +520,7 @@ namespace Cthangover.BaseMenu
_selectedModId = modId; _selectedModId = modId;
_selectedModName.Text = ModSettingsRegistry.GetDisplayTitle(modId); _selectedModName.Text = ModSettingsRegistry.GetDisplayTitle(modId);
BuildModSettingsPanel(modId); BuildModSettingsPanel(modId);
UpdateResetButton();
} }
private void BuildModSettingsPanel(string modId) private void BuildModSettingsPanel(string modId)
@@ -450,7 +558,7 @@ namespace Cthangover.BaseMenu
var label = new Label var label = new Label
{ {
Text = TranslationServer.Translate(def.Name), Text = TranslationServer.Translate(def.Name),
CustomMinimumSize = new Vector2(180, 0), CustomMinimumSize = new Vector2(260, 0),
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
}; };
row.AddChild(label); row.AddChild(label);
@@ -23,6 +23,8 @@ namespace Mods.CollectionEditTest
/// <item>The collection gets the developer toolset (<c>dev_tools</c>: editors, /// <item>The collection gets the developer toolset (<c>dev_tools</c>: editors,
/// the scenario studio, …), saved the way the collection editor saves it (a /// 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> /// 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 /// <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 /// 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> /// 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 AddedToolset = "dev_tools";
private const string AddedMod = "editor_tools"; private const string AddedMod = "editor_tools";
private const string StudioMod = "scenario_studio"; private const string StudioMod = "scenario_studio";
private const string GameMenuId = "game_menu";
private const float TimeoutSeconds = 240f; private const float TimeoutSeconds = 240f;
private readonly List<string> failures = new(); private readonly List<string> failures = new();
@@ -115,21 +118,42 @@ namespace Mods.CollectionEditTest
AddEditorsToCollection(); AddEditorsToCollection();
Check("the edited collection makes the running game outdated", GameCatalog.IsActiveOutdated()); Check("the edited collection makes the running game outdated", GameCatalog.IsActiveOutdated());
GetTree().Root.GetNode<GodotSceneService>("GodotSceneService").SwitchToMenu(); AppServices.Current.Ui.ToggleEntry(GameMenuId);
break; break;
case 1: 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()) if (Busy || !PressMenuPlay())
return; return;
enteredScenes.Clear(); enteredScenes.Clear();
break; break;
case 2: case 4:
if (!StartFromPicker()) if (!StartFromPicker())
return; return;
break; break;
case 3: case 5:
{ {
if (Busy || ActiveGame.Id != GameId || enteredScenes.Count == 0) if (Busy || ActiveGame.Id != GameId || enteredScenes.Count == 0)
return; 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=&lt;dir&gt;</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));
}
}
}