Skip to main content

Runtime contract

The SDK is easy to call; using it correctly depends on a few state contracts. They exist so independent DLLs can coordinate without retaining one another's objects or assuming that FO4VR state remains valid across a load, menu, skeleton rebuild, or weapon transition.

Ownership

registerConsumerV1 returns one ROCK-issued ownerToken. That token scopes all state created by the consumer:

  • owner frame and animation-phase callbacks;
  • queued interaction commands and results;
  • input suppression and offhand leases;
  • weapon-part targets and drives;
  • external bodies and child scopes;
  • native animation, hand visual, runtime, and equipped-handling authority;
  • debug overlay publications and collider focus;
  • touch-grab scopes/targets and their resolved interaction state.

unregisterConsumerV1 is the final cleanup operation. It removes or revokes all of the above. The token becomes invalid and must not be reused.

Child scopeToken values are different: your plugin chooses them to partition external bodies or touch-grab targets beneath its registered parent owner. They are never accepted as owner tokens.

Lifecycle flags

RockProviderFrameSnapshot::lifecycleFlags is the coherent readiness contract.

FlagMeaning for a consumer
WorldAvailableA current physics world is present.
SkeletonReadyThe active player skeleton/provider has completed readiness.
ProviderReadyROCK's interaction provider is initialized.
MenuBlockingA menu currently blocks relevant runtime work/input.
ConfigBlockingConfiguration mode blocks normal interaction work.
LoadingOrWorldTransitionState is crossing a load/world transition; cached identities are unsafe.
GeneratedBodiesValidROCK-generated hand/weapon/body collision data belongs to the current generations.
PhysicsWriteAllowedPhysics-affecting commands/publications may be attempted.
VisualWriteAllowedVisual/animation publications may be attempted.

Check the permission that matches the operation. A camera reading value snapshots does not need PhysicsWriteAllowed; a force-grab request does.

lastLifecycleReason describes the last transition that changed the lifecycle, including world/skeleton/provider changes, menu/config blocks, generated body rebuilds, and shutdown.

Generations

ROCK publishes independent worldGeneration, skeletonGeneration, and providerGeneration counters. collisionGeneration and weaponGenerationKey cover narrower data lifetimes.

Use generation fields in two ways:

  1. Store them beside cached value data and invalidate that data when the matching generation changes.
  2. Copy them into stateful request structures so ROCK rejects or expires work that was authored against old state.

Generation guards follow an optional-zero rule:

  • all three guards set to zero means “do not constrain this request”;
  • a non-zero guard must match the current generation;
  • stale world state returns WorldNotReady;
  • stale skeleton/provider state returns NotReady;
  • a lease with guards is revoked when those generations later change.

For runtime work generated from a frame callback, copying all three current values is the safest default.

request.worldGeneration = snapshot.worldGeneration;
request.skeletonGeneration = snapshot.skeletonGeneration;
request.providerGeneration = snapshot.providerGeneration;

Zero guards are useful only when your request is deliberately generation independent. They should not be used to hide stale-state bugs.

State and change sequences

Use sequences to detect meaningful changes without comparing every field:

  • frameIndex identifies the ROCK frame;
  • stateSequence advances when coherent frame/hand state changes;
  • stateChangeMask says which domains changed;
  • equippedWeaponTransitionSequence and weapon-state sequences track equip transitions;
  • hand interaction state has state, target, grip, and release sequences;
  • raw input has a sample sequence and sample age;
  • cursors use monotonically increasing event/contact sequences.

Sequence counters saturate instead of wrapping at UINT64_MAX where the contract requires monotonicity. Compare for inequality/change; do not assume a small difference always represents elapsed frames.

Callback and thread model

Owner frame callbacks and animation-phase callbacks execute on ROCK's game thread boundary. Keep them bounded:

  • no blocking I/O;
  • no waits on other threads;
  • no unbounded loops or container growth;
  • no exception crossing the ABI;
  • no storing transient pointer witnesses after the callback.

The following families are callback-only because they read or write live scene/presentation state:

  • presented hand frame and equipped grip state;
  • weapon-part pose and drive-result snapshots;
  • scope/sight state;
  • selected authored grip and final presented hand pose;
  • semantic contacts, player collider descriptors, and collision availability;
  • hand visual authority set/clear;
  • debug overlay publish/clear and collider focus;
  • touch-target set/clear/state/yield;
  • exact equipped-weapon hand requests;
  • world raycasts.

Result-returning calls report WrongThread. Older bool calls return false. Call them from an owner frame callback or animation-phase callback.

Callback removal is not a join

ROCK copies a callback slot before invoking it. Removing the registration stops future copies but cannot cancel a copy already in flight. Keep both the callback function and userData alive until the current invocation returns.

If a callback causes a structured exception or lets a C++ exception escape, ROCK treats it as a fault and revokes that owner's callbacks and stateful resources. This prevents a dead addon from leaving authority behind, but it is not a substitute for exception-safe consumer code.

Leases

All V1 leases share the same rule. A publication made on frame F with leaseFrames = N is active while:

currentFrame < F + N

It expires at F + N. Zero is invalid. Values above the family's published maximum are clamped, and expiry arithmetic saturates instead of wrapping.

A refresh replaces the previous expiry and generation guards. It does not add time to the old expiry.

Stateful families with leases include:

  • hand input suppression;
  • weapon-part drives;
  • native animation authority;
  • hand visual authority;
  • native animation runtime publication;
  • equipped weapon handling authority;
  • debug overlay publication;
  • offhand reservation;
  • touch-grab targets;
  • collider visualization override.

Choose a short lease that tolerates an occasional missed frame, refresh it only while the feature remains active, and call the explicit clear/release function when normal shutdown or deactivation is known.

Leases fail closed on expiry, generation change, explicit clear, owner unregister, callback fault, or provider loss.

Provider loss and recovery

Provider loss revokes stateful authority and invalidates current physical data. Registered consumers and owner callbacks are preserved so the integration can observe a new provider generation and republish fresh state.

On recovery:

  1. wait for the required lifecycle flags;
  2. discard cached weapon/body/pointer identities from old generations;
  3. rebuild external body registrations, part targets, or touch targets as needed;
  4. reacquire exclusive authority;
  5. restart rolling leases with the new generation guards.

Do not assume that a previous successful lease resumes automatically.

Cursor streams

Provider events and enriched external contacts use bounded rings. A consumer stores the last copied sequence and asks for records after it:

std::uint64_t cursor = 0;

// On each callback:
RockProviderEventStreamStateV1 stream{};
std::array<RockProviderEventV1, 32> events{};
const auto result = RockProviderApi::inst->copyProviderEventsSinceV1(
ownerToken,
cursor,
events.data(),
static_cast<std::uint32_t>(events.size()),
&stream);

if (result == RockProviderResultV1::Ok && stream.copiedCount != 0) {
cursor = stream.lastCopiedSequence;
}

Inspect GapBeforeFirstCopied and RingOverwroteRecords. A gap means the consumer cannot reconstruct every transition from the stream; resynchronize from the current snapshot/state query, then continue from the newest copied sequence. Never infer “nothing changed” merely because a bounded poll returned no row after the consumer fell behind.

Failure policy

The boundary fails closed:

  • invalid pointers, enum values, flags, sizes, transforms, and finite-number checks are rejected;
  • unregistered owners and missing capabilities cannot mutate state;
  • cross-owner operations return a conflict;
  • capacity is fixed and reported;
  • queued commands are distinct from applied outcomes;
  • no C++ exception is allowed across the plugin boundary.

Read Result codes and public types before turning every non-Ok value into the same log message.