feat: chunk regional simulation state
This commit is contained in:
@@ -0,0 +1,281 @@
|
||||
extends GutTest
|
||||
|
||||
const WORLD_ID := &"regional_world"
|
||||
const ORIGIN_LOCATION_ID := &"location_origin"
|
||||
const DESTINATION_LOCATION_ID := &"location_destination"
|
||||
const ORIGIN_SETTLEMENT_ID := &"settlement_origin"
|
||||
const DESTINATION_SETTLEMENT_ID := &"settlement_destination"
|
||||
const ROUTE_ID := &"route_trade"
|
||||
const DEPARTURE_TICK := 10
|
||||
const ARRIVAL_TICK := 15
|
||||
|
||||
|
||||
func test_mid_route_round_trip_preserves_exact_envelope_and_continuation() -> void:
|
||||
var service := _fixture_service(1)
|
||||
assert_true(_depart_group(service, 0))
|
||||
var expected_envelope := service.to_dictionary()
|
||||
var archive := RegionalChunkedPersistence.capture(service, 1)
|
||||
|
||||
assert_not_null(archive)
|
||||
assert_eq(archive.get_manifest()["envelope_checksum"], service.checksum())
|
||||
assert_eq(archive.get_chunk_ids_for_type(RegionalChunkedPersistence.CHUNK_EVENTS).size(), 2)
|
||||
var restored := archive.restore_service()
|
||||
assert_not_null(restored)
|
||||
assert_eq(restored.to_dictionary(), expected_envelope)
|
||||
assert_eq(restored.checksum(), service.checksum())
|
||||
|
||||
assert_eq(service.process_due(ARRIVAL_TICK, 1).size(), 1)
|
||||
assert_eq(restored.process_due(ARRIVAL_TICK, 1).size(), 1)
|
||||
assert_eq(restored.to_dictionary(), service.to_dictionary())
|
||||
|
||||
|
||||
func test_active_location_metadata_does_not_hydrate_or_discard_unloaded_authority() -> void:
|
||||
var service := _fixture_service(2)
|
||||
var archive := RegionalChunkedPersistence.capture(service)
|
||||
assert_not_null(archive)
|
||||
var archive_before := archive.to_dictionary()
|
||||
|
||||
var partial := archive.load_active_location_metadata(ORIGIN_LOCATION_ID)
|
||||
assert_false(partial.is_empty())
|
||||
assert_eq(
|
||||
partial["loaded_chunk_ids"],
|
||||
[
|
||||
RegionalChunkedPersistence.GLOBAL_CHUNK_ID,
|
||||
"location:%s" % ORIGIN_LOCATION_ID,
|
||||
]
|
||||
)
|
||||
assert_eq(partial["mobile_group_refs"].size(), 2)
|
||||
assert_true("scheduler:regional" in partial["unloaded_chunk_ids"])
|
||||
assert_true("mobile_group:group_00" in partial["unloaded_chunk_ids"])
|
||||
assert_false((partial["global_index"] as Dictionary).has("mobile_groups"))
|
||||
|
||||
(partial["active_location"] as Dictionary)["settlements"] = []
|
||||
(partial["unloaded_chunk_ids"] as Array).clear()
|
||||
assert_eq(archive.to_dictionary(), archive_before)
|
||||
assert_eq(archive.restore_service().checksum(), service.checksum())
|
||||
|
||||
|
||||
func test_missing_duplicate_tampered_and_noncanonical_chunks_are_rejected() -> void:
|
||||
var archive := RegionalChunkedPersistence.capture(_fixture_service(1))
|
||||
assert_not_null(archive)
|
||||
|
||||
var missing := archive.to_dictionary()
|
||||
(missing["chunks"] as Array).pop_back()
|
||||
assert_null(RegionalChunkedPersistence.from_dictionary(missing))
|
||||
|
||||
var duplicate := archive.to_dictionary()
|
||||
var duplicated_chunk: Dictionary = (duplicate["chunks"] as Array)[-1].duplicate(true)
|
||||
var duplicated_descriptor: Dictionary = (
|
||||
((duplicate["manifest"] as Dictionary)["chunks"] as Array)[-1].duplicate(true)
|
||||
)
|
||||
(duplicate["chunks"] as Array).append(duplicated_chunk)
|
||||
((duplicate["manifest"] as Dictionary)["chunks"] as Array).append(duplicated_descriptor)
|
||||
(duplicate["manifest"] as Dictionary)["chunk_count"] = ((duplicate["chunks"] as Array).size())
|
||||
assert_null(RegionalChunkedPersistence.from_dictionary(duplicate))
|
||||
|
||||
var tampered_payload := archive.to_dictionary()
|
||||
var origin_chunk := _find_chunk(tampered_payload, "location:%s" % ORIGIN_LOCATION_ID)
|
||||
(origin_chunk["payload"] as Dictionary)["location_id"] = "location_forged"
|
||||
assert_null(RegionalChunkedPersistence.from_dictionary(tampered_payload))
|
||||
|
||||
var tampered_checksum := archive.to_dictionary()
|
||||
var descriptors: Array = (tampered_checksum["manifest"] as Dictionary)["chunks"]
|
||||
descriptors[0]["checksum"] = "forged"
|
||||
assert_null(RegionalChunkedPersistence.from_dictionary(tampered_checksum))
|
||||
|
||||
var self_consistent_but_noncanonical := archive.to_dictionary()
|
||||
var noncanonical_chunk := _find_chunk(
|
||||
self_consistent_but_noncanonical, "location:%s" % ORIGIN_LOCATION_ID
|
||||
)
|
||||
(noncanonical_chunk["payload"] as Dictionary)["ignored_authority"] = true
|
||||
_refresh_chunk_descriptor(self_consistent_but_noncanonical, noncanonical_chunk)
|
||||
assert_null(RegionalChunkedPersistence.from_dictionary(self_consistent_but_noncanonical))
|
||||
|
||||
var noncanonical := archive.to_dictionary()
|
||||
var chunks: Array = noncanonical["chunks"]
|
||||
var noncanonical_descriptors: Array = (noncanonical["manifest"] as Dictionary)["chunks"]
|
||||
var first_chunk: Variant = chunks[0]
|
||||
chunks[0] = chunks[1]
|
||||
chunks[1] = first_chunk
|
||||
var first_descriptor: Variant = noncanonical_descriptors[0]
|
||||
noncanonical_descriptors[0] = noncanonical_descriptors[1]
|
||||
noncanonical_descriptors[1] = first_descriptor
|
||||
assert_null(RegionalChunkedPersistence.from_dictionary(noncanonical))
|
||||
|
||||
|
||||
func test_payload_contract_rejects_nodes_resources_and_node_paths() -> void:
|
||||
var archive := RegionalChunkedPersistence.capture(_fixture_service(1))
|
||||
assert_not_null(archive)
|
||||
assert_true(_contains_only_primitive_values(archive.to_dictionary()))
|
||||
|
||||
var node_bundle := archive.to_dictionary()
|
||||
var node := Node.new()
|
||||
(_first_chunk_payload(node_bundle))["forbidden"] = node
|
||||
assert_null(RegionalChunkedPersistence.from_dictionary(node_bundle))
|
||||
node.free()
|
||||
|
||||
var resource_bundle := archive.to_dictionary()
|
||||
(_first_chunk_payload(resource_bundle))["forbidden"] = Resource.new()
|
||||
assert_null(RegionalChunkedPersistence.from_dictionary(resource_bundle))
|
||||
|
||||
var path_bundle := archive.to_dictionary()
|
||||
(_first_chunk_payload(path_bundle))["forbidden"] = NodePath("World/Caravan")
|
||||
assert_null(RegionalChunkedPersistence.from_dictionary(path_bundle))
|
||||
|
||||
|
||||
func test_twenty_caravans_remain_structurally_chunked_and_bounded() -> void:
|
||||
var service := _fixture_service(20)
|
||||
for index in range(20):
|
||||
assert_true(_depart_group(service, index))
|
||||
var archive := RegionalChunkedPersistence.capture(service, 8)
|
||||
|
||||
assert_not_null(archive)
|
||||
assert_eq(
|
||||
archive.get_chunk_ids_for_type(RegionalChunkedPersistence.CHUNK_MOBILE_GROUP).size(), 20
|
||||
)
|
||||
assert_eq(archive.get_chunk_ids_for_type(RegionalChunkedPersistence.CHUNK_EVENTS).size(), 5)
|
||||
assert_eq(archive.get_manifest()["chunk_count"], 29)
|
||||
for event_chunk_id: String in archive.get_chunk_ids_for_type(
|
||||
RegionalChunkedPersistence.CHUNK_EVENTS
|
||||
):
|
||||
assert_true(
|
||||
(
|
||||
(archive.get_chunk(StringName(event_chunk_id))["payload"]["events"] as Array).size()
|
||||
<= 8
|
||||
)
|
||||
)
|
||||
var encoded_size := JSON.stringify(archive.to_dictionary()).to_utf8_buffer().size()
|
||||
assert_lt(encoded_size, 250_000)
|
||||
assert_eq(archive.restore_service().checksum(), service.checksum())
|
||||
|
||||
|
||||
func _fixture_service(group_count: int) -> RegionalCaravanService:
|
||||
var origin_location := LocationStateRecord.create(
|
||||
ORIGIN_LOCATION_ID, WORLD_ID, &"settlement", "Origin", _address(ORIGIN_LOCATION_ID)
|
||||
)
|
||||
var destination_location := LocationStateRecord.create(
|
||||
DESTINATION_LOCATION_ID,
|
||||
WORLD_ID,
|
||||
&"settlement",
|
||||
"Destination",
|
||||
_address(DESTINATION_LOCATION_ID)
|
||||
)
|
||||
var origin := SettlementStateRecord.create(
|
||||
ORIGIN_SETTLEMENT_ID,
|
||||
ORIGIN_LOCATION_ID,
|
||||
"Origin",
|
||||
&"polity_origin",
|
||||
1,
|
||||
&"founders_origin",
|
||||
0,
|
||||
{&"food": float(group_count * 2 + 10)}
|
||||
)
|
||||
var destination := SettlementStateRecord.create(
|
||||
DESTINATION_SETTLEMENT_ID,
|
||||
DESTINATION_LOCATION_ID,
|
||||
"Destination",
|
||||
&"polity_destination",
|
||||
1,
|
||||
&"founders_destination",
|
||||
0,
|
||||
{&"food": 4.0}
|
||||
)
|
||||
var route := RouteStateRecord.create(ROUTE_ID, ORIGIN_LOCATION_ID, DESTINATION_LOCATION_ID, 5)
|
||||
var groups: Array[MobileGroupStateRecord] = []
|
||||
for index in range(group_count):
|
||||
groups.append(
|
||||
MobileGroupStateRecord.create_at_location(
|
||||
_group_id(index),
|
||||
&"caravan",
|
||||
"Caravan %02d" % index,
|
||||
&"polity_origin",
|
||||
_address(ORIGIN_LOCATION_ID),
|
||||
[],
|
||||
{},
|
||||
{},
|
||||
2.0
|
||||
)
|
||||
)
|
||||
var origin_polity := PolityStateRecord.create(
|
||||
&"polity_origin", "Origin polity", ORIGIN_LOCATION_ID, [ORIGIN_SETTLEMENT_ID]
|
||||
)
|
||||
var destination_polity := PolityStateRecord.create(
|
||||
&"polity_destination",
|
||||
"Destination polity",
|
||||
DESTINATION_LOCATION_ID,
|
||||
[DESTINATION_SETTLEMENT_ID]
|
||||
)
|
||||
var locations: Array[LocationStateRecord] = [destination_location, origin_location]
|
||||
var settlements: Array[SettlementStateRecord] = [destination, origin]
|
||||
var routes: Array[RouteStateRecord] = [route]
|
||||
var persons: Array[PersonStateRecord] = []
|
||||
var cohorts: Array[PopulationCohortRecord] = []
|
||||
var polities: Array[PolityStateRecord] = [destination_polity, origin_polity]
|
||||
var relations: Array[DiplomaticRelationRecord] = []
|
||||
var world_state := RegionalWorldState.create(
|
||||
WORLD_ID, locations, settlements, routes, groups, persons, cohorts, polities, relations
|
||||
)
|
||||
return RegionalCaravanService.create(
|
||||
world_state, RegionalJobScheduler.new(), WorldEventStore.new()
|
||||
)
|
||||
|
||||
|
||||
func _depart_group(service: RegionalCaravanService, index: int) -> bool:
|
||||
return service.depart(
|
||||
_group_id(index),
|
||||
ORIGIN_SETTLEMENT_ID,
|
||||
DESTINATION_SETTLEMENT_ID,
|
||||
ROUTE_ID,
|
||||
{&"food": 1.0},
|
||||
DEPARTURE_TICK
|
||||
)
|
||||
|
||||
|
||||
func _group_id(index: int) -> StringName:
|
||||
return StringName("group_%02d" % index)
|
||||
|
||||
|
||||
func _address(location_id: StringName) -> SpatialAddress:
|
||||
return SpatialAddress.create(WORLD_ID, location_id, Vector3.ZERO)
|
||||
|
||||
|
||||
func _find_chunk(bundle: Dictionary, chunk_id: String) -> Dictionary:
|
||||
for raw_chunk: Variant in bundle["chunks"]:
|
||||
if raw_chunk is Dictionary and String(raw_chunk.get("chunk_id", "")) == chunk_id:
|
||||
return raw_chunk
|
||||
return {}
|
||||
|
||||
|
||||
func _first_chunk_payload(bundle: Dictionary) -> Dictionary:
|
||||
return ((bundle["chunks"] as Array)[0] as Dictionary)["payload"]
|
||||
|
||||
|
||||
func _refresh_chunk_descriptor(bundle: Dictionary, chunk: Dictionary) -> void:
|
||||
var serialized := JSON.stringify(chunk)
|
||||
for raw_descriptor: Variant in (bundle["manifest"] as Dictionary)["chunks"]:
|
||||
if (
|
||||
raw_descriptor is Dictionary
|
||||
and String(raw_descriptor["chunk_id"]) == String(chunk["chunk_id"])
|
||||
):
|
||||
raw_descriptor["byte_size"] = serialized.to_utf8_buffer().size()
|
||||
raw_descriptor["checksum"] = serialized.sha256_text()
|
||||
return
|
||||
|
||||
|
||||
func _contains_only_primitive_values(value: Variant) -> bool:
|
||||
match typeof(value):
|
||||
TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_STRING:
|
||||
return true
|
||||
TYPE_FLOAT:
|
||||
return is_finite(float(value))
|
||||
TYPE_ARRAY:
|
||||
for item: Variant in value:
|
||||
if not _contains_only_primitive_values(item):
|
||||
return false
|
||||
return true
|
||||
TYPE_DICTIONARY:
|
||||
for key: Variant in value:
|
||||
if not key is String or not _contains_only_primitive_values(value[key]):
|
||||
return false
|
||||
return true
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://bmxqfpf0jl4um
|
||||
Reference in New Issue
Block a user