Skip to main content

Weapon 1.1: inventory equip and retained switching

Weapon 1.1 adds four functions after the unchanged Weapon 1.0 table. Include ROCK/WeaponV1_1.h, use rock::api::weapon::v1_1::Api, and link RPS::ROCKWeapon. Client::acquire(3, table) negotiates exact major 1, minimum minor 1, the complete new table size, and Weapon Read + Write. ROCK/Weapon.h remains the released 1.0 header. Existing consumers need no rebuild or migration. Core and the other providers keep their contracts. This extension is implemented in current source; a downloaded ROCK build must still pass minor/table-size negotiation. Virtual Holsters or another consumer must adopt these calls to use them.

Behavior​

Call only after the consumer accepts an explicit draw gesture. Hand::Right and Hand::Left select physical hands regardless of the configured dominant hand. ROCK rejects an occupied destination. If the equipped weapon is carried only by the other hand, ROCK converts that exact outgoing weapon to a retained loose grab in its current hand using the existing Toggle Drop release controls, then equips the incoming inventory instance in the requested hand. This retention is an explicit contract of this new request; it does not change the consumer setting or the existing trigger-equip path. A sheathed, uncarried weapon may be replaced normally. This is one equipped firearm plus a physically held weapon.

Left firing requires ROCK's existing FRIK pose, node-ownership and parent-hand capabilities, and the matching authored grip readiness. Failure never chooses the opposite hand. The consumer must not run its own native equip concurrently.

The retained switch in both directions​

Draw gestureIncoming equipped weaponPrevious carried weapon
Right-hand holster drawSelected 10mm equips into the right hand.The FAL carried by the left hand becomes a retained loose grab in that same hand.
Left-hand holster drawSelected 10mm equips into the left hand.The FAL carried by the right hand becomes a retained loose grab in that same hand.

The retained FAL uses the existing double-drop / Toggle Drop sequence: the original gesture does not immediately drop it; a fresh grab-and-release gesture releases it. The incoming pistol becomes the equipped firing weapon. This operation does not create two simultaneously equipped firearms.

Entry points​

FunctionPermissionMeaning
captureInventoryWeaponReadCapture an exact native inventory stack by base form and stack index. Reject equipped stacks and throwable weapons.
requestInventoryEquipWriteSubmit the captured stack and destination hand; return RequestQueued plus a command ID on admission.
getInventoryEquipResultReadRead owner-scoped progress and terminal outcome.
cancelInventoryEquipWriteCancel before native commitment; return AlreadyCommitted after outgoing removal or native equip.

All additions require the ROCK frame thread: call in registered Core frame or animation-phase callbacks, outside synchronous Grab callbacks. Capture and request in the same game frame. Resolve the native stack from the consumer's existing exact item selection; a base form alone does not distinguish modified copies. Never save or dereference the opaque stack/instance keys. ROCK validates the capture and retains the exact stack while the switch is pending; stack-index changes after removing another item cannot select a different copy.

One switch may be outstanding because the game has one equipped weapon slot. Another request returns Busy. Queued means no item mutation has committed; Switching means the shared transfer owns the operation. Succeeded requires the incoming firing grip and presentation, plus outgoing reattachment when retention was needed. Failed and Cancelled are terminal too, but do not imply that neither item moved. Flags retain the independently observed outgoing removal/retention and incoming equip/grip/presentation facts. Reconcile actual inventory and Grab hand snapshots before retrying or changing holster state. Do not treat a historical result flag as proof that the player still holds an item. The latest 64 terminal results are retained across owners within this provider instance; an evicted or foreign command returns RequestNotFound.

Before commitment, cancellation or owner loss retires the request without equip. After commitment, ROCK owns completion and recovery even if the consumer stops; it keeps no consumer callback or engine pointer supplied by the consumer. On world/skeleton/provider loss the request is cancelled and stale native work is not resumed in the next world. Provider destruction discards its result history; NotReady or RequestNotFound after lifecycle loss requires fresh snapshots, not an automatic replay. The consumer releases its local command tracking before closing its Core registration. No new permissions are granted by discovery.

Result states and partial outcomes​

Queued and Switching are pending states. Succeeded, Failed and Cancelled are terminal. Poll getInventoryEquipResult with the original owner and command ID; do not infer completion from requestInventoryEquip returning RequestQueued.

Result flagObserved milestone
OutgoingRemovedThe previous equipped instance was removed for its physical handoff.
OutgoingRetainedIts retained grab was successfully acquired.
IncomingEquipAcceptedNative equip and exact incoming stack validation succeeded.
IncomingGripAcquiredThe requested physical firing hand acquired the incoming weapon.
IncomingPresentedIncoming grip presentation completed.

A failed outgoing reattachment can coexist with an equipped incoming weapon. Inspect these flags and fresh inventory/hand state before retrying. Do not automatically equip again, spawn a replacement, or release the outgoing grab. A later manual equip does not erase the earlier inventory command's terminal outcome.

Right and left consumer code​

InventoryWeaponEquip.h is the reusable, compiled helper; InventoryWeaponEquip.cpp shows both draw entry points. It stays inert until the caller supplies a gesture and exact inventory stack.

#include "InventoryWeaponEquip.h" // Copy the linked SDK example helper.

rock::sdk::example::InventoryWeaponEquip equip;
// client is an already connected rock::api::Client.
const auto connected = equip.connect(client); // explicitly requires Weapon 1.1
// Continue only when connected == rock::api::Status::Ok.

// Invoke ONE in the ROCK callback after your accepted draw gesture:
const auto admitted = drawWasLeftHand
? equip.drawLeft(weaponBaseFormId, exactStackIndex)
: equip.drawRight(weaponBaseFormId, exactStackIndex);

// RequestQueued means accepted; do not run another native equip.
// On later callbacks, while equip.pending():
rock::api::weapon::v1_1::EquipResult result{};
const auto status = equip.poll(result);
// On Ok + Succeeded, confirm the draw in your holster state.
// On Failed/Cancelled, inspect flags and reconcile before another request.
// On activity/lifecycle shutdown, call equip.stop() before client.close().

An older ROCK provider reports UnsupportedMinor; do not access the added slots. The old v1 prefix remains callable with its original permissions and behavior. The old Weapon transition snapshot reports this new operation with the existing Unknown source value; use the new owner-scoped result to identify it.

Compatibility and validation​

The extension table contains the unchanged 16-slot weapon::ApiV1 as its first member, v1, followed by four added slots. On Windows x64 the old table is 128 bytes and the extended table is 160 bytes. No old record, permission, enum meaning or Core requirement changes. Existing 1.0 consumers keep their original headers and table-size request; only consumers using the additions need 1.1. The old handling lease remains a separate operation and is not required to be reimplemented by a holster mod.

The runtime and SDK builds, ABI/negotiation checks and both hand example tests passed for this implementation. In-game holster switching is not yet qualified. After integrating the calls, test both tabled directions, the retained weapon's fresh release gesture, a busy drawing hand, and two modified copies of the same base weapon. Verify the selected instance is equipped without duplication or loss.

Complete public declarations​

Complete public declarations: ROCK/WeaponV1_1.h
ROCK/WeaponV1_1.h
#pragma once
#include "Weapon.h"
#include <cstddef>
#include <type_traits>

#pragma pack(push, 8)
namespace rock::api::weapon::v1_1 {
inline constexpr InterfaceId kInterfaceId = weapon::kInterfaceId;
inline constexpr std::uint32_t kMajor = 1;
inline constexpr std::uint32_t kMinor = 1;

// Capture on the ROCK frame thread immediately before requesting equip.
// Keys are opaque comparison witnesses, never pointers to dereference or
// identities to save. stackIndex is the native inventory stack index.
struct InventoryWeapon {
std::uint32_t size{sizeof(InventoryWeapon)};
std::uint32_t baseFormId{};
std::uint32_t stackIndex{};
std::uint32_t count{};
std::uint64_t stackKey{};
std::uint64_t instanceKey{};
std::uint64_t frameIndex{};
std::uint32_t worldGeneration{};
std::uint32_t skeletonGeneration{};
std::uint32_t providerGeneration{};
std::uint32_t reserved{};
};

struct EquipRequest {
std::uint32_t size{sizeof(EquipRequest)};
Hand hand{Hand::None};
InventoryWeapon item{};
};

enum class EquipState : std::uint32_t {
Unknown, Queued, Switching, Succeeded, Failed, Cancelled
};
enum class EquipFailure : std::uint32_t {
None, LifecycleLost, OwnerLost, HandUnavailable, HandBusy,
InventoryChanged, InfrastructureUnavailable, OutgoingTransferFailed,
NativeEquipFailed, GripOrPresentationFailed, CancelledBeforeCommit,
EquippedWeaponChanged, MenuBlocked
};
enum class EquipResultFlag : std::uint32_t {
OutgoingRemoved = 1u << 0,
OutgoingRetained = 1u << 1,
IncomingEquipAccepted = 1u << 2,
IncomingGripAcquired = 1u << 3,
IncomingPresented = 1u << 4
};
struct EquipResult {
std::uint32_t size{sizeof(EquipResult)};
EquipState state{EquipState::Unknown};
std::uint64_t commandId{};
EquipFailure failure{EquipFailure::None};
std::uint32_t flags{};
Hand hand{Hand::None};
std::uint32_t incomingFormId{};
std::uint32_t outgoingFormId{};
std::uint32_t outgoingReferenceFormId{};
std::uint64_t transferSequence{};
std::uint32_t worldGeneration{};
std::uint32_t skeletonGeneration{};
std::uint32_t providerGeneration{};
std::uint32_t reserved{};
};

// Negotiate major 1, minimum minor 1 and sizeof(Api). Bind Weapon Read |
// Write. All four additions are ROCK frame-thread calls, outside synchronous
// Grab event callbacks. The original 1.0 table and headers remain unchanged.
struct Api {
static constexpr InterfaceId interfaceId=kInterfaceId;
static constexpr std::uint32_t majorVersion=kMajor;
static constexpr std::uint32_t minorVersion=kMinor;
weapon::ApiV1 v1;
Status(ROCK_CALL* captureInventoryWeapon)(OwnerToken, std::uint32_t baseFormId,
std::uint32_t stackIndex, InventoryWeapon*) noexcept;
// An explicit draw gesture equips into the requested physical hand.
// A weapon carried only by the other hand is retained there using the
// existing Toggle Drop controls. A busy destination rejects the request.
// One switch may be outstanding. RequestQueued is admission, not success.
Status(ROCK_CALL* requestInventoryEquip)(OwnerToken, const EquipRequest*, std::uint64_t*) noexcept;
// Results are owner-scoped; the latest 64 terminal results are retained
// for this provider lifetime. Inspect flags even when the switch fails.
Status(ROCK_CALL* getInventoryEquipResult)(OwnerToken, std::uint64_t, EquipResult*) noexcept;
// Cancellation is accepted only before outgoing removal/native equip.
// Once committed, returns AlreadyCommitted and ROCK finishes cleanup
// even if the requesting owner disappears. No consumer callback is kept.
Status(ROCK_CALL* cancelInventoryEquip)(OwnerToken, std::uint64_t) noexcept;
};
static_assert(std::is_standard_layout_v<Api>);
static_assert(offsetof(Api, v1) == 0);
static_assert(offsetof(Api, captureInventoryWeapon) == sizeof(weapon::ApiV1));
static_assert(sizeof(Api) == sizeof(weapon::ApiV1) + 4 * sizeof(void*));
}
#pragma pack(pop)