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.
This commit is contained in:
Takeshi
2026-10-11 15:58:00 +09:00
parent 39f6068461
commit b84bade045
235 changed files with 495831 additions and 312 deletions
+62
View File
@@ -0,0 +1,62 @@
extends RefCounted
## QA ordinary Main destruction only; application/window close is separate QA.
## Reuse the existing four fixtures' real WeakRef/mixer drain without editing it.
const ExistingCleanup := preload("res://tests/helpers/ui_audio_teardown.gd")
const OWNER_META := &"_the_tree_ui_close_owners"
static func free_owner_and_wait(tree: SceneTree, owner: Node) -> Dictionary:
var owner_id: int = owner.get_instance_id() if is_instance_valid(owner) else 0
var controls_before: Dictionary = _controls(tree)
# ExistingCleanup captures weak audio targets synchronously before queue_free.
# Only weak targets cross its await. It neither changes Clock flags nor calls
# application close, and Main._exit_tree keeps the ordinary owner contract.
var audio: Dictionary = await ExistingCleanup.free_owner_and_wait(tree, owner, 500000)
var controls_after: Dictionary = _controls(tree)
var before_owners: Array = controls_before[&"owner_ids"]
var expected_remaining: Array = before_owners.duplicate()
expected_remaining.erase(owner_id)
var ordinary_release: bool = bool(controls_before[&"owner_meta_present"]) and not bool(controls_before[&"closing"]) and before_owners.has(owner_id)
if expected_remaining.is_empty():
ordinary_release = ordinary_release and not bool(controls_after[&"owner_meta_present"]) and bool(controls_after[&"auto_accept_quit"]) == bool(controls_before[&"original_auto_accept_quit"])
else:
ordinary_release = ordinary_release and bool(controls_after[&"owner_meta_present"]) and not bool(controls_after[&"closing"]) and controls_after[&"owner_ids"] == expected_remaining and not bool(controls_after[&"auto_accept_quit"])
var clock_unchanged: bool = controls_before[&"clock_process"] == controls_after[&"clock_process"] and controls_before[&"clock_physics_process"] == controls_after[&"clock_physics_process"] and controls_before[&"clock_process_mode"] == controls_after[&"clock_process_mode"]
var root_input_unchanged: bool = controls_before[&"root_gui_disable_input"] == controls_after[&"root_gui_disable_input"]
var report: Dictionary = {&"owned_engine_pid": OS.get_process_id(), &"owner_instance_id": owner_id,
&"before": controls_before, &"after": controls_after, &"expected_remaining_owner_ids": expected_remaining,
&"ordinary_owner_release_verified": ordinary_release, &"clock_process_flags_unchanged": clock_unchanged,
&"root_gui_input_flag_unchanged": root_input_unchanged, &"audio_teardown_passed": bool(audio.get(&"passed", false)),
&"maximum_wait_us": 500000, &"whole_Runtime_field_deletions": 0,
&"gameplay_expectations_changed": false, &"clock_flags_intentionally_modified": false,
&"application_close_called": false, &"SceneTree_quit_overridden": false,
&"WindowQuit_verified": false, &"formal_acceptance": false}
report[&"passed"] = bool(audio.get(&"passed", false)) and ordinary_release and clock_unchanged and root_input_unchanged
var path: String = OS.get_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF")
if path.is_empty() or not path.is_absolute_path() or FileAccess.file_exists(path):
report[&"passed"] = false
return report
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
if file == null:
report[&"passed"] = false
return report
file.store_string(JSON.stringify(report, "\t") + "\n")
file.close()
return report
static func _controls(tree: SceneTree) -> Dictionary:
# Local strong Node/WeakRef.get_ref results are released on helper return.
var clock: Node = tree.root.get_node("GameClock")
var present: bool = tree.root.has_meta(OWNER_META)
var state: Dictionary = tree.root.get_meta(OWNER_META) if present else {}
var ids: Array = []
for reference: WeakRef in state.get(&"owners", []):
var node: Variant = reference.get_ref()
if is_instance_valid(node):
ids.append(node.get_instance_id())
ids.sort()
return {&"clock_process": clock.is_processing(), &"clock_physics_process": clock.is_physics_processing(),
&"clock_process_mode": clock.process_mode, &"root_gui_disable_input": tree.root.gui_disable_input,
&"auto_accept_quit": tree.auto_accept_quit, &"owner_meta_present": present,
&"original_auto_accept_quit": bool(state.get(&"original_auto_accept_quit", tree.auto_accept_quit)),
&"closing": bool(state.get(&"closing", false)), &"owner_ids": ids}
@@ -0,0 +1 @@
uid://csl3kffjxjnfh
+98
View File
@@ -0,0 +1,98 @@
extends RefCounted
## QA teardown only. Real cues run during the fixture; never mute or delete buses.
## Godot stop queues a mixer fade. Wait for actual owned WeakRef release, bounded
## by wall time, before quitting a short test process. WindowQuit is separate QA.
static func free_owner_and_wait(tree: SceneTree, owner: Node, maximum_wait_us := 500000) -> Dictionary:
var runtime: Variant = tree.root.get_node("SimulationRuntime")
var before: Dictionary = runtime.create_snapshot()
var targets := _capture_targets(owner)
var initial := _states(targets)
var owner_id := owner.get_instance_id() if is_instance_valid(owner) else 0
if is_instance_valid(owner):
owner.queue_free()
var started := Time.get_ticks_usec()
await tree.process_frame
while Time.get_ticks_usec() - started < maximum_wait_us and (is_instance_valid(owner) or _any_alive(targets)):
await tree.process_frame
var waited_us := Time.get_ticks_usec() - started
var deadline_reached := waited_us >= maximum_wait_us
var after: Dictionary = runtime.create_snapshot()
var report := {
&"owned_engine_pid": OS.get_process_id(), &"owner_instance_id": owner_id,
&"waited_actual_us": waited_us, &"maximum_wait_us": maximum_wait_us, &"deadline_reached": deadline_reached,
&"owner_freed": not is_instance_valid(owner), &"all_owned_targets_released": not _any_alive(targets),
&"initial_targets": initial, &"final_targets": _states(targets),
&"whole_runtime_unchanged": var_to_bytes(before) == var_to_bytes(after),
&"audio_driver": AudioServer.get_driver_name(), &"bus_deleted": false, &"cues_muted_for_test": false,
&"actual_human_hearing": false, &"WindowQuit_verified": false, &"formal_acceptance": false,
&"full_typed_snapshots": {},
}
var directory := OS.get_environment("THE_TREE_AUDIO_TEARDOWN_PROOF_DIR")
if directory.is_empty():
directory = "user://tests/audio_teardown_%d" % OS.get_process_id()
DirAccess.make_dir_recursive_absolute(directory)
var path := directory.path_join("owner_%d.json" % owner_id)
var written := not FileAccess.file_exists(path)
for part: StringName in {&"before": before, &"after": after}:
var raw_path := path.get_basename() + "-" + String(part) + ".varbytes"
if FileAccess.file_exists(raw_path):
written = false
continue
var bytes := var_to_bytes(before if part == &"before" else after)
var raw := FileAccess.open(raw_path, FileAccess.WRITE)
if raw == null:
written = false
continue
raw.store_buffer(bytes)
raw.close()
report[&"full_typed_snapshots"][part] = {&"path": raw_path, &"bytes": bytes.size(), &"sha256": FileAccess.get_sha256(raw_path), &"field_deletions": 0}
report[&"passed"] = written and not deadline_reached and bool(report[&"owner_freed"]) and bool(report[&"all_owned_targets_released"]) and bool(report[&"whole_runtime_unchanged"])
if written:
var file := FileAccess.open(path, FileAccess.WRITE)
if file == null:
report[&"passed"] = false
else:
file.store_string(JSON.stringify(report, "\t") + "\n")
file.close()
return report
static func _capture_targets(owner: Node) -> Array[Dictionary]:
var result: Array[Dictionary] = []
if not is_instance_valid(owner):
return result
if not owner.has_signal(&"cue_requested"):
var music: Variant = owner.get(&"_paired_bgm")
if is_instance_valid(music) and music.has_method(&"capture_owned_targets"):
result.append_array(music.capture_owned_targets())
var audio: Variant = owner if owner.has_signal(&"cue_requested") else owner.get(&"_audio_feedback")
if not is_instance_valid(audio):
return result
for role: StringName in audio.get(&"_players"):
var player: AudioStreamPlayer = audio.get(&"_players")[role]
if player.stream != null:
result.append({&"role": role, &"kind": &"stream", &"instance_id": player.stream.get_instance_id(), &"weak": weakref(player.stream)})
var packets: Variant = player.stream.get(&"packet_sequence")
if packets is Resource:
result.append({&"role": role, &"kind": &"packet_sequence", &"instance_id": packets.get_instance_id(), &"weak": weakref(packets)})
if player.has_stream_playback():
var playback := player.get_stream_playback()
result.append({&"role": role, &"kind": &"last_playback", &"instance_id": playback.get_instance_id(), &"weak": weakref(playback)})
return result
static func _states(targets: Array[Dictionary]) -> Array[Dictionary]:
# Strong targets are local to this helper and released before every await.
var rows: Array[Dictionary] = []
for entry: Dictionary in targets:
var target: Variant = (entry[&"weak"] as WeakRef).get_ref()
rows.append({&"role": entry[&"role"], &"kind": entry[&"kind"], &"instance_id": entry[&"instance_id"], &"alive": target != null, &"class": target.get_class() if target != null else &""})
return rows
static func _any_alive(targets: Array[Dictionary]) -> bool:
for row: Dictionary in _states(targets):
if row[&"alive"]:
return true
return false
+1
View File
@@ -0,0 +1 @@
uid://csat0urychvsq