class_name EventKnowledgeSystem extends RefCounted const WITNESS_RADIUS := 10.0 const MAX_RECENT_FACTS_PER_KNOWER := 3 const MAX_RECENT_FACTS_PER_NPC := MAX_RECENT_FACTS_PER_KNOWER const MAX_PINNED_FACTS_PER_KNOWER := 4 const COMMUNICATED_CONFIDENCE_FACTOR := 0.9 var known_events: Dictionary = {} func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[KnownEventStateRecord]: var learned: Array[KnownEventStateRecord] = [] if not is_knowable_event(event): return learned var actor_id := int(event.data["actor_id"]) var is_player_actor := actor_id == SimulationIds.PLAYER_ACTOR_ID var actor := null if is_player_actor else _find_npc(actor_id, npcs) if actor == null and not is_player_actor: return learned var event_id := int(event.data["event_id"]) var acquired_tick := int(event.data["tick"]) if is_player_actor: var player_record := _remember( SimulationIds.PLAYER_ACTOR_ID, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED, acquired_tick ) if player_record != null: learned.append(player_record) elif actor != null: var actor_record := _remember( actor.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED, acquired_tick ) if actor_record != null: learned.append(actor_record) var witness_radius_squared := WITNESS_RADIUS * WITNESS_RADIUS var event_position := event.get_world_position() for npc in npcs: if npc.is_dead: continue if not is_player_actor and npc.id == actor.id: continue if npc.position.distance_squared_to(event_position) > witness_radius_squared: continue var witness_record := _remember( npc.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, acquired_tick ) if witness_record != null: learned.append(witness_record) learned.sort_custom(_sort_records) return learned func communicate_event( event: EconomicEventRecord, speaker_id: int, listener_id: int, acquired_tick: int ) -> KnownEventStateRecord: if ( event == null or not KnownEventStateRecord.is_valid_actor_id(speaker_id) or not KnownEventStateRecord.is_valid_actor_id(listener_id) or speaker_id == listener_id or listener_id == int(event.data["actor_id"]) or acquired_tick < 0 or not is_knowable_event(event) ): return null var event_id := int(event.data["event_id"]) var speaker_record := get_record(speaker_id, event_id) if speaker_record == null: return null if ( speaker_record.get_acquisition_method() not in [ SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, ] ): return null return _remember( listener_id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, acquired_tick, speaker_id, speaker_record.get_acquisition_method(), speaker_record.get_hop_count() + 1, speaker_record.get_confidence() * COMMUNICATED_CONFIDENCE_FACTOR, speaker_record.get_salience() ) func communicate_event_to_player( event: EconomicEventRecord, speaker_id: int, acquired_tick: int ) -> KnownEventStateRecord: return communicate_event(event, speaker_id, SimulationIds.PLAYER_ACTOR_ID, acquired_tick) func knows_event(knower_id: int, event_id: int) -> bool: return known_events.has(_key(knower_id, event_id)) func get_record(knower_id: int, event_id: int) -> KnownEventStateRecord: return known_events.get(_key(knower_id, event_id)) as KnownEventStateRecord func pin_event(knower_id: int, event_id: int, pinned: bool = true) -> bool: var record := get_record(knower_id, event_id) if record == null or record.is_pinned() == pinned: return false if pinned and _get_pinned_count(knower_id) >= MAX_PINNED_FACTS_PER_KNOWER: return false return record.set_pinned(pinned) func set_event_confidence(knower_id: int, event_id: int, confidence: float) -> bool: var record := get_record(knower_id, event_id) return record != null and record.set_confidence(confidence) func set_event_salience(knower_id: int, event_id: int, salience: float) -> bool: var record := get_record(knower_id, event_id) return record != null and record.set_salience(salience) func get_pinned_event_ids(knower_id: int) -> Array[int]: var records: Array[KnownEventStateRecord] = [] for record in known_events.values(): if record.get_knower_id() == knower_id and record.is_pinned(): records.append(record) records.sort_custom(_sort_by_acquisition) var event_ids: Array[int] = [] for record in records: event_ids.append(record.get_event_id()) return event_ids func get_knowers(event_id: int) -> Array[int]: var knower_ids: Array[int] = [] for record in known_events.values(): if record.get_event_id() == event_id: knower_ids.append(record.get_knower_id()) knower_ids.sort() return knower_ids func get_known_event_ids(knower_id: int, max_count: int = 3) -> Array[int]: var records: Array[KnownEventStateRecord] = [] for record in known_events.values(): if record.get_knower_id() == knower_id: records.append(record) records.sort_custom(_sort_by_acquisition) var event_ids: Array[int] = [] for record in records: event_ids.append(record.get_event_id()) if max_count <= 0 or event_ids.size() <= max_count: return event_ids var recent_ids: Array[int] = [] for index in range(event_ids.size() - max_count, event_ids.size()): recent_ids.append(event_ids[index]) return recent_ids func get_retained_event_ids( knower_id: int, lasting_event_ids: Array[int], max_count: int = 4 ) -> Array[int]: return _get_ranked_event_ids(knower_id, lasting_event_ids, false, max_count) func get_communicable_event_ids( knower_id: int, lasting_event_ids: Array[int], preferred_event_id: int = -1 ) -> Array[int]: var event_ids := _get_ranked_event_ids(knower_id, lasting_event_ids, true, 0) if preferred_event_id in event_ids: event_ids.erase(preferred_event_id) event_ids.push_front(preferred_event_id) return event_ids func _get_ranked_event_ids( knower_id: int, lasting_event_ids: Array[int], direct_only: bool, max_count: int ) -> Array[int]: var ranked: Array[Dictionary] = [] for record in known_events.values(): if record.get_knower_id() != knower_id: continue if ( direct_only and ( record.get_acquisition_method() not in KnownEventStateRecord.DIRECT_ACQUISITION_METHODS ) ): continue ( ranked . append( { "event_id": record.get_event_id(), "acquired_tick": record.get_acquired_tick(), "lasting": record.get_event_id() in lasting_event_ids, "pinned": record.is_pinned(), "confidence": record.get_confidence(), "salience": record.get_salience(), } ) ) ranked.sort_custom(_sort_by_importance) var event_ids: Array[int] = [] for item in ranked: event_ids.append(int(item["event_id"])) if max_count > 0 and event_ids.size() >= max_count: break return event_ids func maintain_retention( current_tick: int, max_recent_age: int, lasting_records: Array[KnownEventStateRecord], expire_by_age: bool ) -> Array[KnownEventStateRecord]: var lasting_keys := {} for record in lasting_records: lasting_keys[_key(record.get_knower_id(), record.get_event_id())] = true var removal_keys := {} var recent_by_knower := {} for record in known_events.values(): var key := _key(record.get_knower_id(), record.get_event_id()) if lasting_keys.has(key) or record.is_pinned(): continue if expire_by_age and current_tick - record.get_acquired_tick() >= max_recent_age: removal_keys[key] = true continue var recent: Array = recent_by_knower.get(record.get_knower_id(), []) recent.append(record) recent_by_knower[record.get_knower_id()] = recent for recent in recent_by_knower.values(): recent.sort_custom(_sort_by_acquisition) var excess: int = recent.size() - MAX_RECENT_FACTS_PER_KNOWER for index in range(maxi(excess, 0)): var record := recent[index] as KnownEventStateRecord removal_keys[_key(record.get_knower_id(), record.get_event_id())] = true var forgotten: Array[KnownEventStateRecord] = [] for record in get_all_sorted(): var key := _key(record.get_knower_id(), record.get_event_id()) if not removal_keys.has(key): continue known_events.erase(key) forgotten.append(record) return forgotten func get_all_sorted() -> Array[KnownEventStateRecord]: var records: Array[KnownEventStateRecord] = [] for record in known_events.values(): records.append(record) records.sort_custom(_sort_records) return records func restore(records: Array[KnownEventStateRecord]) -> void: known_events.clear() for record in records: known_events[_key(record.get_knower_id(), record.get_event_id())] = record func _remember( knower_id: int, event_id: int, acquisition_method: StringName, acquired_tick: int, source_actor_id: int = KnownEventStateRecord.NO_SOURCE_ACTOR_ID, source_acquisition_method: StringName = &"", hop_count: int = KnownEventStateRecord.AUTO_HOP_COUNT, confidence: float = KnownEventStateRecord.DEFAULT_CONFIDENCE, salience: float = KnownEventStateRecord.DEFAULT_SALIENCE, pinned: bool = false ) -> KnownEventStateRecord: if not KnownEventStateRecord.is_valid_actor_id(knower_id) or event_id < 0 or acquired_tick < 0: return null var key := _key(knower_id, event_id) if known_events.has(key): return null var source_npc_id := ( source_actor_id if source_actor_id >= 0 else KnownEventStateRecord.NO_SOURCE_NPC_ID ) var record := KnownEventStateRecord.create( knower_id, event_id, acquisition_method, source_npc_id, acquired_tick, source_acquisition_method, hop_count, confidence, salience, source_actor_id, pinned ) known_events[key] = record return record func _get_pinned_count(knower_id: int) -> int: var count := 0 for record in known_events.values(): if record.get_knower_id() == knower_id and record.is_pinned(): count += 1 return count static func is_knowable_event(event: EconomicEventRecord) -> bool: if event == null: return false var event_type := StringName(event.data["event_type"]) var item_id := StringName(event.data["item_id"]) if ( event_type in [ SimulationIds.EVENT_COMMITMENT_ACCEPTED, SimulationIds.EVENT_COMMITMENT_FULFILLED, SimulationIds.EVENT_COMMITMENT_BROKEN, SimulationIds.EVENT_COMMITMENT_RELEASED, SimulationIds.EVENT_COMMITMENT_SUPERSEDED, ] ): return ( KnownEventStateRecord.is_valid_actor_id(int(event.data["actor_id"])) and int(event.data.get("commitment_id", -1)) >= 0 and event.data.get("debtor") is Dictionary and event.data.get("creditor") is Dictionary and event.data.get("terms") is Dictionary ) if event_type == SimulationIds.EVENT_TASK_BLOCKED: return ( not ActivityEventValidator.is_legacy_unstructured_blocked(event) and ActivityEventValidator.is_valid_blocked(event) ) if ActivityEventValidator.is_candidate(event): return ActivityEventValidator.is_valid(event) if ( event_type in [ SimulationIds.EVENT_VILLAGER_WEAK, SimulationIds.EVENT_HOME_DAMAGED, ] ): return int(event.data["actor_id"]) >= 0 if float(event.data["amount"]) <= 0.0: return false if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED: return item_id == SimulationIds.RESOURCE_FOOD if event_type == SimulationIds.EVENT_RESOURCE_EXTRACTED: var player_actor_id := int(event.data["actor_id"]) if player_actor_id != SimulationIds.PLAYER_ACTOR_ID: return false return ( StringName(event.data["destination_id"]) in [ SimulationIds.STORAGE_VILLAGE_PANTRY, SimulationIds.STORAGE_VILLAGE_WOODPILE, ] ) if event_type != SimulationIds.EVENT_STORAGE_WITHDRAWN: return false var actor_id := int(event.data["actor_id"]) return ( actor_id >= 0 and item_id == SimulationIds.RESOURCE_FOOD and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY and (StringName(event.data["destination_id"]) == SimulationIds.npc_inventory_id(actor_id)) ) static func _find_npc(npc_id: int, npcs: Array[SimNPC]) -> SimNPC: for npc in npcs: if npc.id == npc_id: return npc return null static func _key(knower_id: int, event_id: int) -> String: return "%d:%d" % [knower_id, event_id] static func _sort_records(first: KnownEventStateRecord, second: KnownEventStateRecord) -> bool: if first.get_knower_id() != second.get_knower_id(): return first.get_knower_id() < second.get_knower_id() return first.get_event_id() < second.get_event_id() static func _sort_by_acquisition( first: KnownEventStateRecord, second: KnownEventStateRecord ) -> bool: if first.get_acquired_tick() != second.get_acquired_tick(): return first.get_acquired_tick() < second.get_acquired_tick() return first.get_event_id() < second.get_event_id() static func _sort_by_importance(first: Dictionary, second: Dictionary) -> bool: if bool(first["pinned"]) != bool(second["pinned"]): return bool(first["pinned"]) if bool(first["lasting"]) != bool(second["lasting"]): return bool(first["lasting"]) if not is_equal_approx(float(first["salience"]), float(second["salience"])): return float(first["salience"]) > float(second["salience"]) if not is_equal_approx(float(first["confidence"]), float(second["confidence"])): return float(first["confidence"]) > float(second["confidence"]) if int(first["acquired_tick"]) != int(second["acquired_tick"]): return int(first["acquired_tick"]) > int(second["acquired_tick"]) return int(first["event_id"]) > int(second["event_id"])