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
#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};
};
#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(®istration, &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:
- Raise the requested minimum table bytes to that family's constant.
- Check its feature helper or feature bit.
- Add the required consumer capability.
- Reject a partial grant and unregister the returned owner.
- 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.