feat: integrate regional caravan persistence
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user