382 lines
11 KiB
GDScript
382 lines
11 KiB
GDScript
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
|