Files
Takeshi b84bade045 Save The Tree implementation and acceptance work in progress
Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
2026-10-11 15:58:00 +09:00

325 lines
19 KiB
GDScript

extends SceneTree
const PairedBGMType = preload("res://scripts/ui/paired_bgm.gd")
const FeedbackType = preload("res://scripts/ui/ui_audio_feedback.gd")
const ShutdownType = preload("res://scripts/ui/ui_audio_owner_shutdown.gd")
var _checks := 0
var _failures := 0
var _main: Control
var _runtime: Variant
var _audio: Variant
var _cues: Array[StringName] = []
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("P22: Paired BGM and actual Main controls")
await _test_paired_bgm_candidate()
await _test_actual_main_bgm()
if _failures == 0:
await _test_bgm_window_close()
return
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
func _test_paired_bgm_candidate() -> void:
var bgm := PairedBGMType.new()
bgm.configure(0)
root.add_child(bgm)
bgm.set_process(false)
_expect(bgm.startup_error.is_empty(), "paired BGM loads both compatible PCM arrangements")
_expect(bgm._synchronized != null and bgm._synchronized.stream_count == 2, "one synchronized stream contains both arrangements")
_expect(bgm._player.playing and bgm.playback_starts == 1, "both arrangements start through one player once")
_expect(bgm.process_mode == Node.PROCESS_MODE_ALWAYS and bgm._player.process_mode == Node.PROCESS_MODE_ALWAYS, "music continues independently of SceneTree pause")
for index: int in range(2):
var stream := bgm._synchronized.get_sync_stream(index) as AudioStreamWAV
_expect(stream.loop_mode == AudioStreamWAV.LOOP_FORWARD and stream.loop_begin == 0 and stream.loop_end == 1881600, "arrangement %d loops all exact PCM frames" % index)
var runtime: Node = root.get_node("SimulationRuntime")
var before := var_to_bytes(runtime.call("create_snapshot"))
_write_full_state("user://crossfade-runtime-before.bin", before)
var playback_id := bgm._player.get_stream_playback().get_instance_id()
bgm.set_layer(1)
_expect(bgm.mix == 0.0 and bgm.target_mix == 1.0, "layer change begins continuously from current mix")
bgm._process(0.625)
_expect(is_equal_approx(bgm.mix, 0.5), "crossfade reaches midpoint")
var gains := Vector2(db_to_linear(bgm._synchronized.get_sync_stream_volume(0)), db_to_linear(bgm._synchronized.get_sync_stream_volume(1)))
_expect(is_equal_approx(gains.length_squared(), 1.0), "midpoint follows equal power gain")
bgm.set_layer(0)
_expect(is_equal_approx(bgm.mix, 0.5), "mid-fade reversal retains current mix")
bgm._process(0.625)
_expect(is_equal_approx(bgm.mix, 0.25), "reversal advances from retained mix")
bgm._process(0.625)
_expect(bgm.mix == 0.0, "reversal arrives exactly at front arrangement")
bgm.set_layer(1)
bgm._process(1.25)
_expect(bgm.mix == 1.0, "complete transition arrives exactly at back arrangement")
bgm.set_layer(1)
bgm._process(-1.0)
bgm._process(NAN)
_expect(bgm.mix == 1.0 and bgm.playback_starts == 1 and bgm._player.get_stream_playback().get_instance_id() == playback_id, "repeat and invalid delta do not restart playback")
_expect(before == var_to_bytes(runtime.call("create_snapshot")), "crossfade operations preserve full typed Runtime snapshot")
_write_full_state("user://crossfade-runtime-after.bin", var_to_bytes(runtime.call("create_snapshot")))
var feedback := FeedbackType.new()
feedback.configure(runtime, "", false)
root.add_child(feedback)
var old_settings: Dictionary = feedback.settings.duplicate(true)
_expect(feedback.update_settings({&"master_volume": 0.3, &"ui_volume": 0.5, &"muted": false}), "existing master volume setting accepts change")
_expect(bgm._player.bus == &"Master" and is_equal_approx(db_to_linear(AudioServer.get_bus_volume_db(0)), 0.3), "BGM routes through existing master volume")
_expect(feedback.update_settings({&"master_volume": 0.3, &"ui_volume": 0.5, &"muted": true}) and AudioServer.is_bus_mute(0), "existing mute setting controls music master bus")
_expect(bgm._player.playing and bgm.playback_starts == 1, "muting retains shared music playback clock")
_expect(feedback.update_settings(old_settings), "test restores prior master settings without writing config")
feedback.queue_free()
bgm._player.seek(42.55)
var loop_started := Time.get_ticks_usec()
var observed_end := false
var loop_observations: Array = []
while Time.get_ticks_usec() - loop_started < 2000000:
var position := bgm._player.get_playback_position()
loop_observations.append({&"elapsed_us": Time.get_ticks_usec() - loop_started, &"position": position})
observed_end = observed_end or position > 40.0
if observed_end and position < 1.0:
break
await process_frame
var after_loop_position := bgm._player.get_playback_position()
print("BGM_LOOP_OBSERVATION " + JSON.stringify({&"playing": bgm._player.playing, &"position": after_loop_position, &"starts": bgm.playback_starts, &"driver": AudioServer.get_driver_name()}))
_expect(bgm._player.playing and observed_end and after_loop_position < 1.0, "actual player wraps end of loop without restart")
var decoder_reports := _test_native_paired_decoder(bgm)
var targets := bgm.capture_owned_targets()
_expect(targets.size() == 6, "captures owner player synchronized resource both WAVs and actual playback")
bgm.queue_free()
var started := Time.get_ticks_usec()
await process_frame
while Time.get_ticks_usec() - started < 500000 and _bgm_any_alive(targets):
await process_frame
_expect(not _bgm_any_alive(targets), "ordinary owner deletion releases all actual music resources within bounded wall time")
var final_targets: Array = []
for row: Dictionary in targets:
final_targets.append({&"kind": row[&"kind"], &"role": row[&"role"], &"instance_id": row[&"instance_id"], &"alive": row[&"weak"].get_ref() != null})
DirAccess.make_dir_recursive_absolute(OS.get_user_data_dir())
var file := FileAccess.open("user://paired-bgm-proof.json", FileAccess.WRITE)
file.store_string(JSON.stringify({&"owned_engine_pid": OS.get_process_id(), &"checks": _checks, &"failures": _failures, &"native_decoder_reports": decoder_reports, &"loop_observations": loop_observations, &"audio_driver": AudioServer.get_driver_name(), &"post_loop_position_seconds": after_loop_position, &"waited_us": Time.get_ticks_usec() - started, &"maximum_wait_us": 500000, &"final_targets": final_targets, &"actual_human_hearing": false, &"normal_Main_accepted": false, &"WindowQuit_accepted": false, &"native_Windows_accepted": false}, "\t"))
file.close()
func _bgm_any_alive(targets: Array[Dictionary]) -> bool:
for row: Dictionary in targets:
if row[&"weak"].get_ref() != null:
return true
return false
func _test_native_paired_decoder(bgm: Node) -> Array:
var reports: Array = []
DirAccess.make_dir_recursive_absolute(OS.get_user_data_dir())
for position: float in [0.0, 0.5, 1.0]:
bgm.mix = position
bgm._apply_mix()
var synchronized: AudioStreamPlayback = bgm._synchronized.instantiate_playback()
var front: AudioStreamPlayback = bgm._synchronized.get_sync_stream(0).instantiate_playback()
var back: AudioStreamPlayback = bgm._synchronized.get_sync_stream(1).instantiate_playback()
for playback: AudioStreamPlayback in [synchronized, front, back]:
playback.start(42.55)
var mixed: PackedVector2Array = synchronized.mix_audio(1.0, 12288)
var front_pcm: PackedVector2Array = front.mix_audio(1.0, 12288)
var back_pcm: PackedVector2Array = back.mix_audio(1.0, 12288)
var front_gain := db_to_linear(bgm._synchronized.get_sync_stream_volume(0))
var back_gain := db_to_linear(bgm._synchronized.get_sync_stream_volume(1))
var maximum_error := 0.0
var energy := 0.0
for index: int in range(mixed.size()):
maximum_error = maxf(maximum_error, (mixed[index] - front_pcm[index] * front_gain - back_pcm[index] * back_gain).length())
energy += mixed[index].length_squared()
_expect(mixed.size() == 12288 and front_pcm.size() == 12288 and back_pcm.size() == 12288, "native mixer returns full frames across loop at mix %.1f" % position)
_expect(maximum_error < 0.000002 and energy > 0.0001, "actual paired mix matches both independently decoded PCM streams at mix %.1f" % position)
_expect(synchronized.is_playing() and synchronized.get_playback_position() < 1.0, "native synchronized decoder loops both arrangements at mix %.1f" % position)
_expect(absf(front.get_playback_position() - back.get_playback_position()) < 0.000001, "both independent arrangement decoders retain same sample position")
var names := ["mixed", "front", "back"]
var buffers := [mixed, front_pcm, back_pcm]
var files: Array = []
for index: int in range(3):
var path := "user://paired-%d-%s.f32" % [int(position * 10), names[index]]
var file := FileAccess.open(path, FileAccess.WRITE)
file.store_buffer(buffers[index].to_byte_array())
file.close()
files.append(path)
reports.append({&"mix": position, &"frames": mixed.size(), &"maximum_error": maximum_error, &"energy": energy, &"front_gain": front_gain, &"back_gain": back_gain, &"loop_count": synchronized.get_loop_count(), &"position": synchronized.get_playback_position(), &"files": files})
for playback: AudioStreamPlayback in [synchronized, front, back]:
playback.stop()
return reports
func _test_actual_main_bgm() -> void:
_runtime = root.get_node("SimulationRuntime")
var clock: Variant = root.get_node("GameClock")
var state: Variant = root.get_node("GameState")
clock.reset(1, 6, 0)
state.reset()
_runtime.initialize(clock, state, 822252)
clock.set_paused(true)
root.size = Vector2i(1440, 810)
_main = load("res://scenes/main/main.tscn").instantiate() as Control
_main.set("audio_settings_path", "user://tests/main_audio_%d/settings.cfg" % OS.get_process_id())
_main.set("manual_save_path", "user://tests/main_audio_%d/manual" % OS.get_process_id())
root.add_child(_main)
_runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
await process_frame
await process_frame
var music: Variant = _main.get("_paired_bgm")
_expect(music != null and music.startup_error.is_empty() and music._player.playing, "actual Main owns playing paired BGM")
music.set_process(false)
var music_playback_id: int = music._player.get_stream_playback().get_instance_id()
(_main.get("back_button") as Button).pressed.emit()
_expect(music.target_mix == 1.0 and state.active_layer == 1, "actual back-layer button targets back music")
music._process(1.25)
(_main.find_child("ManualSaveButton", true, false) as Button).pressed.emit()
_expect(FileAccess.file_exists(String(_main.get("manual_save_path")) + "/world.bin"), "normal Main manual-save writes actual formal world")
(_main.get("front_button") as Button).pressed.emit()
music._process(1.25)
_expect(music.mix == 0.0 and state.active_layer == 0, "actual front-layer button targets front music")
(_main.find_child("SaveLoadMenu", true, false) as MenuButton).get_popup().id_pressed.emit(0)
_expect(state.active_layer == 1 and music.target_mix == 1.0, "actual Main load restores saved layer into music crossfade target")
music._process(1.25)
_expect(music.mix == 1.0 and music.playback_starts == 1 and music._player.get_stream_playback().get_instance_id() == music_playback_id, "save/load and layer controls preserve one uninterrupted music clock")
_audio = _main.get("_audio_feedback")
_expect(_audio != null, "normal Main owns one presentation audio service")
_audio.playback_enabled = false
_audio.cue_requested.connect(_record_cue)
var button := _main.call("_button", "audio binding test", _noop) as Button
_main.call("_apply_button_style", button)
_main.call("_apply_button_style", button)
button.pressed.emit()
_expect(_cues == [&"click"], "normal styled button plays one click after repeated style registration")
_cues.clear()
button.disabled = true
button.pressed.emit()
_expect(_cues.is_empty(), "disabled button never requests audio even through emitted signal")
button.set_pressed_no_signal(true)
_expect(_cues.is_empty(), "programmatic selection is silent")
button.free()
_main.call("_set_status", "queued text", true)
_main.call("_set_status", "rejected text", false)
_main.call("_set_status", "quiet text", true, true)
_main.call("_refresh_dashboard")
_expect(_cues.is_empty(), "status and repeated dashboard refresh are not sound dispatchers")
var key := InputEventKey.new()
key.keycode = KEY_I
key.pressed = true
_main.call("_unhandled_key_input", key)
_expect(_cues == [&"click"], "normal keyboard shortcut requests one click")
_cues.clear()
key.echo = true
_main.call("_unhandled_key_input", key)
_expect(_cues.is_empty(), "keyboard repeat echo is silent")
var hire := (_main.get("staff_candidate_buttons") as Dictionary).get(&"staff_akane") as Button
_expect(hire != null and not hire.disabled, "normal hiring button is actionable")
if hire != null and not hire.disabled:
hire.pressed.emit()
_expect(_cues.count(&"success") == 1 and _cues.count(&"click") == 1, "real paused hire button has one completion and one click")
_expect(_runtime.staff_service.get_staff(&"staff_akane") != null, "normal hire command reaches actual Runtime state")
_cues.clear()
(_main.find_child("AudioSettingsButton", true, false) as Button).pressed.emit()
var dialog: Variant = _main.get("_audio_settings_dialog")
_expect(dialog.visible and _cues == [&"click"], "normal audio settings button opens actual controls")
_cues.clear()
dialog.master_slider.value = 40
_expect(is_equal_approx(db_to_linear(AudioServer.get_bus_volume_db(0)), 0.4) and music._player.bus == &"Master", "actual Main master slider controls music bus")
dialog.ui_slider.value = 25
_expect(is_equal_approx(float(_audio.settings[&"ui_volume"]), 0.25), "normal slider changes AudioServer-linked UI volume")
dialog.mute_button.button_pressed = true
_expect(_audio.settings[&"muted"] and AudioServer.is_bus_mute(AudioServer.get_bus_index(&"Master")), "normal mute control reaches Master bus")
_expect(music._player.playing and music.playback_starts == 1, "actual Main mute retains music sample clock")
dialog.call("_save_settings")
_expect(FileAccess.file_exists(_main.get("audio_settings_path")), "normal settings save writes isolated presentation preference")
_audio.update_settings(UIAudioFeedback.DEFAULT_SETTINGS)
var before: Dictionary = _runtime.create_snapshot()
_main.call("_on_save_operation_completed", {&"ok": false, &"operation": &"load"})
_expect(var_to_bytes(before) == var_to_bytes(_runtime.create_snapshot()), "failed load audio hook leaves Runtime exact bytes")
_expect(_cues.is_empty(), "settings save and failed load do not fabricate command sounds")
var unchanged_before := var_to_bytes(_runtime.create_snapshot())
_write_full_state("user://main-delete-runtime-before.bin", unchanged_before)
var targets: Array[Dictionary] = music.capture_owned_targets()
targets.append_array(ShutdownType._capture_targets(_audio))
targets.append({&"kind": &"main_owner", &"role": &"main", &"weak": weakref(_main), &"instance_id": _main.get_instance_id()})
_main.queue_free()
var started := Time.get_ticks_usec()
await process_frame
while Time.get_ticks_usec() - started < 500000 and _bgm_any_alive(targets):
await process_frame
_expect(not _bgm_any_alive(targets), "actual Main ordinary deletion releases music and cue resources within bounded wall time")
_expect(unchanged_before == var_to_bytes(_runtime.create_snapshot()), "actual Main deletion preserves full paused Runtime snapshot")
_write_full_state("user://main-delete-runtime-after.bin", var_to_bytes(_runtime.create_snapshot()))
var rows: Array = []
for row: Dictionary in targets:
rows.append({&"kind": row[&"kind"], &"role": row[&"role"], &"instance_id": row[&"instance_id"], &"alive": row[&"weak"].get_ref() != null})
var file := FileAccess.open("user://paired-bgm-main-proof.json", FileAccess.WRITE)
file.store_string(JSON.stringify({&"owned_engine_pid": OS.get_process_id(), &"checks": _checks, &"failures": _failures, &"audio_driver": AudioServer.get_driver_name(), &"final_targets": rows, &"maximum_wait_us": 500000, &"waited_us": Time.get_ticks_usec() - started, &"full_Runtime_unchanged_during_delete": unchanged_before == var_to_bytes(_runtime.create_snapshot()), &"normal_OS_input_accepted": false, &"actual_human_hearing": false, &"WindowQuit_accepted": false, &"native_Windows_accepted": false}, "\t"))
file.close()
func _noop() -> void:
pass
func _record_cue(role: StringName, _correlation: Dictionary) -> void:
_cues.append(role)
var _close_main: Control
var _window_before := PackedByteArray()
func _write_full_state(path: String, bytes: PackedByteArray) -> void:
DirAccess.make_dir_recursive_absolute(OS.get_user_data_dir())
var file := FileAccess.open(path, FileAccess.WRITE)
if file == null:
push_error("Cannot write owned typed BGM evidence: " + path)
return
file.store_buffer(bytes)
file.close()
func _test_bgm_window_close() -> void:
_close_main = load("res://scenes/main/main.tscn").instantiate() as Control
_close_main.set("audio_settings_path", "user://tests/window_bgm/settings.cfg")
_close_main.set("manual_save_path", "user://tests/window_bgm/manual")
root.add_child(_close_main)
await process_frame
await process_frame
var music: Variant = _close_main.get("_paired_bgm")
_expect(music.mix == 1.0 and music._player.playing and music.playback_starts == 1, "new Main initializes BGM directly in restored back layer")
_close_main.get("_audio_feedback").request_click()
_window_before = var_to_bytes(_runtime.create_snapshot())
_write_full_state("user://window-runtime-before.bin", _window_before)
_close_main.ui_audio_window_close_drained.connect(_on_bgm_close_drained)
root.close_requested.emit()
func _on_bgm_close_drained(report: Dictionary) -> void:
var rows: Array = report[&"initial_targets"]
var counts := {}
for row: Dictionary in rows:
var kind: String = String(row[&"kind"])
counts[kind] = int(counts.get(kind, 0)) + 1
_expect(bool(report[&"passed"]) and bool(report[&"all_owned_targets_released"]), "real close coordinator drains music and cue ownership before quit")
_expect(counts.get("bgm_owner", 0) == 1 and counts.get("bgm_player", 0) == 1 and counts.get("bgm_synchronized", 0) == 1 and counts.get("bgm_stream", 0) == 2 and counts.get("bgm_playback", 0) == 1, "window close captures all six actual music owners and resources")
_expect(counts.get("stream", 0) == 4 and counts.get("packet_sequence", 0) == 4 and counts.get("playback", 0) >= 1, "window close retains all existing cue resources and live click playback")
_expect(bool(report[&"clock_process_held_stopped"]) and bool(report[&"window_input_held_disabled"]) and not bool(report[&"settings_mutated"]) and not bool(report[&"shared_bus_deleted"]), "close retains existing input clock settings and shared bus contract")
var after := var_to_bytes(_runtime.create_snapshot())
_write_full_state("user://window-runtime-after.bin", after)
_expect(_window_before == after, "actual close keeps full typed Runtime bytes unchanged")
_write_full_state("user://paired-window-proof.json", JSON.stringify({&"owned_engine_pid": OS.get_process_id(), &"checks": _checks, &"failures": _failures, &"report": report, &"normal_OS_input_accepted": false, &"actual_human_hearing": false, &"native_Windows_accepted": false}, "\t").to_utf8_buffer())
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])