feat: define regional world state

This commit is contained in:
Rijad Zuzo
2026-08-12 21:22:04 +02:00
parent 8707942c75
commit c5978a9807
22 changed files with 2703 additions and 0 deletions
+144
View File
@@ -0,0 +1,144 @@
class_name RegionalStateSupport
extends RefCounted
static func normalize_id_array(values: Array, allow_empty: bool = true) -> Variant:
var seen: Dictionary = {}
var normalized: Array[String] = []
for value: Variant in values:
if not value is String and not value is StringName:
return null
var id := String(value)
if id.is_empty() or seen.has(id):
return null
seen[id] = true
normalized.append(id)
if not allow_empty and normalized.is_empty():
return null
normalized.sort()
return normalized
static func normalize_history_event_ids(values: Array) -> Variant:
var seen: Dictionary = {}
var normalized: Array[int] = []
for value: Variant in values:
if not value is int:
return null
var event_id := int(value)
if event_id < 0 or seen.has(event_id):
return null
seen[event_id] = true
normalized.append(event_id)
normalized.sort()
return normalized
static func normalize_amount_ledger(values: Dictionary) -> Variant:
var source_keys: Dictionary = {}
for raw_key: Variant in values:
if not raw_key is String and not raw_key is StringName:
return null
var key := String(raw_key)
if key.is_empty() or source_keys.has(key):
return null
var raw_amount: Variant = values[raw_key]
if not raw_amount is int and not raw_amount is float:
return null
var amount := float(raw_amount)
if not is_finite(amount) or amount < 0.0:
return null
source_keys[key] = raw_key
var keys: Array = source_keys.keys()
keys.sort()
var normalized: Dictionary = {}
for key: String in keys:
normalized[key] = float(values[source_keys[key]])
return normalized
static func normalize_count_ledger(values: Dictionary) -> Variant:
var source_keys: Dictionary = {}
for raw_key: Variant in values:
if not raw_key is String and not raw_key is StringName:
return null
var key := String(raw_key)
if key.is_empty() or source_keys.has(key):
return null
var raw_count: Variant = values[raw_key]
if not raw_count is int or int(raw_count) <= 0:
return null
source_keys[key] = raw_key
var keys: Array = source_keys.keys()
keys.sort()
var normalized: Dictionary = {}
for key: String in keys:
normalized[key] = int(values[source_keys[key]])
return normalized
static func amount_ledger_total(values: Dictionary) -> float:
var total := 0.0
for value: Variant in values.values():
total += float(value)
return total
static func normalize_address(value: Variant) -> Variant:
var address: SpatialAddress
if value is SpatialAddress:
address = value as SpatialAddress
elif value is Dictionary:
address = SpatialAddress.from_dictionary(value)
if address == null or not address.is_valid():
return null
return address.to_dictionary()
static func address_from_data(value: Variant) -> SpatialAddress:
if not value is Dictionary:
return null
return SpatialAddress.from_dictionary(value)
static func address_matches(
value: Variant, expected_world_id: StringName, expected_location_id: StringName
) -> bool:
var address := address_from_data(value)
return (
address != null
and address.get_world_id() == expected_world_id
and address.get_location_id() == expected_location_id
)
static func copy_string_array(values: Variant) -> Array[StringName]:
var copied: Array[StringName] = []
if not values is Array:
return copied
for value: Variant in values:
copied.append(StringName(value))
return copied
static func copy_int_array(values: Variant) -> Array[int]:
var copied: Array[int] = []
if not values is Array:
return copied
for value: Variant in values:
copied.append(int(value))
return copied
static func pair_key(first_id: StringName, second_id: StringName) -> String:
var first := String(first_id)
var second := String(second_id)
return "%s\u001f%s" % [first, second] if first < second else "%s\u001f%s" % [second, first]
static func is_non_empty_id(value: StringName) -> bool:
return not value.is_empty()
static func is_valid_tick(value: int, allow_unset: bool = false) -> bool:
return value >= (-1 if allow_unset else 0)
@@ -0,0 +1 @@
uid://c041d4ms34euf
+858
View File
@@ -0,0 +1,858 @@
class_name RegionalWorldState
extends RefCounted
const SCHEMA_VERSION := 1
var _world_id: StringName
var _locations: Dictionary = {}
var _settlements: Dictionary = {}
var _routes: Dictionary = {}
var _mobile_groups: Dictionary = {}
var _persons: Dictionary = {}
var _cohorts: Dictionary = {}
var _polities: Dictionary = {}
var _diplomatic_relations: Dictionary = {}
var _diplomacy_by_pair: Dictionary = {}
var _route_ids_by_location: Dictionary = {}
var _group_ids_by_location: Dictionary = {}
var _group_ids_by_route: Dictionary = {}
var _person_ids_by_location: Dictionary = {}
var _person_ids_by_group: Dictionary = {}
var _cohort_ids_by_location: Dictionary = {}
var _cohort_ids_by_group: Dictionary = {}
var _settlement_ids_by_location: Dictionary = {}
var _global_entity_types: Dictionary = {}
var _last_validation_errors: Array[String] = []
static func create(
world_id: StringName,
locations: Array[LocationStateRecord],
settlements: Array[SettlementStateRecord],
routes: Array[RouteStateRecord],
mobile_groups: Array[MobileGroupStateRecord],
persons: Array[PersonStateRecord],
cohorts: Array[PopulationCohortRecord],
polities: Array[PolityStateRecord],
diplomatic_relations: Array[DiplomaticRelationRecord]
) -> RegionalWorldState:
var state := RegionalWorldState.new()
return (
state
if state.replace_all(
world_id,
locations,
settlements,
routes,
mobile_groups,
persons,
cohorts,
polities,
diplomatic_relations
)
else null
)
static func from_dictionary(record_data: Dictionary) -> RegionalWorldState:
var state := RegionalWorldState.new()
return state if state.restore_from_dictionary(record_data) else null
func replace_all(
world_id: StringName,
locations: Array[LocationStateRecord],
settlements: Array[SettlementStateRecord],
routes: Array[RouteStateRecord],
mobile_groups: Array[MobileGroupStateRecord],
persons: Array[PersonStateRecord],
cohorts: Array[PopulationCohortRecord],
polities: Array[PolityStateRecord],
diplomatic_relations: Array[DiplomaticRelationRecord]
) -> bool:
var candidate := RegionalWorldState.new()
candidate._world_id = world_id
if world_id.is_empty():
candidate._add_error("world_id must be non-empty")
for source: LocationStateRecord in locations:
var record := (
LocationStateRecord.from_dictionary(source.to_dictionary()) if source != null else null
)
if record == null:
candidate._add_error("invalid location record")
elif not candidate._insert_location(record):
pass
for source: SettlementStateRecord in settlements:
var record := (
SettlementStateRecord.from_dictionary(source.to_dictionary())
if source != null
else null
)
if record == null:
candidate._add_error("invalid settlement record")
elif not candidate._insert_settlement(record):
pass
for source: RouteStateRecord in routes:
var record := (
RouteStateRecord.from_dictionary(source.to_dictionary()) if source != null else null
)
if record == null:
candidate._add_error("invalid route record")
elif not candidate._insert_route(record):
pass
for source: MobileGroupStateRecord in mobile_groups:
var record := (
MobileGroupStateRecord.from_dictionary(source.to_dictionary())
if source != null
else null
)
if record == null:
candidate._add_error("invalid mobile-group record")
elif not candidate._insert_mobile_group(record):
pass
for source: PersonStateRecord in persons:
var record := (
PersonStateRecord.from_dictionary(source.to_dictionary()) if source != null else null
)
if record == null:
candidate._add_error("invalid person record")
elif not candidate._insert_person(record):
pass
for source: PopulationCohortRecord in cohorts:
var record := (
PopulationCohortRecord.from_dictionary(source.to_dictionary())
if source != null
else null
)
if record == null:
candidate._add_error("invalid population-cohort record")
elif not candidate._insert_cohort(record):
pass
for source: PolityStateRecord in polities:
var record := (
PolityStateRecord.from_dictionary(source.to_dictionary()) if source != null else null
)
if record == null:
candidate._add_error("invalid polity record")
elif not candidate._insert_polity(record):
pass
for source: DiplomaticRelationRecord in diplomatic_relations:
var record := (
DiplomaticRelationRecord.from_dictionary(source.to_dictionary())
if source != null
else null
)
if record == null:
candidate._add_error("invalid diplomatic-relation record")
elif not candidate._insert_diplomatic_relation(record):
pass
candidate._validate_cross_references()
if not candidate._last_validation_errors.is_empty():
candidate._last_validation_errors.sort()
_last_validation_errors = candidate._last_validation_errors.duplicate()
return false
candidate._rebuild_membership_indexes()
_adopt(candidate)
return true
func restore_from_dictionary(record_data: Dictionary) -> bool:
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
return _reject("unsupported regional-world schema version")
if not (
record_data
. has_all(
[
"world_id",
"locations",
"settlements",
"routes",
"mobile_groups",
"persons",
"population_cohorts",
"polities",
"diplomatic_relations",
]
)
):
return _reject("regional-world save is missing required fields")
for field: String in [
"locations",
"settlements",
"routes",
"mobile_groups",
"persons",
"population_cohorts",
"polities",
"diplomatic_relations",
]:
if not record_data[field] is Array:
return _reject("regional-world field '%s' must be an array" % field)
var locations: Array[LocationStateRecord] = []
for saved_record: Variant in record_data["locations"]:
if not saved_record is Dictionary:
return _reject("location entry must be a dictionary")
var record := LocationStateRecord.from_dictionary(saved_record)
if record == null:
return _reject("invalid location entry")
locations.append(record)
var settlements: Array[SettlementStateRecord] = []
for saved_record: Variant in record_data["settlements"]:
if not saved_record is Dictionary:
return _reject("settlement entry must be a dictionary")
var record := SettlementStateRecord.from_dictionary(saved_record)
if record == null:
return _reject("invalid settlement entry")
settlements.append(record)
var routes: Array[RouteStateRecord] = []
for saved_record: Variant in record_data["routes"]:
if not saved_record is Dictionary:
return _reject("route entry must be a dictionary")
var record := RouteStateRecord.from_dictionary(saved_record)
if record == null:
return _reject("invalid route entry")
routes.append(record)
var mobile_groups: Array[MobileGroupStateRecord] = []
for saved_record: Variant in record_data["mobile_groups"]:
if not saved_record is Dictionary:
return _reject("mobile-group entry must be a dictionary")
var record := MobileGroupStateRecord.from_dictionary(saved_record)
if record == null:
return _reject("invalid mobile-group entry")
mobile_groups.append(record)
var persons: Array[PersonStateRecord] = []
for saved_record: Variant in record_data["persons"]:
if not saved_record is Dictionary:
return _reject("person entry must be a dictionary")
var record := PersonStateRecord.from_dictionary(saved_record)
if record == null:
return _reject("invalid person entry")
persons.append(record)
var cohorts: Array[PopulationCohortRecord] = []
for saved_record: Variant in record_data["population_cohorts"]:
if not saved_record is Dictionary:
return _reject("population-cohort entry must be a dictionary")
var record := PopulationCohortRecord.from_dictionary(saved_record)
if record == null:
return _reject("invalid population-cohort entry")
cohorts.append(record)
var polities: Array[PolityStateRecord] = []
for saved_record: Variant in record_data["polities"]:
if not saved_record is Dictionary:
return _reject("polity entry must be a dictionary")
var record := PolityStateRecord.from_dictionary(saved_record)
if record == null:
return _reject("invalid polity entry")
polities.append(record)
var relations: Array[DiplomaticRelationRecord] = []
for saved_record: Variant in record_data["diplomatic_relations"]:
if not saved_record is Dictionary:
return _reject("diplomatic-relation entry must be a dictionary")
var record := DiplomaticRelationRecord.from_dictionary(saved_record)
if record == null:
return _reject("invalid diplomatic-relation entry")
relations.append(record)
return replace_all(
StringName(record_data["world_id"]),
locations,
settlements,
routes,
mobile_groups,
persons,
cohorts,
polities,
relations
)
func get_world_id() -> StringName:
return _world_id
func has_global_id(entity_id: StringName) -> bool:
return _global_entity_types.has(entity_id)
func get_entity_type(entity_id: StringName) -> StringName:
return StringName(_global_entity_types.get(entity_id, &""))
func get_location(location_id: StringName) -> LocationStateRecord:
return _locations.get(location_id) as LocationStateRecord
func get_settlement(settlement_id: StringName) -> SettlementStateRecord:
return _settlements.get(settlement_id) as SettlementStateRecord
func get_route(route_id: StringName) -> RouteStateRecord:
return _routes.get(route_id) as RouteStateRecord
func get_mobile_group(group_id: StringName) -> MobileGroupStateRecord:
return _mobile_groups.get(group_id) as MobileGroupStateRecord
func get_person(person_id: StringName) -> PersonStateRecord:
return _persons.get(person_id) as PersonStateRecord
func get_population_cohort(cohort_id: StringName) -> PopulationCohortRecord:
return _cohorts.get(cohort_id) as PopulationCohortRecord
func get_polity(polity_id: StringName) -> PolityStateRecord:
return _polities.get(polity_id) as PolityStateRecord
func get_diplomatic_relation(relation_id: StringName) -> DiplomaticRelationRecord:
return _diplomatic_relations.get(relation_id) as DiplomaticRelationRecord
func get_diplomatic_relation_between(
first_polity_id: StringName, second_polity_id: StringName
) -> DiplomaticRelationRecord:
var pair_key := RegionalStateSupport.pair_key(first_polity_id, second_polity_id)
var relation_id: StringName = _diplomacy_by_pair.get(pair_key, &"")
return get_diplomatic_relation(relation_id)
func get_all_locations() -> Array[LocationStateRecord]:
var records: Array[LocationStateRecord] = []
for id: StringName in _sorted_ids(_locations):
records.append(get_location(id))
return records
func get_all_settlements() -> Array[SettlementStateRecord]:
var records: Array[SettlementStateRecord] = []
for id: StringName in _sorted_ids(_settlements):
records.append(get_settlement(id))
return records
func get_all_routes() -> Array[RouteStateRecord]:
var records: Array[RouteStateRecord] = []
for id: StringName in _sorted_ids(_routes):
records.append(get_route(id))
return records
func get_all_mobile_groups() -> Array[MobileGroupStateRecord]:
var records: Array[MobileGroupStateRecord] = []
for id: StringName in _sorted_ids(_mobile_groups):
records.append(get_mobile_group(id))
return records
func get_all_persons() -> Array[PersonStateRecord]:
var records: Array[PersonStateRecord] = []
for id: StringName in _sorted_ids(_persons):
records.append(get_person(id))
return records
func get_all_population_cohorts() -> Array[PopulationCohortRecord]:
var records: Array[PopulationCohortRecord] = []
for id: StringName in _sorted_ids(_cohorts):
records.append(get_population_cohort(id))
return records
func get_all_polities() -> Array[PolityStateRecord]:
var records: Array[PolityStateRecord] = []
for id: StringName in _sorted_ids(_polities):
records.append(get_polity(id))
return records
func get_all_diplomatic_relations() -> Array[DiplomaticRelationRecord]:
var records: Array[DiplomaticRelationRecord] = []
for id: StringName in _sorted_ids(_diplomatic_relations):
records.append(get_diplomatic_relation(id))
return records
func get_routes_for_location(location_id: StringName) -> Array[RouteStateRecord]:
var records: Array[RouteStateRecord] = []
for route_id: StringName in _index_ids(_route_ids_by_location, location_id):
records.append(get_route(route_id))
return records
func get_mobile_groups_at_location(location_id: StringName) -> Array[MobileGroupStateRecord]:
var records: Array[MobileGroupStateRecord] = []
for group_id: StringName in _index_ids(_group_ids_by_location, location_id):
records.append(get_mobile_group(group_id))
return records
func get_mobile_groups_on_route(route_id: StringName) -> Array[MobileGroupStateRecord]:
var records: Array[MobileGroupStateRecord] = []
for group_id: StringName in _index_ids(_group_ids_by_route, route_id):
records.append(get_mobile_group(group_id))
return records
func get_persons_at_location(location_id: StringName) -> Array[PersonStateRecord]:
var records: Array[PersonStateRecord] = []
for person_id: StringName in _index_ids(_person_ids_by_location, location_id):
records.append(get_person(person_id))
return records
func get_persons_in_group(group_id: StringName) -> Array[PersonStateRecord]:
var records: Array[PersonStateRecord] = []
for person_id: StringName in _index_ids(_person_ids_by_group, group_id):
records.append(get_person(person_id))
return records
func get_cohorts_at_location(location_id: StringName) -> Array[PopulationCohortRecord]:
var records: Array[PopulationCohortRecord] = []
for cohort_id: StringName in _index_ids(_cohort_ids_by_location, location_id):
records.append(get_population_cohort(cohort_id))
return records
func get_cohorts_in_group(group_id: StringName) -> Array[PopulationCohortRecord]:
var records: Array[PopulationCohortRecord] = []
for cohort_id: StringName in _index_ids(_cohort_ids_by_group, group_id):
records.append(get_population_cohort(cohort_id))
return records
func get_settlements_at_location(location_id: StringName) -> Array[SettlementStateRecord]:
var records: Array[SettlementStateRecord] = []
for settlement_id: StringName in _index_ids(_settlement_ids_by_location, location_id):
records.append(get_settlement(settlement_id))
return records
func get_last_validation_errors() -> Array[String]:
return _last_validation_errors.duplicate()
func replace_settlement_and_mobile_group(
settlement: SettlementStateRecord, mobile_group: MobileGroupStateRecord
) -> bool:
if (
settlement == null
or mobile_group == null
or not _settlements.has(settlement.get_settlement_id())
or not _mobile_groups.has(mobile_group.get_group_id())
):
return false
var settlements := get_all_settlements()
for index in range(settlements.size()):
if settlements[index].get_settlement_id() == settlement.get_settlement_id():
settlements[index] = settlement
break
var mobile_groups := get_all_mobile_groups()
for index in range(mobile_groups.size()):
if mobile_groups[index].get_group_id() == mobile_group.get_group_id():
mobile_groups[index] = mobile_group
break
return replace_all(
_world_id,
get_all_locations(),
settlements,
get_all_routes(),
mobile_groups,
get_all_persons(),
get_all_population_cohorts(),
get_all_polities(),
get_all_diplomatic_relations()
)
func to_dictionary() -> Dictionary:
var locations: Array[Dictionary] = []
for id: StringName in _sorted_ids(_locations):
locations.append(get_location(id).to_dictionary())
var settlements: Array[Dictionary] = []
for id: StringName in _sorted_ids(_settlements):
settlements.append(get_settlement(id).to_dictionary())
var routes: Array[Dictionary] = []
for id: StringName in _sorted_ids(_routes):
routes.append(get_route(id).to_dictionary())
var mobile_groups: Array[Dictionary] = []
for id: StringName in _sorted_ids(_mobile_groups):
mobile_groups.append(get_mobile_group(id).to_dictionary())
var persons: Array[Dictionary] = []
for id: StringName in _sorted_ids(_persons):
persons.append(get_person(id).to_dictionary())
var cohorts: Array[Dictionary] = []
for id: StringName in _sorted_ids(_cohorts):
cohorts.append(get_population_cohort(id).to_dictionary())
var polities: Array[Dictionary] = []
for id: StringName in _sorted_ids(_polities):
polities.append(get_polity(id).to_dictionary())
var diplomatic_relations: Array[Dictionary] = []
for id: StringName in _sorted_ids(_diplomatic_relations):
diplomatic_relations.append(get_diplomatic_relation(id).to_dictionary())
return {
"schema_version": SCHEMA_VERSION,
"world_id": String(_world_id),
"locations": locations,
"settlements": settlements,
"routes": routes,
"mobile_groups": mobile_groups,
"persons": persons,
"population_cohorts": cohorts,
"polities": polities,
"diplomatic_relations": diplomatic_relations,
}
func checksum() -> String:
return JSON.stringify(to_dictionary()).sha256_text()
func _insert_location(record: LocationStateRecord) -> bool:
var id := record.get_location_id()
if not _claim_global_id(id, &"location"):
return false
_locations[id] = record
return true
func _insert_settlement(record: SettlementStateRecord) -> bool:
var id := record.get_settlement_id()
if not _claim_global_id(id, &"settlement"):
return false
_settlements[id] = record
return true
func _insert_route(record: RouteStateRecord) -> bool:
var id := record.get_route_id()
if not _claim_global_id(id, &"route"):
return false
_routes[id] = record
return true
func _insert_mobile_group(record: MobileGroupStateRecord) -> bool:
var id := record.get_group_id()
if not _claim_global_id(id, &"mobile_group"):
return false
_mobile_groups[id] = record
return true
func _insert_person(record: PersonStateRecord) -> bool:
var id := record.get_person_id()
if not _claim_global_id(id, &"person"):
return false
_persons[id] = record
return true
func _insert_cohort(record: PopulationCohortRecord) -> bool:
var id := record.get_cohort_id()
if not _claim_global_id(id, &"population_cohort"):
return false
_cohorts[id] = record
return true
func _insert_polity(record: PolityStateRecord) -> bool:
var id := record.get_polity_id()
if not _claim_global_id(id, &"polity"):
return false
_polities[id] = record
return true
func _insert_diplomatic_relation(record: DiplomaticRelationRecord) -> bool:
var id := record.get_relation_id()
if not _claim_global_id(id, &"diplomatic_relation"):
return false
var pair_key := record.get_pair_key()
if _diplomacy_by_pair.has(pair_key):
_add_error("duplicate diplomatic relation for pair '%s'" % pair_key)
return false
_diplomatic_relations[id] = record
_diplomacy_by_pair[pair_key] = id
return true
func _claim_global_id(id: StringName, entity_type: StringName) -> bool:
if _global_entity_types.has(id):
_add_error(
"duplicate global ID '%s' (%s and %s)" % [id, _global_entity_types[id], entity_type]
)
return false
_global_entity_types[id] = entity_type
return true
func _validate_cross_references() -> void:
for location: LocationStateRecord in _locations.values():
if location.get_world_id() != _world_id:
_add_error("location '%s' belongs to another world" % location.get_location_id())
for settlement: SettlementStateRecord in _settlements.values():
_require_id(
_locations,
settlement.get_location_id(),
"settlement location",
settlement.get_settlement_id()
)
_require_id(
_polities,
settlement.get_polity_id(),
"settlement polity",
settlement.get_settlement_id()
)
for route: RouteStateRecord in _routes.values():
_require_id(
_locations, route.get_from_location_id(), "route endpoint", route.get_route_id()
)
_require_id(_locations, route.get_to_location_id(), "route endpoint", route.get_route_id())
for group: MobileGroupStateRecord in _mobile_groups.values():
_validate_mobile_group(group)
for person: PersonStateRecord in _persons.values():
_validate_person(person)
for cohort: PopulationCohortRecord in _cohorts.values():
_validate_cohort(cohort)
for polity: PolityStateRecord in _polities.values():
_validate_polity(polity)
for relation: DiplomaticRelationRecord in _diplomatic_relations.values():
_require_id(
_polities,
relation.get_first_polity_id(),
"diplomatic polity",
relation.get_relation_id()
)
_require_id(
_polities,
relation.get_second_polity_id(),
"diplomatic polity",
relation.get_relation_id()
)
_validate_group_membership_ownership()
func _validate_mobile_group(group: MobileGroupStateRecord) -> void:
_require_id(_polities, group.get_polity_id(), "mobile-group polity", group.get_group_id())
if group.is_at_location():
var address := group.get_location_address()
if address == null or not _locations.has(address.get_location_id()):
_add_error("mobile group '%s' has an orphan location" % group.get_group_id())
elif not RegionalStateSupport.address_matches(
group.data["location_address"], _world_id, address.get_location_id()
):
_add_error(
"mobile group '%s' has a cross-world location address" % group.get_group_id()
)
return
var route := get_route(group.get_route_id())
if route == null:
_add_error("mobile group '%s' has an orphan route" % group.get_group_id())
return
if not route.connects(group.get_departure_location_id(), group.get_arrival_location_id()):
_add_error(
"mobile group '%s' uses invalid route direction/endpoints" % group.get_group_id()
)
if group.get_arrival_tick() - group.get_departure_tick() != route.get_travel_ticks():
_add_error("mobile group '%s' journey duration does not match route" % group.get_group_id())
func _validate_person(person: PersonStateRecord) -> void:
_require_id(_polities, person.get_polity_id(), "person polity", person.get_person_id())
_require_id(_locations, person.get_home_location_id(), "person home", person.get_person_id())
if person.is_at_location():
var address := person.get_location_address()
if address == null or not _locations.has(address.get_location_id()):
_add_error("person '%s' has an orphan location" % person.get_person_id())
elif not RegionalStateSupport.address_matches(
person.data["location_address"], _world_id, address.get_location_id()
):
_add_error("person '%s' has a cross-world location address" % person.get_person_id())
else:
_require_id(
_mobile_groups,
person.get_mobile_group_id(),
"person mobile group",
person.get_person_id()
)
func _validate_cohort(cohort: PopulationCohortRecord) -> void:
_require_id(_polities, cohort.get_polity_id(), "cohort polity", cohort.get_cohort_id())
if cohort.is_at_location():
_require_id(_locations, cohort.get_location_id(), "cohort location", cohort.get_cohort_id())
else:
_require_id(
_mobile_groups,
cohort.get_mobile_group_id(),
"cohort mobile group",
cohort.get_cohort_id()
)
func _validate_polity(polity: PolityStateRecord) -> void:
_require_id(
_locations, polity.get_capital_location_id(), "polity capital", polity.get_polity_id()
)
for settlement_id: StringName in polity.get_settlement_ids():
var settlement := get_settlement(settlement_id)
if settlement == null:
_add_error(
"polity '%s' lists orphan settlement '%s'" % [polity.get_polity_id(), settlement_id]
)
elif settlement.get_polity_id() != polity.get_polity_id():
_add_error("settlement '%s' belongs to another polity" % settlement_id)
for settlement: SettlementStateRecord in _settlements.values():
if (
settlement.get_polity_id() == polity.get_polity_id()
and settlement.get_settlement_id() not in polity.get_settlement_ids()
):
_add_error(
(
"polity '%s' omits settlement '%s'"
% [polity.get_polity_id(), settlement.get_settlement_id()]
)
)
func _validate_group_membership_ownership() -> void:
var person_owner: Dictionary = {}
var cohort_owner: Dictionary = {}
for group: MobileGroupStateRecord in _mobile_groups.values():
for person_id: StringName in group.get_detailed_member_ids():
var person := get_person(person_id)
if person == null:
_add_error(
"mobile group '%s' lists orphan person '%s'" % [group.get_group_id(), person_id]
)
continue
if person_owner.has(person_id):
_add_error("person '%s' is listed by multiple mobile groups" % person_id)
person_owner[person_id] = group.get_group_id()
if person.get_mobile_group_id() != group.get_group_id():
_add_error("person '%s' has non-reciprocal mobile-group membership" % person_id)
if person.get_polity_id() != group.get_polity_id():
_add_error("person '%s' and mobile group have different polities" % person_id)
for cohort_key: Variant in group.get_cohort_counts():
var cohort_id := StringName(cohort_key)
var cohort := get_population_cohort(cohort_id)
if cohort == null:
_add_error(
"mobile group '%s' lists orphan cohort '%s'" % [group.get_group_id(), cohort_id]
)
continue
if cohort_owner.has(cohort_id):
_add_error("cohort '%s' is listed by multiple mobile groups" % cohort_id)
cohort_owner[cohort_id] = group.get_group_id()
if cohort.get_mobile_group_id() != group.get_group_id():
_add_error("cohort '%s' has non-reciprocal mobile-group membership" % cohort_id)
if int(group.get_cohort_counts()[cohort_key]) != cohort.get_count():
_add_error("cohort '%s' count disagrees with its mobile group" % cohort_id)
if cohort.get_polity_id() != group.get_polity_id():
_add_error("cohort '%s' and mobile group have different polities" % cohort_id)
for person: PersonStateRecord in _persons.values():
if person.is_in_mobile_group() and not person_owner.has(person.get_person_id()):
_add_error("person '%s' is omitted by its mobile group" % person.get_person_id())
for cohort: PopulationCohortRecord in _cohorts.values():
if cohort.is_in_mobile_group() and not cohort_owner.has(cohort.get_cohort_id()):
_add_error("cohort '%s' is omitted by its mobile group" % cohort.get_cohort_id())
func _rebuild_membership_indexes() -> void:
for route: RouteStateRecord in _routes.values():
_index_append(_route_ids_by_location, route.get_from_location_id(), route.get_route_id())
_index_append(_route_ids_by_location, route.get_to_location_id(), route.get_route_id())
for settlement: SettlementStateRecord in _settlements.values():
_index_append(
_settlement_ids_by_location,
settlement.get_location_id(),
settlement.get_settlement_id()
)
for group: MobileGroupStateRecord in _mobile_groups.values():
if group.is_at_location():
_index_append(
_group_ids_by_location, group.get_current_location_id(), group.get_group_id()
)
else:
_index_append(_group_ids_by_route, group.get_route_id(), group.get_group_id())
for person: PersonStateRecord in _persons.values():
if person.is_at_location():
_index_append(
_person_ids_by_location,
person.get_location_address().get_location_id(),
person.get_person_id()
)
else:
_index_append(
_person_ids_by_group, person.get_mobile_group_id(), person.get_person_id()
)
for cohort: PopulationCohortRecord in _cohorts.values():
if cohort.is_at_location():
_index_append(_cohort_ids_by_location, cohort.get_location_id(), cohort.get_cohort_id())
else:
_index_append(
_cohort_ids_by_group, cohort.get_mobile_group_id(), cohort.get_cohort_id()
)
func _require_id(index: Dictionary, id: StringName, label: String, owner_id: StringName) -> void:
if not index.has(id):
_add_error("%s '%s' references missing ID '%s'" % [label, owner_id, id])
func _index_append(index: Dictionary, key: StringName, id: StringName) -> void:
var ids: Array[StringName] = index.get(key, [] as Array[StringName])
ids.append(id)
ids.sort_custom(_compare_ids)
index[key] = ids
func _index_ids(index: Dictionary, key: StringName) -> Array[StringName]:
var ids: Array[StringName] = index.get(key, [] as Array[StringName])
return ids.duplicate()
func _sorted_ids(index: Dictionary) -> Array[StringName]:
var ids: Array[StringName] = []
for id: Variant in index:
ids.append(StringName(id))
ids.sort_custom(_compare_ids)
return ids
static func _compare_ids(first: StringName, second: StringName) -> bool:
return String(first) < String(second)
func _reject(message: String) -> bool:
_last_validation_errors = [message]
return false
func _add_error(message: String) -> void:
_last_validation_errors.append(message)
func _adopt(source: RegionalWorldState) -> void:
_world_id = source._world_id
_locations = source._locations
_settlements = source._settlements
_routes = source._routes
_mobile_groups = source._mobile_groups
_persons = source._persons
_cohorts = source._cohorts
_polities = source._polities
_diplomatic_relations = source._diplomatic_relations
_diplomacy_by_pair = source._diplomacy_by_pair
_route_ids_by_location = source._route_ids_by_location
_group_ids_by_location = source._group_ids_by_location
_group_ids_by_route = source._group_ids_by_route
_person_ids_by_location = source._person_ids_by_location
_person_ids_by_group = source._person_ids_by_group
_cohort_ids_by_location = source._cohort_ids_by_location
_cohort_ids_by_group = source._cohort_ids_by_group
_settlement_ids_by_location = source._settlement_ids_by_location
_global_entity_types = source._global_entity_types
_last_validation_errors.clear()
@@ -0,0 +1 @@
uid://dkjcccwgeb82g
@@ -0,0 +1,129 @@
class_name DiplomaticRelationRecord
extends RefCounted
const SCHEMA_VERSION := 1
var data: Dictionary
func _init(record_data: Dictionary = {}) -> void:
data = record_data.duplicate(true)
static func create(
relation_id: StringName,
first_polity_id: StringName,
second_polity_id: StringName,
stance_id: StringName,
trust: float,
started_tick: int,
history_event_ids: Array = []
) -> DiplomaticRelationRecord:
var first := String(first_polity_id)
var second := String(second_polity_id)
if second < first:
var swapped := first
first = second
second = swapped
var normalized_history: Variant = RegionalStateSupport.normalize_history_event_ids(
history_event_ids
)
if normalized_history == null:
return null
var record := (
DiplomaticRelationRecord
. new(
{
"schema_version": SCHEMA_VERSION,
"relation_id": String(relation_id),
"first_polity_id": first,
"second_polity_id": second,
"stance_id": String(stance_id),
"trust": trust,
"started_tick": started_tick,
"history_event_ids": normalized_history,
}
)
)
return record if record.is_valid() else null
static func from_dictionary(record_data: Dictionary) -> DiplomaticRelationRecord:
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
return null
if not (
record_data
. has_all(
[
"relation_id",
"first_polity_id",
"second_polity_id",
"stance_id",
"trust",
"started_tick",
"history_event_ids",
]
)
):
return null
if not record_data["history_event_ids"] is Array:
return null
return create(
StringName(record_data["relation_id"]),
StringName(record_data["first_polity_id"]),
StringName(record_data["second_polity_id"]),
StringName(record_data["stance_id"]),
float(record_data["trust"]),
int(record_data["started_tick"]),
record_data["history_event_ids"]
)
func is_valid() -> bool:
return (
not get_relation_id().is_empty()
and not get_first_polity_id().is_empty()
and not get_second_polity_id().is_empty()
and String(get_first_polity_id()) < String(get_second_polity_id())
and not get_stance_id().is_empty()
and is_finite(get_trust())
and get_trust() >= -1.0
and get_trust() <= 1.0
and get_started_tick() >= 0
)
func get_relation_id() -> StringName:
return StringName(data["relation_id"])
func get_first_polity_id() -> StringName:
return StringName(data["first_polity_id"])
func get_second_polity_id() -> StringName:
return StringName(data["second_polity_id"])
func get_stance_id() -> StringName:
return StringName(data["stance_id"])
func get_trust() -> float:
return float(data["trust"])
func get_started_tick() -> int:
return int(data["started_tick"])
func get_history_event_ids() -> Array[int]:
return RegionalStateSupport.copy_int_array(data["history_event_ids"])
func get_pair_key() -> String:
return RegionalStateSupport.pair_key(get_first_polity_id(), get_second_polity_id())
func to_dictionary() -> Dictionary:
return data.duplicate(true)
@@ -0,0 +1 @@
uid://cd7j3wipdurdt
+110
View File
@@ -0,0 +1,110 @@
class_name LocationStateRecord
extends RefCounted
const SCHEMA_VERSION := 1
var data: Dictionary
func _init(record_data: Dictionary = {}) -> void:
data = record_data.duplicate(true)
static func create(
location_id: StringName,
world_id: StringName,
location_type: StringName,
display_name: String,
address: SpatialAddress,
history_event_ids: Array = []
) -> LocationStateRecord:
var normalized_history: Variant = RegionalStateSupport.normalize_history_event_ids(
history_event_ids
)
var normalized_address: Variant = RegionalStateSupport.normalize_address(address)
if normalized_history == null or normalized_address == null:
return null
var record := (
LocationStateRecord
. new(
{
"schema_version": SCHEMA_VERSION,
"location_id": String(location_id),
"world_id": String(world_id),
"location_type": String(location_type),
"display_name": display_name,
"address": normalized_address,
"history_event_ids": normalized_history,
}
)
)
return record if record.is_valid() else null
static func from_dictionary(record_data: Dictionary) -> LocationStateRecord:
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
return null
if not (
record_data
. has_all(
[
"location_id",
"world_id",
"location_type",
"display_name",
"address",
"history_event_ids",
]
)
):
return null
if not record_data["address"] is Dictionary or not record_data["history_event_ids"] is Array:
return null
return create(
StringName(record_data["location_id"]),
StringName(record_data["world_id"]),
StringName(record_data["location_type"]),
String(record_data["display_name"]),
SpatialAddress.from_dictionary(record_data["address"]),
record_data["history_event_ids"]
)
func is_valid() -> bool:
return (
not get_location_id().is_empty()
and not get_world_id().is_empty()
and not get_location_type().is_empty()
and not get_display_name().is_empty()
and RegionalStateSupport.address_matches(
data.get("address"), get_world_id(), get_location_id()
)
)
func get_location_id() -> StringName:
return StringName(data["location_id"])
func get_world_id() -> StringName:
return StringName(data["world_id"])
func get_location_type() -> StringName:
return StringName(data["location_type"])
func get_display_name() -> String:
return String(data["display_name"])
func get_address() -> SpatialAddress:
return RegionalStateSupport.address_from_data(data["address"])
func get_history_event_ids() -> Array[int]:
return RegionalStateSupport.copy_int_array(data["history_event_ids"])
func to_dictionary() -> Dictionary:
return data.duplicate(true)
@@ -0,0 +1 @@
uid://cu05dxii51mnv
+406
View File
@@ -0,0 +1,406 @@
class_name MobileGroupStateRecord
extends RefCounted
const SCHEMA_VERSION := 1
var data: Dictionary
func _init(record_data: Dictionary = {}) -> void:
data = record_data.duplicate(true)
static func create_at_location(
group_id: StringName,
group_type: StringName,
display_name: String,
polity_id: StringName,
address: SpatialAddress,
detailed_member_ids: Array = [],
cohort_counts: Dictionary = {},
cargo_ledger: Dictionary = {},
cargo_capacity: float = 0.0,
last_cargo_event_id: int = -1,
history_event_ids: Array = []
) -> MobileGroupStateRecord:
return _create(
group_id,
group_type,
display_name,
polity_id,
address,
&"",
&"",
&"",
-1,
-1,
detailed_member_ids,
cohort_counts,
cargo_ledger,
cargo_capacity,
last_cargo_event_id,
history_event_ids
)
static func create_on_route(
group_id: StringName,
group_type: StringName,
display_name: String,
polity_id: StringName,
route_id: StringName,
departure_location_id: StringName,
arrival_location_id: StringName,
departure_tick: int,
arrival_tick: int,
detailed_member_ids: Array = [],
cohort_counts: Dictionary = {},
cargo_ledger: Dictionary = {},
cargo_capacity: float = 0.0,
last_cargo_event_id: int = -1,
history_event_ids: Array = []
) -> MobileGroupStateRecord:
return _create(
group_id,
group_type,
display_name,
polity_id,
null,
route_id,
departure_location_id,
arrival_location_id,
departure_tick,
arrival_tick,
detailed_member_ids,
cohort_counts,
cargo_ledger,
cargo_capacity,
last_cargo_event_id,
history_event_ids
)
static func _create(
group_id: StringName,
group_type: StringName,
display_name: String,
polity_id: StringName,
address: Variant,
route_id: StringName,
departure_location_id: StringName,
arrival_location_id: StringName,
departure_tick: int,
arrival_tick: int,
detailed_member_ids: Array,
cohort_counts: Dictionary,
cargo_ledger: Dictionary,
cargo_capacity: float,
last_cargo_event_id: int,
history_event_ids: Array
) -> MobileGroupStateRecord:
var normalized_members: Variant = RegionalStateSupport.normalize_id_array(detailed_member_ids)
var normalized_cohorts: Variant = RegionalStateSupport.normalize_count_ledger(cohort_counts)
var normalized_cargo: Variant = RegionalStateSupport.normalize_amount_ledger(cargo_ledger)
var normalized_history: Variant = RegionalStateSupport.normalize_history_event_ids(
history_event_ids
)
var normalized_address: Variant = null
if address != null:
normalized_address = RegionalStateSupport.normalize_address(address)
if (
normalized_members == null
or normalized_cohorts == null
or normalized_cargo == null
or normalized_history == null
or (address != null and normalized_address == null)
):
return null
var record := (
MobileGroupStateRecord
. new(
{
"schema_version": SCHEMA_VERSION,
"group_id": String(group_id),
"group_type": String(group_type),
"display_name": display_name,
"polity_id": String(polity_id),
"location_address": normalized_address,
"route_id": String(route_id),
"departure_location_id": String(departure_location_id),
"arrival_location_id": String(arrival_location_id),
"departure_tick": departure_tick,
"arrival_tick": arrival_tick,
"detailed_member_ids": normalized_members,
"cohort_counts": normalized_cohorts,
"cargo_ledger": normalized_cargo,
"cargo_capacity": cargo_capacity,
"last_cargo_event_id": last_cargo_event_id,
"history_event_ids": normalized_history,
}
)
)
return record if record.is_valid() else null
static func from_dictionary(record_data: Dictionary) -> MobileGroupStateRecord:
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
return null
if not (
record_data
. has_all(
[
"group_id",
"group_type",
"display_name",
"polity_id",
"location_address",
"route_id",
"departure_location_id",
"arrival_location_id",
"departure_tick",
"arrival_tick",
"detailed_member_ids",
"cohort_counts",
"cargo_ledger",
"cargo_capacity",
"last_cargo_event_id",
"history_event_ids",
]
)
):
return null
if (
(
record_data["location_address"] != null
and not record_data["location_address"] is Dictionary
)
or not record_data["detailed_member_ids"] is Array
or not record_data["cohort_counts"] is Dictionary
or not record_data["cargo_ledger"] is Dictionary
or not record_data["history_event_ids"] is Array
):
return null
return _create(
StringName(record_data["group_id"]),
StringName(record_data["group_type"]),
String(record_data["display_name"]),
StringName(record_data["polity_id"]),
record_data["location_address"],
StringName(record_data["route_id"]),
StringName(record_data["departure_location_id"]),
StringName(record_data["arrival_location_id"]),
int(record_data["departure_tick"]),
int(record_data["arrival_tick"]),
record_data["detailed_member_ids"],
record_data["cohort_counts"],
record_data["cargo_ledger"],
float(record_data["cargo_capacity"]),
int(record_data["last_cargo_event_id"]),
record_data["history_event_ids"]
)
func is_valid() -> bool:
if (
get_group_id().is_empty()
or get_group_type().is_empty()
or get_display_name().is_empty()
or get_polity_id().is_empty()
or not is_finite(get_cargo_capacity())
or get_cargo_capacity() < 0.0
or RegionalStateSupport.amount_ledger_total(data["cargo_ledger"]) > get_cargo_capacity()
or get_last_cargo_event_id() < -1
or (
get_last_cargo_event_id() >= 0
and get_last_cargo_event_id() not in get_history_event_ids()
)
):
return false
if is_at_location():
return (
get_route_id().is_empty()
and get_departure_location_id().is_empty()
and get_arrival_location_id().is_empty()
and get_departure_tick() == -1
and get_arrival_tick() == -1
)
return (
is_on_route()
and not get_departure_location_id().is_empty()
and not get_arrival_location_id().is_empty()
and get_departure_location_id() != get_arrival_location_id()
and get_departure_tick() >= 0
and get_arrival_tick() > get_departure_tick()
)
func is_at_location() -> bool:
return data["location_address"] is Dictionary
func is_on_route() -> bool:
return data["location_address"] == null and not get_route_id().is_empty()
func get_group_id() -> StringName:
return StringName(data["group_id"])
func get_group_type() -> StringName:
return StringName(data["group_type"])
func get_display_name() -> String:
return String(data["display_name"])
func get_polity_id() -> StringName:
return StringName(data["polity_id"])
func get_location_address() -> SpatialAddress:
return RegionalStateSupport.address_from_data(data["location_address"])
func get_current_location_id() -> StringName:
var address := get_location_address()
return address.get_location_id() if address != null else &""
func get_route_id() -> StringName:
return StringName(data["route_id"])
func get_departure_location_id() -> StringName:
return StringName(data["departure_location_id"])
func get_arrival_location_id() -> StringName:
return StringName(data["arrival_location_id"])
func get_departure_tick() -> int:
return int(data["departure_tick"])
func get_arrival_tick() -> int:
return int(data["arrival_tick"])
func get_detailed_member_ids() -> Array[StringName]:
return RegionalStateSupport.copy_string_array(data["detailed_member_ids"])
func get_cohort_counts() -> Dictionary:
return data["cohort_counts"].duplicate(true)
func get_cargo_ledger() -> Dictionary:
return data["cargo_ledger"].duplicate(true)
func get_cargo_amount(item_id: StringName) -> float:
return float(data["cargo_ledger"].get(String(item_id), 0.0))
func get_total_cargo_amount() -> float:
return RegionalStateSupport.amount_ledger_total(data["cargo_ledger"])
func get_cargo_capacity() -> float:
return float(data["cargo_capacity"])
func get_last_cargo_event_id() -> int:
return int(data["last_cargo_event_id"])
func get_history_event_ids() -> Array[int]:
return RegionalStateSupport.copy_int_array(data["history_event_ids"])
func get_headcount() -> int:
var headcount := get_detailed_member_ids().size()
for count: Variant in data["cohort_counts"].values():
headcount += int(count)
return headcount
func with_location(
address: SpatialAddress, cause_event_id: int = -1
) -> MobileGroupStateRecord:
var history_event_ids := _history_with_event(cause_event_id)
return create_at_location(
get_group_id(),
get_group_type(),
get_display_name(),
get_polity_id(),
address,
data["detailed_member_ids"],
data["cohort_counts"],
data["cargo_ledger"],
get_cargo_capacity(),
get_last_cargo_event_id(),
history_event_ids
)
func with_route_journey(
route_id: StringName,
departure_location_id: StringName,
arrival_location_id: StringName,
departure_tick: int,
arrival_tick: int,
cause_event_id: int = -1
) -> MobileGroupStateRecord:
var history_event_ids := _history_with_event(cause_event_id)
return create_on_route(
get_group_id(),
get_group_type(),
get_display_name(),
get_polity_id(),
route_id,
departure_location_id,
arrival_location_id,
departure_tick,
arrival_tick,
data["detailed_member_ids"],
data["cohort_counts"],
data["cargo_ledger"],
get_cargo_capacity(),
get_last_cargo_event_id(),
history_event_ids
)
func with_cargo_ledger(cargo_ledger: Dictionary, cargo_event_id: int) -> MobileGroupStateRecord:
var history_event_ids := _history_with_event(cargo_event_id)
return _create(
get_group_id(),
get_group_type(),
get_display_name(),
get_polity_id(),
data["location_address"],
get_route_id(),
get_departure_location_id(),
get_arrival_location_id(),
get_departure_tick(),
get_arrival_tick(),
data["detailed_member_ids"],
data["cohort_counts"],
cargo_ledger,
get_cargo_capacity(),
cargo_event_id,
history_event_ids
)
func _history_with_event(event_id: int) -> Array:
var history_event_ids: Array = data["history_event_ids"].duplicate()
if event_id >= 0 and event_id not in history_event_ids:
history_event_ids.append(event_id)
return history_event_ids
func to_dictionary() -> Dictionary:
return data.duplicate(true)
@@ -0,0 +1 @@
uid://jyqncykmnbfi
+183
View File
@@ -0,0 +1,183 @@
class_name PersonStateRecord
extends RefCounted
const SCHEMA_VERSION := 1
var data: Dictionary
func _init(record_data: Dictionary = {}) -> void:
data = record_data.duplicate(true)
static func create_at_location(
person_id: StringName,
display_name: String,
role_id: StringName,
polity_id: StringName,
home_location_id: StringName,
address: SpatialAddress,
history_event_ids: Array = []
) -> PersonStateRecord:
return _create(
person_id,
display_name,
role_id,
polity_id,
home_location_id,
address,
&"",
history_event_ids
)
static func create_in_group(
person_id: StringName,
display_name: String,
role_id: StringName,
polity_id: StringName,
home_location_id: StringName,
mobile_group_id: StringName,
history_event_ids: Array = []
) -> PersonStateRecord:
return _create(
person_id,
display_name,
role_id,
polity_id,
home_location_id,
null,
mobile_group_id,
history_event_ids
)
static func _create(
person_id: StringName,
display_name: String,
role_id: StringName,
polity_id: StringName,
home_location_id: StringName,
address: Variant,
mobile_group_id: StringName,
history_event_ids: Array
) -> PersonStateRecord:
var normalized_address: Variant = null
if address != null:
normalized_address = RegionalStateSupport.normalize_address(address)
var normalized_history: Variant = RegionalStateSupport.normalize_history_event_ids(
history_event_ids
)
if normalized_history == null or (address != null and normalized_address == null):
return null
var record := (
PersonStateRecord
. new(
{
"schema_version": SCHEMA_VERSION,
"person_id": String(person_id),
"display_name": display_name,
"role_id": String(role_id),
"polity_id": String(polity_id),
"home_location_id": String(home_location_id),
"location_address": normalized_address,
"mobile_group_id": String(mobile_group_id),
"history_event_ids": normalized_history,
}
)
)
return record if record.is_valid() else null
static func from_dictionary(record_data: Dictionary) -> PersonStateRecord:
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
return null
if not (
record_data
. has_all(
[
"person_id",
"display_name",
"role_id",
"polity_id",
"home_location_id",
"location_address",
"mobile_group_id",
"history_event_ids",
]
)
):
return null
if (
(
record_data["location_address"] != null
and not record_data["location_address"] is Dictionary
)
or not record_data["history_event_ids"] is Array
):
return null
return _create(
StringName(record_data["person_id"]),
String(record_data["display_name"]),
StringName(record_data["role_id"]),
StringName(record_data["polity_id"]),
StringName(record_data["home_location_id"]),
record_data["location_address"],
StringName(record_data["mobile_group_id"]),
record_data["history_event_ids"]
)
func is_valid() -> bool:
return (
not get_person_id().is_empty()
and not get_display_name().is_empty()
and not get_role_id().is_empty()
and not get_polity_id().is_empty()
and not get_home_location_id().is_empty()
and (is_at_location() != is_in_mobile_group())
)
func get_person_id() -> StringName:
return StringName(data["person_id"])
func get_display_name() -> String:
return String(data["display_name"])
func get_role_id() -> StringName:
return StringName(data["role_id"])
func get_polity_id() -> StringName:
return StringName(data["polity_id"])
func get_home_location_id() -> StringName:
return StringName(data["home_location_id"])
func is_at_location() -> bool:
return data["location_address"] is Dictionary and get_mobile_group_id().is_empty()
func is_in_mobile_group() -> bool:
return data["location_address"] == null and not get_mobile_group_id().is_empty()
func get_location_address() -> SpatialAddress:
return RegionalStateSupport.address_from_data(data["location_address"])
func get_mobile_group_id() -> StringName:
return StringName(data["mobile_group_id"])
func get_history_event_ids() -> Array[int]:
return RegionalStateSupport.copy_int_array(data["history_event_ids"])
func to_dictionary() -> Dictionary:
return data.duplicate(true)
@@ -0,0 +1 @@
uid://bfotwdi60mdnm
+98
View File
@@ -0,0 +1,98 @@
class_name PolityStateRecord
extends RefCounted
const SCHEMA_VERSION := 1
var data: Dictionary
func _init(record_data: Dictionary = {}) -> void:
data = record_data.duplicate(true)
static func create(
polity_id: StringName,
display_name: String,
capital_location_id: StringName,
settlement_ids: Array = [],
history_event_ids: Array = []
) -> PolityStateRecord:
var normalized_settlements: Variant = RegionalStateSupport.normalize_id_array(settlement_ids)
var normalized_history: Variant = RegionalStateSupport.normalize_history_event_ids(
history_event_ids
)
if normalized_settlements == null or normalized_history == null:
return null
var record := (
PolityStateRecord
. new(
{
"schema_version": SCHEMA_VERSION,
"polity_id": String(polity_id),
"display_name": display_name,
"capital_location_id": String(capital_location_id),
"settlement_ids": normalized_settlements,
"history_event_ids": normalized_history,
}
)
)
return record if record.is_valid() else null
static func from_dictionary(record_data: Dictionary) -> PolityStateRecord:
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
return null
if not (
record_data
. has_all(
[
"polity_id",
"display_name",
"capital_location_id",
"settlement_ids",
"history_event_ids",
]
)
):
return null
if not record_data["settlement_ids"] is Array or not record_data["history_event_ids"] is Array:
return null
return create(
StringName(record_data["polity_id"]),
String(record_data["display_name"]),
StringName(record_data["capital_location_id"]),
record_data["settlement_ids"],
record_data["history_event_ids"]
)
func is_valid() -> bool:
return (
not get_polity_id().is_empty()
and not get_display_name().is_empty()
and not get_capital_location_id().is_empty()
)
func get_polity_id() -> StringName:
return StringName(data["polity_id"])
func get_display_name() -> String:
return String(data["display_name"])
func get_capital_location_id() -> StringName:
return StringName(data["capital_location_id"])
func get_settlement_ids() -> Array[StringName]:
return RegionalStateSupport.copy_string_array(data["settlement_ids"])
func get_history_event_ids() -> Array[int]:
return RegionalStateSupport.copy_int_array(data["history_event_ids"])
func to_dictionary() -> Dictionary:
return data.duplicate(true)
@@ -0,0 +1 @@
uid://b1hsgds86deba
+147
View File
@@ -0,0 +1,147 @@
class_name PopulationCohortRecord
extends RefCounted
const SCHEMA_VERSION := 1
var data: Dictionary
func _init(record_data: Dictionary = {}) -> void:
data = record_data.duplicate(true)
static func create_at_location(
cohort_id: StringName,
cohort_type: StringName,
polity_id: StringName,
count: int,
location_id: StringName,
history_event_ids: Array = []
) -> PopulationCohortRecord:
return _create(cohort_id, cohort_type, polity_id, count, location_id, &"", history_event_ids)
static func create_in_group(
cohort_id: StringName,
cohort_type: StringName,
polity_id: StringName,
count: int,
mobile_group_id: StringName,
history_event_ids: Array = []
) -> PopulationCohortRecord:
return _create(
cohort_id, cohort_type, polity_id, count, &"", mobile_group_id, history_event_ids
)
static func _create(
cohort_id: StringName,
cohort_type: StringName,
polity_id: StringName,
count: int,
location_id: StringName,
mobile_group_id: StringName,
history_event_ids: Array
) -> PopulationCohortRecord:
var normalized_history: Variant = RegionalStateSupport.normalize_history_event_ids(
history_event_ids
)
if normalized_history == null:
return null
var record := (
PopulationCohortRecord
. new(
{
"schema_version": SCHEMA_VERSION,
"cohort_id": String(cohort_id),
"cohort_type": String(cohort_type),
"polity_id": String(polity_id),
"count": count,
"location_id": String(location_id),
"mobile_group_id": String(mobile_group_id),
"history_event_ids": normalized_history,
}
)
)
return record if record.is_valid() else null
static func from_dictionary(record_data: Dictionary) -> PopulationCohortRecord:
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
return null
if not (
record_data
. has_all(
[
"cohort_id",
"cohort_type",
"polity_id",
"count",
"location_id",
"mobile_group_id",
"history_event_ids",
]
)
):
return null
if not record_data["history_event_ids"] is Array:
return null
return _create(
StringName(record_data["cohort_id"]),
StringName(record_data["cohort_type"]),
StringName(record_data["polity_id"]),
int(record_data["count"]),
StringName(record_data["location_id"]),
StringName(record_data["mobile_group_id"]),
record_data["history_event_ids"]
)
func is_valid() -> bool:
return (
not get_cohort_id().is_empty()
and not get_cohort_type().is_empty()
and not get_polity_id().is_empty()
and get_count() > 0
and (is_at_location() != is_in_mobile_group())
)
func get_cohort_id() -> StringName:
return StringName(data["cohort_id"])
func get_cohort_type() -> StringName:
return StringName(data["cohort_type"])
func get_polity_id() -> StringName:
return StringName(data["polity_id"])
func get_count() -> int:
return int(data["count"])
func is_at_location() -> bool:
return not get_location_id().is_empty() and get_mobile_group_id().is_empty()
func is_in_mobile_group() -> bool:
return get_location_id().is_empty() and not get_mobile_group_id().is_empty()
func get_location_id() -> StringName:
return StringName(data["location_id"])
func get_mobile_group_id() -> StringName:
return StringName(data["mobile_group_id"])
func get_history_event_ids() -> Array[int]:
return RegionalStateSupport.copy_int_array(data["history_event_ids"])
func to_dictionary() -> Dictionary:
return data.duplicate(true)
@@ -0,0 +1 @@
uid://di6mlijatrbgu
+117
View File
@@ -0,0 +1,117 @@
class_name RouteStateRecord
extends RefCounted
const SCHEMA_VERSION := 1
var data: Dictionary
func _init(record_data: Dictionary = {}) -> void:
data = record_data.duplicate(true)
static func create(
route_id: StringName,
from_location_id: StringName,
to_location_id: StringName,
travel_ticks: int,
bidirectional: bool = true,
history_event_ids: Array = []
) -> RouteStateRecord:
var normalized_history: Variant = RegionalStateSupport.normalize_history_event_ids(
history_event_ids
)
if normalized_history == null:
return null
var record := (
RouteStateRecord
. new(
{
"schema_version": SCHEMA_VERSION,
"route_id": String(route_id),
"from_location_id": String(from_location_id),
"to_location_id": String(to_location_id),
"travel_ticks": travel_ticks,
"bidirectional": bidirectional,
"history_event_ids": normalized_history,
}
)
)
return record if record.is_valid() else null
static func from_dictionary(record_data: Dictionary) -> RouteStateRecord:
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
return null
if not (
record_data
. has_all(
[
"route_id",
"from_location_id",
"to_location_id",
"travel_ticks",
"bidirectional",
"history_event_ids",
]
)
):
return null
if not record_data["bidirectional"] is bool or not record_data["history_event_ids"] is Array:
return null
return create(
StringName(record_data["route_id"]),
StringName(record_data["from_location_id"]),
StringName(record_data["to_location_id"]),
int(record_data["travel_ticks"]),
bool(record_data["bidirectional"]),
record_data["history_event_ids"]
)
func is_valid() -> bool:
return (
not get_route_id().is_empty()
and not get_from_location_id().is_empty()
and not get_to_location_id().is_empty()
and get_from_location_id() != get_to_location_id()
and get_travel_ticks() > 0
)
func get_route_id() -> StringName:
return StringName(data["route_id"])
func get_from_location_id() -> StringName:
return StringName(data["from_location_id"])
func get_to_location_id() -> StringName:
return StringName(data["to_location_id"])
func get_travel_ticks() -> int:
return int(data["travel_ticks"])
func is_bidirectional() -> bool:
return bool(data["bidirectional"])
func get_history_event_ids() -> Array[int]:
return RegionalStateSupport.copy_int_array(data["history_event_ids"])
func connects(first_location_id: StringName, second_location_id: StringName) -> bool:
if first_location_id == get_from_location_id() and second_location_id == get_to_location_id():
return true
return (
is_bidirectional()
and first_location_id == get_to_location_id()
and second_location_id == get_from_location_id()
)
func to_dictionary() -> Dictionary:
return data.duplicate(true)
+1
View File
@@ -0,0 +1 @@
uid://dw73sybt7s5cx
+166
View File
@@ -0,0 +1,166 @@
class_name SettlementStateRecord
extends RefCounted
const SCHEMA_VERSION := 1
var data: Dictionary
func _init(record_data: Dictionary = {}) -> void:
data = record_data.duplicate(true)
static func create(
settlement_id: StringName,
location_id: StringName,
display_name: String,
polity_id: StringName,
tier: int,
founding_group_id: StringName,
founded_tick: int,
stockpile_ledger: Dictionary = {},
history_event_ids: Array = []
) -> SettlementStateRecord:
var normalized_stockpile: Variant = RegionalStateSupport.normalize_amount_ledger(
stockpile_ledger
)
var normalized_history: Variant = RegionalStateSupport.normalize_history_event_ids(
history_event_ids
)
if normalized_stockpile == null or normalized_history == null:
return null
var record := (
SettlementStateRecord
. new(
{
"schema_version": SCHEMA_VERSION,
"settlement_id": String(settlement_id),
"location_id": String(location_id),
"display_name": display_name,
"polity_id": String(polity_id),
"tier": tier,
"founding_group_id": String(founding_group_id),
"founded_tick": founded_tick,
"stockpile_ledger": normalized_stockpile,
"history_event_ids": normalized_history,
}
)
)
return record if record.is_valid() else null
static func from_dictionary(record_data: Dictionary) -> SettlementStateRecord:
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
return null
if not (
record_data
. has_all(
[
"settlement_id",
"location_id",
"display_name",
"polity_id",
"tier",
"founding_group_id",
"founded_tick",
"stockpile_ledger",
"history_event_ids",
]
)
):
return null
if (
not record_data["stockpile_ledger"] is Dictionary
or not record_data["history_event_ids"] is Array
):
return null
return create(
StringName(record_data["settlement_id"]),
StringName(record_data["location_id"]),
String(record_data["display_name"]),
StringName(record_data["polity_id"]),
int(record_data["tier"]),
StringName(record_data["founding_group_id"]),
int(record_data["founded_tick"]),
record_data["stockpile_ledger"],
record_data["history_event_ids"]
)
func is_valid() -> bool:
return (
not get_settlement_id().is_empty()
and not get_location_id().is_empty()
and not get_display_name().is_empty()
and not get_polity_id().is_empty()
and not get_founding_group_id().is_empty()
and get_tier() >= 0
and get_founded_tick() >= 0
)
func get_settlement_id() -> StringName:
return StringName(data["settlement_id"])
func get_location_id() -> StringName:
return StringName(data["location_id"])
func get_display_name() -> String:
return String(data["display_name"])
func get_polity_id() -> StringName:
return StringName(data["polity_id"])
func get_tier() -> int:
return int(data["tier"])
func get_founding_group_id() -> StringName:
return StringName(data["founding_group_id"])
func get_founded_tick() -> int:
return int(data["founded_tick"])
func get_stockpile_ledger() -> Dictionary:
return data["stockpile_ledger"].duplicate(true)
func get_stockpile_amount(item_id: StringName) -> float:
return float(data["stockpile_ledger"].get(String(item_id), 0.0))
func get_total_stockpile_amount() -> float:
return RegionalStateSupport.amount_ledger_total(data["stockpile_ledger"])
func get_history_event_ids() -> Array[int]:
return RegionalStateSupport.copy_int_array(data["history_event_ids"])
func with_stockpile_ledger(
stockpile_ledger: Dictionary, cause_event_id: int = -1
) -> SettlementStateRecord:
var history_event_ids: Array = data["history_event_ids"].duplicate()
if cause_event_id >= 0 and cause_event_id not in history_event_ids:
history_event_ids.append(cause_event_id)
return create(
get_settlement_id(),
get_location_id(),
get_display_name(),
get_polity_id(),
get_tier(),
get_founding_group_id(),
get_founded_tick(),
stockpile_ledger,
history_event_ids
)
func to_dictionary() -> Dictionary:
return data.duplicate(true)
@@ -0,0 +1 @@
uid://8ofxqx8bfmyg
+334
View File
@@ -0,0 +1,334 @@
extends GutTest
func test_canonical_primitive_round_trip_rebuilds_catalog_indexes() -> void:
var state := _fixture_state()
var saved := state.to_dictionary()
var restored := RegionalWorldState.from_dictionary(saved)
assert_not_null(restored)
assert_true(_is_primitive_only(saved))
assert_eq(restored.to_dictionary(), saved)
assert_eq(restored.checksum(), state.checksum())
assert_true(restored.has_global_id(&"group_blue_caravan"))
assert_eq(restored.get_entity_type(&"group_blue_caravan"), &"mobile_group")
assert_eq(restored.get_all_persons().size(), 3)
assert_eq(saved["locations"][0]["location_id"], "location_jajce")
assert_eq(saved["locations"][1]["location_id"], "location_travnik")
assert_eq(saved["mobile_groups"][0]["cargo_ledger"].keys(), ["food", "wood"])
assert_eq(_person_ids(restored.get_persons_in_group(&"group_blue_caravan")), [&"person_ana"])
assert_eq(
_cohort_ids(restored.get_cohorts_in_group(&"group_blue_caravan")), [&"cohort_carriers"]
)
assert_eq(
_route_ids(restored.get_routes_for_location(&"location_jajce")), [&"route_jajce_travnik"]
)
assert_eq(
_group_ids(restored.get_mobile_groups_on_route(&"route_jajce_travnik")),
[&"group_blue_caravan"]
)
assert_same(
restored.get_diplomatic_relation_between(&"polity_travnik", &"polity_jajce"),
restored.get_diplomatic_relation(&"relation_jajce_travnik")
)
func test_equal_local_coordinates_remain_scoped_to_their_locations() -> void:
var state := _fixture_state()
var jajce_people := state.get_persons_at_location(&"location_jajce")
var travnik_people := state.get_persons_at_location(&"location_travnik")
assert_eq(_person_ids(jajce_people), [&"person_boris"])
assert_eq(_person_ids(travnik_people), [&"person_cemal"])
assert_eq(jajce_people[0].get_location_address().get_position(), Vector3(1.0, 0.0, 1.0))
assert_eq(travnik_people[0].get_location_address().get_position(), Vector3(1.0, 0.0, 1.0))
assert_ne(
jajce_people[0].get_location_address().index_key(),
travnik_people[0].get_location_address().index_key()
)
var before := state.to_dictionary()
var wrong_world := before.duplicate(true)
for person_data: Dictionary in wrong_world["persons"]:
if person_data["person_id"] == "person_boris":
person_data["location_address"]["world_id"] = "another_world"
assert_false(state.restore_from_dictionary(wrong_world))
assert_true(_errors_contain(state.get_last_validation_errors(), "cross-world"))
assert_eq(state.to_dictionary(), before)
func test_duplicate_and_orphan_memberships_are_rejected_transactionally() -> void:
var state := _fixture_state()
var before := state.to_dictionary()
var duplicate := before.duplicate(true)
duplicate["persons"].append(duplicate["persons"][0].duplicate(true))
assert_false(state.restore_from_dictionary(duplicate))
assert_true(_errors_contain(state.get_last_validation_errors(), "duplicate global ID"))
assert_eq(state.to_dictionary(), before)
var orphan := before.duplicate(true)
for person_data: Dictionary in orphan["persons"]:
if person_data["person_id"] == "person_ana":
person_data["mobile_group_id"] = "missing_caravan"
assert_false(state.restore_from_dictionary(orphan))
assert_true(_errors_contain(state.get_last_validation_errors(), "missing_caravan"))
assert_eq(state.to_dictionary(), before)
var duplicate_owner := before.duplicate(true)
var second_group := MobileGroupStateRecord.create_on_route(
&"group_second_caravan",
&"caravan",
"Second caravan",
&"polity_jajce",
&"route_jajce_travnik",
&"location_jajce",
&"location_travnik",
20,
32,
[&"person_ana"]
)
duplicate_owner["mobile_groups"].append(second_group.to_dictionary())
assert_false(state.restore_from_dictionary(duplicate_owner))
assert_true(_errors_contain(state.get_last_validation_errors(), "multiple mobile groups"))
assert_eq(state.to_dictionary(), before)
func test_cargo_and_cohort_payloads_are_conservation_friendly() -> void:
assert_null(
MobileGroupStateRecord.create_at_location(
&"overloaded",
&"caravan",
"Overloaded",
&"polity_jajce",
_location_address(&"location_jajce"),
[],
{},
{&"food": 3.0},
2.0
)
)
assert_null(
SettlementStateRecord.create(
&"bad_settlement",
&"location_jajce",
"Bad",
&"polity_jajce",
0,
&"founders",
0,
{&"food": -1.0}
)
)
var state := _fixture_state()
var caravan := state.get_mobile_group(&"group_blue_caravan")
var settlement := state.get_settlement(&"settlement_jajce")
assert_eq(caravan.get_cohort_counts(), {"cohort_carriers": 3})
assert_eq(caravan.get_cargo_ledger(), {"food": 5.0, "wood": 2.0})
assert_eq(caravan.get_last_cargo_event_id(), 41)
var food_before := settlement.get_stockpile_amount(&"food") + caravan.get_cargo_amount(&"food")
var changed_settlement := settlement.with_stockpile_ledger({&"food": 22.0, &"wood": 12.0}, 57)
var changed_cargo := caravan.with_cargo_ledger({&"food": 8.0, &"wood": 2.0}, 57)
assert_not_null(changed_settlement)
assert_not_null(changed_cargo)
assert_eq(changed_cargo.get_last_cargo_event_id(), 57)
assert_eq(changed_cargo.get_cargo_ledger(), {"food": 8.0, "wood": 2.0})
assert_true(57 in changed_cargo.get_history_event_ids())
assert_true(57 in changed_settlement.get_history_event_ids())
assert_eq(
changed_settlement.get_stockpile_amount(&"food") + changed_cargo.get_cargo_amount(&"food"),
food_before
)
assert_eq(caravan.get_cargo_ledger(), {"food": 5.0, "wood": 2.0})
var mismatch := state.to_dictionary()
mismatch["mobile_groups"][0]["cohort_counts"]["cohort_carriers"] = 4
assert_false(state.restore_from_dictionary(mismatch))
assert_true(_errors_contain(state.get_last_validation_errors(), "count disagrees"))
func test_route_membership_requires_exact_endpoints_duration_and_no_address() -> void:
var state := _fixture_state()
var before := state.to_dictionary()
var bad_duration := before.duplicate(true)
bad_duration["mobile_groups"][0]["arrival_tick"] = 33
assert_false(state.restore_from_dictionary(bad_duration))
assert_true(_errors_contain(state.get_last_validation_errors(), "duration"))
assert_eq(state.to_dictionary(), before)
var bad_direction := before.duplicate(true)
bad_direction["routes"][0]["bidirectional"] = false
bad_direction["mobile_groups"][0]["departure_location_id"] = "location_travnik"
bad_direction["mobile_groups"][0]["arrival_location_id"] = "location_jajce"
assert_false(state.restore_from_dictionary(bad_direction))
assert_true(_errors_contain(state.get_last_validation_errors(), "direction/endpoints"))
assert_eq(state.to_dictionary(), before)
func _fixture_state() -> RegionalWorldState:
var jajce := LocationStateRecord.create(
&"location_jajce",
&"regional_bosnia",
&"settlement",
"Jajce",
_location_address(&"location_jajce"),
[8, 2]
)
var travnik := LocationStateRecord.create(
&"location_travnik",
&"regional_bosnia",
&"settlement",
"Travnik",
_location_address(&"location_travnik")
)
var settlement_jajce := SettlementStateRecord.create(
&"settlement_jajce",
&"location_jajce",
"Jajce",
&"polity_jajce",
2,
&"founders_jajce",
0,
{&"wood": 12.0, &"food": 25.0},
[12, 3]
)
var settlement_travnik := SettlementStateRecord.create(
&"settlement_travnik",
&"location_travnik",
"Travnik",
&"polity_travnik",
1,
&"founders_travnik",
1,
{&"food": 18.0}
)
var route := RouteStateRecord.create(
&"route_jajce_travnik", &"location_jajce", &"location_travnik", 12, true, [19]
)
var caravan := MobileGroupStateRecord.create_on_route(
&"group_blue_caravan",
&"caravan",
"Blue caravan",
&"polity_jajce",
&"route_jajce_travnik",
&"location_jajce",
&"location_travnik",
20,
32,
[&"person_ana"],
{&"cohort_carriers": 3},
{&"wood": 2.0, &"food": 5.0},
10.0,
41,
[41, 18]
)
var ana := PersonStateRecord.create_in_group(
&"person_ana", "Ana", &"merchant", &"polity_jajce", &"location_jajce", &"group_blue_caravan"
)
var boris := PersonStateRecord.create_at_location(
&"person_boris",
"Boris",
&"steward",
&"polity_jajce",
&"location_jajce",
SpatialAddress.create(&"regional_bosnia", &"location_jajce", Vector3(1.0, 0.0, 1.0))
)
var cemal := PersonStateRecord.create_at_location(
&"person_cemal",
"Cemal",
&"steward",
&"polity_travnik",
&"location_travnik",
SpatialAddress.create(&"regional_bosnia", &"location_travnik", Vector3(1.0, 0.0, 1.0))
)
var carriers := PopulationCohortRecord.create_in_group(
&"cohort_carriers", &"adult_carriers", &"polity_jajce", 3, &"group_blue_caravan"
)
var residents := PopulationCohortRecord.create_at_location(
&"cohort_travnik_residents", &"residents", &"polity_travnik", 20, &"location_travnik"
)
var jajce_polity := PolityStateRecord.create(
&"polity_jajce", "Jajce council", &"location_jajce", [&"settlement_jajce"]
)
var travnik_polity := PolityStateRecord.create(
&"polity_travnik", "Travnik council", &"location_travnik", [&"settlement_travnik"]
)
var relation := DiplomaticRelationRecord.create(
&"relation_jajce_travnik", &"polity_travnik", &"polity_jajce", &"trade", 0.4, 5, [20]
)
var locations: Array[LocationStateRecord] = [travnik, jajce]
var settlements: Array[SettlementStateRecord] = [settlement_travnik, settlement_jajce]
var routes: Array[RouteStateRecord] = [route]
var mobile_groups: Array[MobileGroupStateRecord] = [caravan]
var persons: Array[PersonStateRecord] = [cemal, ana, boris]
var cohorts: Array[PopulationCohortRecord] = [residents, carriers]
var polities: Array[PolityStateRecord] = [travnik_polity, jajce_polity]
var relations: Array[DiplomaticRelationRecord] = [relation]
return RegionalWorldState.create(
&"regional_bosnia",
locations,
settlements,
routes,
mobile_groups,
persons,
cohorts,
polities,
relations
)
func _location_address(location_id: StringName) -> SpatialAddress:
return SpatialAddress.create(&"regional_bosnia", location_id, Vector3.ZERO)
func _is_primitive_only(value: Variant) -> bool:
match typeof(value):
TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING:
return true
TYPE_ARRAY:
for item: Variant in value:
if not _is_primitive_only(item):
return false
return true
TYPE_DICTIONARY:
for key: Variant in value:
if not key is String or not _is_primitive_only(value[key]):
return false
return true
return false
func _errors_contain(errors: Array[String], fragment: String) -> bool:
for error: String in errors:
if fragment in error:
return true
return false
func _person_ids(records: Array[PersonStateRecord]) -> Array[StringName]:
var ids: Array[StringName] = []
for record: PersonStateRecord in records:
ids.append(record.get_person_id())
return ids
func _cohort_ids(records: Array[PopulationCohortRecord]) -> Array[StringName]:
var ids: Array[StringName] = []
for record: PopulationCohortRecord in records:
ids.append(record.get_cohort_id())
return ids
func _route_ids(records: Array[RouteStateRecord]) -> Array[StringName]:
var ids: Array[StringName] = []
for record: RouteStateRecord in records:
ids.append(record.get_route_id())
return ids
func _group_ids(records: Array[MobileGroupStateRecord]) -> Array[StringName]:
var ids: Array[StringName] = []
for record: MobileGroupStateRecord in records:
ids.append(record.get_group_id())
return ids
@@ -0,0 +1 @@
uid://bgp50g6mkntvb