extends RefCounted signal event_recorded(event: EconomicEventRecord) const DEFAULT_RATE_WINDOW_TICKS := 200 const DEFAULT_TICKS_PER_DAY := 200.0 const FACT_RESERVED_FIELDS := [ "schema_version", "event_id", "event_type", "tick", "actor_id", "source_id", "destination_id", "item_id", "amount", "action_name", "action_id", "required_amount", "world_position", ] var events: Array[EconomicEventRecord] = [] var next_event_id := 0 var world_events := WorldEventStore.new() var _events_by_id: Dictionary = {} var _event_ids_by_actor: Dictionary = {} var _event_ids_by_type: Dictionary = {} var _event_ids_by_tick: Dictionary = {} var _world_id := SimulationIds.REGIONAL_WORLD_BOSNIA var _location_id := SimulationIds.REGIONAL_LOCATION_JAJCE func configure_scope(world_id: StringName, location_id: StringName) -> bool: if world_id.is_empty() or location_id.is_empty(): return false if not events.is_empty(): return world_id == _world_id and location_id == _location_id _world_id = world_id _location_id = location_id world_events = WorldEventStore.new() return true func get_scope() -> Dictionary: return {"world_id": _world_id, "location_id": _location_id} func record_economic( tick: int, event_type: StringName, actor_id: int, source_id: StringName, destination_id: StringName, item_id: StringName, amount: float, world_position: Vector3 = Vector3.ZERO ) -> EconomicEventRecord: if amount <= 0.0: return null var event := EconomicEventRecord.create( next_event_id, event_type, tick, actor_id, source_id, destination_id, item_id, amount, world_position ) _append(event) return event func record_narrative( tick: int, event_type: StringName, actor_id: int, source_id: StringName = &"", action_display: String = "", world_position: Vector3 = Vector3.ZERO, action_id: StringName = &"", item_id: StringName = &"", required_amount: float = 0.0 ) -> EconomicEventRecord: var event := EconomicEventRecord.create_narrative( next_event_id, event_type, tick, actor_id, source_id, action_display, world_position, action_id, item_id, required_amount ) if event_type == SimulationIds.EVENT_TASK_BLOCKED: event.data["blocked_contract_kind"] = String(SimulationIds.BLOCKED_CONTRACT_ACTION_COST) event.data["world_id"] = String(_world_id) event.data["location_id"] = String(_location_id) var normalized := EconomicEventRecord.from_dictionary(event.to_dictionary()) if normalized == null: return null event = normalized _append(event) return event func record_fact( tick: int, event_type: StringName, actor_id: int, source_id: StringName = &"", destination_id: StringName = &"", fact_payload: Dictionary = {}, world_position: Vector3 = Vector3.ZERO ) -> EconomicEventRecord: if tick < 0 or event_type.is_empty(): return null var event := EconomicEventRecord.create_narrative( next_event_id, event_type, tick, actor_id, source_id, "", world_position ) event.data["destination_id"] = String(destination_id) var payload_keys: Array = fact_payload.keys() payload_keys.sort_custom( func(first: Variant, second: Variant) -> bool: return str(first) < str(second) ) var normalized_keys := {} for raw_key in payload_keys: if not raw_key is String and not raw_key is StringName: return null var key := String(raw_key) if key.is_empty() or key in FACT_RESERVED_FIELDS or normalized_keys.has(key): return null normalized_keys[key] = true event.data[key] = fact_payload[raw_key] var normalized := EconomicEventRecord.from_dictionary(event.to_dictionary()) if normalized == null: return null if WorldEventRecord.from_economic_event(normalized, _world_id, _location_id) == null: return null _append(normalized) return normalized func record_activity_completed( tick: int, actor_id: int, target_id: StringName, action_id: StringName, event_type_id: StringName, metric_id: StringName, metric_delta: float, world_position: Vector3 = Vector3.ZERO, cost_item_id: StringName = &"", cost_amount: float = 0.0 ) -> EconomicEventRecord: if ( tick < 0 or actor_id < SimulationIds.PLAYER_ACTOR_ID or target_id.is_empty() or action_id.is_empty() or event_type_id.is_empty() or metric_id not in [&"safety", &"knowledge"] or not is_finite(metric_delta) or metric_delta <= 0.0 or not world_position.is_finite() or not is_finite(cost_amount) or cost_amount < 0.0 or (cost_item_id.is_empty() and cost_amount > 0.0) or (not cost_item_id.is_empty() and cost_amount <= 0.0) ): return null var event := EconomicEventRecord.create_narrative( next_event_id, event_type_id, tick, actor_id, target_id, "", world_position, action_id, cost_item_id, cost_amount ) event.data["metric_id"] = String(metric_id) event.data["metric_delta"] = metric_delta event.data["cost_item_id"] = String(cost_item_id) event.data["cost_amount"] = cost_amount event.data["target_kind"] = String(SimulationIds.TARGET_ACTIVITY) event.data["target_action_id"] = String(action_id) event.data["world_id"] = String(_world_id) event.data["location_id"] = String(_location_id) var normalized := EconomicEventRecord.from_dictionary(event.to_dictionary()) if ( normalized == null or (WorldEventRecord.from_economic_event(normalized, _world_id, _location_id) == null) ): return null _append(normalized) return normalized func record_activity_blocked( tick: int, actor_id: int, target_id: StringName, action_id: StringName, action_display: String, world_position: Vector3, cost_storage_id: StringName, cost_item_id: StringName, cost_amount: float ) -> EconomicEventRecord: var event := EconomicEventRecord.create_narrative( next_event_id, SimulationIds.EVENT_TASK_BLOCKED, tick, actor_id, cost_storage_id, action_display, world_position, action_id, cost_item_id, cost_amount ) if target_id.is_empty(): return null event.data["blocked_contract_kind"] = String(SimulationIds.BLOCKED_CONTRACT_ACTION_COST) event.data["world_id"] = String(_world_id) event.data["location_id"] = String(_location_id) event.data["activity_target_id"] = String(target_id) event.data["target_kind"] = String(SimulationIds.TARGET_ACTIVITY) event.data["target_action_id"] = String(action_id) var normalized := EconomicEventRecord.from_dictionary(event.to_dictionary()) if ( normalized == null or WorldEventRecord.from_economic_event(normalized, _world_id, _location_id) == null ): return null _append(normalized) return normalized func get_for_actor(actor_id: int, max_count: int = 8) -> Array[EconomicEventRecord]: var results: Array[EconomicEventRecord] = [] var event_ids: Array = _event_ids_by_actor.get(actor_id, []) for index in range(event_ids.size() - 1, -1, -1): var event := get_by_id(int(event_ids[index])) if event == null: continue results.push_front(event) if results.size() >= max_count: break return results func get_recent(max_count: int = 5) -> Array[EconomicEventRecord]: var results: Array[EconomicEventRecord] = [] var start := maxi(events.size() - max_count, 0) for index in range(start, events.size()): results.append(events[index]) return results func get_by_id(event_id: int) -> EconomicEventRecord: return _events_by_id.get(event_id) as EconomicEventRecord func get_by_ids(event_ids: Array[int]) -> Array[EconomicEventRecord]: var results: Array[EconomicEventRecord] = [] for event_id in event_ids: var event := get_by_id(event_id) if event != null: results.append(event) return results func get_for_type(event_type: StringName, max_count: int = 0) -> Array[EconomicEventRecord]: var results: Array[EconomicEventRecord] = [] var event_ids: Array = _event_ids_by_type.get(event_type, []) var start := 0 if max_count <= 0 else maxi(event_ids.size() - max_count, 0) for index in range(start, event_ids.size()): var event := get_by_id(int(event_ids[index])) if event != null: results.append(event) return results func get_for_tick(tick: int) -> Array[EconomicEventRecord]: var results: Array[EconomicEventRecord] = [] for event_id in _event_ids_by_tick.get(tick, []): var event := get_by_id(int(event_id)) if event != null: results.append(event) return results func get_world_event(event_id: int) -> WorldEventRecord: return world_events.get_by_id(event_id) func get_consumption_rates( current_tick: int, window_ticks: int = DEFAULT_RATE_WINDOW_TICKS, ticks_per_day: float = DEFAULT_TICKS_PER_DAY ) -> Dictionary: var consumed := { SimulationIds.RESOURCE_FOOD: 0.0, SimulationIds.RESOURCE_WOOD: 0.0, } var first_tick := maxi(current_tick - window_ticks, 0) for event in get_for_type(SimulationIds.EVENT_ITEM_CONSUMED): if int(event.data["tick"]) < first_tick: continue var item_id := StringName(event.data["item_id"]) if consumed.has(item_id): consumed[item_id] = float(consumed[item_id]) + float(event.data["amount"]) var elapsed := maxi(current_tick - first_tick, 1) return { "food_per_day": float(consumed[SimulationIds.RESOURCE_FOOD]) / elapsed * ticks_per_day, "wood_per_day": float(consumed[SimulationIds.RESOURCE_WOOD]) / elapsed * ticks_per_day, } func restore( restored_events: Array[EconomicEventRecord], restored_next_id: int, restored_world_id: StringName = &"", restored_location_id: StringName = &"" ) -> bool: if restored_world_id.is_empty() != restored_location_id.is_empty(): return false events.clear() _events_by_id.clear() _event_ids_by_actor.clear() _event_ids_by_type.clear() _event_ids_by_tick.clear() if not restored_world_id.is_empty(): _world_id = restored_world_id _location_id = restored_location_id world_events = WorldEventStore.new() for event in restored_events: _index(event) next_event_id = restored_next_id return true func _append(event: EconomicEventRecord) -> void: next_event_id += 1 _index(event) event_recorded.emit(event) func _index(event: EconomicEventRecord) -> void: if event == null: return var event_id := int(event.data["event_id"]) if _events_by_id.has(event_id): return events.append(event) _events_by_id[event_id] = event _append_index(_event_ids_by_actor, int(event.data["actor_id"]), event_id) _append_index(_event_ids_by_type, StringName(event.data["event_type"]), event_id) _append_index(_event_ids_by_tick, int(event.data["tick"]), event_id) var event_world_id := StringName(event.data.get("world_id", _world_id)) var event_location_id := StringName(event.data.get("location_id", _location_id)) if event_world_id.is_empty(): event_world_id = _world_id if event_location_id.is_empty(): event_location_id = _location_id world_events.append_economic(event, event_world_id, event_location_id) static func _append_index(index: Dictionary, key: Variant, event_id: int) -> void: var event_ids: Array = index.get(key, []) event_ids.append(event_id) index[key] = event_ids