class_name SimulationStateRecord extends RefCounted const SCHEMA_NAME := "the_steward.simulation" const SCHEMA_VERSION := 15 const LEGACY_SCHEMA_VERSION := 1 const EVENT_LEGACY_SCHEMA_VERSION := 2 const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3 const KNOWLEDGE_LEGACY_SCHEMA_VERSION := 4 const PROVENANCE_LEGACY_SCHEMA_VERSION := 5 const RETENTION_LEGACY_SCHEMA_VERSION := 6 const OPPORTUNITY_LEGACY_SCHEMA_VERSION := 8 const ANIMAL_LEGACY_SCHEMA_VERSION := 9 const ROUTINE_LEGACY_SCHEMA_VERSION := 10 const PLAYER_RELATIONSHIP_SCHEMA_VERSION := 11 const PLAYER_NEEDS_SCHEMA_VERSION := 12 const CONFLICT_SCHEMA_VERSION := 13 const PRE_EMERGENT_SCHEMA_VERSION := 14 const PREVIOUS_SCHEMA_VERSION := 7 var simulation: Dictionary var village: VillageStateRecord var npcs: Array[NPCStateRecord] = [] var animals: Array[AnimalStateRecord] = [] var resources: Array[ResourceStateRecord] = [] var storages: Array[StorageStateRecord] = [] var economic_events: Array[EconomicEventRecord] = [] var relationships: Array[RelationshipStateRecord] = [] var event_knowledge: Array[KnownEventStateRecord] = [] var opportunities: Array[OpportunityStateRecord] = [] var situations: Array[SituationStateRecord] = [] var quest_journal: Array[QuestJournalEntryStateRecord] = [] var commitments: Array[CommitmentStateRecord] = [] var conversation_history: Array[ConversationActStateRecord] = [] var player: PlayerStateRecord var combatants: Array[CombatantStateRecord] = [] var factions: Array[FactionStateRecord] = [] func to_dictionary() -> Dictionary: var npc_data: Array[Dictionary] = [] for npc_record in npcs: npc_data.append(npc_record.to_dictionary()) var animal_data: Array[Dictionary] = [] for animal_record in animals: animal_data.append(animal_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()) var opportunity_data: Array[Dictionary] = [] for opportunity_record in opportunities: opportunity_data.append(opportunity_record.to_dictionary()) var situation_data: Array[Dictionary] = [] for situation_record in situations: situation_data.append(situation_record.to_dictionary()) var journal_data: Array[Dictionary] = [] for journal_record in quest_journal: journal_data.append(journal_record.to_dictionary()) var commitment_data: Array[Dictionary] = [] for commitment_record in commitments: commitment_data.append(commitment_record.to_dictionary()) var conversation_history_data: Array[Dictionary] = [] for conversation_act in conversation_history: conversation_history_data.append(conversation_act.to_dictionary()) var combatant_data: Array[Dictionary] = [] for combatant_record in combatants: combatant_data.append(combatant_record.to_dictionary()) var faction_data: Array[Dictionary] = [] for faction_record in factions: faction_data.append(faction_record.to_dictionary()) return { "schema": SCHEMA_NAME, "schema_version": SCHEMA_VERSION, "simulation": simulation.duplicate(true), "village": village.to_dictionary(), "npcs": npc_data, "animals": animal_data, "resources": resource_data, "storages": storage_data, "economic_events": event_data, "relationships": relationship_data, "event_knowledge": knowledge_data, "opportunities": opportunity_data, "situations": situation_data, "quest_journal": journal_data, "commitments": commitment_data, "conversation_history": conversation_history_data, "player": player.to_dictionary(), "combatants": combatant_data, "factions": faction_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, PROVENANCE_LEGACY_SCHEMA_VERSION, RETENTION_LEGACY_SCHEMA_VERSION, PREVIOUS_SCHEMA_VERSION, OPPORTUNITY_LEGACY_SCHEMA_VERSION, ANIMAL_LEGACY_SCHEMA_VERSION, ROUTINE_LEGACY_SCHEMA_VERSION, PLAYER_RELATIONSHIP_SCHEMA_VERSION, PLAYER_NEEDS_SCHEMA_VERSION, CONFLICT_SCHEMA_VERSION, PRE_EMERGENT_SCHEMA_VERSION, ] ): record_data = _migrate_legacy(record_data, version) elif version != SCHEMA_VERSION: return null if not ( record_data . has_all( [ "simulation", "village", "npcs", "animals", "resources", "storages", "economic_events", "relationships", "event_knowledge", "opportunities", "situations", "quest_journal", "commitments", "conversation_history", "player", "combatants", "factions", ] ) ): 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", "next_opportunity_id", "next_situation_id", "next_journal_entry_id", "next_commitment_id", "next_conversation_act_id", ] ) ): return null var saved_tick_interval := float(simulation_data["tick_interval"]) var saved_tick_count := int(simulation_data["tick_count"]) var saved_clock_accumulator := float(simulation_data["clock_accumulator"]) var saved_clock_elapsed_ticks := int(simulation_data["clock_elapsed_ticks"]) var saved_next_opportunity_id := int(simulation_data["next_opportunity_id"]) var saved_next_situation_id := int(simulation_data["next_situation_id"]) var saved_next_journal_entry_id := int(simulation_data["next_journal_entry_id"]) var saved_next_commitment_id := int(simulation_data["next_commitment_id"]) var saved_next_conversation_act_id := int(simulation_data["next_conversation_act_id"]) if ( not is_finite(saved_tick_interval) or saved_tick_interval <= 0.0 or saved_tick_count < 0 or not is_finite(saved_clock_accumulator) or saved_clock_accumulator < 0.0 or saved_clock_elapsed_ticks < 0 or saved_next_opportunity_id < 0 or saved_next_situation_id < 0 or saved_next_journal_entry_id < 0 or saved_next_commitment_id < 0 or saved_next_conversation_act_id < 0 ): return null if simulation_data.has("cycle_duration_seconds"): var saved_cycle_duration := float(simulation_data["cycle_duration_seconds"]) if not is_finite(saved_cycle_duration) or saved_cycle_duration <= 0.0: 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 animal_data = record_data["animals"] 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"] var opportunity_data = record_data["opportunities"] var situation_data = record_data["situations"] var journal_data = record_data["quest_journal"] var commitment_data = record_data["commitments"] var conversation_history_data = record_data["conversation_history"] if ( not npc_data is Array or not animal_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 or not opportunity_data is Array or not situation_data is Array or not journal_data is Array or not commitment_data is Array or not conversation_history_data is Array ): return null var record := SimulationStateRecord.new() record.simulation = simulation_data.duplicate(true) record.village = village_record var player_data = record_data["player"] if not player_data is Dictionary: return null var player_record := PlayerStateRecord.from_dictionary(player_data) if player_record == null: return null record.player = player_record var combatant_data = record_data["combatants"] var faction_data = record_data["factions"] if not combatant_data is Array or not faction_data is Array: return null var combatant_ids := {} for item in combatant_data: if not item is Dictionary: return null var combatant_record := CombatantStateRecord.from_dictionary(item) if combatant_record == null: return null var combatant_id := String(combatant_record.get_combatant_id()) if combatant_ids.has(combatant_id): return null combatant_ids[combatant_id] = true record.combatants.append(combatant_record) var faction_ids := {} for item in faction_data: if not item is Dictionary: return null var faction_record := FactionStateRecord.from_dictionary(item) if faction_record == null: return null var faction_id := String(faction_record.get_faction_id()) if faction_ids.has(faction_id): return null faction_ids[faction_id] = true record.factions.append(faction_record) var npc_ids := {} var npc_records_by_id := {} 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 npc_records_by_id[npc_id] = npc_record record.npcs.append(npc_record) for combatant_record in record.combatants: if combatant_record.get_kind() != SimulationIds.COMBATANT_KIND_NPC: continue if not npc_ids.has(combatant_record.get_npc_id()): return null var resource_ids := {} var resource_records_by_id := {} 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 resource_records_by_id[resource_id] = resource_record record.resources.append(resource_record) var animal_ids := {} var animal_records_by_id := {} for item in animal_data: if not item is Dictionary: return null var animal_record := AnimalStateRecord.from_dictionary(item) if animal_record == null: return null var animal_id := String(animal_record.data["animal_id"]) if animal_ids.has(animal_id) or resource_ids.has(animal_id): return null var reserved_by := animal_record.get_reserved_by() if reserved_by >= 0: var reserving_npc := npc_records_by_id.get(reserved_by) as NPCStateRecord if ( reserving_npc == null or ( StringName(reserving_npc.data["current_task"]) != SimulationIds.ACTION_FEED_ANIMAL ) or StringName(reserving_npc.data["target_id"]) != animal_record.get_animal_id() or ( StringName(reserving_npc.data["task_state"]) not in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING] ) ): return null animal_ids[animal_id] = true animal_records_by_id[animal_id] = animal_record record.animals.append(animal_record) for npc_record in record.npcs: if ( StringName(npc_record.data["current_task"]) != SimulationIds.ACTION_FEED_ANIMAL or ( StringName(npc_record.data["task_state"]) not in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING] ) ): continue var target_animal := ( animal_records_by_id.get(StringName(npc_record.data["target_id"])) as AnimalStateRecord ) if target_animal == null or target_animal.get_reserved_by() != int(npc_record.data["id"]): return null var storage_ids := {} var storage_records_by_id := {} 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) or animal_ids.has(String(storage_id)): return null storage_ids[storage_id] = true storage_records_by_id[storage_id] = storage_record record.storages.append(storage_record) var pantry := ( storage_records_by_id.get(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageStateRecord ) if ( pantry != null and not is_equal_approx( float(record.village.data["food"]), pantry.get_amount(SimulationIds.RESOURCE_FOOD) ) ): return null var woodpile := ( storage_records_by_id.get(SimulationIds.STORAGE_VILLAGE_WOODPILE) as StorageStateRecord ) if ( woodpile != null and not is_equal_approx( float(record.village.data["wood"]), woodpile.get_amount(SimulationIds.RESOURCE_WOOD) ) ): return null var event_ids := {} var event_records_by_id := {} var highest_event_id := -1 var latest_feed_ticks := {} 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) if StringName(event_record.data["event_type"]) == SimulationIds.EVENT_ANIMAL_FED: if not _is_valid_animal_feed_event( event_record, npc_ids, animal_records_by_id, storage_records_by_id, int(record.simulation["tick_count"]) ): return null var fed_animal_id := StringName(event_record.data["destination_id"]) latest_feed_ticks[fed_animal_id] = maxi( int(latest_feed_ticks.get(fed_animal_id, AnimalStateRecord.NEVER_FED_TICK)), int(event_record.data["tick"]) ) record.economic_events.append(event_record) if int(record.simulation["next_event_id"]) <= highest_event_id: return null for animal_record in record.animals: if ( animal_record.get_last_fed_tick() != int( latest_feed_ticks.get( animal_record.get_animal_id(), AnimalStateRecord.NEVER_FED_TICK ) ) ): 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 ( (knower_id != SimulationIds.PLAYER_ACTOR_ID and 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, int(record.simulation["tick_count"]) ): 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() var is_player_observer := observer_id == SimulationIds.PLAYER_ACTOR_ID var is_player_subject := subject_id == SimulationIds.PLAYER_ACTOR_ID if not is_player_observer and not npc_ids.has(observer_id): return null if not is_player_subject and 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 for dimension in RelationshipStateRecord.DIMENSIONS: var cause_event_id := relationship_record.get_last_cause_event_id(dimension) if cause_event_id == RelationshipStateRecord.NO_CAUSE_EVENT: continue if ( not event_ids.has(cause_event_id) or not knowledge_keys.has("%d:%d" % [observer_id, cause_event_id]) ): return null if dimension == RelationshipStateRecord.DIMENSION_TRUST: var cause_event := event_records_by_id[cause_event_id] as EconomicEventRecord if not _is_valid_relationship_cause(cause_event, subject_id, is_player_subject): return null relationship_keys[relationship_key] = true record.relationships.append(relationship_record) var opportunity_ids := {} var trigger_event_ids := {} var resolution_event_ids := {} var has_open_opportunity := false var highest_opportunity_id := -1 for item in opportunity_data: if not item is Dictionary: return null var opportunity_record := OpportunityStateRecord.from_dictionary(item) if opportunity_record == null: return null var opportunity_id := opportunity_record.get_opportunity_id() var trigger_event_id := opportunity_record.get_trigger_event_id() if opportunity_ids.has(opportunity_id) or trigger_event_ids.has(trigger_event_id): return null if opportunity_record.get_status() == OpportunityStateRecord.STATUS_OPEN: if has_open_opportunity: return null has_open_opportunity = true elif opportunity_record.get_resolution_event_id() != OpportunityStateRecord.NO_EVENT_ID: if resolution_event_ids.has(opportunity_record.get_resolution_event_id()): return null resolution_event_ids[opportunity_record.get_resolution_event_id()] = true if not _is_valid_opportunity( opportunity_record, npc_ids, npc_records_by_id, storage_records_by_id, event_records_by_id, knowledge_records_by_key, resource_records_by_id, int(record.simulation["tick_count"]) ): return null opportunity_ids[opportunity_id] = true trigger_event_ids[trigger_event_id] = true highest_opportunity_id = maxi(highest_opportunity_id, opportunity_id) record.opportunities.append(opportunity_record) if int(record.simulation["next_opportunity_id"]) <= highest_opportunity_id: return null var emergent_records: Variant = EmergentStateValidator.parse( situation_data, journal_data, commitment_data, simulation_data, npc_ids, event_ids ) if emergent_records == null: return null record.situations.assign(emergent_records["situations"]) record.quest_journal.assign(emergent_records["quest_journal"]) record.commitments.assign(emergent_records["commitments"]) var conversation_act_ids: Dictionary = {} var highest_conversation_act_id := -1 for item in conversation_history_data: if not item is Dictionary: return null var conversation_act := ConversationActStateRecord.from_dictionary(item) if ( conversation_act == null or conversation_act_ids.has(conversation_act.get_act_id()) or conversation_act.get_tick() > saved_tick_count or not EmergentStateValidator.is_valid_actor(conversation_act.get_speaker(), npc_ids) or not EmergentStateValidator.is_valid_actor(conversation_act.get_listener(), npc_ids) ): return null conversation_act_ids[conversation_act.get_act_id()] = true highest_conversation_act_id = maxi( highest_conversation_act_id, conversation_act.get_act_id() ) record.conversation_history.append(conversation_act) if saved_next_conversation_act_id <= highest_conversation_act_id: return null return record static func _is_valid_animal_feed_event( event: EconomicEventRecord, npc_ids: Dictionary, animal_records_by_id: Dictionary, storage_records_by_id: Dictionary, current_tick: int ) -> bool: var actor_id := int(event.data["actor_id"]) var definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL) var animal := ( animal_records_by_id.get(StringName(event.data["destination_id"])) as AnimalStateRecord ) var source_id := StringName(event.data["source_id"]) var has_valid_source := source_id == SimulationIds.STORAGE_VILLAGE_PANTRY if actor_id >= 0: # Existing v11 saves contain direct pantry-to-animal NPC feed facts. # New physical deliveries name the matching carried inventory instead. has_valid_source = ( has_valid_source or source_id == SimulationIds.npc_inventory_id(actor_id) ) return ( (actor_id == -1 or npc_ids.has(actor_id)) and int(event.data["tick"]) <= current_tick and animal != null and has_valid_source and storage_records_by_id.has(SimulationIds.STORAGE_VILLAGE_PANTRY) and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD and definition != null and definition.has_completion_cost() and is_equal_approx(float(event.data["amount"]), definition.completion_cost_amount) ) static func _is_valid_knowledge_provenance( known_event: KnownEventStateRecord, npc_ids: Dictionary, event_records_by_id: Dictionary, current_tick: int ) -> 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"]) var event_tick := int(event.data["tick"]) var acquired_tick := known_event.get_acquired_tick() if acquired_tick < event_tick or acquired_tick > current_tick: return false if method == SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY: return true if not EventKnowledgeSystem.is_knowable_event(event): return false match method: SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED: return knower_id == actor_id and acquired_tick == event_tick SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED: return knower_id != actor_id and acquired_tick == event_tick SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED: if knower_id == actor_id: return false var source_actor_id := known_event.get_source_actor_id() if ( source_actor_id != SimulationIds.PLAYER_ACTOR_ID and not npc_ids.has(source_actor_id) ): return false var source_method := known_event.get_source_acquisition_method() if ( (source_method == SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED) != (source_actor_id == actor_id) ): return false if source_method not in KnownEventStateRecord.DIRECT_ACQUISITION_METHODS: return false return known_event.get_hop_count() == 1 return false static func _is_valid_opportunity( opportunity: OpportunityStateRecord, npc_ids: Dictionary, npc_records_by_id: Dictionary, storage_records_by_id: Dictionary, event_records_by_id: Dictionary, knowledge_records_by_key: Dictionary, resource_records_by_id: Dictionary, current_tick: int ) -> bool: if ( not npc_ids.has(opportunity.get_interested_npc_id()) or not storage_records_by_id.has(opportunity.get_target_id()) or not is_finite(opportunity.get_target_amount()) or opportunity.get_target_amount() <= 0.0 ): return false var trigger_event := ( event_records_by_id.get(opportunity.get_trigger_event_id()) as EconomicEventRecord ) if trigger_event == null: return false match opportunity.get_opportunity_type(): SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY: if not _is_valid_pantry_empty_trigger(trigger_event, npc_ids): return false SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD: if not _is_valid_missing_wood_trigger(trigger_event, opportunity, npc_ids): return false SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER: if not _is_valid_weak_trigger(trigger_event, opportunity, npc_ids): return false SimulationIds.OPPORTUNITY_REPAIR_HOME_ROOF: if not _is_valid_home_damaged_trigger(trigger_event, opportunity, npc_ids): return false _: return false var trigger_tick := int(trigger_event.data["tick"]) var created_tick := opportunity.get_created_tick() if created_tick < trigger_tick or created_tick > current_tick: return false var status := opportunity.get_status() var resolution_event_id := opportunity.get_resolution_event_id() var resolved_tick := opportunity.get_resolved_tick() var interested_record := ( npc_records_by_id.get(opportunity.get_interested_npc_id()) as NPCStateRecord ) if status == OpportunityStateRecord.STATUS_OPEN: var target_storage := ( storage_records_by_id.get(opportunity.get_target_id()) as StorageStateRecord ) var evidence_key := ( "%d:%d" % [opportunity.get_interested_npc_id(), opportunity.get_trigger_event_id()] ) var evidence := knowledge_records_by_key.get(evidence_key) as KnownEventStateRecord var is_weak_need := ( opportunity.get_opportunity_type() == SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER ) var interested_alive := ( interested_record != null and not bool(interested_record.data["is_dead"]) ) var storage_satisfied := ( is_weak_need or ( target_storage != null and ( target_storage.get_amount(opportunity.get_resource_id()) < opportunity.get_target_amount() ) ) ) return ( resolution_event_id == OpportunityStateRecord.NO_EVENT_ID and resolved_tick == -1 and interested_alive and target_storage != null and storage_satisfied and evidence != null and evidence.get_acquired_tick() <= created_tick ) if status == OpportunityStateRecord.STATUS_INVALIDATED: if ( ( opportunity.get_opportunity_type() not in [ SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD, SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER, SimulationIds.OPPORTUNITY_REPAIR_HOME_ROOF, ] ) or resolution_event_id != OpportunityStateRecord.NO_EVENT_ID or resolved_tick != -1 or opportunity.get_closed_tick() < created_tick or opportunity.get_closed_tick() > current_tick ): return false if ( opportunity.get_invalidation_reason() == SimulationIds.OPPORTUNITY_INVALIDATED_INTERESTED_DIED ): return interested_record != null and bool(interested_record.data["is_dead"]) return ( opportunity.get_invalidation_reason() == SimulationIds.OPPORTUNITY_INVALIDATED_EVIDENCE_STALE ) if status != OpportunityStateRecord.STATUS_RESOLVED: return false if resolution_event_id == OpportunityStateRecord.NO_EVENT_ID: return false var resolution_event := event_records_by_id.get(resolution_event_id) as EconomicEventRecord if ( resolution_event == null or not _is_valid_supply_resolution( resolution_event, opportunity, npc_ids, resource_records_by_id ) ): return false var resolution_tick := int(resolution_event.data["tick"]) return ( resolution_event_id > opportunity.get_trigger_event_id() and resolution_tick >= created_tick and resolved_tick == resolution_tick and resolved_tick <= current_tick ) static func _is_valid_missing_wood_trigger( event: EconomicEventRecord, opportunity: OpportunityStateRecord, npc_ids: Dictionary ) -> bool: var actor_id := int(event.data["actor_id"]) return ( npc_ids.has(actor_id) and actor_id == opportunity.get_interested_npc_id() and StringName(event.data["event_type"]) == SimulationIds.EVENT_TASK_BLOCKED and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_WOODPILE and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_WOOD and ( StringName(event.data.get("action_id", &"")) in [SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY] ) and is_equal_approx( float(event.data.get("required_amount", 0.0)), opportunity.get_target_amount() ) ) static func _is_valid_pantry_empty_trigger(event: EconomicEventRecord, npc_ids: Dictionary) -> bool: var actor_id := int(event.data["actor_id"]) return ( npc_ids.has(actor_id) and StringName(event.data["event_type"]) == SimulationIds.EVENT_STORAGE_WITHDRAWN and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY and StringName(event.data["destination_id"]) == SimulationIds.npc_inventory_id(actor_id) and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD and float(event.data["amount"]) > 0.0 ) static func _is_valid_weak_trigger( event: EconomicEventRecord, opportunity: OpportunityStateRecord, npc_ids: Dictionary ) -> bool: return ( npc_ids.has(int(event.data["actor_id"])) and int(event.data["actor_id"]) == opportunity.get_interested_npc_id() and StringName(event.data["event_type"]) == SimulationIds.EVENT_VILLAGER_WEAK ) static func _is_valid_home_damaged_trigger( event: EconomicEventRecord, opportunity: OpportunityStateRecord, npc_ids: Dictionary ) -> bool: return ( npc_ids.has(int(event.data["actor_id"])) and int(event.data["actor_id"]) == opportunity.get_interested_npc_id() and StringName(event.data["event_type"]) == SimulationIds.EVENT_HOME_DAMAGED ) static func _is_valid_relationship_cause( cause_event: EconomicEventRecord, subject_id: int, is_player_subject: bool ) -> bool: var event_type := StringName(cause_event.data["event_type"]) if EmergentStateValidator.is_valid_commitment_relationship_cause(cause_event, subject_id): return true if not is_player_subject: return ( int(cause_event.data["actor_id"]) == subject_id and (event_type == SimulationIds.EVENT_STORAGE_DEPOSITED) and StringName(cause_event.data["item_id"]) == SimulationIds.RESOURCE_FOOD and float(cause_event.data["amount"]) > 0.0 ) if int(cause_event.data["actor_id"]) != SimulationIds.PLAYER_ACTOR_ID: return false if StringName(cause_event.data["item_id"]) != SimulationIds.RESOURCE_FOOD: return false if float(cause_event.data["amount"]) <= 0.0: return false if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED: return ( StringName(cause_event.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID and ( StringName(cause_event.data["destination_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY ) ) return ( event_type == SimulationIds.EVENT_RESOURCE_EXTRACTED and StringName(cause_event.data["destination_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY ) static func _is_valid_supply_resolution( event: EconomicEventRecord, opportunity: OpportunityStateRecord, npc_ids: Dictionary, resource_records_by_id: Dictionary ) -> bool: var actor_id := int(event.data["actor_id"]) var event_type := StringName(event.data["event_type"]) if ( opportunity.get_opportunity_type() == SimulationIds.OPPORTUNITY_FEED_WEAK_VILLAGER and event_type == SimulationIds.EVENT_ITEM_CONSUMED ): return ( actor_id == opportunity.get_interested_npc_id() and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD and float(event.data["amount"]) > 0.0 ) if ( StringName(event.data["destination_id"]) != opportunity.get_target_id() or StringName(event.data["item_id"]) != opportunity.get_resource_id() or float(event.data["amount"]) <= 0.0 ): return false if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED: if actor_id == SimulationIds.PLAYER_ACTOR_ID: return StringName(event.data["source_id"]) == SimulationIds.PLAYER_INVENTORY_ID return ( npc_ids.has(actor_id) and StringName(event.data["source_id"]) == SimulationIds.npc_inventory_id(actor_id) ) if event_type == SimulationIds.EVENT_RESOURCE_EXTRACTED and actor_id == -1: var resource_state := ( resource_records_by_id.get(StringName(event.data["source_id"])) as ResourceStateRecord ) return ( resource_state != null and resource_state.get_resource_id() == opportunity.get_resource_id() and resource_state.can_player_use_resource() ) return false static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary: var migrated := legacy_data.duplicate(true) migrated["schema_version"] = SCHEMA_VERSION migrated["situations"] = [] migrated["quest_journal"] = [] migrated["commitments"] = [] migrated["conversation_history"] = [] var emergent_simulation_data: Dictionary = migrated.get("simulation", {}) emergent_simulation_data["next_situation_id"] = 0 emergent_simulation_data["next_journal_entry_id"] = 0 emergent_simulation_data["next_commitment_id"] = 0 emergent_simulation_data["next_conversation_act_id"] = 0 migrated["simulation"] = emergent_simulation_data if not migrated.has("player"): migrated["player"] = PlayerStateRecord.create_default().to_dictionary() if version < CONFLICT_SCHEMA_VERSION: migrated["combatants"] = [] migrated["factions"] = [ ( FactionStateRecord . create(SimulationIds.FACTION_VILLAGE, "Village", 20.0, 0, 0.2, 0.8) . to_dictionary() ), ( FactionStateRecord . create(SimulationIds.FACTION_TRIBE, "Hill Tribe", 4.0, 4, 0.7, 0.7) . to_dictionary() ), ] if version <= ANIMAL_LEGACY_SCHEMA_VERSION: migrated["animals"] = [] 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}, maxf(StorageStateRecord.DEFAULT_CAPACITY, initial_food) ) . to_dictionary() ), ( StorageStateRecord . create( SimulationIds.STORAGE_VILLAGE_WOODPILE, {String(SimulationIds.RESOURCE_WOOD): initial_wood}, maxf(StorageStateRecord.DEFAULT_CAPACITY, 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", []), int((migrated.get("simulation", {}) as Dictionary).get("tick_count", 0)) ) migrated["event_knowledge"] = _migrate_known_event_retention( migrated.get("event_knowledge", []), migrated.get("economic_events", []), int((migrated.get("simulation", {}) as Dictionary).get("tick_count", 0)) ) if ( version not in [ OPPORTUNITY_LEGACY_SCHEMA_VERSION, ANIMAL_LEGACY_SCHEMA_VERSION, ROUTINE_LEGACY_SCHEMA_VERSION, PLAYER_RELATIONSHIP_SCHEMA_VERSION, PLAYER_NEEDS_SCHEMA_VERSION, CONFLICT_SCHEMA_VERSION, PRE_EMERGENT_SCHEMA_VERSION, ] ): migrated["opportunities"] = [] var opportunity_simulation_data: Dictionary = migrated.get("simulation", {}) opportunity_simulation_data["next_opportunity_id"] = 0 migrated["simulation"] = opportunity_simulation_data 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, migration_tick: int ) -> 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, SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY, KnownEventStateRecord.NO_SOURCE_NPC_ID, migration_tick ) . 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_retention( knowledge_data: Variant, event_data: Variant, migration_tick: int ) -> Array: if not knowledge_data is Array: return [] var event_by_id := {} if event_data is Array: for event_item in event_data: if not event_item is Dictionary: continue event_by_id[int(event_item.get("event_id", -1))] = event_item var old_knowledge_by_key := {} for item in knowledge_data: if item is Dictionary and item.has_all(["knower_id", "event_id"]): old_knowledge_by_key["%d:%d" % [int(item["knower_id"]), int(item["event_id"])]] = item 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 var nested_version := int(item.get("schema_version", -1)) if nested_version not in [1, 2] 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 event_item := event_by_id.get(event_id, {}) as Dictionary var event_tick := int(event_item.get("tick", migration_tick)) var event_actor_id := int(event_item.get("actor_id", -2)) var acquisition_method := StringName( item.get("acquisition_method", SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY) ) var source_npc_id := int(item.get("source_npc_id", KnownEventStateRecord.NO_SOURCE_NPC_ID)) if nested_version == 1: acquisition_method = SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY source_npc_id = KnownEventStateRecord.NO_SOURCE_NPC_ID if event_actor_id == knower_id: acquisition_method = SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED var acquired_tick := migration_tick if acquisition_method in KnownEventStateRecord.DIRECT_ACQUISITION_METHODS: acquired_tick = event_tick var source_acquisition_method := &"" if acquisition_method == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED: var source_item: Variant = old_knowledge_by_key.get( "%d:%d" % [source_npc_id, event_id], {} ) if source_item is Dictionary: source_acquisition_method = StringName(source_item.get("acquisition_method", &"")) migrated_knowledge.append( ( KnownEventStateRecord . create( knower_id, event_id, acquisition_method, source_npc_id, acquired_tick, source_acquisition_method ) . to_dictionary() ) ) if can_sort: migrated_knowledge.sort_custom(_knowledge_dictionary_sort) return migrated_knowledge