feat: add provenance-aware npc communication
This commit is contained in:
@@ -16,6 +16,7 @@ signal state_restored
|
||||
signal npc_decision_recorded(npc: SimNPC, decision: ActionSelectionResult)
|
||||
signal relationship_changed(relationship: RelationshipStateRecord, cause_event: EconomicEventRecord)
|
||||
signal event_knowledge_changed(knower_id: int, event: EconomicEventRecord)
|
||||
signal event_knowledge_transferred(speaker_id: int, listener_id: int, event: EconomicEventRecord)
|
||||
|
||||
var village := SimVillage.new()
|
||||
|
||||
@@ -52,6 +53,7 @@ var action_executor := ActionExecutionSystem.new()
|
||||
var target_resolver := ActionTargetResolver.new()
|
||||
var speed_index := 2
|
||||
const SPEED_LEVELS := [0.25, 0.5, 1.0, 2.0, 4.0, 10.0]
|
||||
const KNOWLEDGE_COMMUNICATION_RADIUS := 2.5
|
||||
signal speed_changed(multiplier: float)
|
||||
|
||||
const NPC_NAMES := ["Amina", "Tarik", "Jasmin", "Elma", "Mirza", "Lejla"]
|
||||
@@ -384,18 +386,24 @@ func notify_npc_arrived(npc_id: int) -> void:
|
||||
npc.has_travel_target = false
|
||||
npc.start_working()
|
||||
|
||||
var working_speakers: Array[SimNPC] = []
|
||||
if npc.target_id != &"":
|
||||
for other in npcs:
|
||||
if other.id == npc.id or other.is_dead:
|
||||
continue
|
||||
if other.target_id != npc.target_id:
|
||||
continue
|
||||
if (
|
||||
other.target_id == npc.target_id
|
||||
and (
|
||||
other.task_state
|
||||
in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING]
|
||||
)
|
||||
other.task_state
|
||||
in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING]
|
||||
):
|
||||
relationship_system.increase_shared_work_familiarity(npc.id, other.id)
|
||||
if other.task_state == SimNPC.TASK_STATE_WORKING:
|
||||
working_speakers.append(other)
|
||||
working_speakers.sort_custom(_sort_npcs_by_id)
|
||||
for speaker in working_speakers:
|
||||
if try_communicate_at_shared_activity(speaker.id, npc.id):
|
||||
break
|
||||
|
||||
if debug_logs:
|
||||
print(
|
||||
@@ -583,14 +591,81 @@ func _on_economic_event_recorded(event: EconomicEventRecord) -> void:
|
||||
event, npcs
|
||||
)
|
||||
for learned_record in learned_records:
|
||||
event_knowledge_changed.emit(learned_record.get_knower_id(), event)
|
||||
var knower_ids: Array[int] = event_knowledge_system.get_knowers(int(event.data["event_id"]))
|
||||
_apply_new_event_knowledge(learned_record, event)
|
||||
economic_event_recorded.emit(event)
|
||||
|
||||
|
||||
func try_communicate_at_shared_activity(speaker_id: int, listener_id: int) -> bool:
|
||||
var speaker := _find_npc_by_id(speaker_id)
|
||||
var listener := _find_npc_by_id(listener_id)
|
||||
if not _can_communicate_at_shared_activity(speaker, listener):
|
||||
return false
|
||||
var known_event_ids := event_knowledge_system.get_known_event_ids(speaker.id, 0)
|
||||
for index in range(known_event_ids.size() - 1, -1, -1):
|
||||
var event := event_log.get_by_id(known_event_ids[index])
|
||||
if event == null:
|
||||
continue
|
||||
var learned_record := event_knowledge_system.communicate_event(
|
||||
event, speaker.id, listener.id
|
||||
)
|
||||
if learned_record == null:
|
||||
continue
|
||||
_apply_new_event_knowledge(learned_record, event)
|
||||
event_knowledge_transferred.emit(speaker.id, listener.id, event)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _can_communicate_at_shared_activity(speaker: SimNPC, listener: SimNPC) -> bool:
|
||||
if speaker == null or listener == null or speaker.id == listener.id:
|
||||
return false
|
||||
if speaker.is_dead or listener.is_dead:
|
||||
return false
|
||||
if (
|
||||
speaker.task_state != SimNPC.TASK_STATE_WORKING
|
||||
or listener.task_state != SimNPC.TASK_STATE_WORKING
|
||||
):
|
||||
return false
|
||||
if speaker.target_id.is_empty() or speaker.target_id != listener.target_id:
|
||||
return false
|
||||
if storage_states.has(speaker.target_id):
|
||||
return false
|
||||
var speaker_action := SimulationDefinitions.get_action(speaker.current_task)
|
||||
var listener_action := SimulationDefinitions.get_action(listener.current_task)
|
||||
if (
|
||||
speaker_action == null
|
||||
or listener_action == null
|
||||
or speaker_action.target_type != SimulationIds.TARGET_ACTIVITY
|
||||
or listener_action.target_type != SimulationIds.TARGET_ACTIVITY
|
||||
):
|
||||
return false
|
||||
return (
|
||||
speaker.position.distance_squared_to(listener.position)
|
||||
<= KNOWLEDGE_COMMUNICATION_RADIUS * KNOWLEDGE_COMMUNICATION_RADIUS
|
||||
)
|
||||
|
||||
|
||||
func _apply_new_event_knowledge(
|
||||
known_event: KnownEventStateRecord, event: EconomicEventRecord
|
||||
) -> void:
|
||||
event_knowledge_changed.emit(known_event.get_knower_id(), event)
|
||||
var newly_informed_ids: Array[int] = [known_event.get_knower_id()]
|
||||
var changed_relationships: Array[RelationshipStateRecord] = relationship_system.apply_event(
|
||||
event, npcs, knower_ids
|
||||
event, npcs, newly_informed_ids
|
||||
)
|
||||
for relationship in changed_relationships:
|
||||
relationship_changed.emit(relationship, event)
|
||||
economic_event_recorded.emit(event)
|
||||
|
||||
|
||||
func _find_npc_by_id(npc_id: int) -> SimNPC:
|
||||
for npc in npcs:
|
||||
if npc.id == npc_id:
|
||||
return npc
|
||||
return null
|
||||
|
||||
|
||||
static func _sort_npcs_by_id(first: SimNPC, second: SimNPC) -> bool:
|
||||
return first.id < second.id
|
||||
|
||||
|
||||
func get_primary_relationship(npc_id: int) -> RelationshipStateRecord:
|
||||
@@ -616,6 +691,10 @@ func npc_knows_event(npc_id: int, event_id: int) -> bool:
|
||||
return event_knowledge_system.knows_event(npc_id, event_id)
|
||||
|
||||
|
||||
func get_known_event_record(npc_id: int, event_id: int) -> KnownEventStateRecord:
|
||||
return event_knowledge_system.get_record(npc_id, event_id)
|
||||
|
||||
|
||||
func get_current_speed() -> String:
|
||||
return "%.2fx" % SPEED_LEVELS[speed_index]
|
||||
|
||||
|
||||
@@ -41,6 +41,11 @@ const EVENT_TASK_STARTED := &"task_started"
|
||||
const EVENT_TASK_BLOCKED := &"task_blocked"
|
||||
const EVENT_RESOURCE_DEPLETED := &"resource_depleted"
|
||||
|
||||
const KNOWLEDGE_ACQUISITION_PERFORMED := &"performed"
|
||||
const KNOWLEDGE_ACQUISITION_WITNESSED := &"witnessed"
|
||||
const KNOWLEDGE_ACQUISITION_COMMUNICATED := &"communicated"
|
||||
const KNOWLEDGE_ACQUISITION_LEGACY := &"legacy"
|
||||
|
||||
|
||||
static func npc_inventory_id(npc_id: int) -> StringName:
|
||||
return StringName("npc_%d_inventory" % npc_id)
|
||||
|
||||
@@ -13,7 +13,8 @@ func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[Kno
|
||||
var actor := _find_npc(int(event.data["actor_id"]), npcs)
|
||||
if actor == null:
|
||||
return learned
|
||||
var actor_record := _remember(actor.id, int(event.data["event_id"]))
|
||||
var event_id := int(event.data["event_id"])
|
||||
var actor_record := _remember(actor.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED)
|
||||
if actor_record != null:
|
||||
learned.append(actor_record)
|
||||
var witness_radius_squared := WITNESS_RADIUS * WITNESS_RADIUS
|
||||
@@ -23,17 +24,50 @@ func observe_event(event: EconomicEventRecord, npcs: Array[SimNPC]) -> Array[Kno
|
||||
continue
|
||||
if npc.position.distance_squared_to(event_position) > witness_radius_squared:
|
||||
continue
|
||||
var witness_record := _remember(npc.id, int(event.data["event_id"]))
|
||||
var witness_record := _remember(
|
||||
npc.id, event_id, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
|
||||
)
|
||||
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
|
||||
) -> KnownEventStateRecord:
|
||||
if (
|
||||
event == null
|
||||
or speaker_id == listener_id
|
||||
or listener_id == int(event.data["actor_id"])
|
||||
or not _is_witnessable(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, speaker_id
|
||||
)
|
||||
|
||||
|
||||
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 get_knowers(event_id: int) -> Array[int]:
|
||||
var knower_ids: Array[int] = []
|
||||
for record in known_events.values():
|
||||
@@ -71,11 +105,18 @@ func restore(records: Array[KnownEventStateRecord]) -> void:
|
||||
known_events[_key(record.get_knower_id(), record.get_event_id())] = record
|
||||
|
||||
|
||||
func _remember(knower_id: int, event_id: int) -> KnownEventStateRecord:
|
||||
func _remember(
|
||||
knower_id: int,
|
||||
event_id: int,
|
||||
acquisition_method: StringName,
|
||||
source_npc_id: int = KnownEventStateRecord.NO_SOURCE_NPC_ID
|
||||
) -> KnownEventStateRecord:
|
||||
var key := _key(knower_id, event_id)
|
||||
if known_events.has(key):
|
||||
return null
|
||||
var record := KnownEventStateRecord.create(knower_id, event_id)
|
||||
var record := KnownEventStateRecord.create(
|
||||
knower_id, event_id, acquisition_method, source_npc_id
|
||||
)
|
||||
known_events[key] = record
|
||||
return record
|
||||
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
class_name KnownEventStateRecord
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_VERSION := 1
|
||||
const SCHEMA_VERSION := 2
|
||||
const NO_SOURCE_NPC_ID := -1
|
||||
|
||||
const VALID_ACQUISITION_METHODS := [
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
|
||||
]
|
||||
|
||||
var data: Dictionary
|
||||
|
||||
@@ -10,7 +18,12 @@ func _init(record_data: Dictionary = {}) -> void:
|
||||
data = record_data.duplicate(true)
|
||||
|
||||
|
||||
static func create(knower_id: int, event_id: int) -> KnownEventStateRecord:
|
||||
static func create(
|
||||
knower_id: int,
|
||||
event_id: int,
|
||||
acquisition_method: StringName = SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
|
||||
source_npc_id: int = NO_SOURCE_NPC_ID
|
||||
) -> KnownEventStateRecord:
|
||||
return (
|
||||
KnownEventStateRecord
|
||||
. new(
|
||||
@@ -18,6 +31,8 @@ static func create(knower_id: int, event_id: int) -> KnownEventStateRecord:
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"knower_id": knower_id,
|
||||
"event_id": event_id,
|
||||
"acquisition_method": String(acquisition_method),
|
||||
"source_npc_id": source_npc_id,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -26,13 +41,22 @@ static func create(knower_id: int, event_id: int) -> KnownEventStateRecord:
|
||||
static func from_dictionary(record_data: Dictionary) -> KnownEventStateRecord:
|
||||
if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION:
|
||||
return null
|
||||
if not record_data.has_all(["knower_id", "event_id"]):
|
||||
if not record_data.has_all(["knower_id", "event_id", "acquisition_method", "source_npc_id"]):
|
||||
return null
|
||||
var knower_id := int(record_data["knower_id"])
|
||||
var event_id := int(record_data["event_id"])
|
||||
var acquisition_method := StringName(record_data["acquisition_method"])
|
||||
var source_npc_id := int(record_data["source_npc_id"])
|
||||
if knower_id < 0 or event_id < 0:
|
||||
return null
|
||||
return create(knower_id, event_id)
|
||||
if acquisition_method not in VALID_ACQUISITION_METHODS:
|
||||
return null
|
||||
if acquisition_method == SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
||||
if source_npc_id < 0 or source_npc_id == knower_id:
|
||||
return null
|
||||
elif source_npc_id != NO_SOURCE_NPC_ID:
|
||||
return null
|
||||
return create(knower_id, event_id, acquisition_method, source_npc_id)
|
||||
|
||||
|
||||
func get_knower_id() -> int:
|
||||
@@ -43,5 +67,13 @@ func get_event_id() -> int:
|
||||
return int(data["event_id"])
|
||||
|
||||
|
||||
func get_acquisition_method() -> StringName:
|
||||
return StringName(data["acquisition_method"])
|
||||
|
||||
|
||||
func get_source_npc_id() -> int:
|
||||
return int(data["source_npc_id"])
|
||||
|
||||
|
||||
func to_dictionary() -> Dictionary:
|
||||
return data.duplicate(true)
|
||||
|
||||
@@ -2,11 +2,12 @@ class_name SimulationStateRecord
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA_NAME := "the_steward.simulation"
|
||||
const SCHEMA_VERSION := 5
|
||||
const SCHEMA_VERSION := 6
|
||||
const LEGACY_SCHEMA_VERSION := 1
|
||||
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
||||
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
||||
const PREVIOUS_SCHEMA_VERSION := 4
|
||||
const KNOWLEDGE_LEGACY_SCHEMA_VERSION := 4
|
||||
const PREVIOUS_SCHEMA_VERSION := 5
|
||||
|
||||
var simulation: Dictionary
|
||||
var village: VillageStateRecord
|
||||
@@ -74,6 +75,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
LEGACY_SCHEMA_VERSION,
|
||||
EVENT_LEGACY_SCHEMA_VERSION,
|
||||
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
|
||||
KNOWLEDGE_LEGACY_SCHEMA_VERSION,
|
||||
PREVIOUS_SCHEMA_VERSION,
|
||||
]
|
||||
):
|
||||
@@ -207,6 +209,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
return null
|
||||
|
||||
var knowledge_keys := {}
|
||||
var knowledge_records_by_key := {}
|
||||
for item in knowledge_data:
|
||||
if not item is Dictionary:
|
||||
return null
|
||||
@@ -221,7 +224,13 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
if knowledge_keys.has(knowledge_key):
|
||||
return null
|
||||
knowledge_keys[knowledge_key] = true
|
||||
knowledge_records_by_key[knowledge_key] = known_event_record
|
||||
record.event_knowledge.append(known_event_record)
|
||||
for known_event_record in record.event_knowledge:
|
||||
if not _is_valid_knowledge_provenance(
|
||||
known_event_record, npc_ids, event_records_by_id, knowledge_records_by_key
|
||||
):
|
||||
return null
|
||||
|
||||
var relationship_keys := {}
|
||||
for item in relationship_data:
|
||||
@@ -266,6 +275,54 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
||||
return record
|
||||
|
||||
|
||||
static func _is_valid_knowledge_provenance(
|
||||
known_event: KnownEventStateRecord,
|
||||
npc_ids: Dictionary,
|
||||
event_records_by_id: Dictionary,
|
||||
knowledge_records_by_key: Dictionary
|
||||
) -> bool:
|
||||
var event := event_records_by_id.get(known_event.get_event_id()) as EconomicEventRecord
|
||||
if event == null:
|
||||
return false
|
||||
var method := known_event.get_acquisition_method()
|
||||
var knower_id := known_event.get_knower_id()
|
||||
var actor_id := int(event.data["actor_id"])
|
||||
if method == SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY:
|
||||
return true
|
||||
if (
|
||||
StringName(event.data["event_type"]) != SimulationIds.EVENT_STORAGE_DEPOSITED
|
||||
or StringName(event.data["item_id"]) != SimulationIds.RESOURCE_FOOD
|
||||
or float(event.data["amount"]) <= 0.0
|
||||
):
|
||||
return false
|
||||
match method:
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
|
||||
return knower_id == actor_id
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
|
||||
return knower_id != actor_id
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED:
|
||||
if knower_id == actor_id:
|
||||
return false
|
||||
var source_npc_id := known_event.get_source_npc_id()
|
||||
if not npc_ids.has(source_npc_id):
|
||||
return false
|
||||
var source_record := (
|
||||
knowledge_records_by_key.get("%d:%d" % [source_npc_id, known_event.get_event_id()])
|
||||
as KnownEventStateRecord
|
||||
)
|
||||
return (
|
||||
source_record != null
|
||||
and (
|
||||
source_record.get_acquisition_method()
|
||||
in [
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
|
||||
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
|
||||
]
|
||||
)
|
||||
)
|
||||
return false
|
||||
|
||||
|
||||
static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary:
|
||||
var migrated := legacy_data.duplicate(true)
|
||||
migrated["schema_version"] = SCHEMA_VERSION
|
||||
@@ -301,7 +358,21 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
|
||||
in [LEGACY_SCHEMA_VERSION, EVENT_LEGACY_SCHEMA_VERSION, RELATIONSHIP_LEGACY_SCHEMA_VERSION]
|
||||
):
|
||||
migrated["relationships"] = _migrate_npc_familiarity(legacy_data.get("npcs", []))
|
||||
migrated["event_knowledge"] = _migrate_relationship_causes(migrated.get("relationships", []))
|
||||
if (
|
||||
version
|
||||
in [
|
||||
LEGACY_SCHEMA_VERSION,
|
||||
EVENT_LEGACY_SCHEMA_VERSION,
|
||||
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
|
||||
KNOWLEDGE_LEGACY_SCHEMA_VERSION,
|
||||
]
|
||||
):
|
||||
migrated["event_knowledge"] = _migrate_relationship_causes(
|
||||
migrated.get("relationships", [])
|
||||
)
|
||||
migrated["event_knowledge"] = _migrate_known_event_provenance(
|
||||
migrated.get("event_knowledge", []), migrated.get("economic_events", [])
|
||||
)
|
||||
return migrated
|
||||
|
||||
|
||||
@@ -377,3 +448,41 @@ static func _knowledge_dictionary_sort(first: Dictionary, second: Dictionary) ->
|
||||
if int(first["knower_id"]) != int(second["knower_id"]):
|
||||
return int(first["knower_id"]) < int(second["knower_id"])
|
||||
return int(first["event_id"]) < int(second["event_id"])
|
||||
|
||||
|
||||
static func _migrate_known_event_provenance(knowledge_data: Variant, event_data: Variant) -> Array:
|
||||
if not knowledge_data is Array:
|
||||
return []
|
||||
var actor_by_event_id := {}
|
||||
if event_data is Array:
|
||||
for event_item in event_data:
|
||||
if not event_item is Dictionary:
|
||||
continue
|
||||
actor_by_event_id[int(event_item.get("event_id", -1))] = int(
|
||||
event_item.get("actor_id", -1)
|
||||
)
|
||||
var migrated_knowledge: Array = []
|
||||
var can_sort := true
|
||||
for item in knowledge_data:
|
||||
if not item is Dictionary:
|
||||
migrated_knowledge.append(item)
|
||||
can_sort = false
|
||||
continue
|
||||
if int(item.get("schema_version", -1)) == KnownEventStateRecord.SCHEMA_VERSION:
|
||||
migrated_knowledge.append(item.duplicate(true))
|
||||
continue
|
||||
if int(item.get("schema_version", -1)) != 1 or not item.has_all(["knower_id", "event_id"]):
|
||||
migrated_knowledge.append(item.duplicate(true))
|
||||
can_sort = false
|
||||
continue
|
||||
var knower_id := int(item["knower_id"])
|
||||
var event_id := int(item["event_id"])
|
||||
var acquisition_method := SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY
|
||||
if int(actor_by_event_id.get(event_id, -2)) == knower_id:
|
||||
acquisition_method = SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
|
||||
migrated_knowledge.append(
|
||||
KnownEventStateRecord.create(knower_id, event_id, acquisition_method).to_dictionary()
|
||||
)
|
||||
if can_sort:
|
||||
migrated_knowledge.sort_custom(_knowledge_dictionary_sort)
|
||||
return migrated_knowledge
|
||||
|
||||
Reference in New Issue
Block a user