Preserve approved pixel tower prototype as development baseline

This commit is contained in:
Codex
2026-09-09 07:51:16 +09:00
commit 423fd6dd2e
915 changed files with 95193 additions and 0 deletions
+86
View File
@@ -0,0 +1,86 @@
extends SceneTree
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S6: performance budgets")
var profiler := SimulationPerformanceBudget.new(4)
_expect(not profiler.record_sample(&"", 10)[&"ok"], "empty metric ID is rejected")
_expect(not profiler.record_sample(&"path_query", -1)[&"ok"], "negative duration is rejected")
_expect(not profiler.set_budget(&"bad", 0), "non-positive budget is rejected")
_expect(profiler.set_budget(&"custom", 100, SimulationPerformanceBudget.Aggregation.P95), "custom budget registers")
for value: int in [10, 20, 30, 40, 50]:
profiler.record_sample(&"custom", value)
var custom := profiler.get_report(&"custom")
_expect(custom[&"window_sample_count"] == 4, "sample window remains bounded")
_expect(custom[&"lifetime_sample_count"] == 5, "lifetime count survives window eviction")
_expect(custom[&"average_us"] == 30, "lifetime average uses every recorded sample")
_expect(custom[&"maximum_us"] == 50, "window maximum is exact")
_expect(custom[&"p95_us"] == 50, "nearest-rank P95 is deterministic")
_expect(not custom[&"budget_exceeded"], "measurement below limit passes")
var exceeded: Array[StringName] = []
profiler.budget_exceeded.connect(func(metric_id: StringName, _report: Dictionary) -> void:
exceeded.append(metric_id)
)
profiler.record_sample(&"custom", 101)
_expect(profiler.get_report(&"custom")[&"budget_exceeded"], "P95 above limit fails")
_expect(exceeded == [&"custom"], "budget violation emits its exact metric once")
profiler.record_simulation_tick(3_000, 1)
profiler.record_simulation_tick(11_000, 4)
_expect(profiler.get_report(&"simulation_x1")[&"limit_us"] == 4_000, "x1 uses the MVP average budget")
_expect(profiler.get_report(&"simulation_x4")[&"limit_us"] == 12_000, "x4 uses the MVP P95 budget")
_expect(not profiler.get_report(&"simulation_x4")[&"budget_exceeded"], "x4 sample within budget passes")
profiler.record_sample(&"path_query", 2_001)
_expect(profiler.get_report(&"path_query")[&"budget_exceeded"], "path P95 enforces two milliseconds")
var reports := profiler.get_all_reports()
var ids: Array[String] = []
for report: Dictionary in reports:
ids.append(String(report[&"metric_id"]))
var sorted_ids := ids.duplicate()
sorted_ids.sort()
_expect(ids == sorted_ids, "all reports are returned in stable metric order")
profiler.clear(&"custom")
_expect(profiler.get_report(&"custom")[&"lifetime_sample_count"] == 0, "single metric can be cleared")
profiler.clear()
_expect(profiler.get_report(&"path_query")[&"lifetime_sample_count"] == 0, "all diagnostic samples can be cleared")
var clock = load("res://autoload/game_clock.gd").new()
clock.reset(1, 6, 0)
clock.process_real_delta(clock.REAL_SECONDS_PER_GAME_MINUTE)
_expect(
clock.performance_budget.get_report(&"simulation_x1")[&"lifetime_sample_count"] == 1,
"real-delta Kernel ticks feed the diagnostic profiler"
)
_expect(not clock.get_performance_reports().is_empty(), "Kernel exposes read-only performance reports")
clock.reset(1, 6, 0)
clock.set_speed(4)
var accelerated_ticks = clock.process_real_delta(clock.REAL_SECONDS_PER_GAME_MINUTE)
_expect(accelerated_ticks == 4, "x4 process frame advances the expected four game-minute ticks")
_expect(clock.performance_budget.get_report(&"simulation_x4")[&"lifetime_sample_count"] == 1, "x4 performance measures the aggregate frame once rather than four isolated ticks")
_expect(clock.performance_budget.get_report(&"main_thread_stall")[&"lifetime_sample_count"] == 1, "active simulation frames feed the main-thread stall budget")
clock.free()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
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)
else:
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://c4bgfjyaobl8i
+697
View File
@@ -0,0 +1,697 @@
extends SceneTree
const TEST_PREFIX := "user://saves/s6_runtime_lifecycle"
const PRODUCTION_AUTOSAVE_PATH := "user://saves/autosave"
var _checks := 0
var _failures := 0
var _clock: Variant
var _state: Variant
var _runtime: Variant
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S6: runtime save lifecycle and recovery safety")
await _test_autoload_isolation_and_main_opt_in()
_test_disabled_policy_and_path_timing()
_test_queued_checkpoint_at_tick_boundary()
_test_daily_checkpoint_dedup_generations_and_failure_preservation()
_test_exact_round_trip_recovery_pause_and_performance()
_test_domain_invalid_formal_falls_back_to_backup()
_test_multiple_domain_invalid_candidates_fall_back_to_autosave()
_test_all_domain_invalid_candidates_preserve_complete_live_state()
_test_content_filter_cannot_be_weakened()
_test_whitelisted_recovery_repairs()
_test_non_whitelisted_corruption_is_rejected_atomically()
_cleanup_runtime()
_cleanup_test_saves()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_autoload_isolation_and_main_opt_in() -> void:
var app_clock := root.get_node("GameClock")
var app_state := root.get_node("GameState")
var app_runtime := root.get_node("SimulationRuntime")
app_runtime.configure_save_policy({&"enabled": false})
var default_generation := SaveService.get_autosave_path(
PRODUCTION_AUTOSAVE_PATH,
0
)
var before := _package_fingerprint(default_generation)
app_clock.reset(2, 5, 59)
app_state.reset()
app_runtime.initialize(app_clock, app_state, 60_001)
_expect(not app_runtime.get_save_policy()[&"enabled"], "autoload test runtime starts with disk autosave disabled")
app_clock.advance_one_minute()
_expect(_package_fingerprint(default_generation) == before, "disabled autoload reaches 06:00 without touching the production autosave")
var main := load("res://scenes/main/main.tscn").instantiate() as Control
root.add_child(main)
await process_frame
_expect(
app_runtime.get_save_policy()[&"enabled"]
and app_runtime.get_save_policy()[&"autosave_path"] == PRODUCTION_AUTOSAVE_PATH,
"playable main scene explicitly opts into the production autosave policy"
)
app_runtime.configure_save_policy({&"enabled": false})
main.queue_free()
await process_frame
func _test_disabled_policy_and_path_timing() -> void:
_reset_runtime(60_002, 1, 5, 59)
var path := TEST_PREFIX + "_disabled"
SaveService.delete_save(path)
_expect(
not _runtime.get_save_policy()[&"enabled"]
and _runtime.get_save_policy()[&"autosave_path"] == PRODUCTION_AUTOSAVE_PATH,
"direct Runtime instances remain isolated until explicitly enabled"
)
_clock.advance_one_minute()
_expect(
not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(SaveService.get_autosave_path(path, 0))),
"disabled policy writes no implicit autosave"
)
var before_count := int(
_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"]
)
_runtime.path_service.find_path(
Vector3i(-2, 0, WorldService.LAYER_FRONT),
Vector3i(2, 0, WorldService.LAYER_BACK),
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK],
[&"staff"]
)
var after_count := int(
_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"]
)
_expect(after_count == before_count + 1, "one public path query records one Runtime performance sample")
var snapshot = _runtime.create_snapshot()
_expect(_runtime.restore_snapshot(snapshot), "Runtime restores before reconnecting the Path timing boundary")
before_count = int(_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"])
_runtime.path_service.find_path(Vector3i(-1, 0, 0), Vector3i(1, 0, 0))
after_count = int(_clock.performance_budget.get_report(&"path_query")[&"lifetime_sample_count"])
_expect(after_count == before_count + 1, "restore reconnects Path timing without duplicate signal delivery")
func _test_queued_checkpoint_at_tick_boundary() -> void:
_reset_runtime(60_003)
var path := TEST_PREFIX + "_queued"
SaveService.delete_save(path)
var queued = _runtime.queue_manual_checkpoint(path, {&"display_name": "Queued Runtime"})
_expect(queued[&"ok"] and _runtime.get_pending_checkpoint_count() == 1, "manual checkpoint is queued for the next fixed tick boundary")
_expect(not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(path)), "queued checkpoint does not write before its boundary")
_clock.advance_one_minute()
var loaded := SaveService.load_snapshot(path)
_expect(loaded[&"ok"] and _runtime.get_pending_checkpoint_count() == 0, "queued checkpoint writes exactly once at PUBLISH_SNAPSHOT")
_expect(int(loaded[&"snapshot"][&"clock"][&"absolute_minute"]) == 361, "queued package captures the completed minute boundary")
_expect(_has_domain_event(&"SAVE_CHECKPOINT_SUCCEEDED"), "successful manual checkpoint publishes a DomainEvent")
SaveService.delete_save(path)
func _test_daily_checkpoint_dedup_generations_and_failure_preservation() -> void:
_reset_runtime(60_004, 1, 5, 59)
var path := TEST_PREFIX + "_daily"
SaveService.delete_save(path)
var configured = _runtime.configure_save_policy({
&"enabled": true,
&"daily_checkpoint_enabled": true,
&"autosave_path": path,
&"autosave_generations": 3,
})
_expect(configured[&"ok"], "daily Runtime save policy accepts a safe explicit slot")
_clock.advance_one_minute()
_expect(_runtime.get_last_daily_checkpoint_day() == 1, "day 1 checkpoint runs at exactly 06:00")
var generation_zero := SaveService.load_snapshot(SaveService.get_autosave_path(path, 0))
_expect(generation_zero[&"ok"] and int(generation_zero[&"snapshot"][&"clock"][&"absolute_minute"]) == 360, "first daily checkpoint becomes autosave generation zero")
_runtime.call("_process_save_checkpoints", 360)
_expect(not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(SaveService.get_autosave_path(path, 1))), "same-day 06:00 callback is deduplicated")
_clock.advance_minutes(SimulationKernel.MINUTES_PER_DAY)
_clock.advance_minutes(SimulationKernel.MINUTES_PER_DAY)
var expected_minutes := [3240, 1800, 360]
var all_generations_match := true
for generation in range(3):
var loaded := SaveService.load_snapshot(SaveService.get_autosave_path(path, generation))
all_generations_match = all_generations_match \
and loaded[&"ok"] \
and int(loaded[&"snapshot"][&"clock"][&"absolute_minute"]) == expected_minutes[generation]
_expect(all_generations_match, "daily checkpoints rotate three deterministic newest-first generations")
var newest_world := ProjectSettings.globalize_path(
SaveService.get_autosave_path(path, 0)
).path_join(SaveService.WORLD_FILE)
var newest_before := FileAccess.get_md5(newest_world)
var pending_path := ProjectSettings.globalize_path(path + ".autosave.pending")
DirAccess.make_dir_recursive_absolute(pending_path)
var sentinel := FileAccess.open(pending_path.path_join("not_a_package.txt"), FileAccess.WRITE)
sentinel.store_string("block promotion")
sentinel = null
_clock.advance_minutes(SimulationKernel.MINUTES_PER_DAY)
_expect(
not _runtime.latest_save_operation_result[&"ok"]
and _runtime.get_last_daily_checkpoint_day() == 3,
"failed day 4 checkpoint remains eligible for retry instead of being marked complete"
)
_expect(FileAccess.get_md5(newest_world) == newest_before, "failed autosave leaves the previous newest generation byte-identical")
_expect(_has_domain_event(&"AUTOSAVE_CHECKPOINT_FAILED"), "autosave failure publishes a structured DomainEvent")
DirAccess.remove_absolute(pending_path.path_join("not_a_package.txt"))
DirAccess.remove_absolute(pending_path)
SaveService.delete_save(path)
func _test_exact_round_trip_recovery_pause_and_performance() -> void:
_reset_runtime(60_005, 3, 7, 15)
var path := TEST_PREFIX + "_round_trip"
SaveService.delete_save(path)
_clock.set_speed(4)
var expected_snapshot = _runtime.create_snapshot()
var expected_digest := SimulationDigest.compute(expected_snapshot)
var first_save = _runtime.save_to_path(path, {&"display_name": "Runtime Exact"})
_expect(first_save[&"ok"] and not _clock.paused, "save pauses I/O temporarily and restores the running clock")
_state.add_resource(&"faith", 77)
_clock.advance_minutes(5)
var loaded = _runtime.load_from_path(path)
_expect(loaded[&"ok"] and _runtime.latest_digest == expected_digest, "ordinary Runtime load restores the exact persisted snapshot")
_expect(_clock.absolute_minute == int(expected_snapshot[&"clock"][&"absolute_minute"]) and not _clock.paused, "load preserves the saved clock minute and pause state")
var minute_before_process = _clock.absolute_minute
_expect(_clock.process_real_delta(10.0) == 0 and _clock.absolute_minute == minute_before_process, "first process frame after load never advances an extra minute")
var second_save = _runtime.save_to_path(path, {&"display_name": "Runtime New Formal"})
_expect(second_save[&"ok"], "second formal Runtime save creates a verified prior-generation backup")
_corrupt_world(path)
_state.add_resource(&"faith", 5)
var inventory = _runtime.inspect_recovery_candidates(path)
_expect(inventory[&"ok"] and inventory[&"selected"][&"id"] == &"backup", "explicit candidate inventory identifies the verified backup")
var recovered = _runtime.load_with_recovery(path)
_expect(recovered[&"ok"] and recovered[&"selected"][&"id"] == &"backup", "Runtime recovery skips a corrupt formal package")
_expect(_runtime.latest_digest == expected_digest, "recovery loads the exact prior formal snapshot without promotion")
_expect(recovered[&"recovery_report"][&"restore_committed"] and _has_domain_event(&"SAVE_RECOVERY_LOADED"), "successful recovery returns a structured report and publishes a DomainEvent")
var explicit = _runtime.load_recovery_candidate(path, &"backup")
_expect(explicit[&"ok"] and explicit[&"read_only"], "caller can explicitly load one named recovery candidate")
_expect(int(_clock.performance_budget.get_report(&"save")[&"lifetime_sample_count"]) >= 2, "Runtime records actual save operation timing")
_expect(int(_clock.performance_budget.get_report(&"load")[&"lifetime_sample_count"]) >= 3, "Runtime records load, inventory and recovery timing")
SaveService.delete_save(path)
func _test_domain_invalid_formal_falls_back_to_backup() -> void:
_reset_runtime(60_009, 2, 8, 0)
var path := TEST_PREFIX + "_domain_backup"
SaveService.delete_save(path)
var backup_snapshot: Dictionary = _runtime.create_snapshot()
var backup_digest := SimulationDigest.compute(backup_snapshot)
_expect(
SaveService.save_snapshot(path, backup_snapshot)[&"ok"],
"domain fallback fixture writes a valid prior formal package"
)
var invalid_formal: Dictionary = backup_snapshot.duplicate(true)
invalid_formal[&"game_state"][&"resources"][&"gold"] = int(
invalid_formal[&"game_state"][&"resources"][&"gold"]
) + 1
_expect(
SaveService.save_snapshot(path, invalid_formal)[&"ok"],
"checksum-valid but ledger-invalid formal package is preserved as a package"
)
_state.add_resource(&"faith", 17)
var strict_guard := _authoritative_live_guard()
var strict_load: Dictionary = _runtime.load_from_path(path)
_expect(
not strict_load[&"ok"] and strict_load[&"code"] == &"RESTORE_FAILED",
"ordinary load remains strict and does not fall through to a valid backup"
)
_expect(
_authoritative_live_guard() == strict_guard,
"strict ordinary-load rejection preserves authoritative gameplay state"
)
var explicit_formal: Dictionary = _runtime.load_recovery_candidate(path, &"formal")
_expect(
not explicit_formal[&"ok"] and explicit_formal[&"code"] == &"RESTORE_FAILED",
"explicit candidate load remains strict to the requested domain-invalid package"
)
var recovered: Dictionary = _runtime.load_with_recovery(path)
_expect(
recovered[&"ok"] and recovered[&"selected"][&"id"] == &"backup",
"Runtime rejects a domain-invalid formal package and automatically selects its valid backup"
)
_expect(
recovered[&"candidates"][0][&"package_status"] == &"VALID"
and recovered[&"candidates"][0][&"domain_status"] == &"REJECTED"
and recovered[&"candidates"][0][&"domain_rejection_code"] == &"DOMAIN_OR_CROSSLINK_VALIDATION_FAILED"
and recovered[&"candidates"][0][&"failed_attempt_live_state_preserved"],
"formal candidate report separates successful package verification from domain rejection"
)
_expect(
recovered[&"candidates"][1][&"domain_status"] == &"VALID"
and recovered[&"candidates"][1][&"selected"],
"backup candidate report identifies the first domain-valid committed candidate"
)
_expect(
_runtime.latest_digest == backup_digest,
"domain fallback commits the exact backup snapshot"
)
_expect(
recovered[&"package_candidate_report"][&"selected"][&"id"] == &"formal"
and recovered[&"package_recovery_report"][&"source"][&"candidate_id"] == &"backup"
and recovered[&"restore_safety_report"][&"recovery_mode"],
"result preserves package-level selection evidence and final Runtime restore-safety evidence"
)
SaveService.delete_save(path)
func _test_multiple_domain_invalid_candidates_fall_back_to_autosave() -> void:
_reset_runtime(60_010, 2, 9, 0)
var path := TEST_PREFIX + "_domain_autosave"
SaveService.delete_save(path)
var valid_autosave: Dictionary = _runtime.create_snapshot()
var valid_digest := SimulationDigest.compute(valid_autosave)
var invalid_backup: Dictionary = valid_autosave.duplicate(true)
invalid_backup[&"game_state"][&"resources"][&"gold"] = int(
invalid_backup[&"game_state"][&"resources"][&"gold"]
) + 1
var invalid_formal: Dictionary = valid_autosave.duplicate(true)
invalid_formal[&"game_state"][&"resources"][&"gold"] = int(
invalid_formal[&"game_state"][&"resources"][&"gold"]
) + 2
_expect(
SaveService.save_snapshot(path, invalid_backup)[&"ok"]
and SaveService.save_snapshot(path, invalid_formal)[&"ok"],
"multiple domain-invalid formal generations remain checksum-valid recovery candidates"
)
_expect(
SaveService.save_autosave(path, valid_autosave, {}, 3)[&"ok"],
"valid autosave is written behind both invalid formal generations"
)
var package_guard := {
&"formal": _package_fingerprint(path),
&"backup": _package_fingerprint(path + ".bak"),
&"autosave": _package_fingerprint(SaveService.get_autosave_path(path, 0)),
}
_state.add_resource(&"faith", 23)
var recovered: Dictionary = _runtime.load_with_recovery(path)
_expect(
recovered[&"ok"] and recovered[&"selected"][&"id"] == &"autosave_0",
"Runtime walks past two domain-invalid candidates to the newest valid autosave"
)
_expect(
recovered[&"candidates"][0][&"domain_status"] == &"REJECTED"
and recovered[&"candidates"][1][&"domain_status"] == &"REJECTED"
and recovered[&"candidates"][0][&"failed_attempt_live_state_preserved"]
and recovered[&"candidates"][1][&"failed_attempt_live_state_preserved"]
and recovered[&"candidates"][4][&"domain_status"] == &"VALID",
"candidate attempts retain both ordered domain rejections and the autosave success"
)
_expect(
recovered[&"recovery_report"][&"domain_rejections"].size() == 2
and recovered[&"recovery_report"][&"selected_candidate_id"] == &"autosave_0",
"combined recovery report records every rejected domain candidate and final selection"
)
_expect(_runtime.latest_digest == valid_digest, "autosave fallback restores the exact valid snapshot")
_expect(
package_guard == {
&"formal": _package_fingerprint(path),
&"backup": _package_fingerprint(path + ".bak"),
&"autosave": _package_fingerprint(SaveService.get_autosave_path(path, 0)),
},
"domain fallback remains read-only and never promotes or rewrites any package"
)
SaveService.delete_save(path)
func _test_all_domain_invalid_candidates_preserve_complete_live_state() -> void:
_reset_runtime(60_011, 2, 10, 0)
var path := TEST_PREFIX + "_all_domain_invalid"
SaveService.delete_save(path)
var base: Dictionary = _runtime.create_snapshot()
var invalid_backup: Dictionary = base.duplicate(true)
invalid_backup[&"game_state"][&"resources"][&"gold"] = int(
invalid_backup[&"game_state"][&"resources"][&"gold"]
) + 1
var invalid_formal: Dictionary = base.duplicate(true)
invalid_formal[&"game_state"][&"resources"][&"gold"] = int(
invalid_formal[&"game_state"][&"resources"][&"gold"]
) + 2
var invalid_autosave: Dictionary = base.duplicate(true)
invalid_autosave[&"game_state"][&"resources"][&"gold"] = int(
invalid_autosave[&"game_state"][&"resources"][&"gold"]
) + 3
_expect(
SaveService.save_snapshot(path, invalid_backup)[&"ok"]
and SaveService.save_snapshot(path, invalid_formal)[&"ok"]
and SaveService.save_autosave(path, invalid_autosave, {}, 3)[&"ok"],
"all-invalid fixture exposes three checksum-valid but domain-invalid candidates"
)
_state.add_resource(&"faith", 31)
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.DISABLE,
EventDefinition.ContentRating.GENERAL,
[&"local_atomic_filter"]
)
_clock.set_speed(4)
_clock.set_paused(true)
_expect(
_runtime.queue_manual_checkpoint(TEST_PREFIX + "_never_written")[&"ok"],
"all-invalid atomicity fixture includes a pending Runtime checkpoint"
)
var live_guard := _complete_live_guard()
var rejected: Dictionary = _runtime.load_with_recovery(path)
_expect(
not rejected[&"ok"]
and rejected[&"code"] == &"RESTORE_FAILED"
and rejected[&"recovery_report"][&"rejection_reason"] == &"NO_DOMAIN_VALID_RECOVERY_CANDIDATE",
"all domain-invalid candidates return one explicit terminal recovery failure"
)
_expect(
rejected[&"recovery_report"][&"domain_rejections"].size() == 3
and rejected[&"selected"].is_empty(),
"terminal failure reports all three domain rejections and no selected candidate"
)
_expect(
_complete_live_guard() == live_guard and rejected[&"live_state_preserved"],
"all-candidate failure preserves the complete live snapshot, filter, pause, policy, and pending checkpoint"
)
_expect(
rejected[&"restore_safety_report"] == rejected[&"recovery_report"]
and rejected[&"package_candidate_report"].has(&"candidates"),
"terminal failure preserves both package inventory and Runtime restore-safety reports"
)
SaveService.delete_save(path)
func _test_content_filter_cannot_be_weakened() -> void:
_reset_runtime(60_006)
var weak_path := TEST_PREFIX + "_weak_filter"
var exact_path := TEST_PREFIX + "_exact_filter"
SaveService.delete_save(weak_path)
SaveService.delete_save(exact_path)
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.NONE,
EventDefinition.ContentRating.RESTRICTED,
[&"saved_tag"]
)
_expect(_runtime.save_to_path(weak_path)[&"ok"], "weaker saved content profile fixture is valid")
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.DISABLE,
EventDefinition.ContentRating.GENERAL,
[&"local_tag"]
)
var loaded = _runtime.load_from_path(weak_path)
var profile = _runtime.event_service.get_content_filter_profile()
_expect(
loaded[&"ok"]
and profile[&"filter_mode"] == EventDefinition.FilterBehavior.DISABLE
and profile[&"maximum_content_rating"] == EventDefinition.ContentRating.GENERAL,
"loading a weaker save cannot reduce behavior severity or raise the rating ceiling"
)
_expect(
profile[&"disabled_content_tags"] == [&"local_tag", &"saved_tag"],
"load safety unions local and saved disabled content tags"
)
var filter_report: Dictionary = loaded[&"restore_safety_report"][&"content_filter"]
_expect(filter_report[&"saved_profile_was_weaker"] and filter_report[&"strict_profile_preserved"], "load result explains that the stricter local filter won")
var exact_snapshot = _runtime.create_snapshot()
var exact_digest := SimulationDigest.compute(exact_snapshot)
_expect(_runtime.save_to_path(exact_path)[&"ok"], "effective strict profile saves normally")
_runtime.event_service.configure_content_filter(
EventDefinition.FilterBehavior.NONE,
EventDefinition.ContentRating.RESTRICTED,
[]
)
var exact_loaded = _runtime.load_from_path(exact_path)
_expect(exact_loaded[&"ok"] and _runtime.latest_digest == exact_digest, "ordinary content-profile round trip remains snapshot-exact")
SaveService.delete_save(weak_path)
SaveService.delete_save(exact_path)
func _test_whitelisted_recovery_repairs() -> void:
_reset_runtime(60_007)
var path := TEST_PREFIX + "_allowed_repairs"
SaveService.delete_save(path)
var snapshot = _runtime.create_snapshot()
var runtime_data: Dictionary = snapshot[&"systems"][&"runtime"]
var staff := StaffRuntime.new().configure(&"repair_staff", &"worker", {
&"state": StaffRuntime.State.MOVING_TO_TASK,
&"current_task_id": &"repair_task",
})
var task := StaffTask.new().configure(&"repair_task", &"cleanup", 50, _clock.absolute_minute, {
&"state": StaffTask.State.RESERVED,
&"assigned_staff_id": &"missing_staff",
})
runtime_data[&"staff"][&"staff"].append(staff.create_snapshot())
runtime_data[&"staff"][&"tasks"].append(task.create_snapshot())
_append_staff_path_crosslink(runtime_data, &"repair_staff", Vector3i.ZERO)
var terminal := EventInstance.new().configure(
&"terminal_foreground",
&"relationship.threshold",
&"relationship.threshold",
[&"player"],
100,
_clock.absolute_minute,
-1,
true,
&""
)
terminal.state = EventInstance.State.COMPLETED
runtime_data[&"events"][&"instances"].append(terminal.create_snapshot())
runtime_data[&"events"][&"foreground_instance_id"] = "terminal_foreground"
runtime_data[&"events"][&"actor_locks"] = {"player": "terminal_foreground"}
_expect(SaveService.save_snapshot(path, snapshot)[&"ok"], "checksum-valid orphan repair fixture writes as a package")
var recovered = _runtime.load_with_recovery(path)
_expect(recovered[&"ok"], "recovery accepts only the whitelisted Task and Event Lock damage")
var repaired_task = _runtime.staff_service.get_task(&"repair_task")
var repaired_staff = _runtime.staff_service.get_staff(&"repair_staff")
_expect(
repaired_task != null and repaired_task.state == StaffTask.State.OPEN and repaired_task.assigned_staff_id.is_empty()
and repaired_staff != null and repaired_staff.current_task_id.is_empty(),
"StaffService audit reopens the orphan Task and releases its stale Staff pointer"
)
_expect(_runtime.event_service.get_actor_locks().is_empty() and _runtime.event_service.get_foreground_instance() == null, "terminal foreground recovery releases only its stale Event Locks")
var report: Dictionary = recovered[&"recovery_report"]
_expect(
report[&"repairs"][&"orphan_tasks"][&"reopened_task_ids"] == [&"repair_task"]
and report[&"repairs"][&"orphan_tasks"][&"released_staff_ids"] == [&"repair_staff"],
"repair report names every repaired Task and Staff exactly"
)
_expect(
report[&"repairs"][&"orphan_event_locks"][&"released_actor_ids"] == [&"player"]
and report[&"repairs"][&"orphan_event_locks"][&"released_foreground_instance_id"] == &"terminal_foreground",
"repair report names the released Actor Lock and terminal foreground exactly"
)
_expect(report[&"automatic_repairs_applied"] and not report[&"forbidden_repairs_applied"], "recovery report confirms its authoritative whitelist boundary")
SaveService.delete_save(path)
func _test_non_whitelisted_corruption_is_rejected_atomically() -> void:
_reset_runtime(60_008)
var built = _runtime.submit_command(
&"build_facility",
{&"definition_id": &"reception", &"origin": Vector3i(-6, 0, 0), &"rotation": 0},
&"player", &"", 100, -1, &"s6.invalid.fixture.facility"
)
_expect(built.is_success(), "invalid recovery fixtures start from a valid constructed facility")
_clock.advance_minutes(120)
var base = _runtime.create_snapshot()
var missing_facility = base.duplicate(true)
missing_facility[&"systems"][&"runtime"][&"facilities"][&"instances"] = []
_test_rejected_candidate(
TEST_PREFIX + "_reject_missing_facility",
missing_facility,
"recovery never deletes or invents a missing operational facility"
)
var ledger_mutation = base.duplicate(true)
ledger_mutation[&"game_state"][&"resources"][&"gold"] = int(
ledger_mutation[&"game_state"][&"resources"][&"gold"]
) + 1
_test_rejected_candidate(
TEST_PREFIX + "_reject_ledger",
ledger_mutation,
"recovery never synthesizes Gold or rewrites the ledger projection"
)
var route_mismatch = base.duplicate(true)
var invalid_relationship := RelationshipState.new().configure(
&"invalid_route",
&"actor_a",
&"actor_b",
{
&"route_lock": RelationshipState.RouteLock.EXCLUSIVE_LOCKED,
&"route_id": &"",
}
)
route_mismatch[&"systems"][&"runtime"][&"relationships"][&"relationships"].append(
invalid_relationship.create_snapshot()
)
_test_rejected_candidate(
TEST_PREFIX + "_reject_route",
route_mismatch,
"recovery never guesses an exclusive Relationship route lock"
)
var unreleasable = base.duplicate(true)
var bad_staff := StaffRuntime.new().configure(&"blocked_staff", &"worker", {
&"state": StaffRuntime.State.MOVING_TO_TASK,
&"current_task_id": &"blocked_task",
})
var bad_task := StaffTask.new().configure(&"blocked_task", &"cleanup", 50, _clock.absolute_minute, {
&"state": StaffTask.State.RESERVED,
&"assigned_staff_id": &"missing_staff",
&"reservation_ids": [&"missing_reservation"],
})
unreleasable[&"systems"][&"runtime"][&"staff"][&"staff"].append(bad_staff.create_snapshot())
unreleasable[&"systems"][&"runtime"][&"staff"][&"tasks"].append(bad_task.create_snapshot())
_append_staff_path_crosslink(
unreleasable[&"systems"][&"runtime"],
&"blocked_staff",
Vector3i.ZERO
)
_test_rejected_candidate(
TEST_PREFIX + "_reject_reservation",
unreleasable,
"recovery rejects an orphan Task whose inventory Reservation cannot be released"
)
func _test_rejected_candidate(path: String, snapshot: Dictionary, label: String) -> void:
SaveService.delete_save(path)
_expect(SaveService.save_snapshot(path, snapshot)[&"ok"], "%s fixture is checksum-valid" % label)
var guard := _authoritative_live_guard()
var result = _runtime.load_with_recovery(path)
_expect(not result[&"ok"] and result[&"code"] == &"RESTORE_FAILED", label)
_expect(_authoritative_live_guard() == guard, "%s and leaves authoritative live state unchanged" % label)
_expect(
result[&"recovery_report"][&"restore_committed"] == false
and not result[&"recovery_report"][&"forbidden_repairs_applied"],
"%s reports atomic rejection without forbidden repair" % label
)
SaveService.delete_save(path)
func _append_staff_path_crosslink(
runtime_data: Dictionary,
staff_id: StringName,
cell: Vector3i
) -> void:
var actor_id := StringName("staff:%s" % staff_id)
runtime_data[&"path"][&"actor_cells"].append({
&"actor_id": actor_id,
&"cell": cell,
})
runtime_data[&"path"][&"actor_progress"].append({
&"actor_id": actor_id,
&"last_cell": cell,
&"last_progress_at": _clock.absolute_minute,
&"last_observed_at": _clock.absolute_minute,
&"stuck_reported_at": -1,
})
runtime_data[&"movement_credits"].append({
&"actor_id": String(actor_id),
&"credit": 0,
&"updated_at": _clock.absolute_minute,
})
for traffic: Dictionary in runtime_data[&"path"][&"cell_traffic"]:
if traffic[&"cell"] == cell:
traffic[&"current_density"] = int(traffic[&"current_density"]) + 1
break
func _authoritative_live_guard() -> Dictionary:
return {
&"clock": _clock.clock_snapshot(),
&"game_state": _state.create_snapshot(),
&"economy": _runtime.economy_service.create_snapshot(),
&"inventory": _runtime.inventory_service.create_snapshot(),
&"facilities": _runtime.facility_service.create_snapshot(),
&"construction": _runtime.construction_service.create_snapshot(),
&"staff": _runtime.staff_service.create_snapshot(),
&"relationships": _runtime.relationship_service.create_snapshot(),
&"events": _runtime.event_service.create_snapshot(),
}
func _complete_live_guard() -> Dictionary:
return {
&"snapshot": _runtime.create_snapshot(),
&"clock_paused": _clock.paused,
&"save_policy": _runtime.get_save_policy(),
&"pending_checkpoint_count": _runtime.get_pending_checkpoint_count(),
&"last_daily_checkpoint_day": _runtime.get_last_daily_checkpoint_day(),
&"content_filter": _runtime.event_service.get_content_filter_profile(),
}
func _reset_runtime(seed: int, day := 1, hour := 6, minute := 0) -> void:
_cleanup_runtime()
_clock = load("res://autoload/game_clock.gd").new()
_state = load("res://autoload/game_state.gd").new()
_runtime = load("res://autoload/simulation_runtime.gd").new()
_clock.reset(day, hour, minute)
_state.reset()
var report = _runtime.initialize(_clock, _state, seed)
_expect(report[&"valid"], "Runtime fixture initializes deterministic definitions")
func _cleanup_runtime() -> void:
if _runtime != null:
_runtime.free()
if _clock != null:
_clock.free()
if _state != null:
_state.free()
_runtime = null
_clock = null
_state = null
func _cleanup_test_saves() -> void:
for suffix: String in [
"_disabled", "_queued", "_daily", "_round_trip", "_weak_filter",
"_exact_filter", "_allowed_repairs", "_reject_missing_facility",
"_reject_ledger", "_reject_route", "_reject_reservation",
"_domain_backup", "_domain_autosave", "_all_domain_invalid",
"_never_written",
]:
SaveService.delete_save(TEST_PREFIX + suffix)
func _has_domain_event(event_type: StringName) -> bool:
for event: SimulationDomainEvent in _runtime.domain_events.recent_events():
if event.event_type == event_type:
return true
return false
func _package_fingerprint(save_path: String) -> Dictionary:
var absolute_path := ProjectSettings.globalize_path(save_path)
var result := {&"directory_exists": DirAccess.dir_exists_absolute(absolute_path)}
for file_name: String in [SaveService.MANIFEST_FILE, SaveService.WORLD_FILE, SaveService.THUMBNAIL_FILE]:
var file_path := absolute_path.path_join(file_name)
result[file_name] = FileAccess.get_md5(file_path) if FileAccess.file_exists(file_path) else ""
return result
func _corrupt_world(save_path: String) -> void:
var world_path := ProjectSettings.globalize_path(save_path).path_join(SaveService.WORLD_FILE)
var bytes := FileAccess.get_file_as_bytes(world_path)
if bytes.is_empty():
return
bytes[bytes.size() - 1] = bytes[bytes.size() - 1] ^ 0x5a
var file := FileAccess.open(world_path, FileAccess.WRITE)
file.store_buffer(bytes)
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
@@ -0,0 +1 @@
uid://bamjaic1m17jn
+584
View File
@@ -0,0 +1,584 @@
extends SceneTree
const SaveServiceType = preload("res://scripts/core/save_service.gd")
const MigrationRegistryType = preload("res://scripts/core/save_migration_registry.gd")
var _checks := 0
var _failures := 0
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
print("S6: save rotation, recovery and migration")
_test_path_safety()
_test_package_internal_symlink_load_rejection()
_test_package_contract_identity_and_timing()
_test_autosave_rotation_and_formal_atomicity()
_test_interrupted_formal_save_recovery()
_test_interrupted_autosave_rotation_recovery()
_test_autosave_retention_shrink_prunes_verified_generations()
_test_recovery_priority_checksum_and_read_only_boundary()
_test_migration_registry_continuity_and_purity()
_test_current_format_snapshot_validation()
_test_save_package_migration_and_pre_migration_backup()
_test_migration_failure_preserves_source()
if _failures == 0:
print("PASS: %d checks" % _checks)
else:
push_error("FAIL: %d of %d checks failed" % [_failures, _checks])
quit(_failures)
func _test_path_safety() -> void:
var snapshot := _snapshot(10, &"safe")
var traversal := SaveServiceType.save_snapshot("user://../s6_escape", snapshot)
_expect(not traversal[&"ok"] and traversal[&"code"] == &"UNSAFE_PATH", "parent traversal is rejected before any write")
var relative := SaveServiceType.save_snapshot("relative/s6_save", snapshot)
_expect(not relative[&"ok"] and relative[&"code"] == &"INVALID_PATH", "ambiguous relative save paths are rejected")
var external := SaveServiceType.save_snapshot("/tmp/the_tree_s6_escape", snapshot)
_expect(not external[&"ok"] and external[&"code"] == &"UNSAFE_PATH", "absolute paths outside the Godot user directory are rejected")
_expect(not SaveServiceType.delete_save("user://"), "delete refuses the broad user data root")
_expect(not SaveServiceType.delete_save("/"), "delete refuses the filesystem root")
var non_package_path := ProjectSettings.globalize_path("user://saves/s6_non_package_parent")
DirAccess.make_dir_recursive_absolute(non_package_path)
var keep_file := FileAccess.open(non_package_path.path_join("keep.txt"), FileAccess.WRITE)
keep_file.store_string("must remain")
keep_file = null
_expect(not SaveServiceType.delete_save("user://saves/s6_non_package_parent"), "delete refuses an ordinary parent directory that is not a save package")
_expect(FileAccess.file_exists(non_package_path.path_join("keep.txt")), "refused parent deletion preserves unrelated user data")
_expect(not SaveServiceType.save_snapshot("user://saves/s6_non_package_parent", snapshot)[&"ok"], "save refuses to rotate an ordinary directory into a package backup")
DirAccess.remove_absolute(non_package_path.path_join("keep.txt"))
DirAccess.remove_absolute(non_package_path)
var external_path := "/tmp/the_tree_s6_symlink_%d" % OS.get_process_id()
var link_path := ProjectSettings.globalize_path("user://saves/s6_symlink_parent")
DirAccess.make_dir_recursive_absolute(external_path)
var sentinel := FileAccess.open(external_path.path_join("outside.txt"), FileAccess.WRITE)
sentinel.store_string("outside")
sentinel = null
DirAccess.remove_absolute(link_path)
var link_exit := OS.execute("ln", PackedStringArray(["-s", external_path, link_path]))
_expect(link_exit == 0, "symlink escape fixture is available")
if link_exit == 0:
var linked_save := SaveServiceType.save_snapshot("user://saves/s6_symlink_parent/escaped", snapshot)
_expect(not linked_save[&"ok"] and linked_save[&"code"] == &"UNSAFE_PATH", "write refuses a save directory reached through a symbolic link")
_expect(not SaveServiceType.delete_save("user://saves/s6_symlink_parent/escaped"), "delete refuses a directory reached through a symbolic link")
_expect(FileAccess.get_file_as_string(external_path.path_join("outside.txt")) == "outside", "symlink rejection never touches data outside user storage")
DirAccess.remove_absolute(link_path)
var recursive_save_path := "user://saves/s6_recursive_symlink"
SaveServiceType.delete_save(recursive_save_path)
SaveServiceType.save_snapshot(recursive_save_path, snapshot)
var embedded_link := ProjectSettings.globalize_path(recursive_save_path).path_join("external_link")
var embedded_link_exit := OS.execute("ln", PackedStringArray(["-s", external_path, embedded_link]))
_expect(embedded_link_exit == 0, "recursive symlink fixture is available")
if embedded_link_exit == 0:
var recursive_write := SaveServiceType.save_snapshot(recursive_save_path, snapshot)
_expect(not recursive_write[&"ok"] and recursive_write[&"code"] == &"UNSAFE_PATH", "overwrite refuses a symbolic link nested anywhere inside the package")
_expect(not SaveServiceType.delete_save(recursive_save_path), "recursive delete refuses a package containing a symbolic link")
_expect(FileAccess.file_exists(external_path.path_join("outside.txt")), "recursive symlink guard preserves its external target")
DirAccess.remove_absolute(embedded_link)
SaveServiceType.delete_save(recursive_save_path)
DirAccess.remove_absolute(external_path.path_join("outside.txt"))
DirAccess.remove_absolute(external_path)
func _test_package_internal_symlink_load_rejection() -> void:
var save_path := "user://saves/s6_internal_world_link"
var external_package := "user://saves/s6_external_valid_world"
SaveServiceType.delete_save(save_path)
SaveServiceType.delete_save(external_package)
var shared_snapshot := _snapshot(44, &"external_valid")
_expect(SaveServiceType.save_snapshot(save_path, shared_snapshot)[&"ok"], "internal-link fixture target package is valid before substitution")
_expect(SaveServiceType.save_snapshot(external_package, shared_snapshot)[&"ok"], "external world fixture is a separately verified package")
var target_world := ProjectSettings.globalize_path(save_path).path_join(SaveServiceType.WORLD_FILE)
var external_world := ProjectSettings.globalize_path(external_package).path_join(SaveServiceType.WORLD_FILE)
DirAccess.remove_absolute(target_world)
var link_exit := OS.execute("ln", PackedStringArray(["-s", external_world, target_world]))
_expect(link_exit == 0, "known package-file symlink fixture is available")
if link_exit == 0:
var direct := SaveServiceType.load_snapshot(save_path)
_expect(not direct[&"ok"] and direct[&"code"] == &"UNSAFE_PATH", "direct load rejects a world.bin symlink even when its external bytes are valid")
var explicit := SaveServiceType.load_recovery_candidate(save_path, &"formal")
_expect(not explicit[&"ok"] and explicit[&"code"] == &"UNSAFE_PATH", "explicit recovery-candidate load rejects an internal package symlink")
var inspected := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(not inspected[&"ok"] and inspected[&"code"] == &"UNSAFE_PATH", "recovery inspection rejects an internal package symlink before opening it")
_expect(FileAccess.file_exists(external_world), "load rejection leaves the external valid world untouched")
DirAccess.remove_absolute(target_world)
SaveServiceType.delete_save(save_path)
SaveServiceType.delete_save(external_package)
func _test_package_contract_identity_and_timing() -> void:
var save_path := "user://saves/s6_contract"
SaveServiceType.delete_save(save_path)
SaveServiceType.clear_performance_reports()
var first := SaveServiceType.save_snapshot(save_path, _snapshot(60, &"first"))
_expect(first[&"ok"] and int(first[&"duration_us"]) >= 0, "save result reports actual elapsed time")
var package_path := ProjectSettings.globalize_path(save_path)
var manifest: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(package_path.path_join(SaveServiceType.MANIFEST_FILE)))
_expect(str(manifest["save_id"]) == "s6_contract" and str(manifest["display_name"]) == "s6_contract", "default identity comes from the logical slot rather than its temporary directory")
var contract_keys := ["format_version", "game_version", "save_id", "display_name", "created_at_real", "updated_at_real", "absolute_minute", "day", "seed", "checksum_world", "content_pack_ids", "enabled_filter_profile"]
var all_contract_keys := true
for key: String in contract_keys:
all_contract_keys = all_contract_keys and manifest.has(key)
_expect(all_contract_keys, "manifest generates every required package-contract field")
var thumbnail_bytes := FileAccess.get_file_as_bytes(package_path.path_join(SaveServiceType.THUMBNAIL_FILE))
var thumbnail := Image.new()
_expect(not thumbnail_bytes.is_empty() and thumbnail.load_png_from_buffer(thumbnail_bytes) == OK, "save package always contains a valid thumbnail PNG")
var created_at := str(manifest["created_at_real"])
var second := SaveServiceType.save_snapshot(save_path, _snapshot(61, &"second"), {
&"display_name": "Chosen Name",
&"content_pack_ids": ["base"],
&"custom_note": "preserved",
})
_expect(second[&"ok"], "package overwrite accepts supplied metadata")
var overwritten := SaveServiceType.load_snapshot(save_path)
var second_manifest: Dictionary = overwritten[&"manifest"]
_expect(str(second_manifest["save_id"]) == "s6_contract" and str(second_manifest["display_name"]) == "Chosen Name", "overwrite preserves prior ID while honoring an explicit display name")
_expect(str(second_manifest["created_at_real"]) == created_at and second_manifest["content_pack_ids"] == ["base"] and str(second_manifest["custom_note"]) == "preserved", "overwrite preserves creation identity and custom contract metadata")
_expect(FileAccess.get_file_as_bytes(package_path.path_join(SaveServiceType.THUMBNAIL_FILE)) == thumbnail_bytes, "overwrite preserves the previous thumbnail when none is supplied")
var third := SaveServiceType.save_snapshot(save_path, _snapshot(62, &"third"))
_expect(third[&"ok"] and str(third[&"manifest"]["display_name"]) == "Chosen Name", "later overwrite retains prior display identity by default")
_expect(SaveServiceType.get_performance_report(&"save")[&"lifetime_sample_count"] == 3, "every completed save attempt feeds the save performance budget")
_expect(SaveServiceType.get_performance_report(&"load")[&"lifetime_sample_count"] == 1 and int(overwritten[&"duration_us"]) >= 0, "load operations feed the load performance budget and report elapsed time")
SaveServiceType.delete_save(save_path)
func _test_autosave_rotation_and_formal_atomicity() -> void:
var save_path := "user://saves/s6_rotation"
SaveServiceType.delete_save(save_path)
_expect(SaveServiceType.save_snapshot(save_path, _snapshot(10, &"formal_old"))[&"ok"], "initial formal save succeeds")
_expect(SaveServiceType.save_snapshot(save_path, _snapshot(20, &"formal_current"))[&"ok"], "formal save keeps a verified backup")
for minute in [101, 102, 103, 104]:
var result := SaveServiceType.save_autosave(save_path, _snapshot(minute, StringName("auto_%d" % minute)))
_expect(result[&"ok"] and result[&"formal_save_preserved"], "autosave %d rotates without replacing the formal save" % minute)
var expected_minutes := [104, 103, 102]
for generation in range(3):
var loaded := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, generation))
_expect(loaded[&"ok"], "autosave generation %d remains checksum-valid" % generation)
if loaded[&"ok"]:
_expect(int(loaded[&"snapshot"][&"clock"][&"absolute_minute"]) == expected_minutes[generation], "generation %d has deterministic newest-first order" % generation)
_expect(not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(save_path + ".autosave.3")), "rotation retains exactly three generations")
var formal_world_path := ProjectSettings.globalize_path(save_path).path_join(SaveServiceType.WORLD_FILE)
var formal_before := FileAccess.get_file_as_bytes(formal_world_path)
var invalid := _snapshot(999, &"invalid")
invalid[&"schema_version"] = 999
var failed := SaveServiceType.save_autosave(save_path, invalid)
_expect(not failed[&"ok"] and failed[&"code"] == &"INVALID_SNAPSHOT", "invalid autosave candidate fails before rotation")
_expect(failed[&"formal_save_preserved"] and FileAccess.get_file_as_bytes(formal_world_path) == formal_before, "failed autosave leaves the formal package byte-identical")
var formal_loaded := SaveServiceType.load_snapshot(save_path)
_expect(formal_loaded[&"ok"] and _marker(formal_loaded[&"snapshot"]) == &"formal_current", "formal save remains loadable after autosave failure")
var newest := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 0))
_expect(newest[&"ok"] and _marker(newest[&"snapshot"]) == &"auto_104", "failed candidate does not disturb the previous newest autosave")
SaveServiceType.delete_save(save_path)
func _test_interrupted_formal_save_recovery() -> void:
var save_path := "user://saves/s6_formal_transaction"
var candidate_path := "user://saves/s6_formal_transaction_candidate"
SaveServiceType.delete_save(save_path)
SaveServiceType.delete_save(candidate_path)
_expect(SaveServiceType.save_snapshot(save_path, _snapshot(10, &"durable_old"))[&"ok"], "formal crash fixture writes its durable predecessor")
_expect(SaveServiceType.save_snapshot(candidate_path, _snapshot(20, &"durable_new"))[&"ok"], "formal crash fixture writes a verified replacement candidate")
var target_absolute := ProjectSettings.globalize_path(save_path)
var candidate_absolute := ProjectSettings.globalize_path(candidate_path)
_copy_fixture_tree(candidate_absolute, target_absolute + ".tmp")
DirAccess.rename_absolute(target_absolute, target_absolute + ".bak")
_write_transaction_marker(target_absolute)
var promoted := SaveServiceType.load_snapshot(save_path)
_expect(promoted[&"ok"] and promoted[&"interrupted_save_reconciled"], "load reconciles a crash between formal-to-backup and candidate promotion")
_expect(_marker(promoted[&"snapshot"]) == &"durable_new", "reconciliation promotes only the verified temporary package")
var preserved_backup := SaveServiceType.load_snapshot(save_path + ".bak")
_expect(preserved_backup[&"ok"] and _marker(preserved_backup[&"snapshot"]) == &"durable_old", "candidate promotion retains the verified prior formal backup")
_expect(not FileAccess.file_exists(target_absolute + SaveServiceType.SAVE_TRANSACTION_SUFFIX), "successful formal reconciliation clears its durable marker")
SaveServiceType.delete_save(save_path)
_expect(SaveServiceType.save_snapshot(save_path, _snapshot(30, &"rollback_old"))[&"ok"], "corrupt-candidate fixture writes its verified backup source")
_copy_fixture_tree(candidate_absolute, target_absolute + ".tmp")
_corrupt_world(save_path + ".tmp")
DirAccess.rename_absolute(target_absolute, target_absolute + ".bak")
_write_transaction_marker(target_absolute)
var rolled_back := SaveServiceType.load_snapshot(save_path)
_expect(rolled_back[&"ok"] and _marker(rolled_back[&"snapshot"]) == &"rollback_old", "corrupt temporary candidate is never promoted and verified backup is restored")
_expect(not DirAccess.dir_exists_absolute(target_absolute + ".tmp"), "verified backup recovery discards only the marker-owned corrupt temporary package")
_expect(not FileAccess.file_exists(target_absolute + SaveServiceType.SAVE_TRANSACTION_SUFFIX), "verified backup recovery clears the completed transaction marker")
SaveServiceType.delete_save(save_path)
_expect(SaveServiceType.save_snapshot(save_path, _snapshot(40, &"backup_only"))[&"ok"], "backup-only crash fixture writes a verified formal source")
DirAccess.rename_absolute(target_absolute, target_absolute + ".bak")
_write_transaction_marker(target_absolute)
var inspected := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(inspected[&"ok"] and inspected[&"reconciliation"][&"formal_save"][&"action"] == &"restored_verified_backup", "candidate inspection restores a verified backup when no temporary candidate exists")
_expect(_marker(inspected[&"snapshot"]) == &"backup_only" and inspected[&"selected"][&"id"] == &"formal", "backup-only reconciliation recreates the formal package before candidate selection")
SaveServiceType.delete_save(save_path)
SaveServiceType.delete_save(candidate_path)
func _test_interrupted_autosave_rotation_recovery() -> void:
var save_path := "user://saves/s6_rotation_recovery"
SaveServiceType.delete_save(save_path)
SaveServiceType.save_snapshot(save_path, _snapshot(5, &"formal"))
for minute in [10, 20, 30]:
SaveServiceType.save_autosave(save_path, _snapshot(minute, StringName("auto_%d" % minute)))
var target_path := ProjectSettings.globalize_path(save_path)
var pending_path := target_path + ".autosave.pending"
_copy_fixture_tree(target_path + SaveServiceType.AUTOSAVE_SUFFIX + "0", pending_path)
for generation in range(3):
var generation_path := target_path + SaveServiceType.AUTOSAVE_SUFFIX + str(generation)
DirAccess.rename_absolute(generation_path, generation_path + ".rotate_tmp")
var after_rollback := SaveServiceType.save_autosave(save_path, _snapshot(40, &"auto_40"))
_expect(after_rollback[&"ok"] and after_rollback[&"interrupted_rotation_reconciled"], "next autosave rolls back an interrupted pre-promotion rotation before proceeding")
var expected_after_rollback := [40, 30, 20]
for generation in range(3):
var loaded := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, generation))
_expect(int(loaded[&"snapshot"][&"clock"][&"absolute_minute"]) == expected_after_rollback[generation], "rollback recovery preserves generation %d order" % generation)
# Simulate a crash after the new generation zero was promoted but before old
# generations were shifted out of their transaction holds.
for generation in range(3):
var generation_path := target_path + SaveServiceType.AUTOSAVE_SUFFIX + str(generation)
DirAccess.rename_absolute(generation_path, generation_path + ".rotate_tmp")
_copy_fixture_tree(target_path, target_path + SaveServiceType.AUTOSAVE_SUFFIX + "0")
var recovery := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(recovery[&"ok"] and recovery[&"reconciliation"][&"autosave"][&"action"] == &"completed", "recovery inspection completes an interrupted post-promotion rotation")
var completed_zero := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 0))
var completed_one := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 1))
var completed_two := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 2))
_expect(_marker(completed_zero[&"snapshot"]) == &"formal", "post-promotion recovery keeps the already-promoted newest package")
_expect(_marker(completed_one[&"snapshot"]) == &"auto_40" and _marker(completed_two[&"snapshot"]) == &"auto_30", "post-promotion recovery completes the remaining generation shifts exactly once")
_expect(not DirAccess.dir_exists_absolute(target_path + SaveServiceType.AUTOSAVE_SUFFIX + "0.rotate_tmp"), "rotation recovery removes durable hold markers after verification")
SaveServiceType.delete_save(save_path)
func _test_autosave_retention_shrink_prunes_verified_generations() -> void:
var save_path := "user://saves/s6_retention_shrink"
SaveServiceType.delete_save(save_path)
for minute in [10, 20, 30, 40, 50]:
_expect(SaveServiceType.save_autosave(save_path, _snapshot(minute, StringName("wide_%d" % minute)), {}, 4)[&"ok"], "four-generation autosave fixture accepts minute %d" % minute)
var shrink := SaveServiceType.save_autosave(save_path, _snapshot(60, &"narrow_60"), {}, 2)
_expect(shrink[&"ok"] and shrink[&"pruned_generations"] == [2, 3], "successful 4-to-2 retention change reports every pruned verified generation")
var newest := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 0))
var previous := SaveServiceType.load_snapshot(SaveServiceType.get_autosave_path(save_path, 1))
_expect(_marker(newest[&"snapshot"]) == &"narrow_60" and _marker(previous[&"snapshot"]) == &"wide_50", "retention shrink preserves deterministic newest-first generations")
var target_absolute := ProjectSettings.globalize_path(save_path)
_expect(not DirAccess.dir_exists_absolute(target_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "2") and not DirAccess.dir_exists_absolute(target_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "3"), "retention shrink removes all verified generations above the new limit")
SaveServiceType.delete_save(save_path)
var conflict_path := "user://saves/s6_retention_conflict"
SaveServiceType.delete_save(conflict_path)
_expect(SaveServiceType.save_autosave(conflict_path, _snapshot(10, &"conflict_base"), {}, 4)[&"ok"], "retention conflict fixture starts with a valid owned generation")
var conflict_absolute := ProjectSettings.globalize_path(conflict_path)
var foreign_generation := conflict_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "3"
DirAccess.make_dir_recursive_absolute(foreign_generation)
var sentinel := FileAccess.open(foreign_generation.path_join("foreign.txt"), FileAccess.WRITE)
sentinel.store_string("preserve")
sentinel = null
var newest_before := FileAccess.get_file_as_bytes((conflict_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "0").path_join(SaveServiceType.WORLD_FILE))
var foreign_rejected := SaveServiceType.save_autosave(conflict_path, _snapshot(20, &"must_not_commit"), {}, 2)
_expect(not foreign_rejected[&"ok"] and foreign_rejected[&"code"] == &"AUTOSAVE_PRUNE_CONFLICT", "retention shrink refuses a foreign non-package generation")
_expect(FileAccess.get_file_as_string(foreign_generation.path_join("foreign.txt")) == "preserve" and FileAccess.get_file_as_bytes((conflict_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "0").path_join(SaveServiceType.WORLD_FILE)) == newest_before, "foreign prune conflict preserves both foreign data and the prior newest autosave")
DirAccess.remove_absolute(foreign_generation.path_join("foreign.txt"))
DirAccess.remove_absolute(foreign_generation)
var external_path := ProjectSettings.globalize_path("user://saves/s6_retention_external")
DirAccess.make_dir_recursive_absolute(external_path)
var external_sentinel := FileAccess.open(external_path.path_join("outside.txt"), FileAccess.WRITE)
external_sentinel.store_string("outside")
external_sentinel = null
var linked_generation := conflict_absolute + SaveServiceType.AUTOSAVE_SUFFIX + "3"
var link_exit := OS.execute("ln", PackedStringArray(["-s", external_path, linked_generation]))
_expect(link_exit == 0, "retention prune symlink fixture is available")
if link_exit == 0:
var linked_rejected := SaveServiceType.save_autosave(conflict_path, _snapshot(30, &"must_not_follow"), {}, 2)
_expect(not linked_rejected[&"ok"] and linked_rejected[&"code"] == &"UNSAFE_PATH", "retention shrink refuses a symlink generation before prune")
_expect(FileAccess.get_file_as_string(external_path.path_join("outside.txt")) == "outside", "prune rejection never follows or deletes the symlink target")
DirAccess.remove_absolute(linked_generation)
SaveServiceType.delete_save(conflict_path)
DirAccess.remove_absolute(external_path.path_join("outside.txt"))
DirAccess.remove_absolute(external_path)
func _test_recovery_priority_checksum_and_read_only_boundary() -> void:
var save_path := "user://saves/s6_recovery"
SaveServiceType.delete_save(save_path)
SaveServiceType.save_snapshot(save_path, _snapshot(10, &"backup"))
SaveServiceType.save_snapshot(save_path, _snapshot(20, &"formal"))
for minute in [101, 102, 103, 104]:
SaveServiceType.save_autosave(save_path, _snapshot(minute, StringName("auto_%d" % minute)))
var all_valid := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(all_valid[&"ok"] and all_valid[&"selected"][&"id"] == &"formal", "recovery prefers a valid formal save")
var ids: Array[StringName] = []
for candidate: Dictionary in all_valid[&"candidates"]:
ids.append(StringName(candidate[&"id"]))
_expect(ids == [&"formal", &"backup", &"migration_original", &"pre_migration_backup", &"autosave_0", &"autosave_1", &"autosave_2"], "recovery candidate order explicitly includes migration safety packages")
_corrupt_world(save_path)
var formal_corrupt := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(formal_corrupt[&"ok"] and formal_corrupt[&"selected"][&"id"] == &"backup", "checksum failure in formal save selects the verified formal backup")
_expect(formal_corrupt[&"candidates"][0][&"code"] == &"CHECKSUM_MISMATCH", "corrupt formal world is reported as checksum mismatch")
_corrupt_world(save_path + ".bak")
_corrupt_world(SaveServiceType.get_autosave_path(save_path, 0))
var observed_manifest_path := ProjectSettings.globalize_path(SaveServiceType.get_autosave_path(save_path, 1)).path_join(SaveServiceType.MANIFEST_FILE)
var manifest_before := FileAccess.get_file_as_string(observed_manifest_path)
var fallback := SaveServiceType.load_with_recovery(save_path)
var manifest_after := FileAccess.get_file_as_string(observed_manifest_path)
_expect(fallback[&"ok"] and fallback[&"selected"][&"id"] == &"autosave_1", "corrupt latest candidates fall back to the next valid autosave generation")
_expect(_marker(fallback[&"snapshot"]) == &"auto_103", "fallback returns the exact older generation snapshot")
_expect(manifest_before == manifest_after, "recovery inspection is read-only and does not promote or rewrite candidates")
var explicit := SaveServiceType.load_recovery_candidate(save_path, &"autosave", 2)
_expect(explicit[&"ok"] and explicit[&"read_only"] and _marker(explicit[&"snapshot"]) == &"auto_102", "caller can explicitly select a valid recovery generation")
var report: Dictionary = explicit[&"recovery_report"]
_expect(not report[&"automatic_repairs_applied"] and report[&"repair_policy"] == &"REPORT_ONLY_AUTHORITATIVE_SERVICE_REQUIRED", "recovery report never performs implicit domain repairs")
_expect(report[&"findings"].has(&"orphan_tasks") and report[&"findings"].has(&"orphan_event_locks"), "recovery report exposes Task and Event-lock handoff boundaries")
SaveServiceType.delete_save(save_path)
func _test_migration_registry_continuity_and_purity() -> void:
var registry := MigrationRegistryType.new()
_expect(registry.register_step(1, &"add_first", Callable(self, "_migration_add_first"))[&"ok"], "adjacent migration step 1 -> 2 registers")
_expect(registry.register_step(2, &"add_second", Callable(self, "_migration_add_second"))[&"ok"], "adjacent migration step 2 -> 3 registers")
var source := {&"nested": {&"history": []}, &"stable": 7}
var source_guard := source.duplicate(true)
var migrated := registry.migrate(source, 1, 3)
_expect(migrated[&"ok"], "N -> N+2 migration succeeds through continuous adjacent steps")
_expect(migrated[&"applied_migration_ids"] == [&"add_first", &"add_second"], "migration registry applies every step in exact version order")
_expect(migrated[&"data"][&"nested"][&"history"] == [&"one", &"two"], "second step observes the first step output")
_expect(source == source_guard and source[&"nested"][&"history"].is_empty(), "migration callbacks cannot mutate the caller-owned source")
var missing := MigrationRegistryType.new()
missing.register_step(1, &"only_first", Callable(self, "_migration_add_first"))
var missing_result := missing.migrate(source, 1, 3)
_expect(not missing_result[&"ok"] and missing_result[&"code"] == &"MIGRATION_STEP_MISSING", "registry refuses to skip a missing N+1 -> N+2 step")
_expect(missing_result[&"required_migration_id"] == &"format_2_to_3", "missing step returns its stable required migration ID")
_expect(missing_result[&"applied_migration_ids"] == [&"only_first"], "failure report preserves the exact completed-step audit trail")
var failing := MigrationRegistryType.new()
failing.register_step(1, &"known_failure", Callable(self, "_migration_reject"))
var failed := failing.migrate(source, 1, 2)
_expect(not failed[&"ok"] and failed[&"failed_migration_id"] == &"known_failure", "failed migration reports the exact registered migration ID")
_expect(failed[&"code"] == &"FIXTURE_REJECTED" and source == source_guard, "failed migration propagates its reason and preserves source purity")
func _test_current_format_snapshot_validation() -> void:
var save_path := "user://saves/s6_current_invalid"
SaveServiceType.delete_save(save_path)
SaveServiceType.save_snapshot(save_path, _snapshot(180, &"current_invalid"))
_make_legacy_format_zero(save_path, SaveServiceType.FORMAT_VERSION)
var direct_load := SaveServiceType.load_snapshot(save_path)
_expect(not direct_load[&"ok"] and direct_load[&"code"] == &"INVALID_SNAPSHOT", "current-format load still enforces the current snapshot schema")
var migration_attempt := SaveServiceType.migrate_save(save_path, MigrationRegistryType.new(), SaveServiceType.FORMAT_VERSION)
_expect(not migration_attempt[&"ok"] and migration_attempt[&"code"] == &"INVALID_SNAPSHOT", "no-op migration cannot bypass current snapshot validation")
SaveServiceType.delete_save(save_path)
func _test_save_package_migration_and_pre_migration_backup() -> void:
var save_path := "user://saves/s6_migration"
SaveServiceType.delete_save(save_path)
SaveServiceType.save_snapshot(save_path, _snapshot(200, &"legacy"), {&"display_name": "Legacy fixture"})
_make_legacy_format_zero(save_path)
var package_path := ProjectSettings.globalize_path(save_path)
var world_before := FileAccess.get_file_as_bytes(package_path.path_join(SaveServiceType.WORLD_FILE))
var manifest_before := FileAccess.get_file_as_string(package_path.path_join(SaveServiceType.MANIFEST_FILE))
var unsupported := SaveServiceType.load_snapshot(save_path)
_expect(not unsupported[&"ok"] and unsupported[&"code"] == &"UNSUPPORTED_FORMAT", "legacy package is not loaded before explicit migration")
var registry := MigrationRegistryType.new()
registry.register_step(0, &"legacy_zero_to_current", Callable(self, "_migration_package_zero_to_one"))
var result := SaveServiceType.migrate_save(save_path, registry, SaveServiceType.FORMAT_VERSION)
_expect(result[&"ok"] and result[&"backup_created"], "package migration verifies and promotes a current-format candidate")
_expect(result[&"applied_migration_ids"] == [&"legacy_zero_to_current"], "package migration returns its applied migration audit trail")
var current := SaveServiceType.load_snapshot(save_path)
_expect(current[&"ok"] and current[&"snapshot"][&"systems"][&"migrated_from"] == 0, "promoted package contains the pure migration output")
_expect(str(current[&"manifest"]["migration_note"]) == "zero-to-one", "migration preserves step-authored manifest metadata")
var backup_path := ProjectSettings.globalize_path(save_path + SaveServiceType.MIGRATION_BACKUP_SUFFIX)
_expect(DirAccess.dir_exists_absolute(backup_path), "migration creates a dedicated pre-migration backup package")
_expect(FileAccess.get_file_as_bytes(backup_path.path_join(SaveServiceType.WORLD_FILE)) == world_before, "pre-migration backup preserves world bytes exactly")
_expect(FileAccess.get_file_as_string(backup_path.path_join(SaveServiceType.MANIFEST_FILE)) == manifest_before, "pre-migration backup preserves the original manifest exactly")
var recovery_before_crash := SaveServiceType.inspect_recovery_candidates(save_path)
var migration_backup_info: Dictionary = recovery_before_crash[&"candidates"][3]
_expect(migration_backup_info[&"id"] == &"pre_migration_backup" and migration_backup_info[&"package_valid"] and migration_backup_info[&"requires_migration"], "recovery inventory retains a checksum-valid legacy migration backup without treating it as runtime-loadable")
var backup_old_path := backup_path + ".old"
_copy_fixture_tree(backup_path, backup_old_path)
_corrupt_world(save_path + SaveServiceType.MIGRATION_BACKUP_SUFFIX)
var repaired_backup_inventory := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(repaired_backup_inventory[&"candidates"][3][&"package_valid"] and not DirAccess.dir_exists_absolute(backup_old_path), "recovery replaces a torn pre-migration backup from its verified durable predecessor")
# Crash boundary: original was durably held and candidate was complete, but
# candidate had not yet been promoted to the formal name.
var candidate_path := package_path + ".migration.tmp"
var original_path := package_path + ".migration.original"
_copy_fixture_tree(package_path, candidate_path)
DirAccess.rename_absolute(package_path, original_path)
var recovered := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(recovered[&"ok"] and recovered[&"reconciliation"][&"migration"][&"action"] == &"completed_promotion", "recovery completes an interrupted migration promotion from durable markers")
_expect(SaveServiceType.load_snapshot(save_path)[&"ok"] and not DirAccess.dir_exists_absolute(original_path) and not DirAccess.dir_exists_absolute(candidate_path), "migration reconciliation verifies the formal package before clearing crash markers")
DirAccess.rename_absolute(package_path, original_path)
var rolled_back := SaveServiceType.inspect_recovery_candidates(save_path)
_expect(rolled_back[&"ok"] and rolled_back[&"reconciliation"][&"migration"][&"action"] == &"rolled_back_to_original", "recovery rolls back a held original when no promotable candidate exists")
_expect(SaveServiceType.load_snapshot(save_path)[&"ok"] and not DirAccess.dir_exists_absolute(original_path), "rollback restores the original formal name without losing the verified package")
SaveServiceType.delete_save(save_path)
func _test_migration_failure_preserves_source() -> void:
var save_path := "user://saves/s6_migration_failure"
SaveServiceType.delete_save(save_path)
SaveServiceType.save_snapshot(save_path, _snapshot(300, &"must_survive"))
_make_legacy_format_zero(save_path)
var package_path := ProjectSettings.globalize_path(save_path)
var world_before := FileAccess.get_file_as_bytes(package_path.path_join(SaveServiceType.WORLD_FILE))
var manifest_before := FileAccess.get_file_as_string(package_path.path_join(SaveServiceType.MANIFEST_FILE))
var registry := MigrationRegistryType.new()
registry.register_step(0, &"reject_legacy", Callable(self, "_migration_reject"))
var result := SaveServiceType.migrate_save(save_path, registry, SaveServiceType.FORMAT_VERSION)
_expect(not result[&"ok"] and result[&"failed_migration_id"] == &"reject_legacy", "package migration exposes the failing step ID")
_expect(FileAccess.get_file_as_bytes(package_path.path_join(SaveServiceType.WORLD_FILE)) == world_before, "failed step leaves the formal world byte-identical")
_expect(FileAccess.get_file_as_string(package_path.path_join(SaveServiceType.MANIFEST_FILE)) == manifest_before, "failed step leaves the formal manifest byte-identical")
_expect(not DirAccess.dir_exists_absolute(ProjectSettings.globalize_path(save_path + SaveServiceType.MIGRATION_BACKUP_SUFFIX)), "failed in-memory migration creates no misleading backup or partial promotion")
SaveServiceType.delete_save(save_path)
func _migration_add_first(data: Dictionary, from_version: int, to_version: int) -> Dictionary:
data[&"nested"][&"history"].append(&"one")
data[&"first_edge"] = [from_version, to_version]
return data
func _migration_add_second(data: Dictionary, from_version: int, to_version: int) -> Dictionary:
data[&"nested"][&"history"].append(&"two")
data[&"second_edge"] = [from_version, to_version]
return {&"ok": true, &"data": data}
func _migration_reject(data: Dictionary, _from_version: int, _to_version: int) -> Dictionary:
data[&"would_have_mutated"] = true
return {&"ok": false, &"code": &"FIXTURE_REJECTED", &"message": "intentional test rejection"}
func _migration_package_zero_to_one(data: Dictionary, from_version: int, to_version: int) -> Dictionary:
var legacy_snapshot: Dictionary = data[&"snapshot"]
var restored := _snapshot(
int(legacy_snapshot[&"legacy_absolute_minute"]),
StringName(legacy_snapshot[&"legacy_marker"])
)
restored[&"systems"][&"migrated_from"] = from_version
restored[&"systems"][&"migrated_to"] = to_version
data[&"snapshot"] = restored
data[&"manifest"][&"migration_note"] = "zero-to-one"
return data
func _make_legacy_format_zero(save_path: String, package_format_version := 0) -> void:
var package_path := ProjectSettings.globalize_path(save_path)
var current := SaveServiceType.load_snapshot(save_path)
var current_snapshot: Dictionary = current[&"snapshot"]
var legacy_snapshot := {
&"legacy_schema": 0,
&"legacy_absolute_minute": int(current_snapshot[&"clock"][&"absolute_minute"]),
&"legacy_marker": _marker(current_snapshot),
}
var raw_world := var_to_bytes(legacy_snapshot)
var compressed_world := raw_world.compress(FileAccess.COMPRESSION_ZSTD)
var manifest_path := package_path.path_join(SaveServiceType.MANIFEST_FILE)
var manifest_value: Variant = JSON.parse_string(FileAccess.get_file_as_string(manifest_path))
var manifest: Dictionary = manifest_value
manifest["format_version"] = package_format_version
manifest["checksum_world"] = _sha256(compressed_world)
manifest["uncompressed_size"] = raw_world.size()
var manifest_file := FileAccess.open(manifest_path, FileAccess.WRITE)
manifest_file.store_string(JSON.stringify(manifest, "\t"))
manifest_file = null
var world_path := package_path.path_join(SaveServiceType.WORLD_FILE)
var world_file := FileAccess.open(world_path, FileAccess.WRITE)
world_file.store_buffer(SaveServiceType.MAGIC.to_ascii_buffer())
world_file.store_32(package_format_version)
world_file.store_64(raw_world.size())
world_file.store_buffer(compressed_world)
world_file = null
func _copy_fixture_tree(source_path: String, destination_path: String) -> void:
DirAccess.make_dir_recursive_absolute(destination_path)
var source := DirAccess.open(source_path)
source.list_dir_begin()
var name := source.get_next()
while not name.is_empty():
if name != "." and name != "..":
var source_child := source_path.path_join(name)
var destination_child := destination_path.path_join(name)
if source.current_is_dir():
_copy_fixture_tree(source_child, destination_child)
else:
DirAccess.copy_absolute(source_child, destination_child)
name = source.get_next()
source.list_dir_end()
func _write_transaction_marker(target_absolute: String) -> void:
var marker := FileAccess.open(target_absolute + SaveServiceType.SAVE_TRANSACTION_SUFFIX, FileAccess.WRITE)
marker.store_string(SaveServiceType.SAVE_TRANSACTION_MARKER)
marker = null
func _sha256(bytes: PackedByteArray) -> String:
var context := HashingContext.new()
context.start(HashingContext.HASH_SHA256)
context.update(bytes)
return context.finish().hex_encode()
func _corrupt_world(save_path: String) -> void:
var world_path := ProjectSettings.globalize_path(save_path).path_join(SaveServiceType.WORLD_FILE)
var bytes := FileAccess.get_file_as_bytes(world_path)
if bytes.is_empty():
return
bytes[bytes.size() - 1] = bytes[bytes.size() - 1] ^ 0x5a
var file := FileAccess.open(world_path, FileAccess.WRITE)
file.store_buffer(bytes)
func _snapshot(absolute_minute: int, marker: StringName) -> Dictionary:
return {
&"schema_version": 1,
&"clock": {
&"day": 1 + absolute_minute / 1_440,
&"hour": (absolute_minute / 60) % 24,
&"minute": absolute_minute % 60,
&"absolute_minute": absolute_minute,
&"speed": 1,
&"paused": true,
},
&"game_state": {
&"schema_version": 1,
&"revision": absolute_minute,
&"resources": {&"gold": 5_000},
&"flags": {},
},
&"systems": {
&"marker": marker,
},
}
func _marker(snapshot: Dictionary) -> StringName:
return StringName(snapshot[&"systems"][&"marker"])
func _expect(condition: bool, label: String) -> void:
_checks += 1
if condition:
print(" ✓ %s" % label)
return
_failures += 1
push_error(" ✗ %s" % label)
+1
View File
@@ -0,0 +1 @@
uid://davhixen8ndkv