Data model and units
ROCK crosses the DLL boundary with values, not C++ runtime objects. Most public structures are standard-layout and trivially copyable. That keeps allocation, ownership, compiler-runtime, and exception rules out of the ABI.
Initialize structures with their defaults
Use value initialization:
RockProviderFrameSnapshot snapshot{};
RockProviderScopeSightStateV1 scope{};
RockProviderForceGrabRequestV1 request{};
The default member initializers set size, version, invalid body sentinels,
identity transforms, and enum defaults correctly. Do not memset a structure
after construction and then forget to restore its size and version.
Some original V1 structures require an exact size. Newer appended readback structures generally accept a caller size at least as large as the current public structure. The detailed function reference states which rule applies.
Transform layout
RockProviderTransform contains:
struct RockProviderTransform
{
float rotate[9]; // 3 × 3 matrix, row-major
float translate[3];
float scale; // uniform, non-zero
};
The coordinate space is part of the field name or containing structure:
World/worldTransform— game world space;InWeapon/weaponLocal/WeaponRootLocal— equipped weapon-root local;SourceParentLocal— local to the part's source parent;fingerLocalTransforms— exact local transforms for ROCK's 15 public finger bones per hand.
Do not compose transforms from different frames or weapon generations.
Game units and Havok units
Names are explicit:
- fields ending in
Game,GameUnits, orpointGameuse Fallout game units; - fields ending in
Havokuse Havok world units; - angular velocity is radians per second;
gameToHavokScaleandhavokToGameScaleare published in each frame snapshot, together withphysicsScaleRevision.
Use the snapshot scale for the generation/frame that produced the data. Do not hardcode a conversion constant.
The external-contact field contactPointWeightSum is a sum of Bethesda contact
point weights. It is not impulse magnitude. The legacy union name
aggregateImpulseMagnitude remains for ABI compatibility and must not be used
to interpret the value as physical impulse.
Identities
The SDK uses several intentionally different identities:
| Identity | Lifetime / use |
|---|---|
ownerToken | ROCK-issued consumer lifetime. Authenticates owned calls. |
scopeToken | Consumer-chosen child identity beneath an owner for external-body or touch-target replacement sets. |
targetId + targetGeneration | Consumer-chosen touch mechanism identity and lifetime. |
commandId | One queued interaction command and retained result. |
callbackToken | One registered callback slot. |
frameIndex | One provider frame. |
weaponGenerationKey | Current generated/equipped weapon evidence lifetime. |
| world/skeleton/provider generation | Broader runtime invalidation domains. |
collisionGeneration | Player/generated collision rebuild lifetime. |
formId | Fallout form identity, value-only. |
bodyId | Current hknp body identity; use only with matching generations. |
omodFormId / attachPointFormId | Record-authored installed module and slot identity. |
Do not substitute one token namespace for another. In particular, a child scope is not a consumer owner, and a body ID without matching generation is not a stable object handle.
Pointer-sized fields
Some original V1 structures contain std::uintptr_t fields such as weaponNode,
node, sourceRoot, or interactionRoot. They are non-owning identity
witnesses retained for compatibility.
Rules:
- never dereference them;
- never claim ownership;
- compare them only during the current ROCK callback/query frame;
- require the accompanying frame and generation identities to match;
- prefer form IDs, body IDs, names, part kinds, OMOD IDs, and attach-point IDs in new code.
targetRefr in interaction-command requests is ignored. Supply
targetFormId and, where useful, targetBodyId. Command results return
targetRefr == 0.
Count-and-copy APIs
Several read surfaces use a count/copy pair:
const std::uint32_t count =
RockProviderApi::inst->getWeaponEmitterCountV1();
std::vector<RockProviderWeaponEmitterV1> emitters(
(std::min)(count, limits.maxWeaponEmitters));
const std::uint32_t copied =
RockProviderApi::inst->copyWeaponEmittersV1(
emitters.data(),
static_cast<std::uint32_t>(emitters.size()));
emitters.resize(copied);
In a hot callback, prefer a fixed std::array sized from the published maximum
or a vector reserved outside the callback. Counts can change between calls, so
always trust the returned copied count and accept truncation.
Newer copy calls return RockProviderResultV1 and write the count through an
output pointer. They allow maxCount == 0 with a null data pointer, which is
useful for probing current count without copying.
Touch state copies follow the same bounded pattern. World raycasts are a single-request/single-result value query and never return native world/body pointers; their hit point/normal, fraction, distance, frame, and generations are copied values.
Strings
Public names are fixed-size UTF-8/ASCII byte arrays at the ABI boundary:
- consumer mod name: 64 bytes including terminator;
- weapon evidence/part source name: 64 bytes including terminator;
- overlay text: 128 bytes including terminator.
Always write with a bounded operation such as std::snprintf, and do not submit
an unterminated full-capacity string.
Flags
Flag enums are strongly typed, but storage fields use integer masks. Combine them by explicitly converting to the field width:
request.flags =
static_cast<std::uint32_t>(
RockProviderHandVisualAuthorityFlagV1::WorldTransform) |
static_cast<std::uint32_t>(
RockProviderHandVisualAuthorityFlagV1::FingerLocalTransforms);
Use the provided bit helpers where available. Unknown request flags are rejected; unknown output flags should be ignored so a compatible consumer can continue reading fields it understands.