Skip to main content

Your first consumer

This example establishes the pattern used by every stateful ROCK integration: negotiate an exact table extent, register a unique consumer name, verify the exact capability grant, bind a callback to the owner, and tear everything down in reverse order.

Complete minimal client

RockClient.h
#pragma once

#include "ROCKProviderApi.h"

#include <cstdint>

class RockClient final
{
public:
bool start();
void stop() noexcept;

[[nodiscard]] bool connected() const noexcept { return ownerToken_ != 0; }

private:
static void ROCK_PROVIDER_CALL onFrameThunk(
const rock::provider::RockProviderFrameSnapshot* snapshot,
void* userData);
void onFrame(const rock::provider::RockProviderFrameSnapshot& snapshot);

std::uint64_t ownerToken_{0};
std::uint64_t callbackToken_{0};
};
RockClient.cpp
#include "RockClient.h"

#include <cstdio>

using namespace rock::provider;

bool RockClient::start()
{
if (connected()) {
return true;
}

const int init = RockProviderApi::initialize(
ROCK_PROVIDER_API_VERSION,
ROCK_PROVIDER_API_V1_OWNER_FRAME_CALLBACKS_TABLE_BYTES);
if (init != 0 || !RockProviderApi::inst ||
!supportsOwnerFrameCallbacksV1()) {
return false;
}

constexpr std::uint32_t requested =
static_cast<std::uint32_t>(
RockProviderConsumerCapabilityV1::FrameSnapshots);

RockProviderConsumerRegistrationV1 registration{};
std::snprintf(
registration.modName,
sizeof(registration.modName),
"MyPlugin");
registration.requestedCapabilities = requested;

RockProviderConsumerHandleV1 handle{};
const auto registered =
RockProviderApi::inst->registerConsumerV1(&registration, &handle);
if (registered != RockProviderResultV1::Ok ||
handle.ownerToken == 0 ||
(handle.grantedCapabilities & requested) != requested) {
// A successful registration may still return a partial grant.
if (handle.ownerToken != 0) {
(void)RockProviderApi::inst->unregisterConsumerV1(
handle.ownerToken);
}
return false;
}

ownerToken_ = handle.ownerToken;
const auto callbackResult =
RockProviderApi::inst->registerFrameCallbackForOwnerV1(
ownerToken_,
&RockClient::onFrameThunk,
this,
&callbackToken_);
if (callbackResult != RockProviderResultV1::Ok ||
callbackToken_ == 0) {
(void)RockProviderApi::inst->unregisterConsumerV1(ownerToken_);
ownerToken_ = 0;
return false;
}

return true;
}

void RockClient::stop() noexcept
{
if (!RockProviderApi::inst) {
ownerToken_ = 0;
callbackToken_ = 0;
return;
}

if (ownerToken_ != 0 && callbackToken_ != 0) {
(void)RockProviderApi::inst->unregisterFrameCallbackForOwnerV1(
ownerToken_, callbackToken_);
callbackToken_ = 0;
}

if (ownerToken_ != 0) {
(void)RockProviderApi::inst->unregisterConsumerV1(ownerToken_);
ownerToken_ = 0;
}
}

void ROCK_PROVIDER_CALL RockClient::onFrameThunk(
const RockProviderFrameSnapshot* snapshot,
void* userData)
{
if (snapshot && userData) {
static_cast<RockClient*>(userData)->onFrame(*snapshot);
}
}

void RockClient::onFrame(const RockProviderFrameSnapshot& snapshot)
{
if (!hasLifecycleFlag(
snapshot.lifecycleFlags,
RockProviderLifecycleFlag::ProviderReady)) {
return;
}

// Read-only work may continue when its required values are valid.
const RockProviderHand primary = snapshot.primaryHand;
const RockProviderHand offhand = snapshot.offhandHand;
(void)primary;
(void)offhand;

// Gate any physics-affecting publication or command separately.
if (!hasLifecycleFlag(
snapshot.lifecycleFlags,
RockProviderLifecycleFlag::PhysicsWriteAllowed)) {
return;
}

// Submit generation-guarded work here.
}

Why each step exists

1. Require the final table slot

ROCK_PROVIDER_API_V1_OWNER_FRAME_CALLBACKS_TABLE_BYTES proves the table contains both owner callback calls. A version check alone cannot prove that in pre-launch V1.

2. Request capabilities explicitly

Capabilities are permissions, not feature discovery. requestedCapabilities states what this owner intends to do. ROCK masks the request to what it implements and returns grantedCapabilities; the consumer must check every bit it depends on.

Use a unique, null-terminated modName shorter than 64 bytes. Registering the same name twice returns OwnerConflict.

3. Keep the owner token private

The returned ownerToken identifies every callback, lease, publication, external-body scope, and command owned by your plugin. Never invent one, share one between plugins, or use a child scope token where an owner token is required.

4. Prefer owner callbacks

registerFrameCallbackForOwnerV1 binds the callback to the consumer. If the callback faults, ROCK can revoke that owner's state instead of leaving active leases behind. unregisterConsumerV1 also removes owner-bound callbacks.

The legacy registerFrameCallback is retained for ABI compatibility but has no consumer ownership. New integrations should not use it.

5. Gate writes, not just readiness

providerReady says the interaction provider exists. It does not by itself say that physics or visuals may be changed. Use PhysicsWriteAllowed and VisualWriteAllowed for those separate decisions.

6. Tear down in reverse order

Remove callbacks before destroying the object used as userData, then unregister the owner. Unregistering revokes its commands, bodies, scopes, targets, drives, input suppression, reservations, animation/visual authority, runtime publication, handling authority, and debug overlay.

:::danger Callback lifetime

Unregistering prevents future callback copies, but it is not a waiting barrier for a callback ROCK already copied for dispatch. Keep userData storage alive until any currently executing callback has returned. Do not unload a DLL while one of its callback functions may still be running.

:::

Adding another family

When you add a feature:

  1. Raise the requested minimum table bytes to that family's constant.
  2. Check its feature helper or feature bit.
  3. Add the required consumer capability.
  4. Reject a partial grant and unregister the returned owner.
  5. Observe its thread, generation, lease, and cleanup contract.

The complete API index lists the extent, feature, capability, and detailed page for every call. The minimal example mods reuse this lifecycle shell for hand monitoring, weapon inspection, surface climbing, and contact visualization.