Chapter 02 · The Grand Archive

FiveM Lua Scripting

Turn a prop file into a wearable, sellable item.

Built to Shine

VVS DiamondArchitecture

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.

VVS DiamondShared catalog

Every piece lives in one shared file so client and server agree on the metadata.

luaVVS Diamond
-- 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,
    },
}

VVS Diamondox_inventory item registration

Register each catalog entry as an inventory item so it can be traded, dropped and stored.

luaVVS Diamond
-- 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',
    },
},

VVS DiamondClient-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.

luaVVS Diamond
-- 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)

VVS DiamondServer-side validation

luaVVS Diamond
-- 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)

VVS DiamondFramework variants

  • ESX — replace ox:GetItemCount with xPlayer.getInventoryItem(name).count.
  • QBCorePlayer.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.