class_name WorldEventRecord extends RefCounted const SCHEMA_VERSION := 1 const ENTITY_TYPE_NPC := &"npc" const ENTITY_TYPE_WORLD := &"world_entity" const ROLE_ACTOR := &"actor" const ROLE_SOURCE := &"source" const ROLE_DESTINATION := &"destination" const ECONOMIC_CORE_FIELDS := [ "schema_version", "event_id", "event_type", "tick", "actor_id", "source_id", "destination_id", "world_position", ] var _event_id := -1 var _event_type: StringName var _tick := -1 var _participants: Dictionary = {} var _location: SpatialAddress var _payload: Dictionary = {} static func create( event_id: int, event_type: StringName, tick: int, participants: Dictionary = {}, location: SpatialAddress = null, payload: Dictionary = {} ) -> WorldEventRecord: var normalized_participants: Variant = _normalize_participants(participants) if normalized_participants == null: return null var payload_status := [true] var normalized_payload: Variant = _normalize_serializable_value(payload, payload_status) if not payload_status[0] or not normalized_payload is Dictionary: return null var event := WorldEventRecord.new() event._event_id = event_id event._event_type = event_type event._tick = tick event._participants = normalized_participants event._location = location.duplicate_address() if location != null else null event._payload = normalized_payload return event if event.is_valid() else null static func from_dictionary(record_data: Dictionary) -> WorldEventRecord: if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION: return null if not record_data.has_all( ["event_id", "event_type", "tick", "participants", "location", "payload"] ): return null var saved_participants = record_data["participants"] if not saved_participants is Array: return null var participants: Dictionary = {} for participant_data in saved_participants: if not participant_data is Dictionary or not participant_data.has_all(["role", "entity"]): return null var role := StringName(participant_data["role"]) if ( role.is_empty() or participants.has(role) or not participant_data["entity"] is Dictionary ): return null var entity_ref := WorldEntityRef.from_dictionary(participant_data["entity"]) if entity_ref == null: return null participants[role] = entity_ref var location: SpatialAddress var saved_location = record_data["location"] if saved_location != null: if not saved_location is Dictionary: return null location = SpatialAddress.from_dictionary(saved_location) if location == null: return null if not record_data["payload"] is Dictionary: return null return create( int(record_data["event_id"]), StringName(record_data["event_type"]), int(record_data["tick"]), participants, location, record_data["payload"] ) static func from_economic_event( economic_event: EconomicEventRecord, world_id: StringName = &"", location_id: StringName = &"" ) -> WorldEventRecord: if economic_event == null: return null var economic_data := economic_event.to_dictionary() var participants: Dictionary = { ROLE_ACTOR: WorldEntityRef.create(ENTITY_TYPE_NPC, StringName(str(int(economic_data["actor_id"])))) } var source_id := StringName(economic_data["source_id"]) if not source_id.is_empty(): participants[ROLE_SOURCE] = WorldEntityRef.create(ENTITY_TYPE_WORLD, source_id) var destination_id := StringName(economic_data["destination_id"]) if not destination_id.is_empty(): participants[ROLE_DESTINATION] = WorldEntityRef.create(ENTITY_TYPE_WORLD, destination_id) var payload: Dictionary = {} for field in economic_data: var field_name := String(field) if field_name in ECONOMIC_CORE_FIELDS: continue payload[field_name] = economic_data[field] var location := SpatialAddress.create( world_id, location_id, economic_event.get_world_position() ) return create( int(economic_data["event_id"]), StringName(economic_data["event_type"]), int(economic_data["tick"]), participants, location, payload ) func is_valid() -> bool: if _event_id < 0 or _tick < 0 or _event_type.is_empty(): return false if _location != null and not _location.is_valid(): return false for role in _participants: if StringName(role).is_empty(): return false var entity_ref := _participants[role] as WorldEntityRef if entity_ref == null or not entity_ref.is_valid(): return false return true func get_event_id() -> int: return _event_id func get_event_type() -> StringName: return _event_type func get_tick() -> int: return _tick func has_location() -> bool: return _location != null func get_location() -> SpatialAddress: return _location.duplicate_address() if _location != null else null func get_payload() -> Dictionary: return _payload.duplicate(true) func get_participant(role: StringName) -> WorldEntityRef: var entity_ref := _participants.get(role) as WorldEntityRef return entity_ref.duplicate_ref() if entity_ref != null else null func get_participant_roles() -> Array[StringName]: var roles: Array[StringName] = [] for role in _participants: roles.append(StringName(role)) roles.sort_custom( func(left: StringName, right: StringName) -> bool: return String(left) < String(right) ) return roles func get_participant_refs() -> Array[WorldEntityRef]: var refs: Array[WorldEntityRef] = [] for role in get_participant_roles(): var entity_ref := _participants[role] as WorldEntityRef refs.append(entity_ref.duplicate_ref()) return refs func get_participant_index_keys() -> Array[String]: var keys: Array[String] = [] var seen: Dictionary = {} for role in get_participant_roles(): var entity_ref := _participants[role] as WorldEntityRef var key := entity_ref.index_key() if seen.has(key): continue seen[key] = true keys.append(key) keys.sort() return keys func to_economic_event() -> EconomicEventRecord: var actor := _participants.get(ROLE_ACTOR) as WorldEntityRef if actor == null: return null var actor_text := String(actor.get_entity_id()) if not actor_text.is_valid_int(): return null var source := _participants.get(ROLE_SOURCE) as WorldEntityRef var destination := _participants.get(ROLE_DESTINATION) as WorldEntityRef var economic_data := _payload.duplicate(true) economic_data["schema_version"] = EconomicEventRecord.SCHEMA_VERSION economic_data["event_id"] = _event_id economic_data["event_type"] = String(_event_type) economic_data["tick"] = _tick economic_data["actor_id"] = actor_text.to_int() economic_data["source_id"] = String(source.get_entity_id()) if source != null else "" economic_data["destination_id"] = ( String(destination.get_entity_id()) if destination != null else "" ) economic_data["item_id"] = String(economic_data.get("item_id", "")) economic_data["amount"] = float(economic_data.get("amount", 0.0)) economic_data["action_id"] = String(economic_data.get("action_id", "")) economic_data["required_amount"] = float(economic_data.get("required_amount", 0.0)) var position := _location.get_position() if _location != null else Vector3.ZERO economic_data["world_position"] = [position.x, position.y, position.z] return EconomicEventRecord.from_dictionary(economic_data) func to_dictionary() -> Dictionary: var serialized_participants: Array[Dictionary] = [] for role in get_participant_roles(): var entity_ref := _participants[role] as WorldEntityRef ( serialized_participants . append( { "role": String(role), "entity": entity_ref.to_dictionary(), } ) ) return { "schema_version": SCHEMA_VERSION, "event_id": _event_id, "event_type": String(_event_type), "tick": _tick, "participants": serialized_participants, "location": _location.to_dictionary() if _location != null else null, "payload": _payload.duplicate(true), } static func _normalize_participants(participants: Dictionary) -> Variant: var normalized: Dictionary = {} var role_names: Dictionary = {} for raw_role in participants: if not raw_role is String and not raw_role is StringName: return null var role := StringName(raw_role) var role_text := String(role) if role.is_empty() or role_names.has(role_text): return null var entity_ref := participants[raw_role] as WorldEntityRef if entity_ref == null or not entity_ref.is_valid(): return null role_names[role_text] = true normalized[role] = entity_ref.duplicate_ref() return normalized static func _normalize_serializable_value(value: Variant, status: Array) -> Variant: match typeof(value): TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_STRING: return value TYPE_FLOAT: if not is_finite(float(value)): status[0] = false return value TYPE_STRING_NAME: return String(value) TYPE_ARRAY: var normalized_array: Array = [] for element in value: normalized_array.append(_normalize_serializable_value(element, status)) return normalized_array TYPE_DICTIONARY: return _normalize_serializable_dictionary(value, status) _: status[0] = false return null static func _normalize_serializable_dictionary(value: Dictionary, status: Array) -> Dictionary: var source_keys: Dictionary = {} for raw_key in value: if not raw_key is String and not raw_key is StringName: status[0] = false return {} var key_text := String(raw_key) if source_keys.has(key_text): status[0] = false return {} source_keys[key_text] = raw_key var sorted_keys: Array = source_keys.keys() sorted_keys.sort() var normalized: Dictionary = {} for key_text in sorted_keys: var raw_key = source_keys[key_text] normalized[key_text] = _normalize_serializable_value(value[raw_key], status) return normalized