Files
gamedev-the-steward/simulation/events/ActivityEventValidator.gd
T
2026-08-13 00:08:30 +02:00

294 lines
9.5 KiB
GDScript

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