feat: integrate regional caravan persistence

This commit is contained in:
Rijad Zuzo
2026-08-23 17:20:41 +02:00
parent 49c5bda651
commit 52307f1306
24 changed files with 1939 additions and 92 deletions
+198 -7
View File
@@ -17,7 +17,13 @@ func _run() -> void:
for step in 8:
manager.simulate_tick()
_check(
manager.request_regional_jajce_delivery(3.0),
"The quicksave fixture should start one production regional delivery",
)
var saved_checksum: String = manager.get_state_checksum()
var saved_regional_checksum: String = manager.get_regional_state_checksum()
var saved_world_checksum: String = manager.get_world_state_checksum()
_check(store.save(manager), "A valid simulation should save atomically")
_check(store.has_slot(), "The validated quicksave should be discoverable")
@@ -27,11 +33,28 @@ func _run() -> void:
manager.get_state_checksum() != saved_checksum,
"The scenario should change after the save point"
)
var restore_notifications: Array[StringName] = []
manager.village_changed.connect(
func(_village: SimVillage) -> void: restore_notifications.append(&"village")
)
manager.state_restored.connect(func() -> void: restore_notifications.append(&"state"))
_check(store.load_into(manager), "A valid quicksave should load")
_check(
restore_notifications == [&"village", &"state"],
"A committed combined restore should publish its two notifications exactly once",
)
_check(
manager.get_state_checksum() == saved_checksum,
"Loading should restore the exact saved simulation"
)
_check(
manager.get_regional_state_checksum() == saved_regional_checksum,
"Loading should restore the exact mid-route regional authority",
)
_check(
manager.get_world_state_checksum() == saved_world_checksum,
"Loading should restore both authorities from the same manifest",
)
var final_path := store.get_slot_path()
var backup_path := final_path + ".bak"
@@ -43,19 +66,187 @@ func _run() -> void:
store.load_into(manager),
"A preserved previous save should recover an interrupted replacement"
)
_check(
manager.get_world_state_checksum() == saved_world_checksum,
"Backup recovery should restore the complete combined manifest",
)
_check(
DirAccess.rename_absolute(backup_path, final_path) == OK,
"Test should restore the primary save path"
)
var unchanged_checksum: String = manager.get_state_checksum()
var file := FileAccess.open(final_path, FileAccess.WRITE)
file.store_string('{"schema":"broken"}')
file.close()
_check(not store.load_into(manager), "Invalid save data should be rejected")
var legacy_slot := "legacy_v16"
var legacy_path := store.get_slot_path(legacy_slot)
var legacy_local_checksum: String = manager.get_state_checksum()
var legacy_file := FileAccess.open(legacy_path, FileAccess.WRITE)
_check(legacy_file != null, "Test should create a raw legacy local save")
if legacy_file != null:
legacy_file.store_string(manager.serialize_state())
legacy_file.close()
manager.simulate_tick()
_check(
manager.get_state_checksum() == unchanged_checksum,
"A rejected save must not partially mutate the simulation"
store.load_into(manager, legacy_slot),
"Existing raw v16 quicksaves should remain loadable",
)
_check(
manager.get_state_checksum() == legacy_local_checksum,
"A legacy quicksave should restore its exact local authority",
)
var legacy_regional := RegionalSimulationFacade.from_dictionary(manager.get_regional_snapshot())
_check(legacy_regional != null, "Legacy load should bootstrap a valid regional facade")
if legacy_regional != null:
var legacy_service := legacy_regional.get_service()
var legacy_caravan := legacy_service.get_world_state().get_mobile_group(
SimulationIds.REGIONAL_CARAVAN_BLUE
)
_check(
legacy_regional.get_last_processed_tick() == manager.tick_count,
"Legacy regional bootstrap should align with the saved local tick",
)
_check(
legacy_regional.get_world_seed() == manager.simulation_seed,
"Legacy regional bootstrap should align with the saved world seed",
)
_check(
(
legacy_service.get_scheduler().is_empty()
and legacy_service.get_event_store().is_empty()
),
"Legacy load must not invent a historical regional journey",
)
_check(
(
legacy_caravan != null
and legacy_caravan.is_at_location()
and (
legacy_caravan.get_current_location_id()
== SimulationIds.REGIONAL_LOCATION_TRAVNIK
)
),
"Legacy load should place the idle authored caravan at Travnik",
)
store.delete_slot(legacy_slot)
var custom_adapter := ActiveWorldAdapter.new()
_check(
custom_adapter.configure_context_identity(
&"save_test_context", &"save_test_world", &"save_test_location"
),
"The custom-scope fixture should configure an independent active context",
)
root.add_child(custom_adapter)
var custom_manager: Node = load("res://simulation/SimulationManager.gd").new()
custom_manager.debug_logs = false
custom_manager.active_world_adapter = custom_adapter
root.add_child(custom_manager)
custom_manager.set_process(false)
var custom_slot := "custom_scope"
var custom_saved_local_checksum: String = custom_manager.get_state_checksum()
var custom_saved_regional_checksum: String = custom_manager.get_regional_state_checksum()
_check(
not custom_manager.uses_combined_save_manifest(),
"A non-Jajce adapter scope should keep the supported local-v16 save boundary",
)
_check(
custom_manager.can_serialize_local_state_only(),
"A custom scope should allow local-only saving while regional state is reconstructibly idle",
)
_check(
(
not custom_manager.request_regional_jajce_delivery(1.0)
and custom_manager.get_regional_state_checksum() == custom_saved_regional_checksum
),
"A non-Jajce adapter scope must reject Jajce regional mutation without changing authority",
)
_check(
store.save(custom_manager, custom_slot),
"A supported non-Jajce adapter scope should still save through SaveSlotStore",
)
var custom_file := FileAccess.open(store.get_slot_path(custom_slot), FileAccess.READ)
_check(custom_file != null, "The custom-scope slot should be readable")
if custom_file != null:
var custom_json := custom_file.get_as_text()
custom_file.close()
_check(
(
SimulationSaveManifest.from_json(custom_json) == null
and SimulationStateRecord.from_json(custom_json) != null
),
"A non-Jajce adapter scope should persist the explicit raw local record",
)
custom_manager.simulate_tick()
_check(
store.load_into(custom_manager, custom_slot),
"The custom-scope raw local record should remain loadable",
)
_check(
custom_manager.get_state_checksum() == custom_saved_local_checksum,
"Custom-scope loading should restore the exact local authority",
)
_check(
custom_manager.get_regional_state_checksum() == custom_saved_regional_checksum,
"Custom-scope loading should reconstruct the exact idle regional facade",
)
var rejected_restore_notifications: Array[StringName] = []
custom_manager.village_changed.connect(
func(_village: SimVillage) -> void: rejected_restore_notifications.append(&"village")
)
custom_manager.state_restored.connect(
func() -> void: rejected_restore_notifications.append(&"state")
)
var custom_before_rejected_local: String = custom_manager.get_state_checksum()
var custom_before_rejected_regional: String = custom_manager.get_regional_state_checksum()
_check(
not custom_manager.restore_save_manifest(manager.create_save_manifest()),
"A Jajce manifest should be rejected by a different active adapter scope",
)
_check(
rejected_restore_notifications.is_empty(),
"A rejected combined restore and its rollback must publish no success notifications",
)
_check(
(
custom_manager.get_state_checksum() == custom_before_rejected_local
and custom_manager.get_regional_state_checksum() == custom_before_rejected_regional
),
"A rejected combined restore should roll back both custom-scope authorities",
)
_check(
custom_manager._regional_simulation.depart_jajce_delivery(1.0),
"The fail-closed fixture should create otherwise unreachable live regional state",
)
var custom_live_regional_checksum: String = custom_manager.get_regional_state_checksum()
_check(
not custom_manager.can_serialize_local_state_only(),
"Live regional state must not be treated as reconstructible local-only authority",
)
_check(
not store.save(custom_manager, "custom_live"),
"SaveSlotStore must reject a local-only save that would discard live regional state",
)
_check(
custom_manager.get_regional_state_checksum() == custom_live_regional_checksum,
"Rejecting the lossy custom-scope save must not mutate regional authority",
)
store.delete_slot(custom_slot)
var unchanged_world_checksum: String = manager.get_world_state_checksum()
var rejected_manifest: Dictionary = manager.create_save_manifest().to_dictionary()
rejected_manifest["regional_state"]["active_location_id"] = String(
SimulationIds.REGIONAL_LOCATION_TRAVNIK
)
rejected_manifest["regional_checksum"] = SimulationSaveManifest._checksum_dictionary(
rejected_manifest["regional_state"]
)
var file := FileAccess.open(final_path, FileAccess.WRITE)
_check(file != null, "Test should open the primary save for corruption")
if file != null:
file.store_string(JSON.stringify(rejected_manifest))
file.close()
_check(not store.load_into(manager), "Invalid regional manifest data should be rejected")
_check(
manager.get_world_state_checksum() == unchanged_world_checksum,
"A rejected save must not partially mutate either authority",
)
_check(not store.save(manager, "../escape"), "Slot names must not escape the save directory")