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
- Read weapon evidence or the live part-pose snapshot.
- Match a part by stable value identity.
- Publish grab targets if the part should be interactable.
- Publish a short drive lease if your addon owns its motion.
- Read resolution and application results.
- 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:
bodyIdandweaponGenerationKey;- semantic
partKindplus reload, support, socket, and action roles; probeDistanceGame;- non-owning
interactionRootandsourceRootidentity 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:
| Flag | Compared field |
|---|---|
MatchBodyId | bodyId |
MatchSourceRoot | transient sourceRoot identity |
MatchSourceName | exact, non-empty sourceName |
MatchPartKind | partKind |
MatchReloadRole | reloadRole |
MatchSupportRole | supportRole |
MatchSocketRole | socketRole |
MatchActionRole | actionRole |
weaponGenerationKey == 0 is a wildcard. A nonzero key prevents a stale
publication from applying to the next equipped weapon generation.
Grab modes:
| Mode | Behavior |
|---|---|
FullTwoHandAuthority | The matched part can participate in ROCK's full physical/two-hand solution. |
AttachOnly | The 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;WeaponRootLocalorSourceParentLocaldrive space;- an optional generation guard;
- optional
groupIdandpriorityfor 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:
| Kind | Meaning |
|---|---|
None | The hand has no current weapon grip. |
FiringGrip | The hand owns the firing grip and the weapon rides that hand. |
SupportFullAuthority | Offhand support participates in the two-hand solver. |
SupportVisualOnly | Visual support, such as a sidearm shooting cup. |
PartCarry | A carried part owns weapon motion while the firing hand is detached. |
AttachOnly | The 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.
| Result | Interpretation |
|---|---|
Applied | ROCK resolved and applied the requested local transform. |
Unresolved | No concrete part matched. |
StaleGeneration | The drive was guarded for another weapon generation. |
MissingParent | The selected local space had no usable parent. |
LostPriority | Another applicable drive won arbitration. |
InvalidTransform | The transform was rejected as unusable. |
CapacityRejected | The runtime could not retain/apply the row within bounds. |
Restored | The 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
| Symptom | Likely cause |
|---|---|
InvalidArgument from a drive | Semantic matcher used instead of body/root/name; zero lease; or non-finite transform. |
| A normal grip stops working | An exclusive target activated whitelist behavior. Use NonExclusive for additive controls. |
StaleGeneration | The weapon changed. Re-discover parts before publishing again. |
WrongThread | A live pose/result copy was made outside an owner callback. |
| Part moves but hand does not follow | Publish an AttachOnly target as well as the drive, and honor the grip readback. |
| Intermittent matching by pointer | A retained sourceRoot crossed a frame or weapon-generation boundary. |