feat: add bounded npc memory retention

This commit is contained in:
Rijad Zuzo
2026-07-12 01:10:10 +02:00
parent 9a479ba182
commit 99b8146f75
17 changed files with 945 additions and 126 deletions
+45 -6
View File
@@ -1,7 +1,7 @@
class_name KnownEventStateRecord
extends RefCounted
const SCHEMA_VERSION := 2
const SCHEMA_VERSION := 3
const NO_SOURCE_NPC_ID := -1
const VALID_ACQUISITION_METHODS := [
@@ -10,6 +10,10 @@ const VALID_ACQUISITION_METHODS := [
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
]
const DIRECT_ACQUISITION_METHODS := [
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED,
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED,
]
var data: Dictionary
@@ -22,7 +26,9 @@ static func create(
knower_id: int,
event_id: int,
acquisition_method: StringName = SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY,
source_npc_id: int = NO_SOURCE_NPC_ID
source_npc_id: int = NO_SOURCE_NPC_ID,
acquired_tick: int = 0,
source_acquisition_method: StringName = &""
) -> KnownEventStateRecord:
return (
KnownEventStateRecord
@@ -33,6 +39,8 @@ static func create(
"event_id": event_id,
"acquisition_method": String(acquisition_method),
"source_npc_id": source_npc_id,
"acquired_tick": acquired_tick,
"source_acquisition_method": String(source_acquisition_method),
}
)
)
@@ -41,22 +49,45 @@ static func create(
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", "acquisition_method", "source_npc_id"]):
if not (
record_data
. has_all(
[
"knower_id",
"event_id",
"acquisition_method",
"source_npc_id",
"acquired_tick",
"source_acquisition_method",
]
)
):
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:
var acquired_tick := int(record_data["acquired_tick"])
var source_acquisition_method := StringName(record_data["source_acquisition_method"])
if knower_id < 0 or event_id < 0 or acquired_tick < 0:
return null
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:
if source_acquisition_method not in DIRECT_ACQUISITION_METHODS:
return null
elif source_npc_id != NO_SOURCE_NPC_ID or not source_acquisition_method.is_empty():
return null
return create(knower_id, event_id, acquisition_method, source_npc_id)
return create(
knower_id,
event_id,
acquisition_method,
source_npc_id,
acquired_tick,
source_acquisition_method
)
func get_knower_id() -> int:
@@ -75,5 +106,13 @@ func get_source_npc_id() -> int:
return int(data["source_npc_id"])
func get_acquired_tick() -> int:
return int(data["acquired_tick"])
func get_source_acquisition_method() -> StringName:
return StringName(data["source_acquisition_method"])
func to_dictionary() -> Dictionary:
return data.duplicate(true)
+101 -37
View File
@@ -2,12 +2,13 @@ class_name SimulationStateRecord
extends RefCounted
const SCHEMA_NAME := "the_steward.simulation"
const SCHEMA_VERSION := 6
const SCHEMA_VERSION := 7
const LEGACY_SCHEMA_VERSION := 1
const EVENT_LEGACY_SCHEMA_VERSION := 2
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
const KNOWLEDGE_LEGACY_SCHEMA_VERSION := 4
const PREVIOUS_SCHEMA_VERSION := 5
const PROVENANCE_LEGACY_SCHEMA_VERSION := 5
const PREVIOUS_SCHEMA_VERSION := 6
var simulation: Dictionary
var village: VillageStateRecord
@@ -76,6 +77,7 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
EVENT_LEGACY_SCHEMA_VERSION,
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
KNOWLEDGE_LEGACY_SCHEMA_VERSION,
PROVENANCE_LEGACY_SCHEMA_VERSION,
PREVIOUS_SCHEMA_VERSION,
]
):
@@ -114,6 +116,23 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
]
):
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"])
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
):
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
@@ -209,7 +228,6 @@ 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
@@ -224,11 +242,10 @@ 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
known_event_record, npc_ids, event_records_by_id, int(record.simulation["tick_count"])
):
return null
@@ -279,7 +296,7 @@ static func _is_valid_knowledge_provenance(
known_event: KnownEventStateRecord,
npc_ids: Dictionary,
event_records_by_id: Dictionary,
knowledge_records_by_key: Dictionary
current_tick: int
) -> bool:
var event := event_records_by_id.get(known_event.get_event_id()) as EconomicEventRecord
if event == null:
@@ -287,6 +304,10 @@ static func _is_valid_knowledge_provenance(
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 (
@@ -297,29 +318,22 @@ static func _is_valid_knowledge_provenance(
return false
match method:
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
return knower_id == actor_id
return knower_id == actor_id and acquired_tick == event_tick
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
return knower_id != actor_id
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_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,
]
)
)
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
@@ -368,10 +382,13 @@ static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary
]
):
migrated["event_knowledge"] = _migrate_relationship_causes(
migrated.get("relationships", [])
migrated.get("relationships", []),
int((migrated.get("simulation", {}) as Dictionary).get("tick_count", 0))
)
migrated["event_knowledge"] = _migrate_known_event_provenance(
migrated.get("event_knowledge", []), migrated.get("economic_events", [])
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))
)
return migrated
@@ -417,7 +434,9 @@ static func _relationship_dictionary_sort(first: Dictionary, second: Dictionary)
return int(first["subject_id"]) < int(second["subject_id"])
static func _migrate_relationship_causes(relationship_data: Variant) -> Array[Dictionary]:
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:
@@ -438,7 +457,17 @@ static func _migrate_relationship_causes(relationship_data: Variant) -> Array[Di
continue
known_keys[knowledge_key] = true
migrated_knowledge.append(
KnownEventStateRecord.create(observer_id, cause_event_id).to_dictionary()
(
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
@@ -450,17 +479,21 @@ static func _knowledge_dictionary_sort(first: Dictionary, second: Dictionary) ->
return int(first["event_id"]) < int(second["event_id"])
static func _migrate_known_event_provenance(knowledge_data: Variant, event_data: Variant) -> Array:
static func _migrate_known_event_retention(
knowledge_data: Variant, event_data: Variant, migration_tick: int
) -> Array:
if not knowledge_data is Array:
return []
var actor_by_event_id := {}
var event_by_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)
)
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:
@@ -471,17 +504,48 @@ static func _migrate_known_event_provenance(knowledge_data: Variant, event_data:
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"]):
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 acquisition_method := SimulationIds.KNOWLEDGE_ACQUISITION_LEGACY
if int(actor_by_event_id.get(event_id, -2)) == knower_id:
acquisition_method = SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
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).to_dictionary()
(
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)