class_name SimulationStateRecord extends RefCounted const SCHEMA_NAME := "the_steward.simulation" const SCHEMA_VERSION := 13 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 PREVIOUS_SCHEMA_VERSION := 7 const PLAYER_LEGACY_SCHEMA_VERSION := 11 const PLAYER_STATE_LEGACY_SCHEMA_VERSION := 12 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 player_standing: PlayerStandingRecord var player_quests: Array[PlayerQuestRecord] = [] var player_state: PlayerStateRecord 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 player_quest_data: Array[Dictionary] = [] for player_quest_record in player_quests: player_quest_data.append(player_quest_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, "player_standing": player_standing.to_dictionary(), "player_quests": player_quest_data, "player_state": player_state.to_dictionary() } 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_LEGACY_SCHEMA_VERSION, PLAYER_STATE_LEGACY_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", "player_standing", "player_quests", "player_state", ] ) ): 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", ] ) ): 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"]) 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 ): 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"] 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 ): return null var record := SimulationStateRecord.new() record.simulation = simulation_data.duplicate(true) record.village = village_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) 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 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() 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) var opportunity_ids := {} var opportunity_records_by_id := {} 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 opportunity_records_by_id[opportunity_id] = opportunity_record 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 standing_data = record_data["player_standing"] if not standing_data is Dictionary: return null var standing_record := PlayerStandingRecord.from_dictionary(standing_data) if standing_record == null: return null record.player_standing = standing_record var player_quest_data = record_data["player_quests"] if not player_quest_data is Array: return null var quest_ids := {} var quest_opportunity_ids := {} var highest_quest_id := -1 for item in player_quest_data: if not item is Dictionary: return null var quest_record := PlayerQuestRecord.from_dictionary(item) if quest_record == null: return null var quest_id := quest_record.get_quest_id() var quest_opportunity_id := quest_record.get_opportunity_id() var requester_id := quest_record.get_requester_npc_id() var has_valid_requester := npc_ids.has(requester_id) var has_valid_opportunity := ( quest_record.has_animal_target() or opportunity_ids.has(quest_opportunity_id) ) if ( quest_ids.has(quest_id) or quest_opportunity_ids.has(quest_opportunity_id) or not has_valid_requester or not has_valid_opportunity ): return null if ( quest_record.get_status() == PlayerQuestRecord.STATUS_OPEN and ( (not quest_record.has_animal_target()) and not opportunity_records_by_id.has(quest_opportunity_id) ) ): return null if ( quest_record.get_resolution_event_id() >= 0 and not event_ids.has(quest_record.get_resolution_event_id()) ): return null var dedup_key := ( "animal:%s" % String(quest_record.get_animal_id()) if quest_record.has_animal_target() else "opp:%d" % quest_opportunity_id ) if quest_ids.has(quest_id) or quest_opportunity_ids.has(dedup_key): return null quest_ids[quest_id] = true quest_opportunity_ids[dedup_key] = true highest_quest_id = maxi(highest_quest_id, quest_id) record.player_quests.append(quest_record) var player_state_data = record_data["player_state"] if not player_state_data is Dictionary: return null var player_state_record := PlayerStateRecord.from_dictionary(player_state_data) if player_state_record == null: return null record.player_state = player_state_record 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_npc_id := known_event.get_source_npc_id() if not npc_ids.has(source_npc_id): return false var source_method := known_event.get_source_acquisition_method() if ( (source_method == SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED) != (source_npc_id == actor_id) ): return false return true 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 _: 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 return ( resolution_event_id == OpportunityStateRecord.NO_EVENT_ID and resolved_tick == -1 and interested_record != null and ( opportunity.get_opportunity_type() != SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD or not bool(interested_record.data["is_dead"]) ) and target_storage != null and ( target_storage.get_amount(opportunity.get_resource_id()) < opportunity.get_target_amount() ) and evidence != null and evidence.get_acquired_tick() <= created_tick ) if status == OpportunityStateRecord.STATUS_INVALIDATED: if ( opportunity.get_opportunity_type() != SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD 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_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 ( 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: 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 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, ] ): migrated["opportunities"] = [] var opportunity_simulation_data: Dictionary = migrated.get("simulation", {}) opportunity_simulation_data["next_opportunity_id"] = 0 migrated["simulation"] = opportunity_simulation_data if version <= PLAYER_LEGACY_SCHEMA_VERSION: migrated["player_standing"] = PlayerStandingRecord.create().to_dictionary() migrated["player_quests"] = [] var quest_simulation_data: Dictionary = migrated.get("simulation", {}) quest_simulation_data["next_quest_id"] = 0 migrated["simulation"] = quest_simulation_data if version <= PLAYER_STATE_LEGACY_SCHEMA_VERSION: migrated["player_state"] = PlayerStateRecord.create().to_dictionary() 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