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
+157 -17
View File
@@ -17,6 +17,7 @@ func _run() -> void:
_test_previous_world_relationship_migration()
_test_previous_world_knowledge_migration()
_test_previous_world_provenance_migration()
_test_previous_world_retention_migration()
_test_relationship_schema_rejection()
_test_schema_rejection()
@@ -138,6 +139,15 @@ func _test_schema_rejection() -> void:
)
_check(SimulationStateRecord.from_json("[]") == null, "Non-record JSON should be rejected")
var manager := _create_manager(41)
var invalid_clock_data: Dictionary = manager.create_state_record().to_dictionary()
invalid_clock_data["simulation"]["tick_interval"] = 0.0
_check(
SimulationStateRecord.from_dictionary(invalid_clock_data) == null,
"Saved simulation clocks with a zero tick interval should be rejected"
)
manager.free()
func _test_legacy_resource_migration() -> void:
var migrated := ResourceStateRecord.from_dictionary(
@@ -272,7 +282,7 @@ func _test_previous_world_knowledge_migration() -> void:
var deposit_event := EconomicEventRecord.create(
0,
SimulationIds.EVENT_STORAGE_DEPOSITED,
12,
0,
1,
SimulationIds.npc_inventory_id(1),
SimulationIds.STORAGE_VILLAGE_PANTRY,
@@ -305,11 +315,11 @@ func _test_previous_world_knowledge_migration() -> void:
func _test_previous_world_provenance_migration() -> void:
var manager := _create_manager(60)
var previous_data: Dictionary = manager.create_state_record().to_dictionary()
previous_data["schema_version"] = SimulationStateRecord.PREVIOUS_SCHEMA_VERSION
previous_data["schema_version"] = SimulationStateRecord.PROVENANCE_LEGACY_SCHEMA_VERSION
var deposit_event := EconomicEventRecord.create(
0,
SimulationIds.EVENT_STORAGE_DEPOSITED,
12,
0,
1,
SimulationIds.npc_inventory_id(1),
SimulationIds.STORAGE_VILLAGE_PANTRY,
@@ -358,6 +368,77 @@ func _test_previous_world_provenance_migration() -> void:
manager.free()
func _test_previous_world_retention_migration() -> void:
var manager := _create_manager(61)
var previous_data: Dictionary = manager.create_state_record().to_dictionary()
previous_data["schema_version"] = SimulationStateRecord.PREVIOUS_SCHEMA_VERSION
previous_data["simulation"]["tick_count"] = 50
var deposit_event := EconomicEventRecord.create(
0,
SimulationIds.EVENT_STORAGE_DEPOSITED,
0,
0,
SimulationIds.npc_inventory_id(0),
SimulationIds.STORAGE_VILLAGE_PANTRY,
SimulationIds.RESOURCE_FOOD,
2.0
)
previous_data["economic_events"] = [deposit_event.to_dictionary()]
previous_data["simulation"]["next_event_id"] = 1
var actor_knowledge := (
KnownEventStateRecord
. create(0, 0, SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED)
. to_dictionary()
)
var witness_knowledge := (
KnownEventStateRecord
. create(2, 0, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED)
. to_dictionary()
)
var communicated_knowledge := (
KnownEventStateRecord
. create(
1,
0,
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
2,
0,
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
)
. to_dictionary()
)
for old_record in [actor_knowledge, witness_knowledge, communicated_knowledge]:
old_record["schema_version"] = 2
old_record.erase("acquired_tick")
old_record.erase("source_acquisition_method")
previous_data["event_knowledge"] = [communicated_knowledge, actor_knowledge, witness_knowledge]
previous_data["relationships"] = []
var migrated := SimulationStateRecord.from_dictionary(previous_data)
_check(migrated != null, "World schema v6 should add deterministic retention metadata")
if migrated != null:
var records_by_knower := {}
for record in migrated.event_knowledge:
records_by_knower[record.get_knower_id()] = record
var migrated_actor := records_by_knower.get(0) as KnownEventStateRecord
var migrated_listener := records_by_knower.get(1) as KnownEventStateRecord
_check(
migrated_actor != null and migrated_actor.get_acquired_tick() == 0,
"Direct v6 knowledge should retain the objective event tick"
)
_check(
(
migrated_listener != null
and migrated_listener.get_acquired_tick() == 50
and (
migrated_listener.get_source_acquisition_method()
== SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
)
),
"Communicated v6 knowledge should receive a fresh lease and source snapshot"
)
manager.free()
func _test_relationship_schema_rejection() -> void:
var manager := _create_manager(58)
var missing_cause_data: Dictionary = manager.create_state_record().to_dictionary()
@@ -389,7 +470,7 @@ func _test_relationship_schema_rejection() -> void:
var event := EconomicEventRecord.create(
0,
SimulationIds.EVENT_STORAGE_DEPOSITED,
1,
0,
0,
SimulationIds.npc_inventory_id(0),
SimulationIds.STORAGE_VILLAGE_PANTRY,
@@ -411,7 +492,7 @@ func _test_relationship_schema_rejection() -> void:
var unrelated_deposit := EconomicEventRecord.create(
0,
SimulationIds.EVENT_STORAGE_DEPOSITED,
1,
0,
2,
SimulationIds.npc_inventory_id(2),
SimulationIds.STORAGE_VILLAGE_PANTRY,
@@ -435,7 +516,7 @@ func _test_relationship_schema_rejection() -> void:
var nonpositive_deposit := EconomicEventRecord.create(
0,
SimulationIds.EVENT_STORAGE_DEPOSITED,
1,
0,
1,
SimulationIds.npc_inventory_id(1),
SimulationIds.STORAGE_VILLAGE_PANTRY,
@@ -456,26 +537,40 @@ func _test_relationship_schema_rejection() -> void:
var provenance_event := EconomicEventRecord.create(
0,
SimulationIds.EVENT_STORAGE_DEPOSITED,
1,
0,
0,
SimulationIds.npc_inventory_id(0),
SimulationIds.STORAGE_VILLAGE_PANTRY,
SimulationIds.RESOURCE_FOOD,
1.0
)
var missing_source_data: Dictionary = manager.create_state_record().to_dictionary()
missing_source_data["economic_events"] = [provenance_event.to_dictionary()]
missing_source_data["simulation"]["next_event_id"] = 1
missing_source_data["event_knowledge"] = [
var historical_source_data: Dictionary = manager.create_state_record().to_dictionary()
historical_source_data["economic_events"] = [provenance_event.to_dictionary()]
historical_source_data["simulation"]["next_event_id"] = 1
historical_source_data["event_knowledge"] = [
(
KnownEventStateRecord
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
. create(
1,
0,
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
2,
0,
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
)
. to_dictionary()
)
]
_check(
SimulationStateRecord.from_dictionary(missing_source_data) == null,
"Communicated knowledge must reference a source who knows the same event"
SimulationStateRecord.from_dictionary(historical_source_data) != null,
"A listener's provenance should remain valid after the direct source forgets"
)
var unknown_source_data: Dictionary = historical_source_data.duplicate(true)
unknown_source_data["event_knowledge"][0]["source_npc_id"] = 999
_check(
SimulationStateRecord.from_dictionary(unknown_source_data) == null,
"Communicated knowledge must reference an existing historical source"
)
var wrong_actor_data: Dictionary = manager.create_state_record().to_dictionary()
@@ -499,12 +594,26 @@ func _test_relationship_schema_rejection() -> void:
relayed_source_data["event_knowledge"] = [
(
KnownEventStateRecord
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
. create(
1,
0,
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
2,
0,
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED
)
. to_dictionary()
),
(
KnownEventStateRecord
. create(2, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 0)
. create(
2,
0,
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
0,
0,
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED
)
. to_dictionary()
),
(
@@ -517,6 +626,14 @@ func _test_relationship_schema_rejection() -> void:
SimulationStateRecord.from_dictionary(relayed_source_data) == null,
"Communicated facts must not form a second relay hop in this phase"
)
var reacquired_source_data: Dictionary = relayed_source_data.duplicate(true)
reacquired_source_data["event_knowledge"][0]["source_acquisition_method"] = String(
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
)
_check(
SimulationStateRecord.from_dictionary(reacquired_source_data) != null,
"Historical direct provenance should survive a source reacquiring the fact differently"
)
var order_independent_data: Dictionary = manager.create_state_record().to_dictionary()
order_independent_data["economic_events"] = [provenance_event.to_dictionary()]
@@ -524,7 +641,14 @@ func _test_relationship_schema_rejection() -> void:
order_independent_data["event_knowledge"] = [
(
KnownEventStateRecord
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED, 2)
. create(
1,
0,
SimulationIds.KNOWLEDGE_ACQUISITION_COMMUNICATED,
2,
0,
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED
)
. to_dictionary()
),
(
@@ -542,6 +666,22 @@ func _test_relationship_schema_rejection() -> void:
SimulationStateRecord.from_dictionary(order_independent_data) != null,
"Provenance validation should not depend on knowledge array ordering"
)
var invalid_direct_time_data: Dictionary = manager.create_state_record().to_dictionary()
invalid_direct_time_data["simulation"]["tick_count"] = 2
invalid_direct_time_data["economic_events"] = [provenance_event.to_dictionary()]
invalid_direct_time_data["simulation"]["next_event_id"] = 1
invalid_direct_time_data["event_knowledge"] = [
(
KnownEventStateRecord
. create(1, 0, SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED, -1, 1)
. to_dictionary()
)
]
_check(
SimulationStateRecord.from_dictionary(invalid_direct_time_data) == null,
"Direct witness acquisition must use the immutable objective event tick"
)
manager.free()