Files
step3/scripts/autoload/ui_sound_service.gd
hz 332da7c181 Fix stale-node crash in UI sound auto-wiring
node_added already fires once per node as it enters the tree (including
every descendant of a newly-added subtree), so the recursive call_deferred
in _on_node_added was redundant and held a raw Node reference across a
frame boundary. If that node got freed before the deferred call fired
(e.g. a container torn down and rebuilt the same frame), the engine's
message queue couldn't marshal the argument and threw "Cannot convert
argument 1 from Object to Object". Wiring synchronously removes the
window entirely.

Also silences the intentional integer division in _duration_text,
matching the same annotation already applied to _elapsed_simulated_text.
2026-07-23 15:13:56 -05:00

134 lines
3.7 KiB
GDScript

extends Node
const SETTINGS_PATH := "user://settings.cfg"
const SETTINGS_SECTION := "audio"
const SETTINGS_KEY := "ui_sounds_enabled"
const PLAYER_COUNT := 8
const DEFAULT_VOLUME_DB := -12.0
const SOUND_PATHS := {
"click": "res://assets/audio/kenney_interface_sounds/click_003.ogg",
"select": "res://assets/audio/kenney_interface_sounds/select_003.ogg",
"toggle": "res://assets/audio/kenney_interface_sounds/toggle_002.ogg",
"eliminate": "res://assets/audio/kenney_interface_sounds/scratch_002.ogg",
"open": "res://assets/audio/kenney_interface_sounds/open_001.ogg",
"close": "res://assets/audio/kenney_interface_sounds/close_002.ogg",
"correct": "res://assets/audio/kenney_interface_sounds/confirmation_002.ogg",
"error": "res://assets/audio/kenney_interface_sounds/error_004.ogg",
"back": "res://assets/audio/kenney_interface_sounds/back_002.ogg",
"question": "res://assets/audio/kenney_interface_sounds/question_001.ogg"
}
var sounds := {}
var players: Array[AudioStreamPlayer] = []
var sounds_enabled := true
var next_player_index := 0
func _ready() -> void:
_load_setting()
_load_sounds()
_build_player_pool()
get_tree().node_added.connect(_on_node_added)
call_deferred("_wire_existing_buttons")
func play_sound(sound_name: String) -> void:
if not sounds_enabled or not sounds.has(sound_name) or players.is_empty():
return
var player := _next_available_player()
player.stream = sounds[sound_name]
player.play()
func set_sounds_enabled(is_enabled: bool) -> void:
if sounds_enabled == is_enabled:
return
sounds_enabled = is_enabled
if not sounds_enabled:
for player in players:
player.stop()
_save_setting()
if sounds_enabled:
play_sound("toggle")
func are_sounds_enabled() -> bool:
return sounds_enabled
func set_button_sound(button: Button, sound_name: String) -> void:
button.set_meta("ui_sound", sound_name)
_wire_button(button)
func _load_sounds() -> void:
for sound_name in SOUND_PATHS:
var stream := load(str(SOUND_PATHS[sound_name])) as AudioStream
if stream != null:
sounds[sound_name] = stream
else:
push_warning("Unable to load UI sound: %s" % SOUND_PATHS[sound_name])
func _build_player_pool() -> void:
for _player_index in range(PLAYER_COUNT):
var player := AudioStreamPlayer.new()
player.volume_db = DEFAULT_VOLUME_DB
add_child(player)
players.append(player)
func _next_available_player() -> AudioStreamPlayer:
for player in players:
if not player.playing:
return player
var player := players[next_player_index]
next_player_index = (next_player_index + 1) % players.size()
return player
func _on_node_added(node: Node) -> void:
if node is Button:
_wire_button(node)
func _wire_existing_buttons() -> void:
_wire_node(get_tree().root)
func _wire_node(node: Node) -> void:
if not is_instance_valid(node):
return
if node is Button:
_wire_button(node)
for child in node.get_children():
_wire_node(child)
func _wire_button(button: Button) -> void:
if button.has_meta("ui_sound_wired"):
return
button.set_meta("ui_sound_wired", true)
button.pressed.connect(_on_button_pressed.bind(button))
func _on_button_pressed(button: Button) -> void:
var sound_name := str(button.get_meta("ui_sound", "click"))
if sound_name != "none":
play_sound(sound_name)
func _load_setting() -> void:
var config := ConfigFile.new()
if config.load(SETTINGS_PATH) == OK:
sounds_enabled = bool(config.get_value(SETTINGS_SECTION, SETTINGS_KEY, true))
func _save_setting() -> void:
var config := ConfigFile.new()
config.load(SETTINGS_PATH)
config.set_value(SETTINGS_SECTION, SETTINGS_KEY, sounds_enabled)
var save_error := config.save(SETTINGS_PATH)
if save_error != OK:
push_warning("Unable to save UI sound preference.")