SandCastle Clothing Inventory

A standalone EUP clothing inventory, outfit system, and admin shop for FiveM. Runs on its own, without needing any framework or inventory resource.

Standalone oxmysql required ox_inventory optional v1.0.0
⚠️

Standalone means standalone. This resource works entirely on its own out of the box: no framework, no other inventory system needed to install and use it.

Making it talk to your framework's economy (ESX, QBCore, etc.) or a specific inventory is optional, and it happens by editing config.lua yourself (see the Money bridge section below). That means writing a bit of Lua. It's not a one-click toggle, and it's not something a first-time user with zero coding experience can safely do. If that's not you, either run it standalone as shipped, or get a developer to wire the bridge up for you.

Inventory, character, and outfit panels with items and an equipped outfit
👕 Custom inventory
Its own database-backed inventory and NUI, so it doesn't require ox_inventory, qb-inventory, or any other item system to function.
🧥 Outfits
Players group owned pieces into named outfits, equip them instantly, and give a whole outfit to another player.
🏪 Admin shop creator
Scan any drawable/slot, name items and textures, organize into categories, assign to shops, set per-shop prices.
🪡 Outfit Creator
Build reusable outfit templates and hand them straight to players, skipping the item pickup step entirely.
🎥 Scene editor
Reposition the preview camera/ped spawn used for shop and creator previews, and save multiple camera presets.
🔌 Config bridges
Money, notifications, and Discord role gating are all optional plug-in points in config.lua, none of it hardwired.

Requirements

ResourceNeeded for
oxmysqlRequired. All inventory, outfit, and shop data.
ox_inventoryOptional, only needed if you enable the ox_inventory example in the money bridge (see below).
ox_libOptional, only needed if you enable the ox_lib example in the notification bridge.

No dependency line is declared in fxmanifest.lua for either. The script starts fine without them; those two integrations only run once you wire them up yourself.

Installation

1

Import sql/install.sql into your database once. It creates all 15 tables the script needs (inventory, outfits, shops, categories, scene config, etc.). Safe to re-run: every table uses CREATE TABLE IF NOT EXISTS.

2

Drop the resource folder into resources/. Add it to server.cfg, making sure oxmysql starts first:

ensure oxmysql
ensure SandCastle_ClothingInventory
exec @SandCastle_ClothingInventory/permissions.cfg
3

Edit permissions.cfg and add your own admin license(s):

add_principal identifier.license:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX group.eup_admin

Find your license by connecting to the server and checking the connection logs, or server-side with print(GetPlayerIdentifierByType(source, 'license')).

4

Open config.lua and discord_config.lua and set them up for your server. Details in the Configuration section below.

5

In-game as an admin, run /eupadmin to create categories, shops, and item listings, or open the Outfit Creator to build reusable outfit templates.

Which files can I edit?

Only two files: everything you can configure lives in one of these two, and they're the only ones you should ever need to open.

Every other .lua file (client, server, shared) is protected, so don't try to edit them. The web/ folder (HTML/CSS/JS behind the NUI) isn't locked and you're free to look through it, but there's nothing in there you need to change.

Adding your own images

The resource ships with exactly one image: web/images/default_clothing.png. It's the fallback shown for any piece that doesn't have its own picture. EUP packs vary too much from server to server to ship images for every possible drawable, so building your own set is on you.

Only photograph components, not textures. The script matches an image by slot and drawable only, it never looks at the texture number. Every texture variant of the same drawable shares one image, so shooting each texture separately is wasted effort. One photo per drawable is all it ever uses.
Male and female need separate photos. The same drawable number is a completely different mesh on mp_m_freemode_01 and mp_f_freemode_01, so an image shot on one model doesn't apply to the other. To catalog female pieces in the Shop Creator or Outfit Creator, use the "Preview as Male / Female" toggle above the EUP Library panel, it spawns a separate preview ped of that gender so you can scan and photograph its drawables without touching your own character.
1

Take clean screenshots of each drawable in-game with fivem-greenscreener (external tool, not included with this resource). It poses the ped and isolates each piece against a clean background for you. Run it once per gender if you want images for both.

2

Name each PNG to match what the script looks for:

male_{slotId}_{drawable}.png        (male components, e.g. male_11_73.png)
male_prop_{slotId}_{drawable}.png   (male props, e.g. male_prop_0_228.png)
female_{slotId}_{drawable}.png      (female components, e.g. female_11_58.png)
female_prop_{slotId}_{drawable}.png (female props, e.g. female_prop_0_112.png)

Slot IDs are the native GTA component/prop indexes. You don't need to look them up: open the "All slots" dropdown in the Shop Creator or Outfit Creator in-game and every entry already shows its ID, like Jacket (C11) or Hat / Helmet (P0). C is for component, P is for prop.

3

Convert them to WebP with the converter script already included at web/png_to_webp.py. It needs Python 3 and Pillow:

pip install pillow
python3 web/png_to_webp.py path/to/your/screenshots web/images

Run it with no arguments and it reads from and writes to the current folder instead.

4

Drop the resulting .webp files into web/images/ and restart the resource. Each piece picks up its picture automatically wherever it shows up, inventory, shop catalog, outfit creator, all of it.

Male & female peds

The script tells male and female apart automatically, on both the client and the server, by checking the player's ped model (mp_m_freemode_01 vs mp_f_freemode_01). There's nothing to configure to turn this on, it's always active. The one thing you do need to set up yourself is Config.DefaultDrawables, which has separate male and female tables.

Why this matters at all: the same drawable number is a completely different mesh depending on the model. Drawable 15 on the torso slot might be a t-shirt on the male model and something else entirely on the female one. There's no way to reuse one gender's data for the other, everything below exists because of that.

What's kept separate per gender

When the check happens

Every time, not just once. The gender check re-runs on every single inventory open, on top of the usual spawn/resource-start refresh. If something external to this script changes a player's ped model mid-session, the very next time they press F3 they'll see their own-gender inventory and outfits, and any now-mismatched active outfit gets cleared automatically. No reconnect needed.

Cataloging female items as an admin

The Shop Creator and Outfit Creator both have a "Preview as Male / Female" toggle above the EUP Library panel. Switching it doesn't touch your own character, it spawns a separate preview ped of the chosen gender so you can scan its drawables, screenshot them, and save items or build outfit templates for it, all without leaving your own body. See Adding your own images for the male_/female_ naming convention that goes with it.

If you give a template to yourself from the admin panel while your own character doesn't match the template's gender, the script refuses and tells you to switch model first, it won't silently apply broken drawables to you.

General settings

Everything lives in config.lua at the resource root.

Admin access

Config.AdminAce = 'eup.admin'

The ACE permission checked server-side for every admin action. Matches the group.eup_admin group set up in permissions.cfg; rename them together if you change one.

Chat commands

SettingDefaultEffect
Config.AdminCommandEnabledtrueToggles the /eupadmin command. The panel stays reachable via export/event either way (see Exports & events).
Config.AdminCommandName'eupadmin'Rename the command if it conflicts with another script.
Config.GiveCurrentEupCommandEnabledtrueToggles /givecurrenteup.
Config.GiveCurrentEupCommandName'givecurrenteup'Rename that command too.
Both command toggles are convenience-only. The ACE permission is re-checked server-side regardless, so disabling or renaming a command doesn't weaken security. It just hides the chat entry point.

Default drawables

Used by /givecurrenteup, the Outfit Creator's "Fill from ped" button, and the shop's "Strip" button to know which drawable on each slot means "nothing", so they don't treat your base ped model's default clothes as EUP items. Set separately per gender, since the same drawable number is a different mesh on mp_m_freemode_01 and mp_f_freemode_01.

Config.DefaultDrawables = {
    male = {
        component = {
            [1] = 0,  -- Mask
            [3] = 15, -- Torso / Arms
            [4] = 14, -- Legs
            ...
        },
    },
    female = {
        component = {
            [1] = 0,  -- Mask
            [3] = 15, -- Torso / Arms
            [4] = 14, -- Legs
            ...
        },
    },
}

Animations

Config.Animations, Config.PropAnimations, and Config.OutfitAnimation control the dressing animation played per slot when equipping/unequipping, and the generic one played for a full outfit swap. Each entry is an animation dict/clip pair with a flag and duration.

Money bridge

The script has no dependency on any economy framework. Out of the box, every shop purchase just goes through for free, so you can test the shop right away without setting anything up.

Config.HasMoney = function(src, amount)
    return true
end

Config.RemoveMoney = function(src, amount)
    return true
end
Replace both before going into production. Left as-is, players can buy anything for free forever. Two ready-to-use examples are already written as comments directly above them in config.lua. Uncomment the one you need:
-- ox_inventory example:
Config.HasMoney = function(src, amount)
    return exports.ox_inventory:GetItemCount(src, 'money') >= amount
end
Config.RemoveMoney = function(src, amount)
    return exports.ox_inventory:RemoveItem(src, 'money', amount)
end

-- ESX example:
Config.HasMoney = function(src, amount)
    local xPlayer = exports.es_extended:GetPlayerFromId(src)
    return xPlayer and xPlayer.getMoney() >= amount
end
Config.RemoveMoney = function(src, amount)
    local xPlayer = exports.es_extended:GetPlayerFromId(src)
    if not xPlayer or xPlayer.getMoney() < amount then return false end
    xPlayer.removeMoney(amount)
    return true
end

Both functions are also exposed as exports (HasMoney / RemoveMoney) so any other resource can reuse the exact same bridge instead of duplicating the check.

Notification bridge

Default: native GTA notification, zero dependency.

Config.Notify = function(ntype, title, message, duration)
    SetNotificationTextEntry('STRING')
    AddTextComponentString(('%s: %s'):format(title or '', message or ''))
    DrawNotification(false, true)
end

ox_lib example, also already in config.lua as a comment:

Config.Notify = function(ntype, title, message, duration)
    exports.ox_lib:notify({ type = ntype, title = title, description = message, duration = duration })
end

Also exposed as the Notify client export.

Preview scene

Config.PreviewScene is the fixed spot the player is teleported to (invisible) and the ped clone spawns at, for shop/creator previews. The default coordinates shipped with the resource are just a placeholder.

Reposition this before opening to players. Stand where you want the scene and use /coords to find the values. Or, easier, just use the admin panel's Scene tab in-game: it lets you capture player/ped spawn positions and adjust the camera live with sliders, then save.
Config.PreviewScene = {
    playerSpawn = vector4(x, y, z, heading),
    pedSpawn    = vector4(x, y, z, heading),
    camPos      = vector3(x, y, z),
    camLookAt   = vector3(x, y, z),
    camFov      = 47.0,
}

Discord role gating (optional)

Shop categories can optionally be restricted to players holding a specific Discord role. This lives in its own file, discord_config.lua, deliberately separate from config.lua:

Config.DiscordGuild = ''
Config.DiscordToken = ''
Never move this into a shared_script. discord_config.lua is loaded as a server_script on purpose. A shared_script gets downloaded to every connecting client in the clear, which would leak your bot token to players. Leave both fields empty to disable role-gated categories entirely; they'll simply stay inaccessible.

This is completely separate from admin access. Config.AdminAce (see above) is the only thing gating the admin panel, and it doesn't need Discord at all.

Commands

CommandAccessDescription
F3 keybindEveryoneOpen/close the clothing inventory. Rebindable per-player in FiveM's own key bindings settings.
/eupadminAdminOpen the admin panel: Zones, Outfits (templates), and Scene tabs.
/givecurrenteupAdminScans your ped's currently worn outfit and adds each piece as an EUP item in your inventory.
/wype [license:xxxx]AdminWipes a player's EUP data only (inventory, outfits, equipped slots, active outfit). Doesn't touch their character or any other framework data. Destructive and asks for no confirmation, so use it carefully.
/eupbulkshop confirmAdminBulk-generates a starter shop: scans every EUP drawable not already in your catalog (male and female) and adds it, auto-named, into a shop called "Auto Shop" with one category per slot type. Never touches anything you've already configured, safe to run again after adding a new EUP pack. Running it without confirm just prints the warning, it won't do anything.

Player guide

Right-click context menu on an item
Context menu
Dragging an item onto a highlighted slot
Drag & drop onto a slot
Give to... nearby player picker
Give to a nearby player
Shortcuts and controls help panel
Help panel (?)

Admin guide

Shop Creator

Opened via the admin panel's Shops action. Pick a slot to scan every drawable/texture available on your ped model, name the ones you want sellable, group them into categories (optionally restricted to a Discord role), assign categories to a shop, and set per-shop price overrides.

Shop Creator — EUP library on the left, shops and categories on the right

Here's what a player sees when they walk into a shop: browse drawables/textures on the left, review the cart and pay on the right.

Shop viewer as seen by players, with cart

Bulk shop generator

Don't want to name hundreds of drawables by hand? Run /eupbulkshop confirm and it scans your whole EUP pack, male and female, and dumps everything not already in your catalog into a shop called "Auto Shop", sorted into one category per slot type ("Auto: Jacket", "Auto: Hat / Helmet", etc.). Items are named after their slot and drawable number ("Jacket 15"), textures are named "Texture 0", "Texture 1", and so on, and everything is priced at 0 by default.

This is a starting point, not a finished shop. The names are generic and nothing costs anything until you set prices. Treat it as a fast way to get every piece purchasable immediately, then go back into the Shop Creator to rename, price, and organize whatever you actually care about. It only fills in gaps: anything you've already named yourself (through the Shop Creator or a previous bulk run) is left untouched, so it's safe to run again after adding a new EUP pack.
Run it with nobody else on the server, and be patient. It's writing thousands of database rows, there's a real performance cost while it's running. Let it finish before opening the server to players.

For transparency, here are the real numbers from testing on a full EUP pack: ~5,000 items and ~41,000 textures created in one run. The lag only happens during that generation itself, once it's done, opening the resulting shop is back to a brief, unnoticeable hitch (a few milliseconds).

Build a template outfit piece by piece, save it, and either hand it to yourself from the admin panel's Outfits tab (creates and equips it instantly, no item pickup step) or let another resource call the GiveOutfit export/event.

Outfit Creator with several slots already assigned

Zones tab

Walk to where you want a shop accessible, then click Set here next to that shop in the admin panel to place its interaction zone and radius.

Zones tab listing a shop's coordinates and radius

Scene tab

Adjust and save the shared preview camera/ped spawn used by every shop and creator preview. See Preview scene above for more. You can also save multiple named camera presets and switch between them.

Scene tab with camera sliders and presets

Exports & events

The integration surface for other resources. Internal eup:... events used between this resource's own client and server are not listed. They're implementation detail, not meant to be called externally.

How to call an export

From another resource's Lua file, on the same side (client or server) the export is listed under:

exports['SandCastle_ClothingInventory']:FunctionName(arg1, arg2)

Swap FunctionName for any export name from the tables below.

How to use an event

To trigger one:

TriggerEvent('EventName', arg1, arg2)                   -- locally, same side
TriggerServerEvent('EventName', arg1, arg2)             -- client → server
TriggerClientEvent('EventName', targetSrc, arg1, arg2)  -- server → client

To listen for one:

RegisterNetEvent('EventName', function(arg1, arg2)
    -- your code here
end)

Server-side

TypeNameUsage
ExportGiveOutfit(targetSrc, templateId, outfitId)Creates and equips an outfit directly on a player. Pass templateId for an admin template, or outfitId to restore one they already own.
EventSandCastle_ClothingInventory:giveOutfitSame, as an event: TriggerEvent('SandCastle_ClothingInventory:giveOutfit', targetSrc, templateId)
ExportHasMoney(src, amount)Delegates to Config.HasMoney.
ExportRemoveMoney(src, amount)Delegates to Config.RemoveMoney.

Client-side

TypeNameUsage
ExportGetPlayerInventory()Returns the local player's current inventory.
ExportGetPlayerOutfits()Returns their saved outfits.
ExportEquipSlot(slotType, slotId, drawable, texture)Equip a single piece.
ExportUnequipSlot(slotType, slotId)Remove a piece from a slot.
ExportEquipOutfit(outfitId)Equip a full outfit.
ExportReloadSkin()Immediately re-applies the equipped outfit/slots.
ExportNotify(ntype, title, message, duration)Delegates to Config.Notify.
ExportEupAdminOpen()Opens the admin panel.
EventSandCastle_ClothingInventory:reloadSkinRe-applies the skin after a 300ms delay (built for revive hooks). TriggerEvent(...) locally, or TriggerClientEvent(..., targetSrc) from the server.
EventSandCastle_ClothingInventory:openAdminOpens the admin panel. Event form of EupAdminOpen.