Files
gamedev-the-steward/simulation/state/SimulationStateRecord.gd
T
2026-07-11 19:58:47 +02:00

489 lines
15 KiB
GDScript

class_name SimulationStateRecord
extends RefCounted
const SCHEMA_NAME := "the_steward.simulation"
const SCHEMA_VERSION := 6
const LEGACY_SCHEMA_VERSION := 1
const EVENT_LEGACY_SCHEMA_VERSION := 2
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
const KNOWLEDGE_LEGACY_SCHEMA_VERSION := 4
const PREVIOUS_SCHEMA_VERSION := 5
var simulation: Dictionary
var village: VillageStateRecord
var npcs: Array[NPCStateRecord] = []
var resources: Array[ResourceStateRecord] = []
var storages: Array[StorageStateRecord] = []
var economic_events: Array[EconomicEventRecord] = []
var relationships: Array[RelationshipStateRecord] = []
var event_knowledge: Array[KnownEventStateRecord] = []
func to_dictionary() -> Dictionary:
var npc_data: Array[Dictionary] = []
for npc_record in npcs:
npc_data.append(npc_record.to_dictionary())
var resource_data: Array[Dictionary] = []
for resource_record in resources:
resource_data.append(resource_record.to_dictionary())
var storage_data: Array[Dictionary] = []
for storage_record in storages:
storage_data.append(storage_record.to_dictionary())
var event_data: Array[Dictionary] = []
for event_record in economic_events:
event_data.append(event_record.to_dictionary())
var relationship_data: Array[Dictionary] = []
for relationship_record in relationships:
relationship_data.append(relationship_record.to_dictionary())
var knowledge_data: Array[Dictionary] = []
for known_event_record in event_knowledge:
knowledge_data.append(known_event_record.to_dictionary())
return {
"schema": SCHEMA_NAME,
"schema_version": SCHEMA_VERSION,
"simulation": simulation.duplicate(true),
"village": village.to_dictionary(),
"npcs": npc_data,
"resources": resource_data,
"storages": storage_data,
"economic_events": event_data,
"relationships": relationship_data,
"event_knowledge": knowledge_data
}
func to_json() -> String:
return JSON.stringify(to_dictionary())
static func from_json(json_text: String) -> SimulationStateRecord:
var parsed = JSON.parse_string(json_text)
if not parsed is Dictionary:
return null
return from_dictionary(parsed)
static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
if record_data.get("schema", "") != SCHEMA_NAME:
return null
var version := int(record_data.get("schema_version", -1))
if (
version
in [
LEGACY_SCHEMA_VERSION,
EVENT_LEGACY_SCHEMA_VERSION,
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
KNOWLEDGE_LEGACY_SCHEMA_VERSION,
PREVIOUS_SCHEMA_VERSION,
]
):
record_data = _migrate_legacy(record_data, version)
elif version != SCHEMA_VERSION:
return null
if not (
record_data
. has_all(
[
"simulation",
"village",
"npcs",
"resources",
"storages",
"economic_events",
"relationships",
"event_knowledge",
]
)
):
return null
var simulation_data = record_data["simulation"]
if not simulation_data is Dictionary:
return null
if not simulation_data.has_all(
[
"seed",
"tick_interval",
"tick_count",
"clock_accumulator",
"clock_elapsed_ticks",
"wander_random_streams",
"next_event_id"
]
):
return null
var wander_streams = simulation_data["wander_random_streams"]
if not wander_streams is Array:
return null
for stream_data in wander_streams:
if not stream_data is Dictionary:
return null
if not stream_data.has_all(["npc_id", "seed", "state"]):
return null
var village_data = record_data["village"]
if not village_data is Dictionary:
return null
var village_record := VillageStateRecord.from_dictionary(village_data)
if village_record == null:
return null
var npc_data = record_data["npcs"]
var resource_data = record_data["resources"]
var storage_data = record_data["storages"]
var event_data = record_data["economic_events"]
var relationship_data = record_data["relationships"]
var knowledge_data = record_data["event_knowledge"]
if (
not npc_data is Array
or not resource_data is Array
or not storage_data is Array
or not event_data is Array
or not relationship_data is Array
or not knowledge_data is Array
):
return null
var record := SimulationStateRecord.new()
record.simulation = simulation_data.duplicate(true)
record.village = village_record
var npc_ids := {}
for item in npc_data:
if not item is Dictionary:
return null
var npc_record := NPCStateRecord.from_dictionary(item)
if npc_record == null:
return null
var npc_id := int(npc_record.data["id"])
if npc_ids.has(npc_id):
return null
npc_ids[npc_id] = true
record.npcs.append(npc_record)
var resource_ids := {}
for item in resource_data:
if not item is Dictionary:
return null
var resource_record := ResourceStateRecord.from_dictionary(item)
if resource_record == null:
return null
var resource_id := String(resource_record.data["node_id"])
if resource_ids.has(resource_id):
return null
resource_ids[resource_id] = true
record.resources.append(resource_record)
var storage_ids := {}
for item in storage_data:
if not item is Dictionary:
return null
var storage_record := StorageStateRecord.from_dictionary(item)
if storage_record == null:
return null
var storage_id := storage_record.get_storage_id()
if storage_ids.has(storage_id):
return null
storage_ids[storage_id] = true
record.storages.append(storage_record)
var event_ids := {}
var event_records_by_id := {}
var highest_event_id := -1
for item in event_data:
if not item is Dictionary:
return null
var event_record := EconomicEventRecord.from_dictionary(item)
if event_record == null:
return null
var event_id := int(event_record.data["event_id"])
if event_ids.has(event_id):
return null
event_ids[event_id] = true
event_records_by_id[event_id] = event_record
highest_event_id = maxi(highest_event_id, event_id)
record.economic_events.append(event_record)
if int(record.simulation["next_event_id"]) <= highest_event_id:
return null
var knowledge_keys := {}
var knowledge_records_by_key := {}
for item in knowledge_data:
if not item is Dictionary:
return null
var known_event_record := KnownEventStateRecord.from_dictionary(item)
if known_event_record == null:
return null
var knower_id := known_event_record.get_knower_id()
var known_event_id := known_event_record.get_event_id()
if not npc_ids.has(knower_id) or not event_ids.has(known_event_id):
return null
var knowledge_key := "%d:%d" % [knower_id, known_event_id]
if knowledge_keys.has(knowledge_key):
return null
knowledge_keys[knowledge_key] = true
knowledge_records_by_key[knowledge_key] = known_event_record
record.event_knowledge.append(known_event_record)
for known_event_record in record.event_knowledge:
if not _is_valid_knowledge_provenance(
known_event_record, npc_ids, event_records_by_id, knowledge_records_by_key
):
return null
var relationship_keys := {}
for item in relationship_data:
if not item is Dictionary:
return null
var relationship_record := RelationshipStateRecord.from_dictionary(item)
if relationship_record == null:
return null
var observer_id := relationship_record.get_observer_id()
var subject_id := relationship_record.get_subject_id()
if not npc_ids.has(observer_id) or not npc_ids.has(subject_id):
return null
var relationship_key := "%d:%d" % [observer_id, subject_id]
if relationship_keys.has(relationship_key):
return null
var cause_event_id := relationship_record.get_last_trust_cause_event_id()
if (
cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT
and not event_ids.has(cause_event_id)
):
return null
if (
cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT
and not knowledge_keys.has("%d:%d" % [observer_id, cause_event_id])
):
return null
if cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT:
var cause_event := event_records_by_id[cause_event_id] as EconomicEventRecord
if (
int(cause_event.data["actor_id"]) != subject_id
or (
StringName(cause_event.data["event_type"])
!= SimulationIds.EVENT_STORAGE_DEPOSITED
)
or StringName(cause_event.data["item_id"]) != SimulationIds.RESOURCE_FOOD
or float(cause_event.data["amount"]) <= 0.0
):
return null
relationship_keys[relationship_key] = true
record.relationships.append(relationship_record)
return record
static func _is_valid_knowledge_provenance(
known_event: KnownEventStateRecord,
npc_ids: Dictionary,
event_records_by_id: Dictionary,
knowledge_records_by_key: Dictionary
) -> bool:
var event := event_records_by_id.get(known_event.get_event_id()) as EconomicEventRecord
if event == null:
return false
var method := known_event.get_acquisition_method()
var knower_id := known_event.get_knower_id()
var actor_id := int(event.data["actor_id"])
if method == SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY:
return true
if (
StringName(event.data["event_type"]) != SimulationIds.EVENT_STORAGE_DEPOSITED
or StringName(event.data["item_id"]) != SimulationIds.RESOURCE_FOOD
or float(event.data["amount"]) <= 0.0
):
return false
match method:
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
return knower_id == actor_id
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
return knower_id != actor_id
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
if knower_id == actor_id:
return false
var source_npc_id := known_event.get_source_npc_id()
if not npc_ids.has(source_npc_id):
return false
var source_record := (
knowledge_records_by_key.get("%d:%d" % [source_npc_id, known_event.get_event_id()])
as KnownEventStateRecord
)
return (
source_record != null
and (
source_record.get_acquisition_method()
in [
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
]
)
)
return false
static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary:
var migrated := legacy_data.duplicate(true)
migrated["schema_version"] = SCHEMA_VERSION
if version == LEGACY_SCHEMA_VERSION:
var village_data: Dictionary = legacy_data.get("village", {})
var initial_food := float(village_data.get("food", 0.0))
var initial_wood := float(village_data.get("wood", 0.0))
migrated["storages"] = [
(
StorageStateRecord
. create(
SimulationIds.STORAGE_VILLAGE_PANTRY,
{String(SimulationIds.RESOURCE_FOOD): initial_food}
)
. to_dictionary()
),
(
StorageStateRecord
. create(
SimulationIds.STORAGE_VILLAGE_WOODPILE,
{String(SimulationIds.RESOURCE_WOOD): initial_wood}
)
. to_dictionary()
)
]
if version in [LEGACY_SCHEMA_VERSION, EVENT_LEGACY_SCHEMA_VERSION]:
migrated["economic_events"] = []
var simulation_data: Dictionary = migrated.get("simulation", {})
simulation_data["next_event_id"] = 0
migrated["simulation"] = simulation_data
if (
version
in [LEGACY_SCHEMA_VERSION, EVENT_LEGACY_SCHEMA_VERSION, RELATIONSHIP_LEGACY_SCHEMA_VERSION]
):
migrated["relationships"] = _migrate_npc_familiarity(legacy_data.get("npcs", []))
if (
version
in [
LEGACY_SCHEMA_VERSION,
EVENT_LEGACY_SCHEMA_VERSION,
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
KNOWLEDGE_LEGACY_SCHEMA_VERSION,
]
):
migrated["event_knowledge"] = _migrate_relationship_causes(
migrated.get("relationships", [])
)
migrated["event_knowledge"] = _migrate_known_event_provenance(
migrated.get("event_knowledge", []), migrated.get("economic_events", [])
)
return migrated
static func _migrate_npc_familiarity(npc_data: Variant) -> Array[Dictionary]:
var migrated_relationships: Array[Dictionary] = []
if not npc_data is Array:
return migrated_relationships
for npc_item in npc_data:
if not npc_item is Dictionary:
continue
var observer_id := int(npc_item.get("id", -1))
var familiarity = npc_item.get("familiarity", [])
if familiarity is Array:
for pair in familiarity:
if not pair is Dictionary:
continue
migrated_relationships.append(
(
RelationshipStateRecord
. create(
observer_id, int(pair.get("id", -1)), float(pair.get("score", 0.0))
)
. to_dictionary()
)
)
elif familiarity is Dictionary:
for subject_id in familiarity:
migrated_relationships.append(
(
RelationshipStateRecord
. create(observer_id, int(subject_id), float(familiarity[subject_id]))
. to_dictionary()
)
)
migrated_relationships.sort_custom(_relationship_dictionary_sort)
return migrated_relationships
static func _relationship_dictionary_sort(first: Dictionary, second: Dictionary) -> bool:
if int(first["observer_id"]) != int(second["observer_id"]):
return int(first["observer_id"]) < int(second["observer_id"])
return int(first["subject_id"]) < int(second["subject_id"])
static func _migrate_relationship_causes(relationship_data: Variant) -> Array[Dictionary]:
var migrated_knowledge: Array[Dictionary] = []
var known_keys := {}
if not relationship_data is Array:
return migrated_knowledge
for relationship_item in relationship_data:
if not relationship_item is Dictionary:
continue
var observer_id := int(relationship_item.get("observer_id", -1))
var cause_event_id := int(
relationship_item.get(
"last_trust_cause_event_id", RelationshipStateRecord.NO_CAUSE_EVENT
)
)
if observer_id < 0 or cause_event_id < 0:
continue
var knowledge_key := "%d:%d" % [observer_id, cause_event_id]
if known_keys.has(knowledge_key):
continue
known_keys[knowledge_key] = true
migrated_knowledge.append(
KnownEventStateRecord.create(observer_id, cause_event_id).to_dictionary()
)
migrated_knowledge.sort_custom(_knowledge_dictionary_sort)
return migrated_knowledge
static func _knowledge_dictionary_sort(first: Dictionary, second: Dictionary) -> bool:
if int(first["knower_id"]) != int(second["knower_id"]):
return int(first["knower_id"]) < int(second["knower_id"])
return int(first["event_id"]) < int(second["event_id"])
static func _migrate_known_event_provenance(knowledge_data: Variant, event_data: Variant) -> Array:
if not knowledge_data is Array:
return []
var actor_by_event_id := {}
if event_data is Array:
for event_item in event_data:
if not event_item is Dictionary:
continue
actor_by_event_id[int(event_item.get("event_id", -1))] = int(
event_item.get("actor_id", -1)
)
var migrated_knowledge: Array = []
var can_sort := true
for item in knowledge_data:
if not item is Dictionary:
migrated_knowledge.append(item)
can_sort = false
continue
if int(item.get("schema_version", -1)) == KnownEventStateRecord.SCHEMA_VERSION:
migrated_knowledge.append(item.duplicate(true))
continue
if int(item.get("schema_version", -1)) != 1 or not item.has_all(["knower_id", "event_id"]):
migrated_knowledge.append(item.duplicate(true))
can_sort = false
continue
var knower_id := int(item["knower_id"])
var event_id := int(item["event_id"])
var acquisition_method := SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY
if int(actor_by_event_id.get(event_id, -2)) == knower_id:
acquisition_method = SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
migrated_knowledge.append(
KnownEventStateRecord.create(knower_id, event_id, acquisition_method).to_dictionary()
)
if can_sort:
migrated_knowledge.sort_custom(_knowledge_dictionary_sort)
return migrated_knowledge