927 lines
30 KiB
GDScript
927 lines
30 KiB
GDScript
class_name SimulationStateRecord
|
|
extends RefCounted
|
|
|
|
const SCHEMA_NAME := "the_steward.simulation"
|
|
const SCHEMA_VERSION := 11
|
|
const LEGACY_SCHEMA_VERSION := 1
|
|
const EVENT_LEGACY_SCHEMA_VERSION := 2
|
|
const RELATIONSHIP_LEGACY_SCHEMA_VERSION := 3
|
|
const KNOWLEDGE_LEGACY_SCHEMA_VERSION := 4
|
|
const PROVENANCE_LEGACY_SCHEMA_VERSION := 5
|
|
const RETENTION_LEGACY_SCHEMA_VERSION := 6
|
|
const OPPORTUNITY_LEGACY_SCHEMA_VERSION := 8
|
|
const ANIMAL_LEGACY_SCHEMA_VERSION := 9
|
|
const ROUTINE_LEGACY_SCHEMA_VERSION := 10
|
|
const PREVIOUS_SCHEMA_VERSION := 7
|
|
|
|
var simulation: Dictionary
|
|
var village: VillageStateRecord
|
|
var npcs: Array[NPCStateRecord] = []
|
|
var animals: Array[AnimalStateRecord] = []
|
|
var resources: Array[ResourceStateRecord] = []
|
|
var storages: Array[StorageStateRecord] = []
|
|
var economic_events: Array[EconomicEventRecord] = []
|
|
var relationships: Array[RelationshipStateRecord] = []
|
|
var event_knowledge: Array[KnownEventStateRecord] = []
|
|
var opportunities: Array[OpportunityStateRecord] = []
|
|
|
|
|
|
func to_dictionary() -> Dictionary:
|
|
var npc_data: Array[Dictionary] = []
|
|
for npc_record in npcs:
|
|
npc_data.append(npc_record.to_dictionary())
|
|
|
|
var animal_data: Array[Dictionary] = []
|
|
for animal_record in animals:
|
|
animal_data.append(animal_record.to_dictionary())
|
|
var resource_data: Array[Dictionary] = []
|
|
for resource_record in resources:
|
|
resource_data.append(resource_record.to_dictionary())
|
|
var storage_data: Array[Dictionary] = []
|
|
for storage_record in storages:
|
|
storage_data.append(storage_record.to_dictionary())
|
|
var event_data: Array[Dictionary] = []
|
|
for event_record in economic_events:
|
|
event_data.append(event_record.to_dictionary())
|
|
var relationship_data: Array[Dictionary] = []
|
|
for relationship_record in relationships:
|
|
relationship_data.append(relationship_record.to_dictionary())
|
|
var knowledge_data: Array[Dictionary] = []
|
|
for known_event_record in event_knowledge:
|
|
knowledge_data.append(known_event_record.to_dictionary())
|
|
var opportunity_data: Array[Dictionary] = []
|
|
for opportunity_record in opportunities:
|
|
opportunity_data.append(opportunity_record.to_dictionary())
|
|
|
|
return {
|
|
"schema": SCHEMA_NAME,
|
|
"schema_version": SCHEMA_VERSION,
|
|
"simulation": simulation.duplicate(true),
|
|
"village": village.to_dictionary(),
|
|
"npcs": npc_data,
|
|
"animals": animal_data,
|
|
"resources": resource_data,
|
|
"storages": storage_data,
|
|
"economic_events": event_data,
|
|
"relationships": relationship_data,
|
|
"event_knowledge": knowledge_data,
|
|
"opportunities": opportunity_data
|
|
}
|
|
|
|
|
|
func to_json() -> String:
|
|
return JSON.stringify(to_dictionary())
|
|
|
|
|
|
static func from_json(json_text: String) -> SimulationStateRecord:
|
|
var parsed = JSON.parse_string(json_text)
|
|
if not parsed is Dictionary:
|
|
return null
|
|
return from_dictionary(parsed)
|
|
|
|
|
|
static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord:
|
|
if record_data.get("schema", "") != SCHEMA_NAME:
|
|
return null
|
|
var version := int(record_data.get("schema_version", -1))
|
|
if (
|
|
version
|
|
in [
|
|
LEGACY_SCHEMA_VERSION,
|
|
EVENT_LEGACY_SCHEMA_VERSION,
|
|
RELATIONSHIP_LEGACY_SCHEMA_VERSION,
|
|
KNOWLEDGE_LEGACY_SCHEMA_VERSION,
|
|
PROVENANCE_LEGACY_SCHEMA_VERSION,
|
|
RETENTION_LEGACY_SCHEMA_VERSION,
|
|
PREVIOUS_SCHEMA_VERSION,
|
|
OPPORTUNITY_LEGACY_SCHEMA_VERSION,
|
|
ANIMAL_LEGACY_SCHEMA_VERSION,
|
|
ROUTINE_LEGACY_SCHEMA_VERSION,
|
|
]
|
|
):
|
|
record_data = _migrate_legacy(record_data, version)
|
|
elif version != SCHEMA_VERSION:
|
|
return null
|
|
if not (
|
|
record_data
|
|
. has_all(
|
|
[
|
|
"simulation",
|
|
"village",
|
|
"npcs",
|
|
"animals",
|
|
"resources",
|
|
"storages",
|
|
"economic_events",
|
|
"relationships",
|
|
"event_knowledge",
|
|
"opportunities",
|
|
]
|
|
)
|
|
):
|
|
return null
|
|
|
|
var simulation_data = record_data["simulation"]
|
|
if not simulation_data is Dictionary:
|
|
return null
|
|
if not (
|
|
simulation_data
|
|
. has_all(
|
|
[
|
|
"seed",
|
|
"tick_interval",
|
|
"tick_count",
|
|
"clock_accumulator",
|
|
"clock_elapsed_ticks",
|
|
"wander_random_streams",
|
|
"next_event_id",
|
|
"next_opportunity_id",
|
|
]
|
|
)
|
|
):
|
|
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"])
|
|
var saved_next_opportunity_id := int(simulation_data["next_opportunity_id"])
|
|
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
|
|
or saved_next_opportunity_id < 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
|
|
for stream_data in wander_streams:
|
|
if not stream_data is Dictionary:
|
|
return null
|
|
if not stream_data.has_all(["npc_id", "seed", "state"]):
|
|
return null
|
|
|
|
var village_data = record_data["village"]
|
|
if not village_data is Dictionary:
|
|
return null
|
|
var village_record := VillageStateRecord.from_dictionary(village_data)
|
|
if village_record == null:
|
|
return null
|
|
|
|
var npc_data = record_data["npcs"]
|
|
var animal_data = record_data["animals"]
|
|
var resource_data = record_data["resources"]
|
|
var storage_data = record_data["storages"]
|
|
var event_data = record_data["economic_events"]
|
|
var relationship_data = record_data["relationships"]
|
|
var knowledge_data = record_data["event_knowledge"]
|
|
var opportunity_data = record_data["opportunities"]
|
|
if (
|
|
not npc_data is Array
|
|
or not animal_data is Array
|
|
or not resource_data is Array
|
|
or not storage_data is Array
|
|
or not event_data is Array
|
|
or not relationship_data is Array
|
|
or not knowledge_data is Array
|
|
or not opportunity_data is Array
|
|
):
|
|
return null
|
|
|
|
var record := SimulationStateRecord.new()
|
|
record.simulation = simulation_data.duplicate(true)
|
|
record.village = village_record
|
|
var npc_ids := {}
|
|
var npc_records_by_id := {}
|
|
|
|
for item in npc_data:
|
|
if not item is Dictionary:
|
|
return null
|
|
var npc_record := NPCStateRecord.from_dictionary(item)
|
|
if npc_record == null:
|
|
return null
|
|
var npc_id := int(npc_record.data["id"])
|
|
if npc_ids.has(npc_id):
|
|
return null
|
|
npc_ids[npc_id] = true
|
|
npc_records_by_id[npc_id] = npc_record
|
|
record.npcs.append(npc_record)
|
|
|
|
var resource_ids := {}
|
|
var resource_records_by_id := {}
|
|
for item in resource_data:
|
|
if not item is Dictionary:
|
|
return null
|
|
var resource_record := ResourceStateRecord.from_dictionary(item)
|
|
if resource_record == null:
|
|
return null
|
|
var resource_id := String(resource_record.data["node_id"])
|
|
if resource_ids.has(resource_id):
|
|
return null
|
|
resource_ids[resource_id] = true
|
|
resource_records_by_id[resource_id] = resource_record
|
|
record.resources.append(resource_record)
|
|
|
|
var animal_ids := {}
|
|
var animal_records_by_id := {}
|
|
for item in animal_data:
|
|
if not item is Dictionary:
|
|
return null
|
|
var animal_record := AnimalStateRecord.from_dictionary(item)
|
|
if animal_record == null:
|
|
return null
|
|
var animal_id := String(animal_record.data["animal_id"])
|
|
if animal_ids.has(animal_id) or resource_ids.has(animal_id):
|
|
return null
|
|
var reserved_by := animal_record.get_reserved_by()
|
|
if reserved_by >= 0:
|
|
var reserving_npc := npc_records_by_id.get(reserved_by) as NPCStateRecord
|
|
if (
|
|
reserving_npc == null
|
|
or (
|
|
StringName(reserving_npc.data["current_task"])
|
|
!= SimulationIds.ACTION_FEED_ANIMAL
|
|
)
|
|
or StringName(reserving_npc.data["target_id"]) != animal_record.get_animal_id()
|
|
or (
|
|
StringName(reserving_npc.data["task_state"])
|
|
not in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING]
|
|
)
|
|
):
|
|
return null
|
|
animal_ids[animal_id] = true
|
|
animal_records_by_id[animal_id] = animal_record
|
|
record.animals.append(animal_record)
|
|
for npc_record in record.npcs:
|
|
if (
|
|
StringName(npc_record.data["current_task"]) != SimulationIds.ACTION_FEED_ANIMAL
|
|
or (
|
|
StringName(npc_record.data["task_state"])
|
|
not in [SimNPC.TASK_STATE_TRAVELING, SimNPC.TASK_STATE_WORKING]
|
|
)
|
|
):
|
|
continue
|
|
var target_animal := (
|
|
animal_records_by_id.get(StringName(npc_record.data["target_id"])) as AnimalStateRecord
|
|
)
|
|
if target_animal == null or target_animal.get_reserved_by() != int(npc_record.data["id"]):
|
|
return null
|
|
|
|
var storage_ids := {}
|
|
var storage_records_by_id := {}
|
|
for item in storage_data:
|
|
if not item is Dictionary:
|
|
return null
|
|
var storage_record := StorageStateRecord.from_dictionary(item)
|
|
if storage_record == null:
|
|
return null
|
|
var storage_id := storage_record.get_storage_id()
|
|
if storage_ids.has(storage_id) or animal_ids.has(String(storage_id)):
|
|
return null
|
|
storage_ids[storage_id] = true
|
|
storage_records_by_id[storage_id] = storage_record
|
|
record.storages.append(storage_record)
|
|
var pantry := (
|
|
storage_records_by_id.get(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageStateRecord
|
|
)
|
|
if (
|
|
pantry != null
|
|
and not is_equal_approx(
|
|
float(record.village.data["food"]), pantry.get_amount(SimulationIds.RESOURCE_FOOD)
|
|
)
|
|
):
|
|
return null
|
|
var woodpile := (
|
|
storage_records_by_id.get(SimulationIds.STORAGE_VILLAGE_WOODPILE) as StorageStateRecord
|
|
)
|
|
if (
|
|
woodpile != null
|
|
and not is_equal_approx(
|
|
float(record.village.data["wood"]), woodpile.get_amount(SimulationIds.RESOURCE_WOOD)
|
|
)
|
|
):
|
|
return null
|
|
|
|
var event_ids := {}
|
|
var event_records_by_id := {}
|
|
var highest_event_id := -1
|
|
var latest_feed_ticks := {}
|
|
for item in event_data:
|
|
if not item is Dictionary:
|
|
return null
|
|
var event_record := EconomicEventRecord.from_dictionary(item)
|
|
if event_record == null:
|
|
return null
|
|
var event_id := int(event_record.data["event_id"])
|
|
if event_ids.has(event_id):
|
|
return null
|
|
event_ids[event_id] = true
|
|
event_records_by_id[event_id] = event_record
|
|
highest_event_id = maxi(highest_event_id, event_id)
|
|
if StringName(event_record.data["event_type"]) == SimulationIds.EVENT_ANIMAL_FED:
|
|
if not _is_valid_animal_feed_event(
|
|
event_record,
|
|
npc_ids,
|
|
animal_records_by_id,
|
|
storage_records_by_id,
|
|
int(record.simulation["tick_count"])
|
|
):
|
|
return null
|
|
var fed_animal_id := StringName(event_record.data["destination_id"])
|
|
latest_feed_ticks[fed_animal_id] = maxi(
|
|
int(latest_feed_ticks.get(fed_animal_id, AnimalStateRecord.NEVER_FED_TICK)),
|
|
int(event_record.data["tick"])
|
|
)
|
|
record.economic_events.append(event_record)
|
|
if int(record.simulation["next_event_id"]) <= highest_event_id:
|
|
return null
|
|
for animal_record in record.animals:
|
|
if (
|
|
animal_record.get_last_fed_tick()
|
|
!= int(
|
|
latest_feed_ticks.get(
|
|
animal_record.get_animal_id(), AnimalStateRecord.NEVER_FED_TICK
|
|
)
|
|
)
|
|
):
|
|
return null
|
|
|
|
var knowledge_keys := {}
|
|
var knowledge_records_by_key := {}
|
|
for item in knowledge_data:
|
|
if not item is Dictionary:
|
|
return null
|
|
var known_event_record := KnownEventStateRecord.from_dictionary(item)
|
|
if known_event_record == null:
|
|
return null
|
|
var knower_id := known_event_record.get_knower_id()
|
|
var known_event_id := known_event_record.get_event_id()
|
|
if not npc_ids.has(knower_id) or not event_ids.has(known_event_id):
|
|
return null
|
|
var knowledge_key := "%d:%d" % [knower_id, known_event_id]
|
|
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, int(record.simulation["tick_count"])
|
|
):
|
|
return null
|
|
|
|
var relationship_keys := {}
|
|
for item in relationship_data:
|
|
if not item is Dictionary:
|
|
return null
|
|
var relationship_record := RelationshipStateRecord.from_dictionary(item)
|
|
if relationship_record == null:
|
|
return null
|
|
var observer_id := relationship_record.get_observer_id()
|
|
var subject_id := relationship_record.get_subject_id()
|
|
if not npc_ids.has(observer_id) or not npc_ids.has(subject_id):
|
|
return null
|
|
var relationship_key := "%d:%d" % [observer_id, subject_id]
|
|
if relationship_keys.has(relationship_key):
|
|
return null
|
|
var cause_event_id := relationship_record.get_last_trust_cause_event_id()
|
|
if (
|
|
cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT
|
|
and not event_ids.has(cause_event_id)
|
|
):
|
|
return null
|
|
if (
|
|
cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT
|
|
and not knowledge_keys.has("%d:%d" % [observer_id, cause_event_id])
|
|
):
|
|
return null
|
|
if cause_event_id != RelationshipStateRecord.NO_CAUSE_EVENT:
|
|
var cause_event := event_records_by_id[cause_event_id] as EconomicEventRecord
|
|
if (
|
|
int(cause_event.data["actor_id"]) != subject_id
|
|
or (
|
|
StringName(cause_event.data["event_type"])
|
|
!= SimulationIds.EVENT_STORAGE_DEPOSITED
|
|
)
|
|
or StringName(cause_event.data["item_id"]) != SimulationIds.RESOURCE_FOOD
|
|
or float(cause_event.data["amount"]) <= 0.0
|
|
):
|
|
return null
|
|
relationship_keys[relationship_key] = true
|
|
record.relationships.append(relationship_record)
|
|
|
|
var opportunity_ids := {}
|
|
var trigger_event_ids := {}
|
|
var resolution_event_ids := {}
|
|
var has_open_opportunity := false
|
|
var highest_opportunity_id := -1
|
|
for item in opportunity_data:
|
|
if not item is Dictionary:
|
|
return null
|
|
var opportunity_record := OpportunityStateRecord.from_dictionary(item)
|
|
if opportunity_record == null:
|
|
return null
|
|
var opportunity_id := opportunity_record.get_opportunity_id()
|
|
var trigger_event_id := opportunity_record.get_trigger_event_id()
|
|
if opportunity_ids.has(opportunity_id) or trigger_event_ids.has(trigger_event_id):
|
|
return null
|
|
if opportunity_record.get_status() == OpportunityStateRecord.STATUS_OPEN:
|
|
if has_open_opportunity:
|
|
return null
|
|
has_open_opportunity = true
|
|
elif opportunity_record.get_resolution_event_id() != OpportunityStateRecord.NO_EVENT_ID:
|
|
if resolution_event_ids.has(opportunity_record.get_resolution_event_id()):
|
|
return null
|
|
resolution_event_ids[opportunity_record.get_resolution_event_id()] = true
|
|
if not _is_valid_opportunity(
|
|
opportunity_record,
|
|
npc_ids,
|
|
npc_records_by_id,
|
|
storage_records_by_id,
|
|
event_records_by_id,
|
|
knowledge_records_by_key,
|
|
resource_records_by_id,
|
|
int(record.simulation["tick_count"])
|
|
):
|
|
return null
|
|
opportunity_ids[opportunity_id] = true
|
|
trigger_event_ids[trigger_event_id] = true
|
|
highest_opportunity_id = maxi(highest_opportunity_id, opportunity_id)
|
|
record.opportunities.append(opportunity_record)
|
|
if int(record.simulation["next_opportunity_id"]) <= highest_opportunity_id:
|
|
return null
|
|
|
|
return record
|
|
|
|
|
|
static func _is_valid_animal_feed_event(
|
|
event: EconomicEventRecord,
|
|
npc_ids: Dictionary,
|
|
animal_records_by_id: Dictionary,
|
|
storage_records_by_id: Dictionary,
|
|
current_tick: int
|
|
) -> bool:
|
|
var actor_id := int(event.data["actor_id"])
|
|
var definition := SimulationDefinitions.get_action(SimulationIds.ACTION_FEED_ANIMAL)
|
|
var animal := (
|
|
animal_records_by_id.get(StringName(event.data["destination_id"])) as AnimalStateRecord
|
|
)
|
|
return (
|
|
(actor_id == -1 or npc_ids.has(actor_id))
|
|
and int(event.data["tick"]) <= current_tick
|
|
and animal != null
|
|
and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
|
|
and storage_records_by_id.has(SimulationIds.STORAGE_VILLAGE_PANTRY)
|
|
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
|
and definition != null
|
|
and definition.has_completion_cost()
|
|
and is_equal_approx(float(event.data["amount"]), definition.completion_cost_amount)
|
|
)
|
|
|
|
|
|
static func _is_valid_knowledge_provenance(
|
|
known_event: KnownEventStateRecord,
|
|
npc_ids: Dictionary,
|
|
event_records_by_id: Dictionary,
|
|
current_tick: int
|
|
) -> 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"])
|
|
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 not EventKnowledgeSystem.is_knowable_event(event):
|
|
return false
|
|
match method:
|
|
SimulationIds.KNOWLEDGE_ACQUISITION_PERFORMED:
|
|
return knower_id == actor_id and acquired_tick == event_tick
|
|
SimulationIds.KNOWLEDGE_ACQUISITION_WITNESSED:
|
|
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_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
|
|
|
|
|
|
static func _is_valid_opportunity(
|
|
opportunity: OpportunityStateRecord,
|
|
npc_ids: Dictionary,
|
|
npc_records_by_id: Dictionary,
|
|
storage_records_by_id: Dictionary,
|
|
event_records_by_id: Dictionary,
|
|
knowledge_records_by_key: Dictionary,
|
|
resource_records_by_id: Dictionary,
|
|
current_tick: int
|
|
) -> bool:
|
|
if (
|
|
not npc_ids.has(opportunity.get_interested_npc_id())
|
|
or not storage_records_by_id.has(opportunity.get_target_id())
|
|
or not is_finite(opportunity.get_target_amount())
|
|
or opportunity.get_target_amount() <= 0.0
|
|
):
|
|
return false
|
|
var trigger_event := (
|
|
event_records_by_id.get(opportunity.get_trigger_event_id()) as EconomicEventRecord
|
|
)
|
|
if trigger_event == null:
|
|
return false
|
|
match opportunity.get_opportunity_type():
|
|
SimulationIds.OPPORTUNITY_RESTOCK_EMPTY_PANTRY:
|
|
if not _is_valid_pantry_empty_trigger(trigger_event, npc_ids):
|
|
return false
|
|
SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD:
|
|
if not _is_valid_missing_wood_trigger(trigger_event, opportunity, npc_ids):
|
|
return false
|
|
_:
|
|
return false
|
|
var trigger_tick := int(trigger_event.data["tick"])
|
|
var created_tick := opportunity.get_created_tick()
|
|
if created_tick < trigger_tick or created_tick > current_tick:
|
|
return false
|
|
|
|
var status := opportunity.get_status()
|
|
var resolution_event_id := opportunity.get_resolution_event_id()
|
|
var resolved_tick := opportunity.get_resolved_tick()
|
|
var interested_record := (
|
|
npc_records_by_id.get(opportunity.get_interested_npc_id()) as NPCStateRecord
|
|
)
|
|
if status == OpportunityStateRecord.STATUS_OPEN:
|
|
var target_storage := (
|
|
storage_records_by_id.get(opportunity.get_target_id()) as StorageStateRecord
|
|
)
|
|
var evidence_key := (
|
|
"%d:%d" % [opportunity.get_interested_npc_id(), opportunity.get_trigger_event_id()]
|
|
)
|
|
var evidence := knowledge_records_by_key.get(evidence_key) as KnownEventStateRecord
|
|
return (
|
|
resolution_event_id == OpportunityStateRecord.NO_EVENT_ID
|
|
and resolved_tick == -1
|
|
and interested_record != null
|
|
and (
|
|
opportunity.get_opportunity_type() != SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD
|
|
or not bool(interested_record.data["is_dead"])
|
|
)
|
|
and target_storage != null
|
|
and (
|
|
target_storage.get_amount(opportunity.get_resource_id())
|
|
< opportunity.get_target_amount()
|
|
)
|
|
and evidence != null
|
|
and evidence.get_acquired_tick() <= created_tick
|
|
)
|
|
if status == OpportunityStateRecord.STATUS_INVALIDATED:
|
|
if (
|
|
opportunity.get_opportunity_type() != SimulationIds.OPPORTUNITY_SUPPLY_MISSING_WOOD
|
|
or resolution_event_id != OpportunityStateRecord.NO_EVENT_ID
|
|
or resolved_tick != -1
|
|
or opportunity.get_closed_tick() < created_tick
|
|
or opportunity.get_closed_tick() > current_tick
|
|
):
|
|
return false
|
|
if (
|
|
opportunity.get_invalidation_reason()
|
|
== SimulationIds.OPPORTUNITY_INVALIDATED_INTERESTED_DIED
|
|
):
|
|
return interested_record != null and bool(interested_record.data["is_dead"])
|
|
return (
|
|
opportunity.get_invalidation_reason()
|
|
== SimulationIds.OPPORTUNITY_INVALIDATED_EVIDENCE_STALE
|
|
)
|
|
if status != OpportunityStateRecord.STATUS_RESOLVED:
|
|
return false
|
|
if resolution_event_id == OpportunityStateRecord.NO_EVENT_ID:
|
|
return false
|
|
var resolution_event := event_records_by_id.get(resolution_event_id) as EconomicEventRecord
|
|
if (
|
|
resolution_event == null
|
|
or not _is_valid_supply_resolution(
|
|
resolution_event, opportunity, npc_ids, resource_records_by_id
|
|
)
|
|
):
|
|
return false
|
|
var resolution_tick := int(resolution_event.data["tick"])
|
|
return (
|
|
resolution_event_id > opportunity.get_trigger_event_id()
|
|
and resolution_tick >= created_tick
|
|
and resolved_tick == resolution_tick
|
|
and resolved_tick <= current_tick
|
|
)
|
|
|
|
|
|
static func _is_valid_missing_wood_trigger(
|
|
event: EconomicEventRecord, opportunity: OpportunityStateRecord, npc_ids: Dictionary
|
|
) -> bool:
|
|
var actor_id := int(event.data["actor_id"])
|
|
return (
|
|
npc_ids.has(actor_id)
|
|
and actor_id == opportunity.get_interested_npc_id()
|
|
and StringName(event.data["event_type"]) == SimulationIds.EVENT_TASK_BLOCKED
|
|
and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_WOODPILE
|
|
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_WOOD
|
|
and (
|
|
StringName(event.data.get("action_id", &""))
|
|
in [SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY]
|
|
)
|
|
and is_equal_approx(
|
|
float(event.data.get("required_amount", 0.0)), opportunity.get_target_amount()
|
|
)
|
|
)
|
|
|
|
|
|
static func _is_valid_pantry_empty_trigger(event: EconomicEventRecord, npc_ids: Dictionary) -> bool:
|
|
var actor_id := int(event.data["actor_id"])
|
|
return (
|
|
npc_ids.has(actor_id)
|
|
and StringName(event.data["event_type"]) == SimulationIds.EVENT_STORAGE_WITHDRAWN
|
|
and StringName(event.data["source_id"]) == SimulationIds.STORAGE_VILLAGE_PANTRY
|
|
and StringName(event.data["destination_id"]) == SimulationIds.npc_inventory_id(actor_id)
|
|
and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD
|
|
and float(event.data["amount"]) > 0.0
|
|
)
|
|
|
|
|
|
static func _is_valid_supply_resolution(
|
|
event: EconomicEventRecord,
|
|
opportunity: OpportunityStateRecord,
|
|
npc_ids: Dictionary,
|
|
resource_records_by_id: Dictionary
|
|
) -> bool:
|
|
var actor_id := int(event.data["actor_id"])
|
|
var event_type := StringName(event.data["event_type"])
|
|
if (
|
|
StringName(event.data["destination_id"]) != opportunity.get_target_id()
|
|
or StringName(event.data["item_id"]) != opportunity.get_resource_id()
|
|
or float(event.data["amount"]) <= 0.0
|
|
):
|
|
return false
|
|
if event_type == SimulationIds.EVENT_STORAGE_DEPOSITED:
|
|
return (
|
|
npc_ids.has(actor_id)
|
|
and StringName(event.data["source_id"]) == SimulationIds.npc_inventory_id(actor_id)
|
|
)
|
|
if event_type == SimulationIds.EVENT_RESOURCE_EXTRACTED and actor_id == -1:
|
|
var resource_state := (
|
|
resource_records_by_id.get(StringName(event.data["source_id"])) as ResourceStateRecord
|
|
)
|
|
return (
|
|
resource_state != null
|
|
and resource_state.get_resource_id() == opportunity.get_resource_id()
|
|
and resource_state.can_player_use_resource()
|
|
)
|
|
return false
|
|
|
|
|
|
static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary:
|
|
var migrated := legacy_data.duplicate(true)
|
|
migrated["schema_version"] = SCHEMA_VERSION
|
|
if version <= ANIMAL_LEGACY_SCHEMA_VERSION:
|
|
migrated["animals"] = []
|
|
if version == LEGACY_SCHEMA_VERSION:
|
|
var village_data: Dictionary = legacy_data.get("village", {})
|
|
var initial_food := float(village_data.get("food", 0.0))
|
|
var initial_wood := float(village_data.get("wood", 0.0))
|
|
migrated["storages"] = [
|
|
(
|
|
StorageStateRecord
|
|
. create(
|
|
SimulationIds.STORAGE_VILLAGE_PANTRY,
|
|
{String(SimulationIds.RESOURCE_FOOD): initial_food},
|
|
maxf(StorageStateRecord.DEFAULT_CAPACITY, initial_food)
|
|
)
|
|
. to_dictionary()
|
|
),
|
|
(
|
|
StorageStateRecord
|
|
. create(
|
|
SimulationIds.STORAGE_VILLAGE_WOODPILE,
|
|
{String(SimulationIds.RESOURCE_WOOD): initial_wood},
|
|
maxf(StorageStateRecord.DEFAULT_CAPACITY, initial_wood)
|
|
)
|
|
. to_dictionary()
|
|
)
|
|
]
|
|
if version in [LEGACY_SCHEMA_VERSION, EVENT_LEGACY_SCHEMA_VERSION]:
|
|
migrated["economic_events"] = []
|
|
var simulation_data: Dictionary = migrated.get("simulation", {})
|
|
simulation_data["next_event_id"] = 0
|
|
migrated["simulation"] = simulation_data
|
|
if (
|
|
version
|
|
in [LEGACY_SCHEMA_VERSION, EVENT_LEGACY_SCHEMA_VERSION, RELATIONSHIP_LEGACY_SCHEMA_VERSION]
|
|
):
|
|
migrated["relationships"] = _migrate_npc_familiarity(legacy_data.get("npcs", []))
|
|
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", []),
|
|
int((migrated.get("simulation", {}) as Dictionary).get("tick_count", 0))
|
|
)
|
|
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))
|
|
)
|
|
if (
|
|
version
|
|
not in [
|
|
OPPORTUNITY_LEGACY_SCHEMA_VERSION,
|
|
ANIMAL_LEGACY_SCHEMA_VERSION,
|
|
ROUTINE_LEGACY_SCHEMA_VERSION,
|
|
]
|
|
):
|
|
migrated["opportunities"] = []
|
|
var opportunity_simulation_data: Dictionary = migrated.get("simulation", {})
|
|
opportunity_simulation_data["next_opportunity_id"] = 0
|
|
migrated["simulation"] = opportunity_simulation_data
|
|
return migrated
|
|
|
|
|
|
static func _migrate_npc_familiarity(npc_data: Variant) -> Array[Dictionary]:
|
|
var migrated_relationships: Array[Dictionary] = []
|
|
if not npc_data is Array:
|
|
return migrated_relationships
|
|
for npc_item in npc_data:
|
|
if not npc_item is Dictionary:
|
|
continue
|
|
var observer_id := int(npc_item.get("id", -1))
|
|
var familiarity = npc_item.get("familiarity", [])
|
|
if familiarity is Array:
|
|
for pair in familiarity:
|
|
if not pair is Dictionary:
|
|
continue
|
|
migrated_relationships.append(
|
|
(
|
|
RelationshipStateRecord
|
|
. create(
|
|
observer_id, int(pair.get("id", -1)), float(pair.get("score", 0.0))
|
|
)
|
|
. to_dictionary()
|
|
)
|
|
)
|
|
elif familiarity is Dictionary:
|
|
for subject_id in familiarity:
|
|
migrated_relationships.append(
|
|
(
|
|
RelationshipStateRecord
|
|
. create(observer_id, int(subject_id), float(familiarity[subject_id]))
|
|
. to_dictionary()
|
|
)
|
|
)
|
|
migrated_relationships.sort_custom(_relationship_dictionary_sort)
|
|
return migrated_relationships
|
|
|
|
|
|
static func _relationship_dictionary_sort(first: Dictionary, second: Dictionary) -> bool:
|
|
if int(first["observer_id"]) != int(second["observer_id"]):
|
|
return int(first["observer_id"]) < int(second["observer_id"])
|
|
return int(first["subject_id"]) < int(second["subject_id"])
|
|
|
|
|
|
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:
|
|
return migrated_knowledge
|
|
for relationship_item in relationship_data:
|
|
if not relationship_item is Dictionary:
|
|
continue
|
|
var observer_id := int(relationship_item.get("observer_id", -1))
|
|
var cause_event_id := int(
|
|
relationship_item.get(
|
|
"last_trust_cause_event_id", RelationshipStateRecord.NO_CAUSE_EVENT
|
|
)
|
|
)
|
|
if observer_id < 0 or cause_event_id < 0:
|
|
continue
|
|
var knowledge_key := "%d:%d" % [observer_id, cause_event_id]
|
|
if known_keys.has(knowledge_key):
|
|
continue
|
|
known_keys[knowledge_key] = true
|
|
migrated_knowledge.append(
|
|
(
|
|
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
|
|
|
|
|
|
static func _knowledge_dictionary_sort(first: Dictionary, second: Dictionary) -> bool:
|
|
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_retention(
|
|
knowledge_data: Variant, event_data: Variant, migration_tick: int
|
|
) -> Array:
|
|
if not knowledge_data is Array:
|
|
return []
|
|
var event_by_id := {}
|
|
if event_data is Array:
|
|
for event_item in event_data:
|
|
if not event_item is Dictionary:
|
|
continue
|
|
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:
|
|
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
|
|
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 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,
|
|
source_npc_id,
|
|
acquired_tick,
|
|
source_acquisition_method
|
|
)
|
|
. to_dictionary()
|
|
)
|
|
)
|
|
if can_sort:
|
|
migrated_knowledge.sort_custom(_knowledge_dictionary_sort)
|
|
return migrated_knowledge
|