Skip to main content

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, or pointGame use Fallout game units;
  • fields ending in Havok use Havok world units;
  • angular velocity is radians per second;
  • gameToHavokScale and havokToGameScale are published in each frame snapshot, together with physicsScaleRevision.

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:

IdentityLifetime / use
ownerTokenROCK-issued consumer lifetime. Authenticates owned calls.
scopeTokenConsumer-chosen child identity beneath an owner for external-body or touch-target replacement sets.
targetId + targetGenerationConsumer-chosen touch mechanism identity and lifetime.
commandIdOne queued interaction command and retained result.
callbackTokenOne registered callback slot.
frameIndexOne provider frame.
weaponGenerationKeyCurrent generated/equipped weapon evidence lifetime.
world/skeleton/provider generationBroader runtime invalidation domains.
collisionGenerationPlayer/generated collision rebuild lifetime.
formIdFallout form identity, value-only.
bodyIdCurrent hknp body identity; use only with matching generations.
omodFormId / attachPointFormIdRecord-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.