feat: unify authored activity commands

This commit is contained in:
Rijad Zuzo
2026-08-13 00:08:30 +02:00
parent e5aecee0de
commit b3b98c6456
36 changed files with 3013 additions and 96 deletions
+2 -2
View File
@@ -73,9 +73,9 @@ static func accepted(command_id: StringName, payload: Dictionary = {}) -> Action
static func rejected(
command_id: StringName, reason_code: StringName, message: String = ""
command_id: StringName, reason_code: StringName, message: String = "", payload: Dictionary = {}
) -> ActionResult:
return ActionResult.new(command_id, STATUS_REJECTED, reason_code, message)
return ActionResult.new(command_id, STATUS_REJECTED, reason_code, message, payload)
static func completed(command_id: StringName, payload: Dictionary = {}) -> ActionResult:
@@ -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)
@@ -0,0 +1 @@
uid://ce4eqg4gbqcf8
@@ -17,16 +17,19 @@ var _registry: WorldTargetRegistry
var _catalog: ContentCatalog
var _availability_evaluator: Callable
var _last_query_reason_trace: Array[StringName] = []
var _require_target_provider := false
func _init(
registry: WorldTargetRegistry = null,
catalog: ContentCatalog = null,
availability_evaluator: Callable = Callable()
availability_evaluator: Callable = Callable(),
require_target_provider: bool = false
) -> void:
_registry = registry
_catalog = catalog
_availability_evaluator = availability_evaluator
_require_target_provider = require_target_provider
func is_configured() -> bool:
@@ -49,7 +52,7 @@ func get_offers(actor: WorldEntityRef, target: WorldTargetHandle) -> Array[Actio
if not _actor_is_supported(actor):
_last_query_reason_trace.append(REASON_ACTOR_INVALID)
return []
if not _registry.is_handle_valid(target):
if not _registry.is_handle_valid(target, _require_target_provider):
_last_query_reason_trace.append(REASON_TARGET_STALE)
return []
var descriptor := _registry.resolve_handle(target)