feat: unify authored activity commands
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
class_name ActivityEventValidator
|
||||
extends RefCounted
|
||||
|
||||
static var _core_catalog: ContentCatalog
|
||||
static var _core_storage_routing: StorageRoutingPolicy
|
||||
|
||||
|
||||
static func is_candidate(event: EconomicEventRecord) -> bool:
|
||||
if event == null:
|
||||
return false
|
||||
if StringName(event.data.get("event_type", &"")) == SimulationIds.EVENT_TASK_BLOCKED:
|
||||
return false
|
||||
if StringName(event.data.get("target_kind", &"")) == SimulationIds.TARGET_ACTIVITY:
|
||||
return true
|
||||
var event_type := StringName(event.data.get("event_type", &""))
|
||||
return SimulationDefinitions.has_action_event_type(event_type)
|
||||
|
||||
|
||||
static func is_valid(
|
||||
event: EconomicEventRecord,
|
||||
expected_world_id: StringName = &"",
|
||||
expected_location_id: StringName = &""
|
||||
) -> bool:
|
||||
if event == null or not _has_activity_fields(event):
|
||||
return false
|
||||
var actor_id := int(event.data["actor_id"])
|
||||
var action_id := StringName(event.data["action_id"])
|
||||
var definition := SimulationDefinitions.get_action(action_id)
|
||||
var effect := ActivityActionCommandService._metric_effect(definition)
|
||||
if (
|
||||
not KnownEventStateRecord.is_valid_actor_id(actor_id)
|
||||
or definition == null
|
||||
or effect == null
|
||||
or definition.target_type != SimulationIds.TARGET_ACTIVITY
|
||||
or definition.completion_handler_id != ActivityActionCommandService.HANDLER_METRIC_DELTA
|
||||
or StringName(event.data["event_type"]) != definition.event_type_id
|
||||
or StringName(event.data["source_id"]).is_empty()
|
||||
or not StringName(event.data["destination_id"]).is_empty()
|
||||
or StringName(event.data["target_kind"]) != SimulationIds.TARGET_ACTIVITY
|
||||
or StringName(event.data["target_action_id"]) != action_id
|
||||
or StringName(event.data["world_id"]).is_empty()
|
||||
or StringName(event.data["location_id"]).is_empty()
|
||||
or (
|
||||
not expected_world_id.is_empty()
|
||||
and StringName(event.data["world_id"]) != expected_world_id
|
||||
)
|
||||
or (
|
||||
not expected_location_id.is_empty()
|
||||
and StringName(event.data["location_id"]) != expected_location_id
|
||||
)
|
||||
):
|
||||
return false
|
||||
var metric_delta_value: Variant = event.data["metric_delta"]
|
||||
var cost_amount_value: Variant = event.data["cost_amount"]
|
||||
if (
|
||||
(metric_delta_value is not int and metric_delta_value is not float)
|
||||
or (cost_amount_value is not int and cost_amount_value is not float)
|
||||
):
|
||||
return false
|
||||
var metric_delta := float(metric_delta_value)
|
||||
var cost_amount := float(cost_amount_value)
|
||||
var maximum_delta := _maximum_delta(effect, actor_id)
|
||||
return (
|
||||
is_finite(metric_delta)
|
||||
and metric_delta > 0.0
|
||||
and is_finite(maximum_delta)
|
||||
and maximum_delta > 0.0
|
||||
and (metric_delta < maximum_delta or is_equal_approx(metric_delta, maximum_delta))
|
||||
and is_finite(cost_amount)
|
||||
and StringName(event.data["metric_id"]) == effect.subject_key
|
||||
and StringName(event.data["cost_item_id"]) == definition.completion_cost_resource_id
|
||||
and StringName(event.data["item_id"]) == definition.completion_cost_resource_id
|
||||
and is_equal_approx(cost_amount, definition.completion_cost_amount)
|
||||
and is_equal_approx(float(event.data["required_amount"]), definition.completion_cost_amount)
|
||||
)
|
||||
|
||||
|
||||
static func is_valid_for_state(
|
||||
event: EconomicEventRecord,
|
||||
npc_ids: Dictionary,
|
||||
current_tick: int,
|
||||
expected_world_id: StringName = &"",
|
||||
expected_location_id: StringName = &""
|
||||
) -> bool:
|
||||
if (
|
||||
not is_valid(event, expected_world_id, expected_location_id)
|
||||
or int(event.data["tick"]) > current_tick
|
||||
):
|
||||
return false
|
||||
var actor_id := int(event.data["actor_id"])
|
||||
return actor_id == SimulationIds.PLAYER_ACTOR_ID or npc_ids.has(actor_id)
|
||||
|
||||
|
||||
static func is_blocked_candidate(event: EconomicEventRecord) -> bool:
|
||||
return (
|
||||
event != null
|
||||
and StringName(event.data.get("event_type", &"")) == SimulationIds.EVENT_TASK_BLOCKED
|
||||
)
|
||||
|
||||
|
||||
static func is_valid_blocked(
|
||||
event: EconomicEventRecord,
|
||||
expected_world_id: StringName = &"",
|
||||
expected_location_id: StringName = &""
|
||||
) -> bool:
|
||||
if event == null or not _has_blocked_fields(event):
|
||||
return false
|
||||
if is_legacy_unstructured_blocked(event):
|
||||
return _is_valid_legacy_unstructured_blocked(event, expected_world_id, expected_location_id)
|
||||
var actor_id := int(event.data["actor_id"])
|
||||
var action_id := StringName(event.data["action_id"])
|
||||
var definition := SimulationDefinitions.get_action(action_id)
|
||||
if (
|
||||
StringName(event.data["event_type"]) != SimulationIds.EVENT_TASK_BLOCKED
|
||||
or not KnownEventStateRecord.is_valid_actor_id(actor_id)
|
||||
or definition == null
|
||||
or not definition.has_completion_cost()
|
||||
or (
|
||||
StringName(event.data["blocked_contract_kind"])
|
||||
!= SimulationIds.BLOCKED_CONTRACT_ACTION_COST
|
||||
)
|
||||
or StringName(event.data["item_id"]) != definition.completion_cost_resource_id
|
||||
or not is_equal_approx(
|
||||
float(event.data["required_amount"]), definition.completion_cost_amount
|
||||
)
|
||||
or StringName(event.data["source_id"]).is_empty()
|
||||
or not StringName(event.data["destination_id"]).is_empty()
|
||||
or not is_zero_approx(float(event.data["amount"]))
|
||||
or StringName(event.data["world_id"]).is_empty()
|
||||
or StringName(event.data["location_id"]).is_empty()
|
||||
or (
|
||||
not expected_world_id.is_empty()
|
||||
and StringName(event.data["world_id"]) != expected_world_id
|
||||
)
|
||||
or (
|
||||
not expected_location_id.is_empty()
|
||||
and StringName(event.data["location_id"]) != expected_location_id
|
||||
)
|
||||
):
|
||||
return false
|
||||
var route := _get_core_storage_routing().resolve_definition(
|
||||
definition.completion_cost_resource_id
|
||||
)
|
||||
if route == null or not _blocked_source_matches(event, action_id, actor_id, route.storage_id):
|
||||
return false
|
||||
if event.data.has("activity_target_id"):
|
||||
return (
|
||||
definition.target_type == SimulationIds.TARGET_ACTIVITY
|
||||
and (
|
||||
definition.completion_handler_id
|
||||
== SimulationIds.ACTION_HANDLER_ACTIVITY_METRIC_DELTA
|
||||
)
|
||||
and not StringName(event.data["activity_target_id"]).is_empty()
|
||||
and StringName(event.data.get("target_kind", &"")) == SimulationIds.TARGET_ACTIVITY
|
||||
and StringName(event.data.get("target_action_id", &"")) == action_id
|
||||
)
|
||||
return true
|
||||
|
||||
|
||||
static func _blocked_source_matches(
|
||||
event: EconomicEventRecord, action_id: StringName, actor_id: int, routed_storage_id: StringName
|
||||
) -> bool:
|
||||
var source_id := StringName(event.data["source_id"])
|
||||
if source_id == routed_storage_id:
|
||||
return true
|
||||
return (
|
||||
action_id == SimulationIds.ACTION_FEED_ANIMAL
|
||||
and actor_id >= 0
|
||||
and source_id == SimulationIds.npc_inventory_id(actor_id)
|
||||
)
|
||||
|
||||
|
||||
static func is_valid_blocked_for_state(
|
||||
event: EconomicEventRecord,
|
||||
npc_ids: Dictionary,
|
||||
storage_records_by_id: Dictionary,
|
||||
current_tick: int,
|
||||
expected_world_id: StringName = &"",
|
||||
expected_location_id: StringName = &""
|
||||
) -> bool:
|
||||
if (
|
||||
not is_valid_blocked(event, expected_world_id, expected_location_id)
|
||||
or int(event.data["tick"]) > current_tick
|
||||
):
|
||||
return false
|
||||
var actor_id := int(event.data["actor_id"])
|
||||
if actor_id != SimulationIds.PLAYER_ACTOR_ID and not npc_ids.has(actor_id):
|
||||
return false
|
||||
if is_legacy_unstructured_blocked(event):
|
||||
return true
|
||||
var source_id := StringName(event.data["source_id"])
|
||||
return (
|
||||
storage_records_by_id.has(source_id)
|
||||
or (
|
||||
StringName(event.data["action_id"]) == SimulationIds.ACTION_FEED_ANIMAL
|
||||
and actor_id >= 0
|
||||
and source_id == SimulationIds.npc_inventory_id(actor_id)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func is_legacy_unstructured_blocked(event: EconomicEventRecord) -> bool:
|
||||
return (
|
||||
event != null
|
||||
and StringName(event.data.get("event_type", &"")) == SimulationIds.EVENT_TASK_BLOCKED
|
||||
and (
|
||||
StringName(event.data.get("blocked_contract_kind", &""))
|
||||
== SimulationIds.BLOCKED_CONTRACT_LEGACY_UNSTRUCTURED
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func _is_valid_legacy_unstructured_blocked(
|
||||
event: EconomicEventRecord, expected_world_id: StringName, expected_location_id: StringName
|
||||
) -> bool:
|
||||
var legacy_version := int(event.data.get("legacy_world_schema_version", -1))
|
||||
return (
|
||||
(
|
||||
legacy_version
|
||||
in range(
|
||||
EconomicEventRecord.LEGACY_BLOCKED_WORLD_SCHEMA_MIN,
|
||||
EconomicEventRecord.LEGACY_BLOCKED_WORLD_SCHEMA_MAX + 1
|
||||
)
|
||||
)
|
||||
and KnownEventStateRecord.is_valid_actor_id(int(event.data["actor_id"]))
|
||||
and StringName(event.data["action_id"]).is_empty()
|
||||
and StringName(event.data["item_id"]).is_empty()
|
||||
and is_zero_approx(float(event.data["required_amount"]))
|
||||
and StringName(event.data["destination_id"]).is_empty()
|
||||
and is_zero_approx(float(event.data["amount"]))
|
||||
and not StringName(event.data["world_id"]).is_empty()
|
||||
and not StringName(event.data["location_id"]).is_empty()
|
||||
and (
|
||||
expected_world_id.is_empty() or StringName(event.data["world_id"]) == expected_world_id
|
||||
)
|
||||
and (
|
||||
expected_location_id.is_empty()
|
||||
or StringName(event.data["location_id"]) == expected_location_id
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func _has_activity_fields(event: EconomicEventRecord) -> bool:
|
||||
return (
|
||||
event
|
||||
. data
|
||||
. has_all(
|
||||
[
|
||||
"metric_id",
|
||||
"metric_delta",
|
||||
"cost_item_id",
|
||||
"cost_amount",
|
||||
"target_kind",
|
||||
"target_action_id",
|
||||
"world_id",
|
||||
"location_id",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func _has_blocked_fields(event: EconomicEventRecord) -> bool:
|
||||
return (
|
||||
event
|
||||
. data
|
||||
. has_all(
|
||||
[
|
||||
"blocked_contract_kind",
|
||||
"world_id",
|
||||
"location_id",
|
||||
"action_id",
|
||||
"item_id",
|
||||
"required_amount",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
static func _maximum_delta(effect: ActionEffect, actor_id: int) -> float:
|
||||
var maximum_delta := effect.value
|
||||
if actor_id != SimulationIds.PLAYER_ACTOR_ID:
|
||||
return maximum_delta
|
||||
var raw_multiplier: Variant = effect.parameters.get("player_multiplier", 1.0)
|
||||
if raw_multiplier is not int and raw_multiplier is not float:
|
||||
return -1.0
|
||||
return maximum_delta * float(raw_multiplier)
|
||||
|
||||
|
||||
static func _get_core_storage_routing() -> StorageRoutingPolicy:
|
||||
if _core_storage_routing == null:
|
||||
_core_catalog = ContentCatalog.create_core()
|
||||
_core_storage_routing = StorageRoutingPolicy.new(_core_catalog)
|
||||
return _core_storage_routing
|
||||
@@ -0,0 +1 @@
|
||||
uid://spr5p8ebsyc1
|
||||
@@ -28,6 +28,23 @@ 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(
|
||||
@@ -80,6 +97,14 @@ func record_narrative(
|
||||
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
|
||||
|
||||
@@ -115,11 +140,105 @@ func record_fact(
|
||||
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 (
|
||||
WorldEventRecord.from_economic_event(
|
||||
normalized, SimulationIds.WORLD_CORE, SimulationIds.LOCATION_JAJCE
|
||||
)
|
||||
== null
|
||||
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)
|
||||
@@ -207,16 +326,27 @@ func get_consumption_rates(
|
||||
}
|
||||
|
||||
|
||||
func restore(restored_events: Array[EconomicEventRecord], restored_next_id: int) -> void:
|
||||
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:
|
||||
@@ -236,7 +366,13 @@ func _index(event: EconomicEventRecord) -> void:
|
||||
_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)
|
||||
world_events.append_economic(event, SimulationIds.WORLD_CORE, SimulationIds.LOCATION_JAJCE)
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user