START HERE·PRACTICE TEMPLATES·Verified June 2026 · Lua 5.4 · ox_lib 3.x
Learning with an AI assistant?
Copies this whole lesson - every step, code block, and the exact console errors - plus 2026 ground rules (no lua54 'yes', Cfx.re Portal, correct callback signatures) as a ready-to-paste mentor prompt.
Start Here · Practice Templates

Player customization (advanced)

This is not a tutorial you copy. It is a half-finished resource called qu_customize with the part that actually changes the player left out. The command is registered and a config of named outfits is sitting there ready to use, but nothing is applied to the ped yet. Type /customize suit right now and nothing happens. Your job is to write the missing logic that takes a named outfit from the config and puts it on the player's body, piece by piece. A working solution is one click away when you want to check yourself.

Goal
A /customize command that changes a player's clothing and look on their ped, driven by a config of outfits.
Difficulty
Advanced
Estimated time
~30 minutes
Skills practiced
client natives, ped components, commands, config tables, client/server split
BEFORE YOU START

The goal

You are finishing a small appearance resource named qu_customize. It has two moving parts:

  • A config table, Config.Outfits, that maps an outfit name like suit or hoodie to a set of clothing pieces. Each piece names a body slot and which model and texture to put there.
  • A client script that registers a /customize <outfit> command. When you run it, it should look the outfit up and dress the local player in it.

When it is done, this is the exact behaviour you want:

  • Type /customize suit in chat and your character is wearing the suit outfit in game, on screen, right away.
  • Type /customize hoodie and the look swaps to the hoodie outfit.
  • Type a name that is not in the config, like /customize banana, and nothing changes. The command rejects it cleanly instead of crashing.

Vocabulary

Ped
Short for pedestrian. The character model standing in the world. Your player is a ped, and PlayerPedId() hands you the one your camera is attached to right now.
Component
One clothing slot on a ped, identified by a number. Slot 11 is the torso or jacket, 4 is the legs, 6 is the shoes, and so on. A full outfit is just a few of these slots set together.
Drawable
Which model is shown in a component slot. Drawable 0 in the legs slot might be jeans, drawable 4 might be shorts. It is the index of the piece of clothing, picked per slot.
Texture
Which colour or variant of a drawable is shown. The same jacket drawable can come in several textures, so texture 0 is one colour and texture 2 is another.

What you start with

The resource ships as a scaffold across three files. The config is complete and given to you, so you can focus on the one piece of logic that matters: turning a named outfit into native calls that dress the ped. One file has a gap in it, marked with a TODO, and that gap is your job. Read every file before you touch anything.

The manifest. Note there is no lua54 line: Lua 5.4 is the only runtime now, so that old setting is gone. Everything here runs on the client, because changing how a ped looks is a client-side job.

lua
-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
 
shared_script 'config.lua'
client_script 'client.lua'

The config. Each outfit is a list of clothing pieces. A piece is a small table with three numbers: component is the body slot, drawable is which model goes in that slot, and texture is which colour of that model. You do not need to memorise the numbers, you read them off this table when you apply the outfit.

lua
-- config.lua
Config = {}
 
-- component: the body slot (11 = torso/jacket, 4 = legs, 6 = shoes, 8 = undershirt)
-- drawable:  which model goes in that slot
-- texture:   which colour or variant of that model
Config.Outfits = {
    suit = {
        { component = 11, drawable = 4,  texture = 0 }, -- jacket
        { component = 8,  drawable = 15, texture = 0 }, -- undershirt
        { component = 4,  drawable = 10, texture = 0 }, -- trousers
        { component = 6,  drawable = 10, texture = 0 }, -- dress shoes
    },
 
    hoodie = {
        { component = 11, drawable = 7,  texture = 2 }, -- hoodie
        { component = 8,  drawable = 1,  texture = 0 }, -- t-shirt under it
        { component = 4,  drawable = 1,  texture = 0 }, -- jeans
        { component = 6,  drawable = 4,  texture = 0 }, -- sneakers
    },
 
    tracksuit = {
        { component = 11, drawable = 15, texture = 0 }, -- tracksuit top
        { component = 8,  drawable = 15, texture = 0 }, -- no undershirt shown
        { component = 4,  drawable = 5,  texture = 0 }, -- joggers
        { component = 6,  drawable = 4,  texture = 0 }, -- trainers
    },
}

The client. This is your gap. The /customize command is registered and it already pulls the outfit name out of what you typed, but the line that would actually dress the ped is missing. The -- TODO block marks exactly where your logic goes.

lua
-- client.lua
 
-- Applies one named outfit from Config.Outfits to the local player's ped.
local function applyOutfit(outfitName)
    local outfit = Config.Outfits[outfitName]
 
    -- TODO 1: reject unknown outfits.
    -- If outfit is nil, the name is not in Config.Outfits. Tell the player
    -- and return early so the rest of the function never runs on bad input.
 
    -- TODO 2: get the local player's ped.
    -- PlayerPedId() returns the ped id for THIS player on THIS client.
 
    -- TODO 3: apply each clothing piece.
    -- Loop the outfit's pieces and, for each one, call SetPedComponentVariation
    -- with the ped, the piece's component, drawable, and texture.
    -- The native signature is:
    --   SetPedComponentVariation(ped, componentId, drawableId, textureId, paletteId)
    -- paletteId is almost always 0 for clothing.
end
 
-- false = any player may run it from chat. This only changes how YOUR OWN
-- character looks on YOUR OWN screen, so it is safe to leave open here.
RegisterCommand('customize', function(_, args)
    local outfitName = args[1] -- e.g. "suit"
    if not outfitName then
        print('[qu_customize] usage: /customize <outfit>')
        return
    end
 
    applyOutfit(outfitName)
end, false)

Your job

Fill in the three TODO gaps inside applyOutfit so a named outfit actually lands on the ped. Work through this checklist:

Checklist

  • TODO 1, reject unknown names first. Read Config.Outfits[outfitName] into outfit. If it is nil, the outfit does not exist, so print a short message and return before anything else runs. Validating the input before acting on it is the same habit you used in the shop template.
  • TODO 2, get the local player ped. Call PlayerPedId() and store it. This is the ped on this client, the one the camera follows. You pass it as the first argument to every appearance native.
  • TODO 3, apply each piece in a loop. The outfit is a list of pieces, so walk it with ipairs. For each piece, call SetPedComponentVariation(ped, piece.component, piece.drawable, piece.texture, 0). The four numbers come straight off the config row, and the last argument, the palette, is 0 for ordinary clothing.
  • Keep it client-side. Do not reach for the server here. There is no event to trigger and no money to charge. The whole feature is local natives changing a local ped.

This stays a client-only resource on purpose. If you wanted the chosen outfit to still be on the player after they disconnect and rejoin, you would add a server script and a database (oxmysql) to store the outfit name per player and reapply it on spawn. That persistence layer is out of scope here and is taught in Track B.

Hints

If you are stuck, read these in order
  • PlayerPedId() is the local player. It takes no arguments and returns the ped id for the character you are currently controlling on this client. Call it fresh each time you dress the ped, because the id can change after a respawn.
  • Component ids are fixed slots on every ped: 11 is the torso or jacket, 8 is the undershirt, 4 is the legs, 6 is the shoes. A full outfit is just a handful of these slots set together, which is exactly what each entry in Config.Outfits is.
  • Loop the outfit with ipairs, since it is a numbered list of pieces: for _, piece in ipairs(outfit) do ... end. Inside the loop, piece.component, piece.drawable, and piece.texture are the three values from the config row.
  • The native order is SetPedComponentVariation(ped, componentId, drawableId, textureId, paletteId). Mixing up drawable and texture is the classic mistake: drawable is which item, texture is which colour of that item. The palette is the fifth argument and is 0 for clothing.
  • Reject unknown outfit names early. Config.Outfits['banana'] is nil, so a single if not outfit then ... return end at the top keeps the loop from ever running on a name that does not exist.

Reveal the solution

Stuck or done? Reveal a working solution

Here is the finished resource. The config is unchanged from what you started with, and the client now validates the name, grabs the local ped, and applies every piece.

The config, for reference. It is the single source of truth for what each outfit is made of.

lua
-- config.lua
Config = {}
 
-- component: the body slot (11 = torso/jacket, 4 = legs, 6 = shoes, 8 = undershirt)
-- drawable:  which model goes in that slot
-- texture:   which colour or variant of that model
Config.Outfits = {
    suit = {
        { component = 11, drawable = 4,  texture = 0 }, -- jacket
        { component = 8,  drawable = 15, texture = 0 }, -- undershirt
        { component = 4,  drawable = 10, texture = 0 }, -- trousers
        { component = 6,  drawable = 10, texture = 0 }, -- dress shoes
    },
 
    hoodie = {
        { component = 11, drawable = 7,  texture = 2 }, -- hoodie
        { component = 8,  drawable = 1,  texture = 0 }, -- t-shirt under it
        { component = 4,  drawable = 1,  texture = 0 }, -- jeans
        { component = 6,  drawable = 4,  texture = 0 }, -- sneakers
    },
 
    tracksuit = {
        { component = 11, drawable = 15, texture = 0 }, -- tracksuit top
        { component = 8,  drawable = 15, texture = 0 }, -- no undershirt shown
        { component = 4,  drawable = 5,  texture = 0 }, -- joggers
        { component = 6,  drawable = 4,  texture = 0 }, -- trainers
    },
}

The client, completed. Every gap is filled, with the validation up top and the apply loop at the bottom.

lua
-- client.lua
 
-- Applies one named outfit from Config.Outfits to the local player's ped.
local function applyOutfit(outfitName)
    local outfit = Config.Outfits[outfitName]
 
    -- TODO 1 done: reject unknown outfits before touching the ped.
    if not outfit then
        print(('[qu_customize] unknown outfit "%s"'):format(tostring(outfitName)))
        return
    end
 
    -- TODO 2 done: the ped for THIS player on THIS client.
    local ped = PlayerPedId()
 
    -- TODO 3 done: apply each clothing piece in order.
    for _, piece in ipairs(outfit) do
        SetPedComponentVariation(
            ped,             -- the local player's ped
            piece.component, -- which body slot (11 jacket, 4 legs, 6 shoes, 8 undershirt)
            piece.drawable,  -- which model goes in that slot
            piece.texture,   -- which colour of that model
            0                -- palette, 0 for ordinary clothing
        )
    end
 
    print(('[qu_customize] applied outfit "%s"'):format(outfitName))
end
 
-- false = any player may run it from chat. This only changes how YOUR OWN
-- character looks on YOUR OWN screen, so it is safe to leave open here.
RegisterCommand('customize', function(_, args)
    local outfitName = args[1] -- e.g. "suit"
    if not outfitName then
        print('[qu_customize] usage: /customize <outfit>')
        return
    end
 
    applyOutfit(outfitName)
end, false)

Why it is built this way:

  • Appearance is client-side. PlayerPedId() and SetPedComponentVariation only exist on the client, because the ped is rendered on this player's machine. The change is instant and local: the moment the loop runs, the clothes are on screen. No server round trip happens, and none is needed for the look itself.
  • Component, drawable, texture map to clothing. A ped has a fixed set of numbered slots called components. Slot 11 is the torso or jacket, 8 the undershirt, 4 the legs, 6 the shoes. For each slot you choose a drawable, which is the item itself, and a texture, which is the colour of that item. One call to SetPedComponentVariation sets one slot, so a four-piece outfit is four calls, which is exactly what the loop does.
  • The config is the single source of truth. Every outfit is just data in Config.Outfits. Adding a new look means adding a new entry there, with zero changes to client.lua. The logic stays the same no matter how many outfits you define, which is the whole point of driving behaviour from a config table.
  • Validation before action. The if not outfit then return end guard runs before PlayerPedId() is ever called, so a typo like /customize banana exits cleanly instead of looping over nil. This is the same return-early habit you used to harden the shop.

What you practiced

  • Read an outfit out of a Config table and rejected unknown names with a return-early guard.
  • Got the local player's ped with PlayerPedId() and understood it is per-client.
  • Set one clothing slot at a time with SetPedComponentVariation(ped, component, drawable, texture, palette).
  • Looped a list of clothing pieces with ipairs so one outfit is just a few native calls.
  • Knew why appearance code lives entirely on the client and never touches the server.
  • Drove the whole feature from data, so a new outfit is a new config entry, not new logic.

Nicely done. You just wrote the core of every clothing menu, character creator, and job-uniform system in FiveM: read a look from data, find the local ped, and set its components one slot at a time. What you did not do here is make the look stick. Right now the outfit is gone the moment the player reconnects, because nothing was saved. Making a look survive across sessions is a server plus oxmysql job: store the chosen outfit per player in a database and reapply it on spawn. That is exactly the kind of script you build in Track B. You are ready for Track B. Go open it and start building real scripts.