|
|
|
@@ -0,0 +1,265 @@
|
|
|
|
|
class_name ActivityActionCommandService
|
|
|
|
|
extends ActionCommandService
|
|
|
|
|
|
|
|
|
|
const HANDLER_METRIC_DELTA := SimulationIds.ACTION_HANDLER_ACTIVITY_METRIC_DELTA
|
|
|
|
|
const ACTOR_KIND_PLAYER := SimulationIds.ENTITY_PLAYER
|
|
|
|
|
const ACTOR_KIND_PERSON := SimulationIds.ENTITY_PERSON
|
|
|
|
|
const REASON_INVALID_COMMAND := &"invalid_command"
|
|
|
|
|
const REASON_STALE_TARGET := &"stale_target"
|
|
|
|
|
const REASON_STALE_REVISION := &"stale_revision"
|
|
|
|
|
const REASON_UNSUPPORTED_ACTION := &"unsupported_action"
|
|
|
|
|
const REASON_UNSUPPORTED_ACTOR := &"unsupported_actor"
|
|
|
|
|
const REASON_TARGET_CAPABILITY := &"target_capability_missing"
|
|
|
|
|
const REASON_OUT_OF_RANGE := &"out_of_range"
|
|
|
|
|
const REASON_INVALID_EFFECT := &"invalid_effect"
|
|
|
|
|
const REASON_COMPLETION_FAILED := &"completion_failed"
|
|
|
|
|
|
|
|
|
|
var _registry: WorldTargetRegistry
|
|
|
|
|
var _definition_lookup: Callable
|
|
|
|
|
var _actor_position_lookup: Callable
|
|
|
|
|
var _state_revision_lookup: Callable
|
|
|
|
|
var _completion_handler: Callable
|
|
|
|
|
var _effect_planner := ActionEffectPlanner.new()
|
|
|
|
|
var _require_target_provider := false
|
|
|
|
|
var _enforce_spatial_range := true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _init(
|
|
|
|
|
registry: WorldTargetRegistry = null,
|
|
|
|
|
definition_lookup: Callable = Callable(),
|
|
|
|
|
actor_position_lookup: Callable = Callable(),
|
|
|
|
|
state_revision_lookup: Callable = Callable(),
|
|
|
|
|
completion_handler: Callable = Callable(),
|
|
|
|
|
require_target_provider: bool = false,
|
|
|
|
|
enforce_spatial_range: bool = true
|
|
|
|
|
) -> void:
|
|
|
|
|
_registry = registry
|
|
|
|
|
_definition_lookup = definition_lookup
|
|
|
|
|
_actor_position_lookup = actor_position_lookup
|
|
|
|
|
_state_revision_lookup = state_revision_lookup
|
|
|
|
|
_completion_handler = completion_handler
|
|
|
|
|
_require_target_provider = require_target_provider
|
|
|
|
|
_enforce_spatial_range = enforce_spatial_range
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func try_execute(command: ActionCommand) -> ActionResult:
|
|
|
|
|
var result := _execute(command)
|
|
|
|
|
command_resolved.emit(result)
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _execute(command: ActionCommand) -> ActionResult:
|
|
|
|
|
if command == null or not command.is_valid() or not _services_are_valid():
|
|
|
|
|
return _reject(command, REASON_INVALID_COMMAND, "Command or service wiring is invalid")
|
|
|
|
|
var definition := _definition_lookup.call(command.get_action_id()) as ActionDefinition
|
|
|
|
|
if (
|
|
|
|
|
definition == null
|
|
|
|
|
or definition.target_type != SimulationIds.TARGET_ACTIVITY
|
|
|
|
|
or definition.completion_handler_id != HANDLER_METRIC_DELTA
|
|
|
|
|
):
|
|
|
|
|
return _reject(command, REASON_UNSUPPORTED_ACTION, "Action has no activity metric handler")
|
|
|
|
|
var actor := command.get_actor()
|
|
|
|
|
if actor.get_entity_type() not in [ACTOR_KIND_PLAYER, ACTOR_KIND_PERSON]:
|
|
|
|
|
return _reject(command, REASON_UNSUPPORTED_ACTOR, "Actor kind cannot perform activity work")
|
|
|
|
|
var effect_plan := _effect_planner.plan(definition, actor)
|
|
|
|
|
var metric_operation := _metric_operation(effect_plan)
|
|
|
|
|
if metric_operation.is_empty():
|
|
|
|
|
return _reject(command, REASON_INVALID_EFFECT, "Action metric effect plan is invalid")
|
|
|
|
|
var handle := command.get_target()
|
|
|
|
|
if not _registry.is_handle_valid(handle, _require_target_provider):
|
|
|
|
|
return _reject(command, REASON_STALE_TARGET, "Activity target is no longer available")
|
|
|
|
|
var descriptor := _registry.resolve_handle(handle)
|
|
|
|
|
if (
|
|
|
|
|
handle.get_target_kind() != SimulationIds.TARGET_ACTIVITY
|
|
|
|
|
or descriptor == null
|
|
|
|
|
or descriptor.get_target_kind() != SimulationIds.TARGET_ACTIVITY
|
|
|
|
|
or not descriptor.has_capability(command.get_action_id())
|
|
|
|
|
):
|
|
|
|
|
return _reject(command, REASON_TARGET_CAPABILITY, "Activity target lacks the action")
|
|
|
|
|
var current_revision := int(_state_revision_lookup.call())
|
|
|
|
|
if (
|
|
|
|
|
command.get_expected_state_revision() >= 0
|
|
|
|
|
and command.get_expected_state_revision() != current_revision
|
|
|
|
|
):
|
|
|
|
|
return _reject(command, REASON_STALE_REVISION, "Authoritative state revision changed")
|
|
|
|
|
var capability := descriptor.get_capability(command.get_action_id())
|
|
|
|
|
var actor_position: Variant = _actor_position_lookup.call(actor)
|
|
|
|
|
if actor_position is not Vector3 or not actor_position.is_finite():
|
|
|
|
|
return _reject(command, REASON_UNSUPPORTED_ACTOR, "Actor has no valid world position")
|
|
|
|
|
if _enforce_spatial_range:
|
|
|
|
|
var raw_maximum_range: Variant = capability.get_attribute(&"interaction_range", 0.0)
|
|
|
|
|
if raw_maximum_range is not int and raw_maximum_range is not float:
|
|
|
|
|
return _reject(command, REASON_INVALID_EFFECT, "Activity range metadata is invalid")
|
|
|
|
|
var maximum_range := float(raw_maximum_range)
|
|
|
|
|
if (
|
|
|
|
|
not is_finite(maximum_range)
|
|
|
|
|
or maximum_range <= 0.0
|
|
|
|
|
or actor_position.distance_to(descriptor.get_local_position()) > maximum_range
|
|
|
|
|
):
|
|
|
|
|
return _reject(command, REASON_OUT_OF_RANGE, "Actor is outside the activity range")
|
|
|
|
|
var effect_payload: Dictionary = metric_operation["payload"]
|
|
|
|
|
var metric_id := StringName(effect_payload["subject_key"])
|
|
|
|
|
var amount := float(effect_payload["delta"])
|
|
|
|
|
var completion: Variant = _completion_handler.call(
|
|
|
|
|
actor,
|
|
|
|
|
command.get_action_id(),
|
|
|
|
|
descriptor.get_target_id(),
|
|
|
|
|
metric_id,
|
|
|
|
|
amount,
|
|
|
|
|
(effect_payload["parameters"] as Dictionary).duplicate(true),
|
|
|
|
|
actor_position
|
|
|
|
|
)
|
|
|
|
|
if not _completion_shape_is_valid(completion):
|
|
|
|
|
return ActionResult.failed(
|
|
|
|
|
command.get_command_id(),
|
|
|
|
|
REASON_COMPLETION_FAILED,
|
|
|
|
|
"Authoritative activity completion returned an invalid result"
|
|
|
|
|
)
|
|
|
|
|
var completion_result := completion as Dictionary
|
|
|
|
|
if not bool(completion_result["succeeded"]):
|
|
|
|
|
var failure_reason := StringName(completion_result.get("reason_code", ""))
|
|
|
|
|
var failure_message := String(completion_result.get("message", ""))
|
|
|
|
|
if failure_reason.is_empty() or failure_message.is_empty():
|
|
|
|
|
return ActionResult.failed(
|
|
|
|
|
command.get_command_id(),
|
|
|
|
|
REASON_COMPLETION_FAILED,
|
|
|
|
|
"Authoritative activity completion was rejected"
|
|
|
|
|
)
|
|
|
|
|
var failure_event_id := int(completion_result["event_id"])
|
|
|
|
|
var failure_revision := int(completion_result["state_revision"])
|
|
|
|
|
var failure_payload := {}
|
|
|
|
|
if failure_event_id >= 0:
|
|
|
|
|
if failure_event_id != current_revision or failure_revision < current_revision + 1:
|
|
|
|
|
return ActionResult.failed(
|
|
|
|
|
command.get_command_id(),
|
|
|
|
|
REASON_COMPLETION_FAILED,
|
|
|
|
|
"Authoritative failure fact did not advance state"
|
|
|
|
|
)
|
|
|
|
|
failure_payload = {
|
|
|
|
|
"event_id": failure_event_id,
|
|
|
|
|
"state_revision": failure_revision,
|
|
|
|
|
"reason_trace": ["command_rejected", "authoritative_failure_fact_recorded"],
|
|
|
|
|
}
|
|
|
|
|
elif failure_revision != current_revision:
|
|
|
|
|
return ActionResult.failed(
|
|
|
|
|
command.get_command_id(),
|
|
|
|
|
REASON_COMPLETION_FAILED,
|
|
|
|
|
"Rejected completion mutated state without a failure fact"
|
|
|
|
|
)
|
|
|
|
|
return ActionResult.rejected(
|
|
|
|
|
command.get_command_id(), failure_reason, failure_message, failure_payload
|
|
|
|
|
)
|
|
|
|
|
var event_id := int(completion_result["event_id"])
|
|
|
|
|
var resulting_revision := int(completion_result["state_revision"])
|
|
|
|
|
var applied_amount := float(completion_result.get("applied_amount", amount))
|
|
|
|
|
if (
|
|
|
|
|
event_id != current_revision
|
|
|
|
|
or resulting_revision < current_revision + 1
|
|
|
|
|
or not is_finite(applied_amount)
|
|
|
|
|
):
|
|
|
|
|
return ActionResult.failed(
|
|
|
|
|
command.get_command_id(),
|
|
|
|
|
REASON_COMPLETION_FAILED,
|
|
|
|
|
"Authoritative activity completion did not advance state"
|
|
|
|
|
)
|
|
|
|
|
var spatial_trace := (
|
|
|
|
|
"range_revalidated" if _enforce_spatial_range else "abstract_completion_authorized"
|
|
|
|
|
)
|
|
|
|
|
return (
|
|
|
|
|
ActionResult
|
|
|
|
|
. completed(
|
|
|
|
|
command.get_command_id(),
|
|
|
|
|
{
|
|
|
|
|
"action_id": String(command.get_action_id()),
|
|
|
|
|
"target_id": String(descriptor.get_target_id()),
|
|
|
|
|
"metric_id": String(metric_id),
|
|
|
|
|
"amount": applied_amount,
|
|
|
|
|
"event_id": event_id,
|
|
|
|
|
"state_revision": resulting_revision,
|
|
|
|
|
"reason_trace":
|
|
|
|
|
[
|
|
|
|
|
"command_shape_valid",
|
|
|
|
|
"actor_authorized",
|
|
|
|
|
"target_handle_live",
|
|
|
|
|
"target_capability_matched",
|
|
|
|
|
"revision_matched",
|
|
|
|
|
spatial_trace,
|
|
|
|
|
"authoritative_metric_mutated",
|
|
|
|
|
"completion_fact_recorded",
|
|
|
|
|
"causal_followup_facts_committed",
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _services_are_valid() -> bool:
|
|
|
|
|
return (
|
|
|
|
|
_registry != null
|
|
|
|
|
and _definition_lookup.is_valid()
|
|
|
|
|
and _actor_position_lookup.is_valid()
|
|
|
|
|
and _state_revision_lookup.is_valid()
|
|
|
|
|
and _completion_handler.is_valid()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static func _metric_effect(definition: ActionDefinition) -> ActionEffect:
|
|
|
|
|
if definition == null or definition.effects.size() != 1:
|
|
|
|
|
return null
|
|
|
|
|
var selected: ActionEffect
|
|
|
|
|
for resource in definition.effects:
|
|
|
|
|
var effect := resource as ActionEffect
|
|
|
|
|
if effect == null or effect.effect_kind != ActionEffect.KIND_METRIC_DELTA:
|
|
|
|
|
continue
|
|
|
|
|
if selected != null:
|
|
|
|
|
return null
|
|
|
|
|
selected = effect
|
|
|
|
|
if selected == null:
|
|
|
|
|
return null
|
|
|
|
|
if (
|
|
|
|
|
selected.subject_key not in [&"safety", &"knowledge"]
|
|
|
|
|
or not is_finite(selected.value)
|
|
|
|
|
or selected.value <= 0.0
|
|
|
|
|
):
|
|
|
|
|
return null
|
|
|
|
|
for raw_key in selected.parameters:
|
|
|
|
|
if String(raw_key) not in ["player_multiplier", "npc_productivity"]:
|
|
|
|
|
return null
|
|
|
|
|
var player_multiplier: Variant = selected.parameters.get("player_multiplier", 1.0)
|
|
|
|
|
if (
|
|
|
|
|
(player_multiplier is not int and player_multiplier is not float)
|
|
|
|
|
or not is_finite(float(player_multiplier))
|
|
|
|
|
or float(player_multiplier) <= 0.0
|
|
|
|
|
):
|
|
|
|
|
return null
|
|
|
|
|
if selected.parameters.get("npc_productivity", false) is not bool:
|
|
|
|
|
return null
|
|
|
|
|
return selected
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static func _metric_operation(plan: ActionEffectPlan) -> Dictionary:
|
|
|
|
|
if plan == null or not plan.is_valid() or plan.get_operation_count() != 1:
|
|
|
|
|
return {}
|
|
|
|
|
var operation: Dictionary = plan.get_operations()[0]
|
|
|
|
|
if StringName(operation.get("operation_kind", &"")) != ActionEffect.KIND_METRIC_DELTA:
|
|
|
|
|
return {}
|
|
|
|
|
return operation.duplicate(true)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static func _completion_shape_is_valid(completion: Variant) -> bool:
|
|
|
|
|
if completion is not Dictionary:
|
|
|
|
|
return false
|
|
|
|
|
var result := completion as Dictionary
|
|
|
|
|
if not result.has_all(["succeeded", "event_id", "state_revision"]):
|
|
|
|
|
return false
|
|
|
|
|
return (
|
|
|
|
|
result["succeeded"] is bool
|
|
|
|
|
and result["event_id"] is int
|
|
|
|
|
and result["state_revision"] is int
|
|
|
|
|
and not WorldTargetCapability._contains_object(result)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static func _reject(command: ActionCommand, reason: StringName, message: String) -> ActionResult:
|
|
|
|
|
var command_id := command.get_command_id() if command != null else &"invalid_command"
|
|
|
|
|
return ActionResult.rejected(command_id, reason, message)
|