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