P06-03: Show Mana supply, consumption and storage; prove new-PID recovery

HUD tooltip reports world tree supply, upkeep consumption, net, stored amount
and the last boundary's shortage/overflow. A writer/reader pair saves mid-outage,
recovers in a new process and matches the writer exactly while DESTROYED and
RAIDED suspensions stay in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Takeshi
2026-10-20 08:59:59 +09:00
co-authored by Claude Opus 5.5
parent db5ca62e42
commit 826ef606df
5 changed files with 187 additions and 1 deletions
+146
View File
@@ -0,0 +1,146 @@
extends SceneTree
## P06-03 outage recovery across a real process boundary. "-- writer" saves
## mid-outage and records its own recovery; "-- reader" restores that save in a
## new Godot process, recovers the same way and must match exactly. Recovery
## must never clear DESTROYED or RAIDED suspension.
const ManaOutage = preload("res://tests/helpers/mana_outage.gd")
const OUTAGE_SAVE := "user://saves/p06_03_recovery_new_pid"
const EXPECTED_PATH := "user://saves/p06_03_recovery_new_pid_expected.json"
const RECOVERY_MINUTES := 30
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
var mode := "reader" if "reader" in OS.get_cmdline_user_args() else "writer"
print("S2: P06-03 Mana recovery new-PID %s (pid %d)" % [mode, OS.get_process_id()])
if mode == "writer":
_write()
else:
_read()
print("%s: %d checks" % ["PASS" if _failures == 0 else "FAIL", _checks])
quit(_failures)
func _new_runtime() -> Array:
var clock: Variant = load("res://autoload/game_clock.gd").new()
var state: Variant = load("res://autoload/game_state.gd").new()
var runtime: Variant = load("res://autoload/simulation_runtime.gd").new()
clock.reset(1, 6, 0)
state.reset()
runtime.initialize(clock, state, 6606)
clock.set_paused(true)
return [clock, state, runtime]
func _facility(runtime: Variant, definition_id: StringName) -> FacilityInstance:
for facility: FacilityInstance in runtime.facility_service.get_instances():
if facility.definition_id == definition_id:
return facility
return null
func _summary(runtime: Variant, state: Variant) -> Dictionary:
var rows := {}
for facility: FacilityInstance in runtime.facility_service.get_instances():
rows[String(facility.definition_id)] = {
"power": facility.power_available,
"suspension": String(facility.suspension_reason),
"operation": facility.operation_state,
}
return {"mana": state.get_resource(&"mana"), "facilities": rows}
func _recover(clock: Variant, state: Variant, runtime: Variant) -> void:
ManaOutage.release(runtime, state, 0)
clock.advance_minutes(RECOVERY_MINUTES)
func _write() -> void:
var nodes := _new_runtime()
var clock: Variant = nodes[0]
var state: Variant = nodes[1]
var runtime: Variant = nodes[2]
_expect(runtime.submit_command(&"build_facility", {&"definition_id": &"reception", &"origin": Vector3i(-6, 0, 0)}).is_success(), "build reception")
clock.advance_minutes(120)
_expect(runtime.submit_command(&"build_facility", {&"definition_id": &"standard_room", &"origin": Vector3i(-6, 2, 0)}).is_success(), "build room on supported floor")
var cafeteria_built := false
for x: int in [-2, 2, -8, -10]:
if runtime.submit_command(&"build_facility", {&"definition_id": &"cafeteria", &"origin": Vector3i(x, 0, 0)}).is_success():
cafeteria_built = true
break
_expect(cafeteria_built, "build cafeteria on ground floor")
clock.advance_minutes(180)
var reception := _facility(runtime, &"reception")
var cafeteria := _facility(runtime, &"cafeteria")
var room := _facility(runtime, &"standard_room")
_expect(reception != null and cafeteria != null and room != null, "facilities complete normally")
if reception == null or cafeteria == null or room == null:
return
runtime.facility_service.set_operational_status(cafeteria.instance_id, {&"suspension_reason": &"DESTROYED"}, clock.absolute_minute)
runtime.facility_service.set_operational_status(room.instance_id, {&"suspension_reason": &"RAIDED"}, clock.absolute_minute)
ManaOutage.hold(runtime, state)
clock.advance_minutes(15)
_expect(not _facility(runtime, &"reception").power_available, "outage stops the tower")
var outage: Dictionary = runtime.create_snapshot()
_expect(SaveService.save_snapshot(OUTAGE_SAVE, outage)[&"ok"], "writer saves mid-outage")
_recover(clock, state, runtime)
var summary := _summary(runtime, state)
_check_recovered(summary)
var file := FileAccess.open(EXPECTED_PATH, FileAccess.WRITE)
_expect(file != null, "writer records recovery")
if file != null:
file.store_string(JSON.stringify({
"writer_pid": OS.get_process_id(),
"summary": summary,
"digest": SimulationDigest.compute(runtime.create_snapshot()),
}))
file.close()
for node: Variant in nodes:
node.free()
func _read() -> void:
var expected: Variant = JSON.parse_string(FileAccess.get_file_as_string(EXPECTED_PATH))
_expect(expected is Dictionary, "reader finds writer record")
if not expected is Dictionary:
return
_expect(int(expected["writer_pid"]) != OS.get_process_id(), "reader runs in a different process")
var nodes := _new_runtime()
var clock: Variant = nodes[0]
var state: Variant = nodes[1]
var runtime: Variant = nodes[2]
var loaded := SaveService.load_snapshot(OUTAGE_SAVE)
_expect(loaded[&"ok"] and runtime.restore_snapshot(loaded[&"snapshot"]), "reader restores mid-outage save")
_expect(state.get_resource(&"mana") == 0 and not _facility(runtime, &"reception").power_available, "restored tower is still stopped")
_recover(clock, state, runtime)
var summary := _summary(runtime, state)
_check_recovered(summary)
_expect(JSON.stringify(JSON.parse_string(JSON.stringify(summary))) == JSON.stringify(expected["summary"]), "recovered facility states match the writer")
_expect(SimulationDigest.compute(runtime.create_snapshot()) == String(expected["digest"]), "whole recovered state digest matches the writer")
SaveService.delete_save(OUTAGE_SAVE)
DirAccess.remove_absolute(ProjectSettings.globalize_path(EXPECTED_PATH))
for node: Variant in nodes:
node.free()
func _check_recovered(summary: Dictionary) -> void:
var rows: Dictionary = summary["facilities"]
_expect(int(summary["mana"]) > 0, "world tree refills the reserve after the outage")
_expect(bool(rows["reception"]["power"]) and String(rows["reception"]["suspension"]) == "", "ordinary facility power returns")
_expect(bool(rows["cafeteria"]["power"]) and String(rows["cafeteria"]["suspension"]) == "DESTROYED" and int(rows["cafeteria"]["operation"]) == FacilityInstance.OperationState.SUSPENDED, "recovery does not clear destruction")
_expect(bool(rows["standard_room"]["power"]) and String(rows["standard_room"]["suspension"]) == "RAIDED" and int(rows["standard_room"]["operation"]) == FacilityInstance.OperationState.SUSPENDED, "recovery does not clear raid suspension")
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
else:
_failures += 1
push_error(" ✗ %s" % label)
+2
View File
@@ -64,6 +64,8 @@ func _run() -> void:
_state.set_resource(&"mana", 0)
_expect("停止" in mana_label.text and "0" in mana_label.text, "resource signal visibly identifies zero-reserve outage")
_expect("残量と配電" in mana_label.tooltip_text and "休止は解除されません" in mana_label.tooltip_text, "mana HUD explains recovery without promising suspension reset")
_expect("供給 +20" in mana_label.tooltip_text and "消費 −" in mana_label.tooltip_text and "蓄積 0/300" in mana_label.tooltip_text, "mana HUD shows A06 supply, consumption and storage")
_expect(mana_label.text.ends_with("/300・停止"), "mana HUD shows capacity with outage")
if OS.get_environment("THE_TREE_MANA_CAPTURE") == "1":
await process_frame
await RenderingServer.frame_post_draw