Skip to main content

Weapon-part interaction and drives

ROCK turns the equipped weapon into a set of stable, semantic parts: magazine, bolt, pump, charging handle, scope, and other authored or inferred components. The API lets an addon select which parts may be grabbed, drive those parts in a known local space, and inspect exactly what ROCK resolved and applied.

This is the core surface for physical reload systems, moving weapon controls, folding or sliding attachments, inspection tools, and guided interaction modes.

The interaction pipeline

  1. Read weapon evidence or the live part-pose snapshot.
  2. Match a part by stable value identity.
  3. Publish grab targets if the part should be interactable.
  4. Publish a short drive lease if your addon owns its motion.
  5. Read resolution and application results.
  6. Clear both publications when the session ends.

:::tip Prefer value identity

Match by bodyId or sourceName whenever possible. sourceRoot is a frame/generation-bound engine identity witness. Never dereference or retain it.

:::

queryWeaponContactAtPoint · slot 6

bool queryWeaponContactAtPoint(
const RockProviderWeaponContactQuery* query,
RockProviderWeaponContactResult* outResult);

Finds the nearest generated weapon-evidence part inside a game-space probe sphere. Both structures must carry their exact current size; the provider must be ready; pointGame and radiusGame must describe a finite query.

On success, outResult.valid identifies a match and the result supplies:

  • bodyId and weaponGenerationKey;
  • semantic partKind plus reload, support, socket, and action roles;
  • probeDistanceGame;
  • non-owning interactionRoot and sourceRoot identity witnesses.

The function returns false when the query itself could not be serviced. A successful query with no nearby part is represented by a valid output structure whose valid field is zero.

RockProviderWeaponContactQuery probe{};
probe.pointGame[0] = handPoint.x;
probe.pointGame[1] = handPoint.y;
probe.pointGame[2] = handPoint.z;
probe.radiusGame = 3.0f;

RockProviderWeaponContactResult hit{};
if (RockProviderApi::inst->queryWeaponContactAtPoint(&probe, &hit) && hit.valid) {
// Keep bodyId + weaponGenerationKey. Do not retain hit.sourceRoot alone.
}

Grab-target publication

setWeaponPartTargetsV1 · slot 29

RockProviderResultV1 setWeaponPartTargetsV1(
std::uint64_t ownerToken,
const RockProviderWeaponPartTargetV1* targets,
std::uint32_t targetCount);

Transactionally replaces this consumer's complete target set. It requires WeaponPartInteraction; the current maximum is 128 targets. Every row needs an exact size, supported version, at least one valid matcher, and a grab mode other than None. Passing targetCount == 0 replaces the set with an empty set.

Matcher flags are combined with AND, not OR:

FlagCompared field
MatchBodyIdbodyId
MatchSourceRoottransient sourceRoot identity
MatchSourceNameexact, non-empty sourceName
MatchPartKindpartKind
MatchReloadRolereloadRole
MatchSupportRolesupportRole
MatchSocketRolesocketRole
MatchActionRoleactionRole

weaponGenerationKey == 0 is a wildcard. A nonzero key prevents a stale publication from applying to the next equipped weapon generation.

Grab modes:

ModeBehavior
FullTwoHandAuthorityThe matched part can participate in ROCK's full physical/two-hand solution.
AttachOnlyThe hand follows the part but the part never steers the weapon. Useful for glue-like controls and addon-driven motion.

Exclusive versus additive targets

Without NonExclusive, any applicable target from any owner activates a whitelist: unmatched part grips are rejected. This is appropriate during a reload sequence where only specific components should be touchable.

With NonExclusive, a matching row grants its mode without activating the whitelist. Normal grips continue to work elsewhere. This is better for an always-available bolt, button, or folding attachment.

When targets compete, ROCK selects the highest priority; a tie prefers FullTwoHandAuthority, then the numerically smaller owner token. groupId is consumer-defined correlation data returned by the observability surfaces.

clearWeaponPartTargetsV1 · slot 30

RockProviderResultV1 clearWeaponPartTargetsV1(std::uint64_t ownerToken);

Idempotently removes all targets owned by the consumer. Call it at the end of the interaction session even if a later consumer unregister also performs cleanup.

Part-drive publication

setWeaponPartDriveTargetsV1 · slot 31

RockProviderResultV1 setWeaponPartDriveTargetsV1(
std::uint64_t ownerToken,
const RockProviderWeaponPartDriveTargetV1* targets,
std::uint32_t targetCount);

Transactionally replaces this consumer's complete set of driven part poses. It requires WeaponPartInteraction; the current maximum is 64 rows.

Drive matchers deliberately accept only concrete identity flags:

  • MatchBodyId;
  • MatchSourceRoot;
  • MatchSourceName.

Do not use semantic-only flags such as MatchPartKind for a drive. A drive must resolve one concrete scene part. Multiple selected matchers are ANDed.

Each row also requires:

  • a finite targetTransform;
  • leaseFrames > 0, clamped to 120 frames;
  • WeaponRootLocal or SourceParentLocal drive space;
  • an optional generation guard;
  • optional groupId and priority for correlation and arbitration.

Refresh a drive before its lease expires. If publication stops, the generation changes, or the owner disappears, ROCK restores ownership instead of leaving a permanent half-driven part.

clearWeaponPartDriveTargetsV1 · slot 32

RockProviderResultV1 clearWeaponPartDriveTargetsV1(
std::uint64_t ownerToken);

Removes every active drive owned by the consumer and begins the normal restore path.

Grip-state readback

getWeaponPartGripStateV1 · slot 34

bool getWeaponPartGripStateV1(
RockProviderHand hand,
RockProviderWeaponPartGripStateV1* outState);

Returns the current physical weapon grip for one hand. This is a public value query and does not require an owner token. Use gripSequence to detect a fresh grab even when the same part is grabbed again.

Grip kinds:

KindMeaning
NoneThe hand has no current weapon grip.
FiringGripThe hand owns the firing grip and the weapon rides that hand.
SupportFullAuthorityOffhand support participates in the two-hand solver.
SupportVisualOnlyVisual support, such as a sidearm shooting cup.
PartCarryA carried part owns weapon motion while the firing hand is detached.
AttachOnlyThe hand follows the selected part but cannot steer the weapon.

The state also carries part identity, winning owner/group/mode, the weapon generation, and handPartLocal: the hand frame captured at grip start in the reported local space. Compose that value with the part's current transform to derive the glued hand target. Treat sourceRoot as transient.

Resolution observability

queryWeaponPartTargetResolutionV1 · slot 64

RockProviderResultV1 queryWeaponPartTargetResolutionV1(
std::uint64_t ownerToken,
const RockProviderWeaponPartResolutionQueryV1* query,
RockProviderWeaponPartResolutionResultV1* outResolution);

Requires WeaponPartObservability. This synchronized query can be called outside the frame callback. It reports the provider's final answer for one part identity:

  • whether an exclusive whitelist is active;
  • whether the query matched;
  • the winning grab mode, group, priority, and owner;
  • the weapon generation and evaluation frame.

Use this when a UI needs to explain why a part is or is not grabbable. It is also the best way to diagnose conflicts between addons without reading either addon's internal state.

copyWeaponPartPoseSnapshotV1 · slot 65

RockProviderResultV1 copyWeaponPartPoseSnapshotV1(
std::uint64_t ownerToken,
RockProviderWeaponPartPoseV1* outParts,
std::uint32_t maxParts,
std::uint32_t* outPartCount);

Copies up to 128 live parts. Requires WeaponPartObservability and must run inside an owner frame/animation callback on the game thread. Each row includes body/name identity, OMOD and attach-point identity, and valid sourceParentLocal and/or weaponRootLocal transforms.

Use the output count returned through outPartCount; a zero-capacity call with a null data buffer is valid and can be used to discover the current count.

copyWeaponPartDriveApplicationResultsV1 · slot 66

RockProviderResultV1 copyWeaponPartDriveApplicationResultsV1(
std::uint64_t ownerToken,
RockProviderWeaponPartDriveApplicationResultV1* outResults,
std::uint32_t maxResults,
std::uint32_t* outResultCount);

Copies this owner's current drive outcomes, up to 64 rows. It is also callback-only and requires WeaponPartObservability.

ResultInterpretation
AppliedROCK resolved and applied the requested local transform.
UnresolvedNo concrete part matched.
StaleGenerationThe drive was guarded for another weapon generation.
MissingParentThe selected local space had no usable parent.
LostPriorityAnother applicable drive won arbitration.
InvalidTransformThe transform was rejected as unusable.
CapacityRejectedThe runtime could not retain/apply the row within bounds.
RestoredThe drive ended and ROCK restored normal ownership.

Complete pattern: drive the current magazine

The example discovers the concrete magazine pose, publishes a non-exclusive attach-only grip, and refreshes a short source-parent-local drive. It belongs in an owner callback because the pose snapshot is a live read.

using namespace rock::provider;

void ROCK_PROVIDER_CALL onFrame(
const RockProviderFrameSnapshot* frame,
void* userData)
{
auto& client = *static_cast<ClientState*>(userData);
if (!frame || !RockProviderApi::inst || client.ownerToken == 0) {
return;
}

std::array<RockProviderWeaponPartPoseV1,
ROCK_PROVIDER_MAX_WEAPON_PART_POSES_V1> parts{};
std::uint32_t count = 0;
if (RockProviderApi::inst->copyWeaponPartPoseSnapshotV1(
client.ownerToken, parts.data(), parts.size(), &count) !=
RockProviderResultV1::Ok) {
return;
}

const auto magazine = std::find_if(
parts.begin(), parts.begin() + count,
[](const RockProviderWeaponPartPoseV1& part) {
return part.partKind == static_cast<std::uint32_t>(
RockProviderWeaponPartKindV1::Magazine);
});
if (magazine == parts.begin() + count) {
RockProviderApi::inst->clearWeaponPartTargetsV1(client.ownerToken);
RockProviderApi::inst->clearWeaponPartDriveTargetsV1(client.ownerToken);
return;
}

RockProviderWeaponPartTargetV1 grip{};
grip.flags =
static_cast<std::uint32_t>(RockProviderWeaponPartTargetFlagV1::MatchBodyId) |
static_cast<std::uint32_t>(RockProviderWeaponPartTargetFlagV1::NonExclusive);
grip.grabMode = RockProviderWeaponPartGrabModeV1::AttachOnly;
grip.weaponGenerationKey = magazine->weaponGenerationKey;
grip.bodyId = magazine->bodyId;
grip.groupId = 1;
grip.priority = 100;
RockProviderApi::inst->setWeaponPartTargetsV1(
client.ownerToken, &grip, 1);

RockProviderWeaponPartDriveTargetV1 drive{};
drive.flags = static_cast<std::uint32_t>(
RockProviderWeaponPartTargetFlagV1::MatchBodyId);
drive.driveSpace = RockProviderWeaponPartDriveSpaceV1::SourceParentLocal;
drive.weaponGenerationKey = magazine->weaponGenerationKey;
drive.bodyId = magazine->bodyId;
drive.groupId = 1;
drive.priority = 100;
drive.leaseFrames = 3;
drive.targetTransform = computeMagazinePose(*magazine, client.reloadProgress);
RockProviderApi::inst->setWeaponPartDriveTargetsV1(
client.ownerToken, &drive, 1);
}

Production code should inspect every returned RockProviderResultV1, stop refreshing on generation changes, and explicitly clear targets/drives when the reload session closes.

Common failure modes

SymptomLikely cause
InvalidArgument from a driveSemantic matcher used instead of body/root/name; zero lease; or non-finite transform.
A normal grip stops workingAn exclusive target activated whitelist behavior. Use NonExclusive for additive controls.
StaleGenerationThe weapon changed. Re-discover parts before publishing again.
WrongThreadA live pose/result copy was made outside an owner callback.
Part moves but hand does not followPublish an AttachOnly target as well as the drive, and honor the grip readback.
Intermittent matching by pointerA retained sourceRoot crossed a frame or weapon-generation boundary.