Skip to content

lumo-inventory — back items ​

Weapons and gear slung on a player's back. Lives in client/backitems.lua, configured under Config.BackItems in config/config.lua.

What changed and why ​

1. Guns are weapon objects, not props ​

The old renderer spawned CreateObject('w_ar_carbinerifle'). A prop of a weapon model has no component slots and no tint palette, so a scope, a suppressor, a grip, an extended clip and a tint index all had nowhere to go — the back always showed the stock, bare gun no matter what the player was actually carrying.

Weapons are now built with CreateWeaponObject, which accepts GiveWeaponComponentToWeaponObject and SetWeaponObjectTintIndex. Both are read straight off the item's info.attachments / info.tint, so the gun on the back is the same gun in the inventory. This is the same mechanism the in-inventory weapon preview already used.

Non-weapon gear (armour, parachute, drill, radio) stays on ordinary props — there is nothing a weapon object would add.

2. Entities are local, driven by a statebag ​

Previously every player spawned networked props (CreateObject(..., true, true, false)). That put every back item on the shared entity budget, replicated it to the whole server, and left orphaned "ghost guns" behind whenever a player crashed or disconnected mid-frame — the standard failure mode of prop-based back-item scripts.

Now each client publishes a small description of its own loadout to a replicated player statebag (lumo_backitems), and every client draws its own local copies for the players streamed in around it. Result: zero networked entities, zero entity replication traffic, and ghost props are structurally impossible.

The published payload is deliberately tiny — item name, slot, component hashes, tint index — a few dozen bytes per player, sent only when the loadout actually changes.

Incoming payloads are sanitised before rendering: the entry count is clamped to maxDisplayed, names must be in the server's own allow-list, slots must exist, component lists are capped and the tint is range-checked. A player owns their own statebag, so for everyone else it is untrusted input.

3. Nothing polls inventories any more ​

The old loop rebuilt the whole loadout — a full scan of PlayerData.items, a sort, and a signature string — every 700 ms, forever, whether or not anything had changed, and QBCore:Player:SetPlayerData (which fires on every money, job and metadata change) forced an immediate extra rebuild on top of that.

Rebuilds are now event-driven and debounced (refreshDebounce, 250 ms), so a burst of inventory writes costs one build. The only remaining poll is a 1500 ms sweep that attaches and detaches as players stream in and out; it never touches an inventory.

4. Placement: the offsets were on the wrong axis ​

A spine bone's local axes are not world axes. On SKEL_Spine1 (24816):

AxisDirection
offset.xalong the spine, + up towards the head
offset.ydepth, − out behind the back
offset.zacross the back, ± to either side

The old config separated the two weapon slots with x = +0.19 and x = -0.19, expecting a side-by-side pair. X is the spine axis, so what it actually did was put one gun 38 cm higher up the spine than the other — one stacked on top of the other, the upper one punching through the head. Two guns are separated on Z.

The placement was also standing the weapon upright (rotation = 0,0,0) on SKEL_Spine3 (24818). Every established weapons-on-back script instead uses SKEL_Spine1 (24816) — low enough that upper-body animation does not swing the gun around — lifted ~0.22 up the spine, ~0.18 out behind the back, and rotated ~170° on Y so the weapon hangs muzzle-down along the back:

ScriptBoneOffsetRotation
TGIANN weapons-on-back248160.285, -0.17, 0.13 / 0.0 / -0.130, 170, 0
qb-weaponsonback248160.2275, -0.16, 0.0160, 0, 1
OneWithoutAName weaponsonback248160.15, -0.18, -0.020, 165, 0

Note that TGIANN's three back slots differ only in Z (+0.13 / 0.0 / −0.13), which is the confirmation that Z is the side-to-side axis. And OneWithoutAName special-cases melee to x = -0.40 — a bat is carried low on the back — which is the confirmation that X runs along the spine.

The shipped slots follow that consensus: 24816, (0.22, -0.175, ±0.105), (0, 170, 0), with the second slot 2 cm deeper on Y so the two never intersect.

On top of that, each weapon gets a size class correction (Config.BackItems.Classes), resolved automatically from GetWeapontypeGroup. A micro SMG and a heavy sniper differ by half a metre in length; one slot transform cannot suit both, so the corrections mostly ride on X — a longer gun is carried a little higher up the spine. Melee is the large exception, since a bat is slung low and diagonally rather than hung vertically; its correction reproduces OneWithoutAName's melee posture. Override the automatic classification per weapon in ClassOf.

Resolution order for a weapon's final transform:

Weapons[name].offset/rotation      -- if set, replaces the slot outright
  else Slots[n].offset/rotation  +  Classes[class].offset/rotation

Does showing tints and attachments cost performance? ​

No, and this is worth being precise about.

An attached entity costs the script nothing per frame — AttachEntityToEntity is handled engine-side, there is no Lua running to maintain it. Components and the tint are applied exactly once, at spawn. So a fully kitted, tinted rifle on a back is not measurably more expensive to keep there than a bare prop; the difference is a handful of natives at creation time and some extra geometry for the GPU to draw.

What actually costs is rebuild churn — respawning entities on every inventory event, and polling loops that rebuild state nobody asked for. That is what points 2 and 3 above address, and it is where every "back items are killing my server" thread on the forums ends up.

The one real cost of weapon objects over props is streaming: RequestWeaponAsset pulls more than a single prop model. That is mitigated by never releasing weapon assets while the resource runs — the same guns reappear constantly on a populated server, so keeping them resident avoids a stream hitch every time somebody holsters. They are freed on resource stop.

Tuning the placement ​

Bone-space offsets cannot be worked out on paper: they depend on the ped model, the bone and each weapon's own pivot. The shipped values are a starting point.

/backitemtune 1
KeyAction
LEFT / RIGHTswitch axis (offset x/y/z, rotation x/y/z)
UP / DOWNadjust the selected axis
LEFT ALTcoarse steps
ENTERprint the config line to the F8 console
BACKSPACEclose

The readout names what each axis actually moves — up the spine, out from the back, across the back — so you are not guessing at the bone's local axes.

Stand with the weapon on your back, orbit the camera, nudge until it sits right, press ENTER and paste the printed line into Config.BackItems.Slots (and the matching MaleSlots / FemaleSlots).

The tuner edits the slot baseline, not the finished transform — the class correction still stacks on top — so a slot tuned with a carbine stays correct for the sniper that lands in it later. The on-screen readout names the weapon currently in the slot and its class. Everything about the tuner is local and visual; it changes nothing on the server and nothing other players see. Set Config.BackItems.tuner = false to disable the command in production.

Config reference ​

KeyDefaultMeaning
enabledtrueMaster switch
renderDistance30.0Metres; back items are not readable past this
maxRenderedPlayers12Hard cap on simultaneously drawn players, nearest first
streamInterval1500ms between attach/detach sweeps
publishInterval500ms between checks of our own weapon / vehicle / death state
refreshDebounce250ms to coalesce a burst of inventory updates
tunertrueEnables /backitemtune
hideInVehicletrueClears the back while seated
maxDisplayed / maxWeapons / maxItems2 / 2 / 0Weapons-only back by default
weaponSlots / itemSlots{1,2} / {3,4}Which slots each kind may fill
Slots / MaleSlots / FemaleSlots—Bone + offset + rotation per slot
Classes—Size corrections added on top of the slot transform
ClassOf—Per-weapon override of the automatic size class
Priority—Higher wins the prime upper-back slots
Weapons—Allow-list of weapons that may appear, with optional slot / offset / rotation
Items—Non-weapon gear, with the prop model to use

Weapons[name].model is no longer used — the gun is built from the weapon hash so it keeps its components and tint. The field is kept as documentation of what the entry represents.

Known limitation ​

Variant-model finishes (luxury / lowrider / goon, i.e. Config.TintOverridingAttachments in qb-weapons) do not swap the model on a weapon object the way they do on a ped, so such a weapon renders as the base model on the back. Its tint is deliberately skipped in that case rather than painting a base model a colour the ped does not show. This matches the behaviour of the in-inventory weapon preview.

Refreshing from another resource ​

lua
TriggerEvent('qb-inventory:client:refreshBackItems')

Rebuilds and republishes immediately. Not usually needed — inventory updates, QBCore:Player:SetPlayerData and weapon equip/unequip are already hooked.