Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
903fc43
Gate navigation sounds behind UI focus
ikostan Jun 28, 2026
6fdc14c
Update test_audio_integration.gd
ikostan Jun 28, 2026
98ed589
Delete run_unit_tests.sh
ikostan Jun 28, 2026
b515a51
Update globals.gd
ikostan Jun 28, 2026
2215091
Update globals.gd
ikostan Jun 28, 2026
91a127d
Refactor UI audio to data-driven approach
ikostan Jun 28, 2026
24ba24d
Update globals.gd
ikostan Jun 28, 2026
c0d824d
Add unique_ids to SFX scene & simplify input
ikostan Jun 28, 2026
cde5aed
Mute global ui_accept for UI controls; add tests
ikostan Jun 28, 2026
203cba3
Update test_audio_hierarchy_and_sliders.gd
ikostan Jun 28, 2026
eaa7d61
Update test_globals_button_hooks.gd
ikostan Jun 28, 2026
3a63bc9
Use BaseButton in ui_accept guard; refresh tests
ikostan Jun 28, 2026
78032ed
Update globals.gd
ikostan Jun 28, 2026
c63cdbe
Track input device & improve menu input handling
ikostan Jun 29, 2026
0a943d2
Update globals.gd
ikostan Jun 29, 2026
006cade
Update test_globals_input_guards.gd
ikostan Jun 29, 2026
28c4c2e
Update test_nav_escape_sfx.gd
ikostan Jun 29, 2026
88ff1b6
Update globals.gd
ikostan Jun 29, 2026
6a5fb62
Update test_nav_escape_sfx.gd
ikostan Jun 29, 2026
9710d8e
Update test_quit_game_confirm_dialog_sfx.gd
ikostan Jun 29, 2026
a51e65b
Update globals.gd
ikostan Jun 29, 2026
c594c98
Update globals.gd
ikostan Jun 29, 2026
36c7a45
Update audio_constants.gd
ikostan Jun 29, 2026
6041a74
Update test_audio_integration.gd
ikostan Jun 29, 2026
76cb333
Update test_audio_integration.gd
ikostan Jun 29, 2026
be056a9
Decouple SFX paths with asset map and optimize input handling
ikostan Jun 29, 2026
51a693c
Update test_audio_sfx_centralization.gd
ikostan Jun 29, 2026
ea05249
Update test_globals_button_hooks.gd
ikostan Jun 29, 2026
e0fb2a1
Update globals.gd
ikostan Jun 29, 2026
f4b3d4e
Update globals.gd
ikostan Jun 29, 2026
92fd9d6
Update globals.gd
ikostan Jun 29, 2026
795cfbc
Update test_globals_button_hooks.gd
ikostan Jun 29, 2026
e89bc3a
Update test_integration_key_mapping.gd
ikostan Jun 29, 2026
912c858
Update globals.gd
ikostan Jun 29, 2026
3f55676
Create Part_3_Centralize_UI_SFX_mapping_and_tighten_global_input_audi…
ikostan Jun 29, 2026
aff090e
Route all UI SFX through the same bus.
ikostan Jun 29, 2026
7fb96da
Update globals.gd
ikostan Jun 29, 2026
7566125
Update test_audio_integration.gd
ikostan Jun 29, 2026
1e4d7ce
This does not exercise the legacy fallback branch.
ikostan Jun 29, 2026
519082b
Move Globals.options_open cleanup into after_each().
ikostan Jun 29, 2026
dfaad0f
Update Part_3_Centralize_UI_SFX_mapping_and_tighten_global_input_audi…
ikostan Jun 29, 2026
10f6f9f
Update Part_3_Centralize_UI_SFX_mapping_and_tighten_global_input_audi…
ikostan Jun 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions scenes/volume_controls/sfx_menu_volume_control.tscn
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[gd_scene load_steps=4 format=3 uid="uid://cm2nruujxhdhu"]
[gd_scene format=3 uid="uid://cm2nruujxhdhu"]

[ext_resource type="FontFile" uid="uid://borwvgqdgawbj" path="res://files/fonts/EMPIREST.TTF" id="1_f34wo"]
[ext_resource type="Script" uid="uid://8hd1kvmb5bt1" path="res://scripts/ui/components/volume_slider.gd" id="2_3a0os"]
Expand All @@ -10,7 +10,7 @@ corner_radius_bottom_right = 20
corner_radius_bottom_left = 20
shadow_size = 5

[node name="SFXMenu" type="Control"]
[node name="SFXMenu" type="Control" unique_id=1615954462]
custom_minimum_size = Vector2(385, 24)
layout_mode = 3
anchors_preset = 0
Expand All @@ -20,7 +20,7 @@ size_flags_horizontal = 3
size_flags_vertical = 3
tooltip_text = "SFX Menu Volume Control"

[node name="SFXMenuLabel" type="Label" parent="."]
[node name="SFXMenuLabel" type="Label" parent="." unique_id=2003122289]
layout_mode = 0
offset_top = 3.0
offset_right = 100.0
Expand All @@ -30,7 +30,7 @@ theme_override_fonts/font = ExtResource("1_f34wo")
theme_override_font_sizes/font_size = 18
text = "{MENU}"

[node name="HSlider" type="HSlider" parent="."]
[node name="HSlider" type="HSlider" parent="." unique_id=1622043815]
layout_direction = 2
layout_mode = 0
offset_left = 125.0
Expand All @@ -48,7 +48,7 @@ ticks_on_borders = true
script = ExtResource("2_3a0os")
bus_name = "SFX_Menu"

[node name="Mute" type="CheckButton" parent="."]
[node name="Mute" type="CheckButton" parent="." unique_id=1618523756]
layout_direction = 1
layout_mode = 0
offset_left = 360.0
Expand Down
148 changes: 109 additions & 39 deletions scripts/core/globals.gd
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const UI_NAV_SOUND_PATH: String = "res://files/sounds/sfx/ui_navigation.wav"

# --- TASK #529: Encryption Key Management ---
## Centralized key for securing local configuration files.
## This ensures consistent encryption/decryption across different game systems. [cite: 3]
## This ensures consistent encryption/decryption across different game systems.
## Define the variable by pulling from ProjectSettings.
## If the setting doesn't exist, it falls back to a non-secure string.
var save_encryption_pass: String = _get_encryption_key()
Expand Down Expand Up @@ -383,61 +383,129 @@ static func set_game_version_for_tests(value: String) -> void:

## Use _input instead of _unhandled_input to catch events BEFORE the UI consumes them.
func _input(event: InputEvent) -> void:
# The Ultimate Menu Check: Does a UI element currently have keyboard/gamepad focus?
# Gate 1: Echo Input Mitigation
if event.is_echo():
return

# Gate 2: Comprehensive Hardware Device Tracking
_track_input_device(event)

var focus_owner: Control = get_viewport().gui_get_focus_owner()
var ui_has_focus: bool = is_instance_valid(focus_owner)

# Gate 1: Only play UI sounds if a UI element is focused OR we are in a known menu state
# Gate 3: Broadened Menu Layer Context Safeguards
if not _check_menu_context(ui_has_focus):
return

# Gate 4: Drop heavy frame-rate mouse wiggles or non-action events early
if event is InputEventMouseMotion or not event.is_action_type():
return

# Gate 5: Process Menu UI Sound Effects via Data-Driven Lookup
_process_ui_navigation_sfx(event, focus_owner, ui_has_focus)


## Private helper to dynamically track the active hardware user control scheme.
func _track_input_device(event: InputEvent) -> void:
if event is InputEventKey or event is InputEventMouseButton or event is InputEventMouseMotion:
current_input_device = "keyboard"
elif event is InputEventJoypadButton or event is InputEventJoypadMotion:
current_input_device = "gamepad"


## Evaluates tree states and explicit markers to safeguard input contexts.
func _check_menu_context(ui_has_focus: bool) -> bool:
var is_menu_context: bool = (
get_tree().paused or options_open or not hidden_menus.is_empty() or ui_has_focus
)

# Test helper fallback: support menu context detection via current_scene name for GUT tests
# Gated strictly behind debug/ci features to prevent leakage into production shipped builds.
if (
(OS.has_feature("debug") or OS.has_feature("ci"))
and not is_menu_context
and get_tree().current_scene
and "Menu" in get_tree().current_scene.name
):
is_menu_context = true
# Cache the current scene reference once to guarantee mid-frame evaluation consistency
var active_scene: Node = get_tree().current_scene if get_tree() else null
if not is_instance_valid(active_scene):
return is_menu_context

# Explicit marker evaluation with a substring fallback for legacy compliance
if not is_menu_context:
return
if (
active_scene.is_in_group("menu_context")
or active_scene.has_meta("is_menu_context")
or "Menu" in active_scene.name
):
is_menu_context = true

# ADDED: Sound selection effect on hitting ESC/ui_cancel within any valid menu context
if event.is_action_pressed("ui_cancel", false):
# Bypass triggers when value-editing, toggle, or selection controls have active focus
# Test helper fallback: support menu context detection during automated test suite runs
if (OS.has_feature("debug") or OS.has_feature("ci")) and not is_menu_context:
if (
focus_owner is LineEdit
or focus_owner is TextEdit
or focus_owner is Range
or focus_owner is CheckButton
or focus_owner is OptionButton
"Menu" in active_scene.name
or active_scene.has_meta("is_menu_context")
or active_scene.is_in_group("menu_context")
):
return
is_menu_context = true

# Secure bypass gate for custom InputRemapButton configurations
if is_instance_valid(focus_owner) and focus_owner.get_script() != null:
if (
"action" in focus_owner
or "action_name" in focus_owner
or focus_owner.has_method("cancel_remap")
):
return
return is_menu_context

AudioManager.play_sfx("ui_cancel")
return

for action: String in _nav_actions:
# FIXED: Changed from 'Input.is_action_just_pressed' to pass the automated verification
## Matches actions against AudioConstants configurations to execute global menu sfx.
func _process_ui_navigation_sfx(
event: InputEvent, focus_owner: Control, ui_has_focus: bool
) -> void:
for action: String in AudioConstants.UI_SFX.keys():
if event.is_action_pressed(action, false):
# Prevent double-audio when adjusting sliders.
if focus_owner is Slider and (action == "ui_left" or action == "ui_right"):
# Context Guard A: Handle Escape/Cancellation Safeguards
if action == "ui_cancel":
_handle_ui_cancel_action(focus_owner, action)
return

_play_ui_navigation_sfx()
return # Exit once sound is triggered to avoid double-plays
# Context Guard C: Quietly drop events handling native element submissions
if action == "ui_accept":
if (
focus_owner is BaseButton
or focus_owner is Slider
or focus_owner is LineEdit
or focus_owner is TextEdit
):
return

# Context Guard B: Handle Directional & Focus Navigation Safeguards
_handle_ui_navigation_action(action, focus_owner, ui_has_focus)
return


## Separated execution logic for UI cancellation actions.
func _handle_ui_cancel_action(focus_owner: Control, action: String) -> void:
var is_editing_control: bool = (
focus_owner is LineEdit
or focus_owner is TextEdit
or focus_owner is Range
or focus_owner is CheckButton
or focus_owner is OptionButton
)

# Upgraded remap detection to avoid brittle 'in' operator checks on Node instances
var is_remap_control: bool = false
if is_instance_valid(focus_owner):
is_remap_control = (
focus_owner.has_method("cancel_remap")
or focus_owner.get("action") != null
or focus_owner.get("action_name") != null
)

if not is_editing_control and not is_remap_control:
var logical_id: String = AudioConstants.UI_SFX[action]
AudioManager.play_sfx(logical_id, AudioConstants.BUS_SFX)


## Separated execution logic for directional UI focus swaps.
func _handle_ui_navigation_action(action: String, focus_owner: Control, ui_has_focus: bool) -> void:
var is_horizontal_slider: bool = (
focus_owner is Slider and (action == "ui_left" or action == "ui_right")
)
var is_nav_action: bool = action in _nav_actions
var should_play_nav_sfx: bool = (ui_has_focus or is_nav_action) and not is_horizontal_slider

if should_play_nav_sfx:
var logical_id: String = AudioConstants.UI_SFX[action]
AudioManager.play_sfx(logical_id, AudioConstants.BUS_SFX)
Comment thread
coderabbitai[bot] marked this conversation as resolved.


## Internal helper to play the navigation sound through the dedicated Menu SFX bus.
Expand Down Expand Up @@ -643,4 +711,6 @@ func _on_node_added(node: Node) -> void:

## Centralized button audio execution target to prevent lambda churn
func _on_global_button_pressed() -> void:
AudioManager.play_sfx("ui_accept")
# Explicitly route button accepts through the shared UI SFX bus to guarantee consistent
# muting behavior
AudioManager.play_sfx("ui_accept", AudioConstants.BUS_SFX)
12 changes: 10 additions & 2 deletions scripts/managers/audio_manager.gd
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ func reset_volumes() -> void:

## Centralized SFX Playback API (Issue #570)
## Handles non-positional audio with LRU caching and auto-cleanup.
## :param sfx_name: The filename without extension (e.g., "slider").
## :param sfx_name: The logical identifier (e.g., "ui_navigation" or "slider").
## :param bus_name: Target audio bus (defaults to SFX_Menu).
## :param pitch_scale: Pitch override for variety.
## :param volume_db: Volume offset in decibels.
Expand All @@ -388,7 +388,15 @@ func play_sfx(

# 1. Resolve and Cache the AudioStream (with LRU Eviction)
if not _sfx_cache.has(sfx_name):
var full_path: String = SFX_DIR_PATH + sfx_name + ".wav"
# Resolve the logical name to its exact mapped file name
var file_name: String = sfx_name
if sfx_name in AudioConstants.SFX_ASSET_MAP:
file_name = AudioConstants.SFX_ASSET_MAP[sfx_name]
else:
# Structural fallback safely preserving legacy/direct calls
file_name += ".wav"

var full_path: String = SFX_DIR_PATH + file_name

# Safety guard against non-existent files to block core engine loader errors from polluting tests
if not ResourceLoader.exists(full_path):
Expand Down
26 changes: 26 additions & 0 deletions scripts/resources/audio_constants.gd
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,29 @@ const BUS_CONFIG: Dictionary = {
"default_muted": false
}
}

# --- Global UI SFX Mappings (Issue #490 Compliance) ---
# Decoupled from file extensions and folder layouts to prevent silent breaks on asset moves
const UI_SFX: Dictionary = {
"ui_up": "ui_navigation",
"ui_down": "ui_navigation",
"ui_left": "ui_navigation",
"ui_right": "ui_navigation",
"ui_focus_next": "ui_navigation",
"ui_focus_prev": "ui_navigation",
"ui_accept": "ui_accept",
"ui_cancel": "ui_cancel"
}

# --- SFX Asset Path Resolution Map ---
# Maps logical SFX identifiers to their exact filenames with extensions.
# This prevents asset changes from requiring script alterations.
const SFX_ASSET_MAP: Dictionary = {
"slider": "slider.wav",
"mute_toggle": "check.wav",
"ui_navigation": "ui_navigation.wav",
"ui_accept": "ui_accept.wav",
"ui_cancel": "ui_cancel.wav",
"airplane_prop": "airplane_prop.ogg",
"retro_laser": "retro-laser-1-236669.mp3"
}
Loading
Loading