extends SceneTree ## P07-01 F7, writer phase. Plays the minute script, writes ordinary save files ## mid-rain, just before and after the rain end and just before and after the ## day 1 draw, and hands the exact save packages to the reader suite ## (test_p07_weather_restart_2_read.gd), which runs as a separate engine process. ## The fog scene (WX7) writes saves around the 07:00 fog mana boundary and the ## fog end, reloads each one in this process and continues it, and hands those ## packages to the reader too. ## The handoff is the declared generated report res://artifacts/p07-01/restart-handoff.json ## (tools/regression/suite-policy.json fixed_generated_report_routes). const Fixture := preload("res://tests/helpers/p07_weather_restart_fixture.gd") var _checks := 0 var _failures := 0 func _initialize() -> void: call_deferred("_run") func _run() -> void: print("P07-01: restart writer") var fixtures := Fixture.new(_expect) var straight := fixtures.new_fixture() var straight_runtime: Variant = straight[&"runtime"] var held_guest: Variant = straight_runtime.guest_service.get_guest(Fixture.HELD_GUEST) var held := {"hold": {}, "reason": &"", "after": {}} fixtures.play(straight, Fixture.FIRST_MINUTE, Fixture.LAST_MINUTE, func(minute: int) -> void: if (held["hold"] as Dictionary).is_empty(): held["hold"] = straight_runtime.weather.get_rain_hold(Fixture.HELD_GUEST) if StringName(held["reason"]).is_empty(): held["reason"] = held_guest.exit_reason if minute == 363: _expect(straight_runtime.guest_service.get_guest(Fixture.HELD_GUEST) != null and held_guest.state == GuestRuntime.State.LEAVING and straight_runtime.weather.get_rain_hold(Fixture.HELD_GUEST) == Fixture.HOLD, "at 06:03 the held guest is still present and leaving with its hold") if minute == 364: _expect(straight_runtime.guest_service.get_guest(Fixture.HELD_GUEST) == null and straight_runtime.weather.get_rain_holds().is_empty(), "at 06:04 the held guest is gone and its hold left with it") if minute - 1 in Fixture.SAVE_MINUTES: held["after"][minute] = fixtures.state_bytes(straight_runtime)) # The hold only moves the planned exit; every other current rule still applies, # so in this world without facilities the guest leaves for lack of one. _expect(held["hold"] == Fixture.HOLD and held_guest.planned_exit_at == 602 and held["reason"] == &"NO_SUITABLE_FACILITY", "the guest due at 06:02 was held to 10:02 and still left by the current rules (%s)" % String(held["reason"])) _expect(straight[&"draws"][&"count"] == Fixture.DRAW_MINUTES.size() and straight_runtime.weather.get_last_draw_day() == 1, "the uninterrupted script draws day 1 once") var saving := fixtures.new_fixture() var saving_runtime: Variant = saving[&"runtime"] var saved_states := {} var saved_paths := {} fixtures.play(saving, Fixture.FIRST_MINUTE, Fixture.LAST_MINUTE, func(minute: int) -> void: if minute in Fixture.SAVE_MINUTES: if minute == 363: _expect(saving_runtime.guest_service.get_guest(Fixture.HELD_GUEST) != null and saving_runtime.weather.get_rain_holds() == {String(Fixture.HELD_GUEST): Fixture.HOLD} and saving_runtime.guest_service.get_guest(Fixture.HELD_GUEST).planned_exit_at == 602, "minute 363 saves the hold of the present held guest") if minute == 364: _expect(saving_runtime.guest_service.get_guest(Fixture.HELD_GUEST) == null and saving_runtime.weather.get_rain_holds().is_empty(), "minute 364 saves just after the held guest left, without its hold") if minute == 500: _expect(saving_runtime.weather.get_kind() == &"rain", "minute 500 saves mid-rain") var path := Fixture.WRITER_SAVE % minute saved_states[minute] = fixtures.state_bytes(saving_runtime) _expect(bool(saving_runtime.save_to_path(path).get(&"ok", false)), "save file at minute %d" % minute) saved_paths[minute] = path) _expect(fixtures.state_bytes(saving_runtime) == fixtures.state_bytes(straight_runtime), "writing the save files leaves the weather state as without saving") var final_state := fixtures.state_bytes(straight_runtime) for minute: int in Fixture.SAVE_MINUTES: var reload := fixtures.new_fixture(false) var reload_runtime: Variant = reload[&"runtime"] _expect(bool(reload_runtime.load_from_path(str(saved_paths.get(minute, Fixture.WRITER_SAVE % minute))).get(&"ok", false)) and fixtures.state_bytes(reload_runtime) == saved_states.get(minute, PackedByteArray()) and reload[&"clock"].absolute_minute == minute, "this process reloads the save file of minute %d exactly" % minute) if minute == 363: _expect(reload_runtime.weather.get_rain_holds() == {String(Fixture.HELD_GUEST): Fixture.HOLD} and reload_runtime.guest_service.get_guest(Fixture.HELD_GUEST).planned_exit_at == 602, "the reloaded minute 363 save keeps the one hold and the extended plan") fixtures.play(reload, minute, minute + 1, Callable()) _expect(fixtures.state_bytes(reload_runtime) == held["after"].get(minute + 1, PackedByteArray()), "one minute after the minute %d reload equals the uninterrupted run (no second extension)" % minute) fixtures.play(reload, minute + 1, Fixture.LAST_MINUTE, Callable()) _expect(fixtures.state_bytes(reload_runtime) == final_state, "the minute %d reload continues to the uninterrupted final state" % minute) fixtures.free_fixture(reload) var saves := _packages(Fixture.SAVE_MINUTES, saved_paths, saved_states, Fixture.WRITER_SAVE) var after := {} for minute: int in held["after"]: var bytes: PackedByteArray = held["after"][minute] after[str(minute)] = {"sha256": Fixture.sha256(bytes), "base64": Marshalls.raw_to_base64(bytes)} var fog := _fog_scene(fixtures) var handoff := { "schema": Fixture.HANDOFF_SCHEMA, "writer_pid": OS.get_process_id(), "writer_source_sha256": FileAccess.get_sha256(Fixture.WRITER_SOURCE), "seed": Fixture.SEED, "last_minute": Fixture.LAST_MINUTE, "straight_final_state_sha256": Fixture.sha256(final_state), "straight_final_state_base64": Marshalls.raw_to_base64(final_state), "after_states": after, "saves": saves, "fog": fog, } DirAccess.make_dir_recursive_absolute(Fixture.HANDOFF_PATH.get_base_dir()) var file := FileAccess.open(Fixture.HANDOFF_PATH, FileAccess.WRITE) _expect(file != null, "handoff file opens for writing") if file != null: file.store_string(JSON.stringify(handoff, "\t") + "\n") file.close() print("P07_RESTART_WRITER pid=%d handoff_sha256=%s" % [OS.get_process_id(), FileAccess.get_sha256(Fixture.HANDOFF_PATH)]) fixtures.free_fixture(straight) fixtures.free_fixture(saving) await process_frame if _failures == 0: print("PASS: %d checks" % _checks) else: push_error("FAIL: %d of %d checks failed" % [_failures, _checks]) quit(_failures) ## WX7 fog scene: the uninterrupted run, the run that writes the save files, and ## a same-process reload of every save file continued to the end. func _fog_scene(fixtures: Variant) -> Dictionary: var straight: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED) var runtime: Variant = straight[&"runtime"] _expect(runtime.weather.get_kind() == &"mana_fog" and int(runtime.weather.get_today_draw().get("end", -1)) == 600, "seed 1592 opens the fog scene with mana fog 06:00-10:00") var seen := {"intimacy": {}, "rises": 0, "fog_rises": 0, "rises_ok": true, "flows": 0, "fog_flows": 0, "flows_ok": true, "after": {}} fixtures.play(straight, Fixture.FIRST_MINUTE, Fixture.FOG_LAST_MINUTE, func(minute: int) -> void: var fog_now: bool = runtime.weather.get_kind() == &"mana_fog" var last: Dictionary = seen["intimacy"] for guest_id: StringName in Fixture.FOG_ADULTS.values(): var guest: Variant = runtime.guest_service.get_guest(guest_id) if guest == null: continue var now := int(guest.needs[&"intimacy"]) if last.has(guest_id) and now < 100: seen["rises"] += 1 seen["rises_ok"] = seen["rises_ok"] and now - int(last[guest_id]) == (4 if fog_now else 3) if fog_now: seen["fog_rises"] += 1 last[guest_id] = now if minute % 10 == 0: seen["flows"] += 1 seen["flows_ok"] = seen["flows_ok"] and int(runtime.mana_reserve.get_last_flow().get(&"generation", -1)) == (24 if fog_now else 20) if fog_now: seen["fog_flows"] += 1 if minute - 1 in Fixture.FOG_SAVE_MINUTES: seen["after"][minute] = fixtures.state_bytes(runtime)) _expect(seen["rises_ok"] and int(seen["fog_rises"]) >= 3 and int(seen["rises"]) - int(seen["fog_rises"]) >= 2, "adult-verified intimacy rises by 4 a minute in fog and 3 after it (%d rises, %d in fog)" % [int(seen["rises"]), int(seen["fog_rises"])]) _expect(seen["flows_ok"] and int(seen["fog_flows"]) >= 20 and int(seen["flows"]) > int(seen["fog_flows"]), "each 10-minute mana boundary adds 24 in fog and 20 after it (%d boundaries, %d in fog)" % [int(seen["flows"]), int(seen["fog_flows"])]) var saving: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED) var saving_runtime: Variant = saving[&"runtime"] var states := {} var paths := {} fixtures.play(saving, Fixture.FIRST_MINUTE, Fixture.FOG_LAST_MINUTE, func(minute: int) -> void: if minute in Fixture.FOG_SAVE_MINUTES: var path := Fixture.FOG_WRITER_SAVE % minute states[minute] = fixtures.state_bytes(saving_runtime) _expect(bool(saving_runtime.save_to_path(path).get(&"ok", false)), "fog save file at minute %d" % minute) paths[minute] = path) var final_state: PackedByteArray = fixtures.state_bytes(runtime) _expect(fixtures.state_bytes(saving_runtime) == final_state, "writing the fog save files leaves the state as without saving") for minute: int in Fixture.FOG_SAVE_MINUTES: var reload: Dictionary = fixtures.new_fixture(false, Fixture.FOG_SEED) var reload_runtime: Variant = reload[&"runtime"] _expect(bool(reload_runtime.load_from_path(str(paths.get(minute, Fixture.FOG_WRITER_SAVE % minute))).get(&"ok", false)) and fixtures.state_bytes(reload_runtime) == states.get(minute, PackedByteArray()) and reload[&"clock"].absolute_minute == minute, "this process reloads the fog save file of minute %d exactly" % minute) fixtures.play(reload, minute, minute + 1, Callable()) _expect(fixtures.state_bytes(reload_runtime) == seen["after"].get(minute + 1, PackedByteArray()), "one minute after the minute %d fog reload equals the uninterrupted run (the fog yield and intimacy apply once)" % minute) fixtures.play(reload, minute + 1, Fixture.FOG_LAST_MINUTE, Callable()) _expect(fixtures.state_bytes(reload_runtime) == final_state, "the minute %d fog reload continues to the uninterrupted final state" % minute) fixtures.free_fixture(reload) var after := {} for minute: int in seen["after"]: var bytes: PackedByteArray = seen["after"][minute] after[str(minute)] = {"sha256": Fixture.sha256(bytes), "base64": Marshalls.raw_to_base64(bytes)} var saves := _packages(Fixture.FOG_SAVE_MINUTES, paths, states, Fixture.FOG_WRITER_SAVE) fixtures.free_fixture(straight) fixtures.free_fixture(saving) return { "seed": Fixture.FOG_SEED, "last_minute": Fixture.FOG_LAST_MINUTE, "straight_final_state_sha256": Fixture.sha256(final_state), "straight_final_state_base64": Marshalls.raw_to_base64(final_state), "after_states": after, "saves": saves, } ## The exact save packages and the compared state at each save minute. func _packages(minutes: Array[int], paths: Dictionary, states: Dictionary, path_format: String) -> Array: var saves: Array = [] for minute: int in minutes: var directory := ProjectSettings.globalize_path(str(paths.get(minute, path_format % minute))) _expect(DirAccess.dir_exists_absolute(directory) and DirAccess.get_directories_at(directory).is_empty(), "save package %s holds only files" % (path_format % minute)) var files := {} for name: String in DirAccess.get_files_at(directory): var bytes := FileAccess.get_file_as_bytes(directory.path_join(name)) files[name] = {"sha256": Fixture.sha256(bytes), "bytes": bytes.size(), "base64": Marshalls.raw_to_base64(bytes)} _expect(files.has("manifest.json") and files.has("world.bin"), "save package %s has its manifest and world" % (path_format % minute)) var state: PackedByteArray = states.get(minute, PackedByteArray()) saves.append({"minute": minute, "files": files, "state_sha256": Fixture.sha256(state), "state_base64": Marshalls.raw_to_base64(state)}) return saves func _expect(condition: bool, message: String) -> void: _checks += 1 if not condition: _failures += 1 push_error(message)