Skip to main content

Consumer recipes

These examples assume you already completed installation and owner registration. They focus on the call pattern; replace placeholder logging, math, and product actions with your own bounded implementation.

All snippets use:

#include <ROCKProviderApi.h>

#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <optional>

using namespace rock::provider;

Shared capability helper

[[nodiscard]] bool hasCapability(
const RockProviderConsumerHandleV1& handle,
const RockProviderConsumerCapabilityV1 capability)
{
return hasConsumerCapabilityV1(handle.grantedCapabilities, capability);
}

Test support at startup, not repeatedly in every hot callback. A feature needs all three: sufficient table extent, the corresponding feature bit, and a granted capability when the function is owner-bound.

Recipe 1: drain provider events without losing gaps

Use the provider event stream to invalidate product caches and observe terminal transitions. Maintain the cursor for the life of one registered owner.

struct EventDrain
{
std::uint64_t cursor{ 0 };

void poll(std::uint64_t ownerToken)
{
std::array<RockProviderEventV1, 32> events{};
RockProviderEventStreamStateV1 stream{};

const auto result = RockProviderApi::inst->copyProviderEventsSinceV1(
ownerToken,
cursor,
events.data(),
static_cast<std::uint32_t>(events.size()),
&stream);
if (result != RockProviderResultV1::Ok) {
return;
}

const bool gap =
(stream.flags & static_cast<std::uint32_t>(
RockProviderEventStreamFlagV1::GapBeforeFirstCopied)) != 0;
if (gap) {
rebuildFromCurrentSnapshots();
}

for (std::uint32_t index = 0; index < stream.copiedCount; ++index) {
const auto& event = events[index];
switch (event.kind) {
case RockProviderEventKindV1::LifecycleChanged:
invalidateGenerationBoundCaches();
break;
case RockProviderEventKindV1::EquippedWeaponTransitionTerminal:
refreshWeaponMetadata(event.weaponGenerationKey);
break;
case RockProviderEventKindV1::AuthorityLost:
stopPublishingAuthority(
static_cast<RockProviderAuthorityKindV1>(event.data[0]));
break;
case RockProviderEventKindV1::InteractionCommandTerminal:
finishCommand(event.subjectSequence, event.result);
break;
case RockProviderEventKindV1::GrabStateChanged:
refreshHandState(event.hand);
break;
default:
break;
}
}

if (stream.copiedCount != 0) {
cursor = stream.lastCopiedSequence;
}
}
};

If your buffer fills, call again with the updated last-copied cursor. Do not jump to latestEmittedSequence until all pages you care about are drained.

Recipe 2: submit and track a force grab

Force grab is asynchronous. The submission result and the final command result answer different questions.

struct PendingGrab
{
std::uint64_t commandId{ 0 };

RockProviderResultV1 submit(
std::uint64_t ownerToken,
RockProviderHand hand,
std::uint32_t targetFormId,
const RockProviderFrameSnapshot& frame)
{
RockProviderForceGrabRequestV1 request{};
request.hand = hand;
request.targetFormId = targetFormId; // required stable target identity
request.maxDistanceGame = 48.0f;
request.worldGeneration = frame.worldGeneration;
request.skeletonGeneration = frame.skeletonGeneration;
request.providerGeneration = frame.providerGeneration;

commandId = 0;
return RockProviderApi::inst->requestForceGrabV1(
ownerToken, &request, &commandId);
}

std::optional<RockProviderInteractionCommandStateV1> poll(
std::uint64_t ownerToken)
{
if (commandId == 0) {
return std::nullopt;
}

RockProviderInteractionCommandResultV1 result{};
const auto status = RockProviderApi::inst->getInteractionCommandResultV1(
ownerToken, commandId, &result);
if (status == RockProviderResultV1::RequestNotFound) {
commandId = 0;
return std::nullopt;
}
if (status != RockProviderResultV1::Ok) {
return std::nullopt;
}

if (result.state == RockProviderInteractionCommandStateV1::Succeeded ||
result.state == RockProviderInteractionCommandStateV1::Rejected ||
result.state == RockProviderInteractionCommandStateV1::Cancelled) {
commandId = 0;
}
return result.state;
}
};

targetRefr is retained only for ABI shape and is ignored. Use form/body value identity. For force grab, provide targetFormId; optional targetBodyId can narrow the physical target.

To cancel:

const auto cancelled = RockProviderApi::inst->cancelInteractionCommandV1(
ownerToken, pending.commandId);
if (cancelled == RockProviderResultV1::AlreadyCommitted) {
// The physical action crossed its cancellation fence. Continue observing
// the terminal result instead of pretending it was cancelled.
}

Recipe 3: release with explicit velocity

RockProviderForceReleaseRequestV1 release{};
release.hand = RockProviderHand::Right;
release.flags =
static_cast<std::uint32_t>(RockProviderForceReleaseFlagV1::UseVelocityHavok) |
static_cast<std::uint32_t>(
RockProviderForceReleaseFlagV1::ImmediateCollisionRestore);
release.targetFormId = heldFormId;
release.targetBodyId = heldBodyId;
release.linearVelocityHavok[0] = velocity.x;
release.linearVelocityHavok[1] = velocity.y;
release.linearVelocityHavok[2] = velocity.z;
release.angularVelocityRadiansPerSecond[0] = angular.x;
release.angularVelocityRadiansPerSecond[1] = angular.y;
release.angularVelocityRadiansPerSecond[2] = angular.z;
release.worldGeneration = frame.worldGeneration;
release.skeletonGeneration = frame.skeletonGeneration;
release.providerGeneration = frame.providerGeneration;

std::uint64_t commandId = 0;
const auto queued = RockProviderApi::inst->requestForceReleaseV1(
ownerToken, &release, &commandId);

requestThrownDropV1 uses the same identity, generation, velocity, and asynchronous-result pattern but marks the intent as a thrown drop. Velocity is validated for finiteness, not clamped to a gameplay-safe magnitude; apply your own policy bounds.

Recipe 4: read a fresh raw button and own its native action briefly

The API exposes button level, not edge queues. Derive an edge only from a fresh sample transition.

struct ButtonEdge
{
bool priorHeld{ false };
std::uint64_t priorSequence{ 0 };

bool pressed(RockProviderHand hand, std::uint32_t buttonId)
{
RockProviderRawWandButtonStateV1 state{};
if (!RockProviderApi::inst->getRawWandButtonStateV1(
hand, buttonId, &state) ||
state.available == 0 ||
state.availabilityReason !=
RockProviderInputAvailabilityReasonV1::Available) {
priorHeld = false;
priorSequence = state.sampleSequence;
return false;
}

const bool newSample = state.sampleSequence != priorSequence;
const bool edge = newSample && state.held != 0 && !priorHeld;
priorHeld = state.held != 0;
priorSequence = state.sampleSequence;
return edge;
}
};

When the product really replaces a native action, refresh a narrow lease:

RockProviderHandInputSuppressionRequestV1 suppression{};
suppression.hand = RockProviderHand::Left;
suppression.flags = static_cast<std::uint32_t>(
RockProviderHandInputSuppressionFlagV1::SuppressHeldWeaponTriggerEquip);
suppression.leaseFrames = 2;
suppression.worldGeneration = frame.worldGeneration;
suppression.skeletonGeneration = frame.skeletonGeneration;
suppression.providerGeneration = frame.providerGeneration;

RockProviderApi::inst->setHandInputSuppressionV1(
ownerToken, &suppression);

Read getHandInputSuppressionStateV1 when diagnosing shared behavior: its callerFlags are yours, while effectiveFlags include every active owner.

Recipe 5: cache weapon metadata by generation and composition signature

struct WeaponMetadataCache
{
std::uint64_t generation{ 0 };
std::uint64_t compositionSignature{ 0 };
RockProviderWeaponClassificationV1 classification{};
std::array<RockProviderWeaponCompositionEntryV1,
ROCK_PROVIDER_MAX_WEAPON_COMPOSITION_ENTRIES_V1> entries{};
std::uint32_t entryCount{ 0 };

bool refresh(std::uint64_t ownerToken)
{
RockProviderWeaponClassificationV1 nextClassification{};
RockProviderWeaponCompositionStateV1 composition{};

if (!RockProviderApi::inst->queryEquippedWeaponClassificationV1(
&nextClassification) ||
nextClassification.valid == 0 ||
RockProviderApi::inst->getWeaponCompositionStateV1(
ownerToken, &composition) != RockProviderResultV1::Ok) {
*this = {};
return false;
}

if (generation == nextClassification.weaponGenerationKey &&
compositionSignature == composition.compositionSignature) {
return true;
}

std::uint32_t copied = 0;
if (RockProviderApi::inst->copyWeaponCompositionEntriesV1(
ownerToken,
entries.data(),
static_cast<std::uint32_t>(entries.size()),
&copied) != RockProviderResultV1::Ok) {
return false;
}

classification = nextClassification;
generation = nextClassification.weaponGenerationKey;
compositionSignature = composition.compositionSignature;
entryCount = copied;
return true;
}
};

Use evidence details for physical meshes/roles and composition entries for installed record identity. They answer complementary questions.

Recipe 6: consume scope state inside the callback boundary

void ROCK_PROVIDER_CALL onFrame(
const RockProviderFrameSnapshot* frame,
void* userData)
{
auto& scope = *static_cast<ScopeConsumer*>(userData);
if (!frame || frame->providerReady == 0) {
scope.hide();
return;
}

RockProviderScopeSightStateV1 state{};
const auto result = RockProviderApi::inst->getScopeSightStateV1(
scope.ownerToken, &state);
if (result != RockProviderResultV1::Ok) {
scope.hide();
return;
}

const bool active =
(state.flags & static_cast<std::uint32_t>(
RockProviderScopeSightFlagV1::Active)) != 0;
const bool anchorValid =
(state.flags & static_cast<std::uint32_t>(
RockProviderScopeSightFlagV1::AnchorValid)) != 0;

if (active && anchorValid) {
scope.presentAt(
state.anchorWeaponLocal,
state.weaponGenerationKey,
state.activationSource);
} else {
scope.hide();
}
}

Do not assume Available means Active, or that an active sight necessarily has a native overlay. The flags split those facts intentionally.

Recipe 7: register a multi-body reload scope

std::array<RockProviderExternalBodyRegistration, 2> bodies{};

bodies[0].bodyId = magazineBodyId;
bodies[0].ownerToken = reloadScope;
bodies[0].generation = reloadGeneration;
bodies[0].role = RockProviderExternalBodyRole::ReloadMobile;
bodies[0].contactPolicy = RockProviderExternalBodyContactPolicy::ReportHandContacts;
bodies[0].ownerHand = RockProviderHand::Left;

bodies[1].bodyId = magwellSocketBodyId;
bodies[1].ownerToken = reloadScope;
bodies[1].generation = reloadGeneration;
bodies[1].role = RockProviderExternalBodyRole::ReloadSocket;
bodies[1].contactPolicy = static_cast<RockProviderExternalBodyContactPolicy>(
static_cast<std::uint32_t>(
RockProviderExternalBodyContactPolicy::ReportHandContacts) |
static_cast<std::uint32_t>(
RockProviderExternalBodyContactPolicy::ReportAllSourceKinds));

const auto result = RockProviderApi::inst->registerExternalBodiesForScopeV1(
ownerToken,
reloadScope,
bodies.data(),
static_cast<std::uint32_t>(bodies.size()));

Replace the scope registration when its set changes. Before destroying either body:

RockProviderApi::inst->clearExternalBodiesForScopeV1(
ownerToken, reloadScope);

The contacts reference includes the cursor drain pattern for these scoped records.

Recipe 8: inspect current interaction state before acting

RockProviderHandInteractionStateV1 hand{};
const auto result = RockProviderApi::inst->getHandInteractionStateV1(
ownerToken, RockProviderHand::Left, &hand);

if (result == RockProviderResultV1::Ok) {
const bool holding = hand.phase == RockProviderHandInteractionPhaseV1::Holding;
const bool writesAvailable =
(hand.collisionAvailabilityFlags & static_cast<std::uint32_t>(
RockProviderHandCollisionAvailabilityFlagV1::PhysicsWritesAllowed)) != 0;
if (!holding && writesAvailable) {
offerInteractionCandidate(hand.targetFormId);
}
}

The snapshot distinguishes hand phase, target identity, held body list, input suppression, collision availability, and their independent sequences. Avoid reconstructing these from a single touch flag.

For fixed-surface grabs, read the authoritative anchor and distinguish a real mesh seat from the collision-shell control or fallback:

const auto hasFlag = [](std::uint32_t flags,
RockProviderHandInteractionFlagV1 flag) {
return (flags & static_cast<std::uint32_t>(flag)) != 0;
};

if (result == RockProviderResultV1::Ok &&
hasFlag(hand.flags,
RockProviderHandInteractionFlagV1::SurfaceAnchorValid)) {
const RockProviderPoint3 anchor = hand.surfaceAnchorGame;

switch (hand.surfaceGripMode) {
case RockProviderSurfaceGripModeV1::MeshAnchor:
usePreciseSurfaceAnchor(anchor);
if (hasFlag(hand.flags,
RockProviderHandInteractionFlagV1::MeshFingerPose)) {
observeMeshConformedHand();
}
break;
case RockProviderSurfaceGripModeV1::CollisionFallback:
case RockProviderSurfaceGripModeV1::CollisionAnchor:
default:
useCollisionCompatibleAnchor(anchor);
break;
}
}

Do not treat MeshSurfaceAnchor and MeshFingerPose as synonyms. The precise mesh anchor can remain valid when a bounded finger solve is unavailable.

Recipe 9: verify a public structure before a generated binding uses it

const auto providerSize = RockProviderApi::inst->getPublicStructureSizeV1(
RockProviderStructureIdV1::ScopeSightState);
if (providerSize != sizeof(RockProviderScopeSightStateV1)) {
disableScopeBinding("ROCK structure-size mismatch");
}

Native C++ consumers normally get compile-time static_asserts from the SDK header. This runtime function is especially useful for generated language bindings and diagnostic compatibility reports.

Recipe 10: publish independent left/right climbing surfaces

std::array<RockProviderTouchGrabTargetV1, 2> targets{};
targets[0].targetId = 0x1001;
targets[0].flags =
static_cast<std::uint32_t>(RockProviderTouchGrabTargetFlagV1::AllowRightHand) |
static_cast<std::uint32_t>(RockProviderTouchGrabTargetFlagV1::MatchAnyBody) |
static_cast<std::uint32_t>(RockProviderTouchGrabTargetFlagV1::MatchStaticMotion);
targets[1] = targets[0];
targets[1].targetId = 0x1002;
targets[1].flags =
static_cast<std::uint32_t>(RockProviderTouchGrabTargetFlagV1::AllowLeftHand) |
static_cast<std::uint32_t>(RockProviderTouchGrabTargetFlagV1::MatchAnyBody) |
static_cast<std::uint32_t>(RockProviderTouchGrabTargetFlagV1::MatchStaticMotion);

for (auto& target : targets) {
target.targetGeneration = surfaceGeneration;
target.kind = RockProviderTouchGrabKindV1::FixedAnchor;
target.allowedLayerMask = climbableLayerMask;
target.leaseFrames = 2;
target.worldGeneration = frame.worldGeneration;
target.skeletonGeneration = frame.skeletonGeneration;
target.providerGeneration = frame.providerGeneration;
}

(void)RockProviderApi::inst->setTouchGrabTargetsForScopeV1(
ownerToken, surfaceScope, targets.data(),
static_cast<std::uint32_t>(targets.size()));

Two descriptors allow the hands to resolve different surface bodies. Keep the layer mask intentional, refresh only while climbing is enabled, and clear the scope whenever physics writes are blocked.

Recipe 11: yield a physical control before scripted motion

const auto yielding = RockProviderApi::inst->requestTouchGrabYieldV1(
ownerToken, mechanismScope, targetId, targetGeneration);

Treat Ok as “yield requested,” not “safe to move.” Poll copyTouchGrabStatesForScopeV1 and begin scripted/native motion only after the matching target reaches RockProviderTouchGrabPhaseV1::Yielded. Remove it or republish with a new generation before the next acquisition cycle.

Recipe 12: run one bounded world probe

RockProviderWorldRaycastRequestV1 request{};
request.startGame = muzzleOrigin;
request.directionGame = muzzleDirection;
request.maxDistanceGame = 2048.0f;
request.worldGeneration = frame.worldGeneration;
request.skeletonGeneration = frame.skeletonGeneration;
request.providerGeneration = frame.providerGeneration;

RockProviderWorldRaycastResultV1 hit{};
const auto queried = RockProviderApi::inst->queryWorldRaycastV1(
ownerToken, &request, &hit);
if (queried == RockProviderResultV1::Ok && hit.hit != 0) {
consumeHit(hit.hitPointGame, hit.hitNormalGame);
}

Call only in the owner frame callback and stay inside maxWorldRaycastsPerOwnerPerFrame. ROCK normalizes direction and chooses the validated filter; consumers do not inject Havok filter bits.

Recipe 13: request the equipped weapon's physical hand

RockProviderEquippedWeaponHandRequestV1 request{};
request.hand = RockProviderHand::Left;
request.weaponFormId = frame.weaponFormId;
request.weaponGenerationKey = frame.weaponGenerationKey;
request.worldGeneration = frame.worldGeneration;
request.skeletonGeneration = frame.skeletonGeneration;
request.providerGeneration = frame.providerGeneration;

const auto handResult = RockProviderApi::inst->requestEquippedWeaponHandV1(
ownerToken, &request);

This requires the caller's active handling lease; left-hand requests require its AmbidextrousHandoff flag. Accept RequestQueued as asynchronous admission and observe handling state until the effective hand changes.

Cleanup recipe

Every stateful example needs the matching end path:

void Consumer::releasePublicationsFromCallback()
{
RockProviderApi::inst->clearDebugOverlayV1(ownerToken);
RockProviderApi::inst->clearColliderVisualizationOverrideV1(ownerToken);
RockProviderApi::inst->clearTouchGrabTargetsForScopeV1(
ownerToken, surfaceScope);
RockProviderApi::inst->clearHandVisualAuthorityV1(
ownerToken, RockProviderHand::None);
}

void Consumer::releaseSynchronizedState()
{
RockProviderApi::inst->clearWeaponPartDriveTargetsV1(ownerToken);
RockProviderApi::inst->clearWeaponPartTargetsV1(ownerToken);
RockProviderApi::inst->clearNativeAnimationAuthorityV1(ownerToken);
RockProviderApi::inst->clearNativeAnimationRuntimeV1(ownerToken);
RockProviderApi::inst->clearEquippedWeaponHandlingAuthorityV1(ownerToken);
RockProviderApi::inst->clearHandInputSuppressionV1(
ownerToken, RockProviderHand::Right);
RockProviderApi::inst->clearHandInputSuppressionV1(
ownerToken, RockProviderHand::Left);
RockProviderApi::inst->releaseOffhandReservationV1(ownerToken);
}

Call only functions your table extent and granted capabilities support, and perform callback-only clears while you still have a valid callback boundary. Then unregister callbacks and finally unregister the owner.