feat: unify authored activity commands
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user