415 lines
13 KiB
GDScript
415 lines
13 KiB
GDScript
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"])
|