Chapter 02 · The Grand Archive
FiveM Lua Scripting
Turn a prop file into a wearable, sellable item.
— Built to Shine —
Architecture
The jewelry resource has three responsibilities:
- Catalog — a shared table describing every piece (model, bone, offset, price).
- Inventory — items players can own (usually via
ox_inventory/qb-inventory). - Wear system — client-side attachment of the prop to the player's bone, synced to nearby players.
Shared catalog
Every piece lives in one shared file so client and server agree on the metadata.
lua
-- shared/catalog.lua
Catalog = {
['chain_cuban_gold'] = {
label = 'Cuban Link Chain — Gold',
model = 'jewel_chain_cuban_gold',
bone = 39317, -- SKEL_Neck_1
pos = vec3(0.0, 0.08, 0.0),
rot = vec3(0.0, 0.0, 0.0),
slot = 'neck',
price = 25000,
},
['ring_signet'] = {
label = 'Signet Ring',
model = 'jewel_ring_signet',
bone = 26610, -- BONETAG_L_FINGER1
pos = vec3(0.0, 0.0, 0.0),
rot = vec3(0.0, 0.0, 0.0),
slot = 'finger_l',
price = 8000,
},
['watch_ap_gold'] = {
label = 'AP Royal Oak — Gold',
model = 'jewel_watch_ap_gold',
bone = 60309, -- SKEL_L_Hand
pos = vec3(0.02, 0.0, 0.0),
rot = vec3(0.0, 90.0, 0.0),
slot = 'wrist_l',
price = 120000,
},
}
ox_inventory item registration
Register each catalog entry as an inventory item so it can be traded, dropped and stored.
lua
-- ox_inventory/data/items.lua (partial)
['chain_cuban_gold'] = {
label = 'Cuban Link Chain — Gold',
weight = 200,
stack = false,
close = true,
client = {
image = 'chain_cuban_gold.png',
event = 'jewelry:client:equip',
slot = 'neck',
},
},
Client-side wear system
Each player tracks what they're wearing in a local table. Attaching is CreateObject + AttachEntityToEntity. State replicates via a state bag so other players see the prop.
lua
-- client/attach.lua
local worn = {} -- [slot] = objectHandle
local function equip(itemName)
local piece = Catalog[itemName]
if not piece then return end
-- Remove anything already in this slot
if worn[piece.slot] then
DeleteObject(worn[piece.slot])
worn[piece.slot] = nil
end
local model = joaat(piece.model)
lib.requestModel(model, 5000)
local ped = PlayerPedId()
local obj = CreateObject(model, 0.0, 0.0, 0.0, true, true, false)
AttachEntityToEntity(
obj, ped, GetPedBoneIndex(ped, piece.bone),
piece.pos.x, piece.pos.y, piece.pos.z,
piece.rot.x, piece.rot.y, piece.rot.z,
true, true, false, true, 1, true
)
worn[piece.slot] = obj
SetModelAsNoLongerNeeded(model)
-- Sync to other players via state bag
LocalPlayer.state:set('jewelry', worn_to_state(), true)
end
RegisterNetEvent('jewelry:client:equip', function(data)
equip(data.name)
end)
RegisterNetEvent('jewelry:client:unequip', function(slot)
if worn[slot] then
DeleteObject(worn[slot])
worn[slot] = nil
LocalPlayer.state:set('jewelry', worn_to_state(), true)
end
end)
-- Rebuild worn pieces on nearby players
AddStateBagChangeHandler('jewelry', nil, function(bagName, _, value)
local ply = GetPlayerFromStateBagName(bagName)
if ply == 0 or ply == PlayerId() then return end
rebuild_remote(ply, value)
end)
-- Clean up on death / respawn
AddEventHandler('playerSpawned', function()
for slot, _ in pairs(worn) do worn[slot] = nil end
-- re-equip from server-authoritative state here
end)
Server-side validation
lua
-- server/main.lua
local ox = exports.ox_inventory
RegisterNetEvent('jewelry:server:equip', function(itemName)
local src = source
local piece = Catalog[itemName]
if not piece then return end
-- Player must actually own the item
local count = ox:GetItemCount(src, itemName)
if count < 1 then
return -- silently reject; log if you care
end
TriggerClientEvent('jewelry:client:equip', src, { name = itemName })
end)
RegisterNetEvent('jewelry:server:unequip', function(slot)
TriggerClientEvent('jewelry:client:unequip', source, slot)
end)
Framework variants
- ESX — replace
ox:GetItemCountwithxPlayer.getInventoryItem(name).count. - QBCore —
Player.Functions.GetItemByName(name). - QBox — same as QBCore but the export is
exports.qbx_core. - Standalone — persist wear state in a KVP or database table keyed by license identifier.

