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.