feat: integrate regional caravan persistence
This commit is contained in:
@@ -47,8 +47,11 @@ func _test_core_pack() -> void:
|
||||
"Core professions should enumerate by stable ID"
|
||||
)
|
||||
_check(
|
||||
_item_ids(catalog.get_items()) == [&"food", &"herb", &"item_claw", &"item_sword", &"wood"],
|
||||
"Core items should include legacy items and herbs in stable-ID order"
|
||||
(
|
||||
_item_ids(catalog.get_items())
|
||||
== [&"food", &"herb", &"item_claw", &"item_sword", &"regional_trade_goods", &"wood"]
|
||||
),
|
||||
"Core items should include local and regional resources in stable-ID order"
|
||||
)
|
||||
_check(
|
||||
_enemy_ids(catalog.get_enemies()) == [&"enemy_boar", &"enemy_raider", &"enemy_wolf"],
|
||||
@@ -102,14 +105,22 @@ func _test_deterministic_enumeration_and_dependencies() -> void:
|
||||
_check(
|
||||
(
|
||||
_item_ids(catalog.get_items())
|
||||
== [&"amber", &"food", &"herb", &"item_claw", &"item_sword", &"wood"]
|
||||
== [
|
||||
&"amber",
|
||||
&"food",
|
||||
&"herb",
|
||||
&"item_claw",
|
||||
&"item_sword",
|
||||
&"regional_trade_goods",
|
||||
&"wood",
|
||||
]
|
||||
),
|
||||
"Merged item enumeration should be globally stable-ID sorted"
|
||||
)
|
||||
var returned_items := catalog.get_items()
|
||||
returned_items.clear()
|
||||
_check(
|
||||
catalog.get_items().size() == 6,
|
||||
catalog.get_items().size() == 7,
|
||||
"Callers should not be able to mutate the catalog's ordered arrays"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
extends SceneTree
|
||||
|
||||
const DELIVERY_AMOUNT := 3.0
|
||||
|
||||
var failures: Array[String] = []
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var uninterrupted := _create_manager(4242)
|
||||
var local_control := _create_manager(4242)
|
||||
_check(
|
||||
uninterrupted.request_regional_jajce_delivery(DELIVERY_AMOUNT, &"loaded"),
|
||||
"The production facade should dispatch the authored Travnik-to-Jajce delivery",
|
||||
)
|
||||
for step in 6:
|
||||
uninterrupted.simulate_tick()
|
||||
local_control.simulate_tick()
|
||||
var saved_manifest_json: String = uninterrupted.serialize_save_manifest()
|
||||
var saved_manifest := SimulationSaveManifest.from_json(saved_manifest_json)
|
||||
_check(saved_manifest != null, "A mid-route combined manifest should validate")
|
||||
|
||||
var restored := _create_manager(9999)
|
||||
_check(
|
||||
restored.restore_save_manifest(saved_manifest),
|
||||
"The local and regional authorities should restore from one manifest",
|
||||
)
|
||||
_check(
|
||||
restored.get_world_state_checksum() == uninterrupted.get_world_state_checksum(),
|
||||
"A mid-route restore should reproduce the complete authoritative checksum",
|
||||
)
|
||||
for step in RegionalSimulationFacade.ROUTE_TRAVEL_TICKS - 6:
|
||||
uninterrupted.simulate_tick()
|
||||
restored.simulate_tick()
|
||||
local_control.simulate_tick()
|
||||
|
||||
_check(
|
||||
restored.get_world_state_checksum() == uninterrupted.get_world_state_checksum(),
|
||||
"Restored regional travel should continue identically without a loaded caravan visual",
|
||||
)
|
||||
_assert_delivery_arrived(restored)
|
||||
_check(
|
||||
restored.get_state_checksum() == local_control.get_state_checksum(),
|
||||
"A regional delivery must not change the local simulation authority",
|
||||
)
|
||||
_check(
|
||||
_local_economy_snapshot(restored) == _local_economy_snapshot(local_control),
|
||||
"Regional trade goods must not mutate the local pantry, woodpile, or apothecary",
|
||||
)
|
||||
_assert_regional_events_stay_out_of_local_history(restored)
|
||||
|
||||
uninterrupted.free()
|
||||
restored.free()
|
||||
local_control.free()
|
||||
if failures.is_empty():
|
||||
print("[TEST] Production regional facade integration passed")
|
||||
quit(0)
|
||||
return
|
||||
for failure in failures:
|
||||
push_error("[TEST] " + failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _create_manager(seed_value: int) -> Node:
|
||||
var manager: Node = load("res://simulation/SimulationManager.gd").new()
|
||||
manager.simulation_seed = seed_value
|
||||
manager.debug_logs = false
|
||||
root.add_child(manager)
|
||||
manager.set_process(false)
|
||||
return manager
|
||||
|
||||
|
||||
func _local_economy_snapshot(manager: Node) -> Dictionary:
|
||||
var apothecary: StorageStateRecord = manager.economy.get_storage(&"village_apothecary")
|
||||
return {
|
||||
"pantry": manager.get_pantry().to_dictionary(),
|
||||
"woodpile": manager.get_woodpile().to_dictionary(),
|
||||
"apothecary": apothecary.to_dictionary() if apothecary != null else {},
|
||||
}
|
||||
|
||||
|
||||
func _assert_delivery_arrived(manager: Node) -> void:
|
||||
var facade := RegionalSimulationFacade.from_dictionary(manager.get_regional_snapshot())
|
||||
_check(facade != null, "The production regional snapshot should remain valid")
|
||||
if facade == null:
|
||||
return
|
||||
var service := facade.get_service()
|
||||
var state := service.get_world_state()
|
||||
var caravan := state.get_mobile_group(SimulationIds.REGIONAL_CARAVAN_BLUE)
|
||||
_check(caravan != null and caravan.is_at_location(), "The caravan should finish at a location")
|
||||
if caravan != null:
|
||||
_check(
|
||||
caravan.get_current_location_id() == SimulationIds.REGIONAL_LOCATION_JAJCE,
|
||||
"The inbound caravan should finish in Jajce",
|
||||
)
|
||||
_check(
|
||||
caravan.get_cargo_amount(SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS) == 0.0,
|
||||
"Arrival should empty the caravan's exact cargo ledger",
|
||||
)
|
||||
var jajce := state.get_settlement(SimulationIds.REGIONAL_SETTLEMENT_JAJCE)
|
||||
_check(
|
||||
(
|
||||
jajce != null
|
||||
and (
|
||||
jajce.get_stockpile_amount(SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS)
|
||||
== DELIVERY_AMOUNT
|
||||
)
|
||||
),
|
||||
"Jajce's regional ledger should receive the exact delivered amount",
|
||||
)
|
||||
_check(
|
||||
service.get_event_store().size() == 4, "The regional causal chain should have four facts"
|
||||
)
|
||||
|
||||
|
||||
func _assert_regional_events_stay_out_of_local_history(manager: Node) -> void:
|
||||
var regional_types := [
|
||||
RegionalCaravanService.EVENT_CARGO_LOADED,
|
||||
RegionalCaravanService.EVENT_DEPARTED,
|
||||
RegionalCaravanService.EVENT_ARRIVED,
|
||||
RegionalCaravanService.EVENT_CARGO_DEPOSITED,
|
||||
]
|
||||
for event: EconomicEventRecord in manager.economic_events:
|
||||
_check(
|
||||
StringName(event.data.get("event_type", "")) not in regional_types,
|
||||
"Regional caravan facts must not enter the local numeric event-ID domain",
|
||||
)
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bl8hqwx8a8458
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
extends GutTest
|
||||
|
||||
const CARGO_AMOUNT := 3.0
|
||||
|
||||
|
||||
func test_jajce_route_bootstrap_has_stable_required_ids_and_independent_stock() -> void:
|
||||
var facade := RegionalSimulationFacade.create_jajce_route(4242, 7)
|
||||
|
||||
assert_not_null(facade)
|
||||
assert_eq(facade.get_world_seed(), 4242)
|
||||
assert_eq(facade.get_last_processed_tick(), 7)
|
||||
assert_eq(facade.get_active_location_id(), SimulationIds.REGIONAL_LOCATION_JAJCE)
|
||||
assert_eq(
|
||||
facade.to_dictionary().keys(),
|
||||
[
|
||||
"schema_version",
|
||||
"world_seed",
|
||||
"last_processed_tick",
|
||||
"active_location_id",
|
||||
"caravan_service",
|
||||
]
|
||||
)
|
||||
var service := facade.get_service()
|
||||
var state := service.get_world_state()
|
||||
assert_eq(state.get_world_id(), SimulationIds.REGIONAL_WORLD_BOSNIA)
|
||||
assert_not_null(state.get_location(RegionalSimulationFacade.LOCATION_JAJCE))
|
||||
assert_not_null(state.get_location(RegionalSimulationFacade.LOCATION_TRAVNIK))
|
||||
assert_not_null(state.get_settlement(RegionalSimulationFacade.SETTLEMENT_JAJCE))
|
||||
assert_not_null(state.get_settlement(RegionalSimulationFacade.SETTLEMENT_TRAVNIK))
|
||||
assert_not_null(state.get_route(RegionalSimulationFacade.ROUTE_JAJCE_TRAVNIK))
|
||||
assert_not_null(state.get_person(RegionalSimulationFacade.PERSON_ANA))
|
||||
assert_not_null(state.get_population_cohort(RegionalSimulationFacade.COHORT_CARRIERS))
|
||||
assert_not_null(state.get_polity(RegionalSimulationFacade.POLITY_JAJCE))
|
||||
assert_not_null(state.get_polity(RegionalSimulationFacade.POLITY_TRAVNIK))
|
||||
assert_not_null(state.get_diplomatic_relation(RegionalSimulationFacade.RELATION_JAJCE_TRAVNIK))
|
||||
var route := state.get_route(RegionalSimulationFacade.ROUTE_JAJCE_TRAVNIK)
|
||||
assert_true(
|
||||
route.connects(
|
||||
RegionalSimulationFacade.LOCATION_JAJCE, RegionalSimulationFacade.LOCATION_TRAVNIK
|
||||
)
|
||||
)
|
||||
assert_eq(route.get_travel_ticks(), RegionalSimulationFacade.ROUTE_TRAVEL_TICKS)
|
||||
var caravan := state.get_mobile_group(RegionalSimulationFacade.CARAVAN_BLUE)
|
||||
assert_true(caravan.is_at_location())
|
||||
assert_eq(caravan.get_current_location_id(), RegionalSimulationFacade.LOCATION_TRAVNIK)
|
||||
assert_eq(caravan.get_headcount(), 4)
|
||||
assert_eq(caravan.get_cargo_ledger(), {})
|
||||
assert_eq(
|
||||
state.get_settlement(RegionalSimulationFacade.SETTLEMENT_JAJCE).get_stockpile_ledger(), {}
|
||||
)
|
||||
assert_eq(
|
||||
state.get_settlement(RegionalSimulationFacade.SETTLEMENT_TRAVNIK).get_stockpile_amount(
|
||||
SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS
|
||||
),
|
||||
RegionalSimulationFacade.TRAVNIK_INITIAL_TRADE_GOODS
|
||||
)
|
||||
assert_eq(
|
||||
service.get_scheduler().get_cursor()["last_drain_tick"], facade.get_last_processed_tick()
|
||||
)
|
||||
_assert_regional_stock_is_independent(state)
|
||||
|
||||
|
||||
func test_departure_and_arrival_use_facade_tick_and_conserve_regional_authority() -> void:
|
||||
var facade := RegionalSimulationFacade.create_jajce_route(4242, 10)
|
||||
var state := facade.get_service().get_world_state()
|
||||
var goods_before := _regional_trade_goods(state)
|
||||
var headcount_before := _regional_headcount(state)
|
||||
|
||||
assert_true(_depart(facade, CARGO_AMOUNT, &"loaded"))
|
||||
var caravan := state.get_mobile_group(RegionalSimulationFacade.CARAVAN_BLUE)
|
||||
assert_true(caravan.is_on_route())
|
||||
assert_eq(caravan.get_departure_tick(), 10)
|
||||
assert_eq(caravan.get_arrival_tick(), 10 + RegionalSimulationFacade.ROUTE_TRAVEL_TICKS)
|
||||
assert_eq(caravan.get_cargo_amount(SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS), CARGO_AMOUNT)
|
||||
assert_eq(_regional_trade_goods(state), goods_before)
|
||||
assert_eq(_regional_headcount(state), headcount_before)
|
||||
assert_true(facade.advance_to_tick(caravan.get_arrival_tick() - 1, &"unloaded"))
|
||||
assert_true(caravan.is_on_route())
|
||||
assert_true(facade.advance_to_tick(caravan.get_arrival_tick(), &"always"))
|
||||
|
||||
state = facade.get_service().get_world_state()
|
||||
caravan = state.get_mobile_group(RegionalSimulationFacade.CARAVAN_BLUE)
|
||||
assert_true(caravan.is_at_location())
|
||||
assert_eq(caravan.get_current_location_id(), RegionalSimulationFacade.LOCATION_JAJCE)
|
||||
assert_eq(caravan.get_cargo_amount(SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS), 0.0)
|
||||
assert_eq(
|
||||
state.get_settlement(RegionalSimulationFacade.SETTLEMENT_JAJCE).get_stockpile_amount(
|
||||
SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS
|
||||
),
|
||||
RegionalSimulationFacade.JAJCE_INITIAL_TRADE_GOODS + CARGO_AMOUNT
|
||||
)
|
||||
assert_eq(_regional_trade_goods(state), goods_before)
|
||||
assert_eq(_regional_headcount(state), headcount_before)
|
||||
assert_eq(facade.get_service().get_event_store().size(), 4)
|
||||
_assert_regional_stock_is_independent(state)
|
||||
|
||||
|
||||
func test_ticks_are_monotonic_and_equal_tick_is_idempotent() -> void:
|
||||
var facade := RegionalSimulationFacade.create_jajce_route()
|
||||
|
||||
assert_true(facade.advance_to_tick(4))
|
||||
var checksum_at_four := facade.checksum()
|
||||
assert_true(facade.advance_to_tick(4))
|
||||
assert_eq(facade.checksum(), checksum_at_four)
|
||||
assert_false(facade.advance_to_tick(3))
|
||||
assert_true("cannot move backward" in facade.get_last_error())
|
||||
assert_eq(facade.get_last_processed_tick(), 4)
|
||||
assert_eq(facade.checksum(), checksum_at_four)
|
||||
|
||||
|
||||
func test_advance_rolls_back_all_ticks_when_a_scheduled_handler_fails() -> void:
|
||||
var facade := RegionalSimulationFacade.create_jajce_route()
|
||||
assert_not_null(
|
||||
facade.get_service().get_scheduler().schedule_new(
|
||||
&"unsupported_job", &"unsupported_job", 2, 0, RegionalSimulationFacade.CARAVAN_BLUE
|
||||
)
|
||||
)
|
||||
var before := facade.to_dictionary()
|
||||
|
||||
assert_false(facade.advance_to_tick(2))
|
||||
assert_true("no registered handler" in facade.get_last_error())
|
||||
assert_eq(facade.to_dictionary(), before)
|
||||
assert_eq(facade.get_last_processed_tick(), 0)
|
||||
|
||||
|
||||
func test_save_restore_mid_route_continues_exactly() -> void:
|
||||
var uninterrupted := RegionalSimulationFacade.create_jajce_route(4242, 3)
|
||||
assert_true(_depart(uninterrupted, 4.0))
|
||||
assert_true(uninterrupted.advance_to_tick(8))
|
||||
var saved := uninterrupted.to_dictionary()
|
||||
var restored := RegionalSimulationFacade.from_dictionary(saved)
|
||||
|
||||
assert_not_null(restored)
|
||||
assert_eq(restored.to_dictionary(), saved)
|
||||
assert_eq(restored.checksum(), uninterrupted.checksum())
|
||||
assert_eq(restored.get_last_processed_tick(), 8)
|
||||
var arrival_tick := 3 + RegionalSimulationFacade.ROUTE_TRAVEL_TICKS
|
||||
assert_true(uninterrupted.advance_to_tick(arrival_tick, &"always"))
|
||||
assert_true(restored.advance_to_tick(arrival_tick, &"never"))
|
||||
assert_eq(restored.to_dictionary(), uninterrupted.to_dictionary())
|
||||
assert_eq(restored.checksum(), uninterrupted.checksum())
|
||||
|
||||
|
||||
func test_presentation_modes_are_inert_observers() -> void:
|
||||
var always := _complete_route(&"always", &"always")
|
||||
var never := _complete_route(&"never", &"never")
|
||||
var loaded_unloaded := _complete_route(&"loaded", &"unloaded")
|
||||
|
||||
assert_eq(always, never)
|
||||
assert_eq(always, loaded_unloaded)
|
||||
|
||||
|
||||
func test_departure_rejects_local_or_mixed_resource_ledgers_without_mutation() -> void:
|
||||
var facade := RegionalSimulationFacade.create_jajce_route()
|
||||
var before := facade.to_dictionary()
|
||||
|
||||
assert_false(
|
||||
facade.depart_caravan(
|
||||
RegionalSimulationFacade.CARAVAN_BLUE,
|
||||
RegionalSimulationFacade.SETTLEMENT_JAJCE,
|
||||
RegionalSimulationFacade.SETTLEMENT_TRAVNIK,
|
||||
RegionalSimulationFacade.ROUTE_JAJCE_TRAVNIK,
|
||||
{SimulationIds.RESOURCE_FOOD: 1.0}
|
||||
)
|
||||
)
|
||||
assert_true("only the trade-goods" in facade.get_last_error())
|
||||
assert_eq(facade.to_dictionary(), before)
|
||||
assert_false(
|
||||
(
|
||||
facade
|
||||
. depart_caravan(
|
||||
RegionalSimulationFacade.CARAVAN_BLUE,
|
||||
RegionalSimulationFacade.SETTLEMENT_JAJCE,
|
||||
RegionalSimulationFacade.SETTLEMENT_TRAVNIK,
|
||||
RegionalSimulationFacade.ROUTE_JAJCE_TRAVNIK,
|
||||
{
|
||||
SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS: 1.0,
|
||||
SimulationIds.RESOURCE_WOOD: 1.0,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
assert_eq(facade.to_dictionary(), before)
|
||||
|
||||
|
||||
func test_restore_rejects_malformed_or_wrong_scope_envelopes() -> void:
|
||||
var facade := RegionalSimulationFacade.create_jajce_route(4242, 5)
|
||||
var saved := facade.to_dictionary()
|
||||
|
||||
assert_null(RegionalSimulationFacade.from_dictionary({}))
|
||||
var missing_field := saved.duplicate(true)
|
||||
missing_field.erase("caravan_service")
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(missing_field))
|
||||
var extra_field := saved.duplicate(true)
|
||||
extra_field["unexpected"] = true
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(extra_field))
|
||||
var wrong_schema := saved.duplicate(true)
|
||||
wrong_schema["schema_version"] = RegionalSimulationFacade.SCHEMA_VERSION + 1
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(wrong_schema))
|
||||
var coerced_schema := saved.duplicate(true)
|
||||
coerced_schema["schema_version"] = str(RegionalSimulationFacade.SCHEMA_VERSION)
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(coerced_schema))
|
||||
var coerced_seed := saved.duplicate(true)
|
||||
coerced_seed["world_seed"] = "4242"
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(coerced_seed))
|
||||
var negative_tick := saved.duplicate(true)
|
||||
negative_tick["last_processed_tick"] = -1
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(negative_tick))
|
||||
var coerced_tick := saved.duplicate(true)
|
||||
coerced_tick["last_processed_tick"] = "5"
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(coerced_tick))
|
||||
var cursor_mismatch := saved.duplicate(true)
|
||||
cursor_mismatch["last_processed_tick"] = 6
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(cursor_mismatch))
|
||||
var wrong_active_location := saved.duplicate(true)
|
||||
wrong_active_location["active_location_id"] = String(RegionalSimulationFacade.LOCATION_TRAVNIK)
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(wrong_active_location))
|
||||
var malformed_service := saved.duplicate(true)
|
||||
malformed_service["caravan_service"] = {}
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(malformed_service))
|
||||
var wrong_world := saved.duplicate(true)
|
||||
wrong_world["caravan_service"]["world_state"]["world_id"] = "another_world"
|
||||
for location: Dictionary in wrong_world["caravan_service"]["world_state"]["locations"]:
|
||||
location["world_id"] = "another_world"
|
||||
location["address"]["world_id"] = "another_world"
|
||||
for group: Dictionary in wrong_world["caravan_service"]["world_state"]["mobile_groups"]:
|
||||
if group["location_address"] != null:
|
||||
group["location_address"]["world_id"] = "another_world"
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(wrong_world))
|
||||
var unsupported_stock := saved.duplicate(true)
|
||||
for settlement: Dictionary in unsupported_stock["caravan_service"]["world_state"]["settlements"]:
|
||||
if settlement["settlement_id"] == String(RegionalSimulationFacade.SETTLEMENT_TRAVNIK):
|
||||
settlement["stockpile_ledger"][String(SimulationIds.RESOURCE_FOOD)] = 1.0
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(unsupported_stock))
|
||||
var forged_total := saved.duplicate(true)
|
||||
for settlement: Dictionary in forged_total["caravan_service"]["world_state"]["settlements"]:
|
||||
if settlement["settlement_id"] == String(RegionalSimulationFacade.SETTLEMENT_TRAVNIK):
|
||||
settlement["stockpile_ledger"][String(RegionalSimulationFacade.TRADE_GOODS)] = 5.0
|
||||
assert_null(RegionalSimulationFacade.from_dictionary(forged_total))
|
||||
|
||||
|
||||
func _depart(
|
||||
facade: RegionalSimulationFacade, amount: float, presentation_mode: StringName = &"never"
|
||||
) -> bool:
|
||||
return facade.depart_jajce_delivery(amount, presentation_mode)
|
||||
|
||||
|
||||
func _complete_route(
|
||||
departure_presentation: StringName, arrival_presentation: StringName
|
||||
) -> Dictionary:
|
||||
var facade := RegionalSimulationFacade.create_jajce_route()
|
||||
assert_true(_depart(facade, CARGO_AMOUNT, departure_presentation))
|
||||
assert_true(
|
||||
facade.advance_to_tick(RegionalSimulationFacade.ROUTE_TRAVEL_TICKS, arrival_presentation)
|
||||
)
|
||||
return facade.to_dictionary()
|
||||
|
||||
|
||||
func _regional_trade_goods(state: RegionalWorldState) -> float:
|
||||
var total := 0.0
|
||||
for settlement: SettlementStateRecord in state.get_all_settlements():
|
||||
total += settlement.get_stockpile_amount(SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS)
|
||||
for group: MobileGroupStateRecord in state.get_all_mobile_groups():
|
||||
total += group.get_cargo_amount(SimulationIds.RESOURCE_REGIONAL_TRADE_GOODS)
|
||||
return total
|
||||
|
||||
|
||||
func _regional_headcount(state: RegionalWorldState) -> int:
|
||||
var total := 0
|
||||
for group: MobileGroupStateRecord in state.get_all_mobile_groups():
|
||||
total += group.get_headcount()
|
||||
for cohort: PopulationCohortRecord in state.get_all_population_cohorts():
|
||||
if cohort.is_at_location():
|
||||
total += cohort.get_count()
|
||||
for person: PersonStateRecord in state.get_all_persons():
|
||||
if person.is_at_location():
|
||||
total += 1
|
||||
return total
|
||||
|
||||
|
||||
func _assert_regional_stock_is_independent(state: RegionalWorldState) -> void:
|
||||
for settlement: SettlementStateRecord in state.get_all_settlements():
|
||||
assert_false(settlement.get_stockpile_ledger().has(String(SimulationIds.RESOURCE_FOOD)))
|
||||
assert_false(settlement.get_stockpile_ledger().has(String(SimulationIds.RESOURCE_WOOD)))
|
||||
for group: MobileGroupStateRecord in state.get_all_mobile_groups():
|
||||
assert_false(group.get_cargo_ledger().has(String(SimulationIds.RESOURCE_FOOD)))
|
||||
assert_false(group.get_cargo_ledger().has(String(SimulationIds.RESOURCE_WOOD)))
|
||||
@@ -0,0 +1 @@
|
||||
uid://0flo0tck1bwn
|
||||
@@ -0,0 +1,280 @@
|
||||
extends GutTest
|
||||
|
||||
const MANAGER_SCRIPT := preload("res://simulation/SimulationManager.gd")
|
||||
|
||||
var manager: Node
|
||||
|
||||
|
||||
func before_each() -> void:
|
||||
manager = MANAGER_SCRIPT.new()
|
||||
add_child_autofree(manager)
|
||||
|
||||
|
||||
func test_canonical_manifest_round_trips_defensively() -> void:
|
||||
var local_state: SimulationStateRecord = manager.create_state_record()
|
||||
var regional_facade := RegionalSimulationFacade.create_jajce_route()
|
||||
var manifest := SimulationSaveManifest.create(local_state, regional_facade)
|
||||
assert_not_null(manifest)
|
||||
|
||||
var saved := manifest.to_dictionary()
|
||||
var expected_fields := [
|
||||
"local_checksum",
|
||||
"local_state",
|
||||
"regional_checksum",
|
||||
"regional_state",
|
||||
"schema",
|
||||
"schema_version",
|
||||
]
|
||||
var saved_fields := saved.keys()
|
||||
saved_fields.sort()
|
||||
assert_eq(saved_fields, expected_fields)
|
||||
assert_eq(saved["schema"], SimulationSaveManifest.SCHEMA_NAME)
|
||||
assert_eq(saved["schema_version"], SimulationSaveManifest.SCHEMA_VERSION)
|
||||
assert_eq(
|
||||
saved["local_checksum"],
|
||||
SimulationSaveManifest._checksum_dictionary(saved["local_state"]),
|
||||
)
|
||||
assert_eq(
|
||||
saved["regional_checksum"],
|
||||
SimulationSaveManifest._checksum_dictionary(saved["regional_state"]),
|
||||
)
|
||||
|
||||
var restored := SimulationSaveManifest.from_dictionary(saved)
|
||||
var restored_json := SimulationSaveManifest.from_json(manifest.to_json())
|
||||
assert_not_null(restored)
|
||||
assert_not_null(restored_json)
|
||||
assert_eq(restored.to_dictionary(), saved)
|
||||
assert_eq(restored.checksum(), manifest.checksum())
|
||||
assert_eq(restored_json.checksum(), manifest.checksum())
|
||||
assert_not_same(manifest.get_local_state(), local_state)
|
||||
assert_not_same(manifest.get_regional_facade(), regional_facade)
|
||||
|
||||
local_state.simulation["tick_count"] = 99
|
||||
var copied_local := manifest.get_local_state()
|
||||
copied_local.simulation["tick_count"] = 77
|
||||
assert_eq(int(manifest.get_local_state().simulation["tick_count"]), 0)
|
||||
var copied_regional := manifest.get_regional_facade().to_dictionary()
|
||||
copied_regional["last_processed_tick"] = 88
|
||||
assert_eq(manifest.get_regional_facade().get_last_processed_tick(), 0)
|
||||
|
||||
|
||||
func test_parser_rejects_missing_extra_and_wrong_manifest_fields() -> void:
|
||||
var valid := _valid_manifest_dictionary()
|
||||
for field: String in SimulationSaveManifest.MANIFEST_FIELDS:
|
||||
var missing := valid.duplicate(true)
|
||||
missing.erase(field)
|
||||
assert_null(SimulationSaveManifest.from_dictionary(missing), "Missing %s" % field)
|
||||
|
||||
var extra := valid.duplicate(true)
|
||||
extra["unexpected"] = true
|
||||
assert_null(SimulationSaveManifest.from_dictionary(extra))
|
||||
|
||||
var wrong_schema := valid.duplicate(true)
|
||||
wrong_schema["schema"] = &"the_steward.save_manifest"
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_schema))
|
||||
var wrong_version := valid.duplicate(true)
|
||||
wrong_version["schema_version"] = "1"
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_version))
|
||||
var wrong_float_version := valid.duplicate(true)
|
||||
wrong_float_version["schema_version"] = 1.0
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_float_version))
|
||||
var wrong_local := valid.duplicate(true)
|
||||
wrong_local["local_state"] = []
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_local))
|
||||
var wrong_regional := valid.duplicate(true)
|
||||
wrong_regional["regional_state"] = "regional"
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_regional))
|
||||
var wrong_local_checksum := valid.duplicate(true)
|
||||
wrong_local_checksum["local_checksum"] = 1
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_local_checksum))
|
||||
var wrong_regional_checksum := valid.duplicate(true)
|
||||
wrong_regional_checksum["regional_checksum"] = []
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_regional_checksum))
|
||||
|
||||
assert_null(SimulationSaveManifest.from_json("[]"))
|
||||
assert_null(SimulationSaveManifest.from_json("not json"))
|
||||
|
||||
|
||||
func test_nested_tampering_and_checksum_mismatches_fail_closed() -> void:
|
||||
var valid := _valid_manifest_dictionary()
|
||||
var local_checksum_mismatch := valid.duplicate(true)
|
||||
local_checksum_mismatch["local_checksum"] = "0".repeat(64)
|
||||
assert_null(SimulationSaveManifest.from_dictionary(local_checksum_mismatch))
|
||||
|
||||
var regional_checksum_mismatch := valid.duplicate(true)
|
||||
regional_checksum_mismatch["regional_checksum"] = "f".repeat(64)
|
||||
assert_null(SimulationSaveManifest.from_dictionary(regional_checksum_mismatch))
|
||||
|
||||
var tampered_local := valid.duplicate(true)
|
||||
tampered_local["local_state"]["simulation"]["seed"] = 4004
|
||||
assert_null(SimulationSaveManifest.from_dictionary(tampered_local))
|
||||
|
||||
var tampered_regional := valid.duplicate(true)
|
||||
tampered_regional["regional_state"]["last_processed_tick"] = 1
|
||||
assert_null(SimulationSaveManifest.from_dictionary(tampered_regional))
|
||||
|
||||
var nested_extra := valid.duplicate(true)
|
||||
nested_extra["local_state"]["unexpected"] = true
|
||||
nested_extra["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
||||
nested_extra["local_state"]
|
||||
)
|
||||
assert_null(SimulationSaveManifest.from_dictionary(nested_extra))
|
||||
assert_null(SimulationSaveManifest.from_json(JSON.stringify(nested_extra)))
|
||||
|
||||
|
||||
func test_local_seed_and_tick_require_exact_integer_types() -> void:
|
||||
var valid := _valid_manifest_dictionary()
|
||||
var invalid_boundaries: Array[Dictionary] = [
|
||||
{"field": "seed", "value": "1337"},
|
||||
{"field": "seed", "value": 1337.5},
|
||||
{"field": "tick_count", "value": "0"},
|
||||
{"field": "tick_count", "value": 0.5},
|
||||
]
|
||||
|
||||
for invalid_boundary: Dictionary in invalid_boundaries:
|
||||
var field := String(invalid_boundary["field"])
|
||||
var value: Variant = invalid_boundary["value"]
|
||||
var malformed := valid.duplicate(true)
|
||||
malformed["local_state"]["simulation"][field] = value
|
||||
malformed["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
||||
malformed["local_state"]
|
||||
)
|
||||
assert_null(
|
||||
SimulationSaveManifest.from_dictionary(malformed),
|
||||
"Dictionary parser should reject %s=%s" % [field, value],
|
||||
)
|
||||
assert_null(
|
||||
SimulationSaveManifest.from_json(JSON.stringify(malformed)),
|
||||
"JSON parser should reject %s=%s" % [field, value],
|
||||
)
|
||||
|
||||
var local_state: SimulationStateRecord = manager.create_state_record()
|
||||
local_state.simulation[field] = value
|
||||
assert_null(
|
||||
SimulationSaveManifest.create(
|
||||
local_state, RegionalSimulationFacade.create_jajce_route()
|
||||
),
|
||||
"Manifest creation should reject %s=%s" % [field, value],
|
||||
)
|
||||
|
||||
|
||||
func test_json_parser_rejects_nonnumeric_regional_ledger_values() -> void:
|
||||
var string_ledger := _valid_manifest_dictionary()
|
||||
_set_regional_stockpile_value(
|
||||
string_ledger["regional_state"],
|
||||
RegionalSimulationFacade.SETTLEMENT_TRAVNIK,
|
||||
"4",
|
||||
)
|
||||
assert_null(SimulationSaveManifest.from_json(JSON.stringify(string_ledger)))
|
||||
|
||||
var boolean_ledger := _valid_manifest_dictionary()
|
||||
_set_regional_stockpile_value(
|
||||
boolean_ledger["regional_state"],
|
||||
RegionalSimulationFacade.SETTLEMENT_JAJCE,
|
||||
false,
|
||||
)
|
||||
var canonical_regional_state: Dictionary = boolean_ledger["regional_state"].duplicate(true)
|
||||
_set_regional_stockpile_value(
|
||||
canonical_regional_state,
|
||||
RegionalSimulationFacade.SETTLEMENT_JAJCE,
|
||||
0.0,
|
||||
)
|
||||
boolean_ledger["regional_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
||||
canonical_regional_state
|
||||
)
|
||||
assert_null(SimulationSaveManifest.from_json(JSON.stringify(boolean_ledger)))
|
||||
|
||||
|
||||
func test_world_location_and_tick_must_match_the_regional_boundary() -> void:
|
||||
var valid := _valid_manifest_dictionary()
|
||||
|
||||
var wrong_world := valid.duplicate(true)
|
||||
wrong_world["local_state"]["simulation"]["world_id"] = "regional_elsewhere"
|
||||
wrong_world["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
||||
wrong_world["local_state"]
|
||||
)
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_world))
|
||||
|
||||
var wrong_location := valid.duplicate(true)
|
||||
wrong_location["local_state"]["simulation"]["location_id"] = String(
|
||||
RegionalSimulationFacade.LOCATION_TRAVNIK
|
||||
)
|
||||
wrong_location["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
||||
wrong_location["local_state"]
|
||||
)
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_location))
|
||||
|
||||
var wrong_seed := valid.duplicate(true)
|
||||
wrong_seed["local_state"]["simulation"]["seed"] = (
|
||||
RegionalSimulationFacade.DEFAULT_WORLD_SEED + 1
|
||||
)
|
||||
wrong_seed["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
||||
wrong_seed["local_state"]
|
||||
)
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_seed))
|
||||
|
||||
var wrong_regional_seed := valid.duplicate(true)
|
||||
wrong_regional_seed["regional_state"]["world_seed"] = (
|
||||
RegionalSimulationFacade.DEFAULT_WORLD_SEED + 1
|
||||
)
|
||||
wrong_regional_seed["regional_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
||||
wrong_regional_seed["regional_state"]
|
||||
)
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_regional_seed))
|
||||
|
||||
var wrong_tick := valid.duplicate(true)
|
||||
wrong_tick["local_state"]["simulation"]["tick_count"] = 1
|
||||
wrong_tick["local_checksum"] = SimulationSaveManifest._checksum_dictionary(
|
||||
wrong_tick["local_state"]
|
||||
)
|
||||
assert_null(SimulationSaveManifest.from_dictionary(wrong_tick))
|
||||
assert_null(
|
||||
SimulationSaveManifest.create(
|
||||
manager.create_state_record(),
|
||||
RegionalSimulationFacade.create_jajce_route(
|
||||
RegionalSimulationFacade.DEFAULT_WORLD_SEED, 1
|
||||
)
|
||||
)
|
||||
)
|
||||
assert_null(
|
||||
SimulationSaveManifest.create(
|
||||
manager.create_state_record(),
|
||||
RegionalSimulationFacade.create_jajce_route(
|
||||
RegionalSimulationFacade.DEFAULT_WORLD_SEED + 1, 0
|
||||
)
|
||||
)
|
||||
)
|
||||
var negative_seed_local: SimulationStateRecord = manager.create_state_record()
|
||||
negative_seed_local.simulation["seed"] = -42
|
||||
assert_not_null(
|
||||
SimulationSaveManifest.create(
|
||||
negative_seed_local, RegionalSimulationFacade.create_jajce_route(-42, 0)
|
||||
)
|
||||
)
|
||||
|
||||
manager.simulate_tick()
|
||||
var synchronized := SimulationSaveManifest.create(
|
||||
manager.create_state_record(),
|
||||
RegionalSimulationFacade.create_jajce_route(RegionalSimulationFacade.DEFAULT_WORLD_SEED, 1)
|
||||
)
|
||||
assert_not_null(synchronized)
|
||||
assert_eq(synchronized.get_regional_facade().get_last_processed_tick(), 1)
|
||||
assert_eq(int(synchronized.get_local_state().simulation["tick_count"]), 1)
|
||||
|
||||
|
||||
func _valid_manifest_dictionary() -> Dictionary:
|
||||
var manifest := SimulationSaveManifest.create(
|
||||
manager.create_state_record(), RegionalSimulationFacade.create_jajce_route()
|
||||
)
|
||||
assert_not_null(manifest)
|
||||
return manifest.to_dictionary()
|
||||
|
||||
|
||||
func _set_regional_stockpile_value(
|
||||
regional_state: Dictionary, settlement_id: StringName, value: Variant
|
||||
) -> void:
|
||||
for settlement: Dictionary in regional_state["caravan_service"]["world_state"]["settlements"]:
|
||||
if StringName(settlement["settlement_id"]) == settlement_id:
|
||||
settlement["stockpile_ledger"][String(RegionalSimulationFacade.TRADE_GOODS)] = value
|
||||
return
|
||||
assert_true(false, "Expected regional settlement '%s'" % settlement_id)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cradryxj45n86
|
||||
Reference in New Issue
Block a user