Skip to main content

Interaction commands

Interaction commands ask ROCK to use its normal interaction paths. The API call validates and queues work; it does not mutate a hand immediately. Success means the command was accepted into the queue, then committed and applied at a ROCK-owned update point.

Require the InteractionCommands capability. Check the named support helper for each command, and supportsCommandCancellationV1 if you cancel work.

Shared command lifecycle

Request calls return RequestQueued and a non-zero command ID when accepted. Poll the command or consume InteractionCommandTerminal provider events.

Accepted → Queued → Committed → Applied → Terminal

The public state enum is Queued, Succeeded, Rejected, or Cancelled. RockProviderCommandStageV1 provides the finer milestone. Acceptance, commitment, and application frame numbers remain available in the result.

The provider holds up to 32 active commands and 64 retained completed results. Poll or consume events promptly; an old result can eventually age out and return RequestNotFound.

requestForceGrabV1 · slot 23

RockProviderResultV1 requestForceGrabV1(
std::uint64_t ownerToken,
const RockProviderForceGrabRequestV1* request,
std::uint64_t* outCommandId);

Queues a near grab through ROCK's normal dynamic grab path. On application, settling, finger posing, collision ownership, haptics, and later release behavior match a normal ROCK grab.

Required request fields:

  • exact structure size and V1 version;
  • explicit Left or Right hand;
  • non-zero targetFormId;
  • optional current targetBodyId to narrow identity;
  • optional generation guards;
  • finite maxDistanceGame.

targetRefr is ignored. The initial public scope is a live loose object or loose weapon near the requested hand. maxDistanceGame == 0 uses ROCK's own near-grab range; a positive value sets an additional caller limit.

Set UsePreferredGrabPointGame only when preferredGrabPointGame[3] contains a finite world-space game-unit point.

Only one queued force grab may reserve a given hand. A second one returns HandBusy before queueing.

requestForceReleaseV1 · slot 25

RockProviderResultV1 requestForceReleaseV1(
std::uint64_t ownerToken,
const RockProviderForceReleaseRequestV1* request,
std::uint64_t* outCommandId);

Queues release of the object held by an explicit hand. With no velocity flag, this is a gentle physical drop: ROCK uses its normal cleanup path without reusing captured controller throw history.

Flags:

FlagEffect
ImmediateCollisionRestoreRequests immediate collision restoration where the normal release policy allows it.
RequireMatchingTargetReject unless the held object matches supplied form ID and/or body ID. At least one identity must be supplied.
UseVelocityHavokApply caller-provided linear Havok velocity and angular radians/second after detach.

Explicit velocity vectors are finite-checked but intentionally not clamped. They are trusted physical inputs; validate and bound them in your addon.

requestThrownDropV1 · slot 26

RockProviderResultV1 requestThrownDropV1(
std::uint64_t ownerToken,
const RockProviderThrownDropRequestV1* request,
std::uint64_t* outCommandId);

Queues a release intended to preserve or author throw motion. Without UseVelocityHavok, ROCK captures the current held release motion before detaching. With the flag, it uses the supplied linear Havok and angular radians/second vectors.

ImmediateCollisionRestore and RequireMatchingTarget have the same meaning as force release. Explicit velocities are finite-checked and not clamped.

Request return values

Before queueing, all three calls can return:

  • InvalidArgument — null/zero inputs, unsupported flags, non-finite values, missing required identity, or malformed flag-dependent data;
  • InvalidSize or UnsupportedVersion;
  • HandUnavailable for None or an invalid hand;
  • NotReady when the provider is not initialized;
  • OwnerNotRegistered or PermissionDenied;
  • HandBusy for a conflicting force-grab reservation;
  • CapacityFull when the command queue is full;
  • RequestQueued with a non-zero ID on acceptance.

World/skeleton/provider guard mismatch may also return WorldNotReady or NotReady.

getInteractionCommandResultV1 · slot 24

RockProviderResultV1 getInteractionCommandResultV1(
std::uint64_t ownerToken,
std::uint64_t commandId,
RockProviderInteractionCommandResultV1* outResult);

Reads an active or retained result belonging to the caller. The output size must be at least ROCK_PROVIDER_INTERACTION_COMMAND_RESULT_V1_PREFIX_SIZE (112 bytes). ROCK prefix-copies up to the current 120-byte structure.

Key fields:

  • kind, state, and failure — high-level outcome;
  • stage and failureStage — the lifecycle point reached and where failure was assigned;
  • acceptedFrame, committedFrame, appliedFrame — milestone timing;
  • frameIndex — latest result update frame;
  • hand, form/body identity, and generation fields.

The legacy targetRefr field is always zero.

Returns RequestNotFound if the ID does not belong to the owner, has aged out, or never existed.

cancelInteractionCommandV1 · slot 75

RockProviderResultV1 cancelInteractionCommandV1(
std::uint64_t ownerToken,
std::uint64_t commandId);

Cancels a command only while it is still in the provider queue. A successful cancel stores a terminal Cancelled result and releases a force-grab hand reservation.

Returns:

  • Ok — still queued and now cancelled;
  • AlreadyCommitted — provider execution already owns the work;
  • RequestNotFound — unknown, already terminal, or no longer retained;
  • standard owner/capability/argument errors.

Cancellation is race-aware. AlreadyCommitted means the consumer must observe the eventual terminal outcome rather than assuming the action did not happen.

Terminal failure reasons

RockProviderInteractionFailureV1 distinguishes:

FailureMeaning
ProviderNotReadyProvider disappeared or was not ready at execution.
PhysicsWritesBlockedLifecycle did not permit the physical operation.
OwnerNotRegisteredOwner was removed before execution.
InvalidRequestRequest failed execution-stage validation.
StaleWorldGenerationWorld guard no longer matched.
StaleSkeletonGenerationSkeleton guard no longer matched.
StaleProviderGenerationProvider guard no longer matched.
TargetMissingRequested form/reference could not be resolved.
TargetUnavailableTarget exists but cannot currently participate.
TargetBodyMissingRequired body is absent.
TargetAlreadyOwnedAnother interaction owns the target.
HandInvalidHand request is invalid at execution.
HandDisabledROCK collision/interaction has disabled the hand.
HandBusyHand has incompatible active work.
HandNotHoldingRelease/drop hand owns no held object.
HeldObjectMismatchRequireMatchingTarget did not match.

An accepted command can still end rejected for any execution-time condition.

Example: generation-safe force grab

std::uint64_t queueGrab(
std::uint64_t ownerToken,
const RockProviderFrameSnapshot& frame,
RockProviderHand hand,
std::uint32_t targetFormId,
std::uint32_t targetBodyId)
{
if (!hasLifecycleFlag(
frame.lifecycleFlags,
RockProviderLifecycleFlag::PhysicsWriteAllowed)) {
return 0;
}

RockProviderForceGrabRequestV1 request{};
request.hand = hand;
request.targetFormId = targetFormId;
request.targetBodyId = targetBodyId;
request.worldGeneration = frame.worldGeneration;
request.skeletonGeneration = frame.skeletonGeneration;
request.providerGeneration = frame.providerGeneration;
request.maxDistanceGame = 18.0f;

std::uint64_t commandId = 0;
const auto result = RockProviderApi::inst->requestForceGrabV1(
ownerToken, &request, &commandId);
return result == RockProviderResultV1::RequestQueued ? commandId : 0;
}

Example: poll without losing acceptance history

bool commandFinished(
std::uint64_t ownerToken,
std::uint64_t commandId,
bool& succeeded)
{
RockProviderInteractionCommandResultV1 result{};
const auto query = RockProviderApi::inst->getInteractionCommandResultV1(
ownerToken, commandId, &result);
if (query != RockProviderResultV1::Ok) {
return false;
}

switch (result.state) {
case RockProviderInteractionCommandStateV1::Succeeded:
succeeded = true;
return true;
case RockProviderInteractionCommandStateV1::Rejected:
case RockProviderInteractionCommandStateV1::Cancelled:
succeeded = false;
return true;
default:
return false;
}
}

For many concurrent commands, the provider event cursor is more efficient than polling every ID every frame. Keep result polling for targeted status UI or when the event stream reports a gap.