Command Palette
Search for a command to run...
CUSTOM FRAMEWORK INTEGRATION#
onex-creation supports custom framework integration, allowing you to connect any player management system with full outfit and appearance functionality.
Overview#
Custom framework integration enables:
- Full control over player identification and data storage
- Integration with your own money system
- Custom database schema for appearance/outfit storage
- Compatibility with any player management system
- All features available in QB/ESX modes
Requirements#
server.cfgensure ox_lib ensure onex-base ensure onex-interaction ensure onex-creation
Architecture Overview#
The integration system has two main layers:
| Layer | Location | Purpose |
|---|---|---|
| Compat Interface | modules/framework/server/custom.lua | Core appearance operations (save/load/money) |
| CustomFx Interface | integrations/custom_framework/server.lua | Outfit system operations (CRUD for outfits) |
Script Loading Order#
Scripts load in this specific order (critical for proper initialization):
Server:
modules/framework/server/compat.lua(creates Compat table)- Framework modules (esx.lua, qb.lua, standalone.lua)
integrations/custom_framework/server.lua(your custom code - loads last)
Client:
modules/framework/client/global.lua- Framework client modules
integrations/custom_framework/client.lua(your custom hooks)
Your custom integration files load LAST, allowing you to override any default behavior.
Step 1: Enable Custom Framework Mode#
To enable custom framework mode, configure the fallback setting in onex-base:
onex-base/config/config.luaonexFx.config.framework.fallback = 'custom'
Step 2: Database Setup#
Required Table: player_outfits#
SQL.sqlCREATE TABLE IF NOT EXISTS `player_outfits` ( `id` INT AUTO_INCREMENT PRIMARY KEY, `identifier` VARCHAR(255) NOT NULL, `outfitname` VARCHAR(255) NOT NULL, `description` TEXT, `skin` LONGTEXT NOT NULL, `code` VARCHAR(5) NOT NULL, UNIQUE KEY `unique_code` (`code`), INDEX `idx_identifier` (`identifier`), INDEX `idx_code` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Column Descriptions#
| Column | Type | Description |
|---|---|---|
identifier | VARCHAR(255) | Player identifier from your framework |
outfitname | VARCHAR(255) | Display name for the outfit |
description | TEXT | Optional description |
skin | LONGTEXT | JSON-encoded clothing data |
code | VARCHAR(5) | Unique 5-character share code |
The code column is used for the outfit sharing system. Each outfit gets a unique 5-character alphanumeric code that players can share with others.
Step 3: Implement Compat Interface#
Create the file modules/framework/server/custom.lua:
modules/framework/server/custom.luaif GlobalState.xframework ~= 'custom' then return end --[[ COMPAT INTERFACE These 4 functions handle core appearance operations ]] -- Save player's main appearance(character creation/modification) function Compat.SaveAppearance(appearance, identifier) -- appearance: table - Full appearance data(model, components, overlays, etc.) -- identifier: string - Player identifier from your framework -- Example implementation: local encoded = json.encode(appearance) MySQL.update.await( 'INSERT INTO player_appearance(identifier, appearance) VALUES(?, ?) ON DUPLICATE KEY UPDATE appearance = ?', { identifier, encoded, encoded } ) return true end -- Load player's saved appearance function Compat.GetAppearance(identifier) -- identifier: string - Player identifier -- Returns: table(appearance data) or nil local result = MySQL.query.await( 'SELECT appearance FROM player_appearance WHERE identifier = ?', { identifier } ) if result and result[1] and result[1].appearance then return json.decode(result[1].appearance) end return nil end -- Check if player has sufficient money function Compat.HasMoney(src, moneyType, amount) -- src: number - Player server ID -- moneyType: string - 'cash' or 'bank' -- amount: number - Amount to check -- Returns: boolean -- Example: Integrate with your money system local player = YourFramework.GetPlayer(src) if not player then return false end return player:GetMoney(moneyType) >= amount end -- Remove money from player function Compat.RemoveMoney(src, moneyType, amount) -- src: number - Player server ID -- moneyType: string - 'cash' or 'bank' -- amount: number - Amount to remove -- Returns: boolean - true on success local player = YourFramework.GetPlayer(src) if not player then return false end if player:GetMoney(moneyType) >= amount then player:RemoveMoney(moneyType, amount) return true end return false end
Reference Implementations#
Step 4: Implement CustomFx Interface (Outfit System)#
Edit the file integrations/custom_framework/server.lua:
CRITICAL: These 5 functions are required for the outfit system to work. The outfit system actively calls these functions when GlobalState.xframework == 'custom'.
Helper Functions (Pre-Defined)#
These helper functions are already available in the custom framework file:
Available Helpers-- Get player identifier from your framework local function GetPlayerIdentifier(src) -- Implement based on your framework for _, id in ipairs(GetPlayerIdentifiers(src)) do if string.match(id, 'license:') then return id end end return nil end -- Generate unique 5-character code for outfits local function GenerateUniqueCode() local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' local code repeat code = '' for i = 1, 5 do local idx = math.random(1, #chars) code = code .. chars:sub(idx, idx) end local exists = MySQL.query.await( 'SELECT code FROM player_outfits WHERE code = ?', { code } ) until not exists or #exists == 0 return code end -- Filter outfit for import(removes incompatible items) local function FilterOutfitForImport(imported_outfit, current_skinData) -- Filters clothing items that may not be compatible -- with the importing player's character model -- Returns filtered outfit table end
Required Functions#
1. GetPlayerOutfitData_FromDatabase#
integrations/custom_framework/server.lua--[[ Fetch all outfits belonging to a player Called by: modules/outfit/server/outfits.lua:77 @param src(number) - Player server ID @return table - Array of outfit objects ]] function CustomFx.GetPlayerOutfitData_FromDatabase(src) local identifier = GetPlayerIdentifier(src) if not identifier then return {} end local result = MySQL.query.await( 'SELECT identifier, outfitname, description, skin, code FROM player_outfits WHERE identifier = ?', { identifier } ) return result or {} end
Return Format:
{ { identifier = "license:abc123...", outfitname = "Casual Outfit", description = "My weekend clothes", skin = "{...}", -- JSON string code = "aB3dE" }, -- ... more outfits }
2. SaveNewOutfitData_InDatabase#
Important: The skindata parameter is ALREADY JSON-ENCODED. Do NOT call json.encode() on it again!
integrations/custom_framework/server.lua--[[ Save a new outfit to the database Called by: modules/outfit/server/outfits.lua:167 @param src(number) - Player server ID @param outfitname(string) - Display name for the outfit @param description(string) - Optional description @param code(string) - Pre-generated 5-character unique code @param skindata(string) - ALREADY JSON-ENCODED outfit data @return boolean - true on success ]] function CustomFx.SaveNewOutfitData_InDatabase(src, outfitname, description, code, skindata) local identifier = GetPlayerIdentifier(src) if not identifier then return false end -- skindata is ALREADY JSON encoded - do NOT encode again! local success = MySQL.insert.await( 'INSERT INTO player_outfits(identifier, outfitname, description, skin, code) VALUES(?, ?, ?, ?, ?)', { identifier, outfitname, description, skindata, code } ) return success ~= nil end
3. Delete_SelectedOutfit#
integrations/custom_framework/server.lua--[[ Delete an outfit by its code Called by: modules/outfit/server/outfits.lua:108 @param src(number) - Player server ID @param code(string) - 5-character outfit code to delete @return boolean - true on success ]] function CustomFx.Delete_SelectedOutfit(src, code) local identifier = GetPlayerIdentifier(src) if not identifier then return false end -- Security: Only delete if outfit belongs to the player local affected = MySQL.update.await( 'DELETE FROM player_outfits WHERE code = ? AND identifier = ?', { code, identifier } ) return affected > 0 end
4. UpdateExistingOutfitData_ByCode#
Critical Difference: Unlike SaveNewOutfitData_InDatabase, this function receives a RAW LUA TABLE. You MUST call json.encode() on the skindata!
integrations/custom_framework/server.lua--[[ Update an existing outfit's clothing data Called by: modules/outfit/server/outfits.lua:189 @param src(number) - Player server ID @param skindata(table) - RAW LUA TABLE - must encode yourself! @param code(string) - 5-character outfit code to update @return boolean - true on success ]] function CustomFx.UpdateExistingOutfitData_ByCode(src, skindata, code) local identifier = GetPlayerIdentifier(src) if not identifier then return false end -- MUST encode the skindata - it's a raw Lua table! local encoded = json.encode(skindata) local affected = MySQL.update.await( 'UPDATE player_outfits SET skin = ? WHERE code = ? AND identifier = ?', { encoded, code, identifier } ) return affected > 0 end
5. ImportOutfit_ByCode#
integrations/custom_framework/server.lua--[[ Import another player's outfit by share code Called by: modules/outfit/server/outfits.lua:278 @param src(number) - Importing player's server ID @param code(string) - 5-character code of outfit to import @param skinData(table) - Importing player's current appearance @param model(string) - Importing player's current ped model @return boolean - true on success, false on failure ]] function CustomFx.ImportOutfit_ByCode(src, code, skinData, model) local identifier = GetPlayerIdentifier(src) if not identifier then return false end -- Step 1: Fetch the outfit to import local result = MySQL.query.await( 'SELECT * FROM player_outfits WHERE code = ?', { code } ) if not result or #result == 0 then -- Outfit not found return false end local outfit = result[1] -- Step 2: Check if player already owns this outfit local existing = MySQL.query.await( 'SELECT id FROM player_outfits WHERE identifier = ? AND outfitname = ?', { identifier, outfit.outfitname } ) if existing and #existing > 0 then -- Player already has outfit with this name return false end -- Step 3: Decode and filter the outfit local importedSkin = json.decode(outfit.skin) local filteredOutfit = FilterOutfitForImport(importedSkin, skinData) -- Step 4: Generate new unique code for the imported outfit local newCode = GenerateUniqueCode() -- Step 5: Save as new outfit for the importing player local success = MySQL.insert.await( 'INSERT INTO player_outfits(identifier, outfitname, description, skin, code) VALUES(?, ?, ?, ?, ?)', { identifier, outfit.outfitname, outfit.description or '', json.encode(filteredOutfit), newCode } ) return success ~= nil end
Step 5: Client-Side Integration#
Edit integrations/custom_framework/client.lua to hook into your framework's events:
integrations/custom_framework/client.luaif GlobalState.xframework ~= 'custom' then return end --[[ CLIENT-SIDE HOOKS Register your framework's events to trigger onex-creation ]] -- Example: Player loaded event RegisterNetEvent('yourframework:playerLoaded') AddEventHandler('yourframework:playerLoaded', function(playerData) -- Load saved appearance local appearance = exports['onex-creation']:FetchCurretPlayerClothesFromDB() if appearance then exports['onex-creation']:LoadPedSkin(appearance) end end) -- Example: New character creation trigger RegisterNetEvent('yourframework:createCharacter') AddEventHandler('yourframework:createCharacter', function() -- Open character creation menu TriggerEvent('onex-creation:client:CreateNewChar') end) -- Example: Open clothing menu RegisterNetEvent('yourframework:openClothingShop') AddEventHandler('yourframework:openClothingShop', function() exports['onex-creation']:openCreationMenu('clothes') end) -- Callbacks for appearance changes CustomFx = CustomFx or {} CustomFx.Client = {} -- Called when appearance is loaded CustomFx.Client.OnAppearanceLoaded = function(appearance) -- Your custom logic here print('[CustomFx] Appearance loaded') end -- Called when appearance is saved CustomFx.Client.OnAppearanceSaved = function(appearance) -- Your custom logic here print('[CustomFx] Appearance saved') end -- Get player job(for job outfits) CustomFx.Client.GetJob = function() -- Return job data from your framework return { name = 'unemployed', grade = 0, label = 'Civilian' } end
Common Framework Event Patterns#
Outfit Data Structure#
Understanding what data is stored in outfits:
What Outfits Include (Clothing Only)#
Outfit Componentslocal outfitComponents = { 'shirt', -- Undershirt 'pants', -- Legs 'arms', -- Arms/Sleeves 'torso', -- Torso/Jacket 'vest', -- Body Armor/Vest 'bag', -- Bags/Parachute 'shoes', -- Shoes 'mask', -- Masks 'hat', -- Hats/Helmets 'glass', -- Glasses 'ear', -- Earrings 'watch', -- Watches 'bracelet', -- Bracelets 'accessory', -- Accessories 'decals' -- Decals/Badges }
What Outfits Exclude#
Outfits intentionally do NOT include:
- Character model
- Face features (headBlend, faceFeatures)
- Hair and hair color
- Beard/facial hair
- Makeup and overlays
- Tattoos
- Eye color
This separation ensures that importing someone's outfit doesn't overwrite your character's unique features - only the clothing changes.
Complete Implementation Example#
Full Server Implementation#
integrations/custom_framework/server.luaif GlobalState.xframework ~= 'custom' then return end -- Helper: Get player identifier local function GetPlayerIdentifier(src) -- Customize based on your identifier type for _, id in ipairs(GetPlayerIdentifiers(src)) do if string.match(id, 'license:') then return id end end return nil end -- Helper: Generate unique outfit code local function GenerateUniqueCode() local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' local code repeat code = '' for i = 1, 5 do local idx = math.random(1, #chars) code = code .. chars:sub(idx, idx) end local exists = MySQL.query.await('SELECT code FROM player_outfits WHERE code = ?', { code }) until not exists or #exists == 0 return code end -- Initialize CustomFx table CustomFx = CustomFx or {} -- 1. Fetch player outfits function CustomFx.GetPlayerOutfitData_FromDatabase(src) local identifier = GetPlayerIdentifier(src) if not identifier then return {} end return MySQL.query.await( 'SELECT identifier, outfitname, description, skin, code FROM player_outfits WHERE identifier = ?', { identifier } ) or {} end -- 2. Save new outfit(skindata is PRE-ENCODED JSON) function CustomFx.SaveNewOutfitData_InDatabase(src, outfitname, description, code, skindata) local identifier = GetPlayerIdentifier(src) if not identifier then return false end local success = MySQL.insert.await( 'INSERT INTO player_outfits(identifier, outfitname, description, skin, code) VALUES(?, ?, ?, ?, ?)', { identifier, outfitname, description, skindata, code } ) return success ~= nil end -- 3. Delete outfit function CustomFx.Delete_SelectedOutfit(src, code) local identifier = GetPlayerIdentifier(src) if not identifier then return false end local affected = MySQL.update.await( 'DELETE FROM player_outfits WHERE code = ? AND identifier = ?', { code, identifier } ) return affected > 0 end -- 4. Update outfit(skindata is RAW TABLE - must encode!) function CustomFx.UpdateExistingOutfitData_ByCode(src, skindata, code) local identifier = GetPlayerIdentifier(src) if not identifier then return false end local affected = MySQL.update.await( 'UPDATE player_outfits SET skin = ? WHERE code = ? AND identifier = ?', { json.encode(skindata), code, identifier } ) return affected > 0 end -- 5. Import outfit by share code function CustomFx.ImportOutfit_ByCode(src, code, skinData, model) local identifier = GetPlayerIdentifier(src) if not identifier then return false end local result = MySQL.query.await('SELECT * FROM player_outfits WHERE code = ?', { code }) if not result or #result == 0 then return false end local outfit = result[1] -- Check for duplicate local existing = MySQL.query.await( 'SELECT id FROM player_outfits WHERE identifier = ? AND outfitname = ?', { identifier, outfit.outfitname } ) if existing and #existing > 0 then return false end -- Filter and save local importedSkin = json.decode(outfit.skin) local filteredOutfit = FilterOutfitForImport(importedSkin, skinData) local newCode = GenerateUniqueCode() local success = MySQL.insert.await( 'INSERT INTO player_outfits(identifier, outfitname, description, skin, code) VALUES(?, ?, ?, ?, ?)', { identifier, outfit.outfitname, outfit.description or '', json.encode(filteredOutfit), newCode } ) return success ~= nil end print('[onex-creation] Custom framework integration loaded')
Framework Configuration#
Shared Configuration Options#
modules/framework/shared/framework.luaFramework = { -- API export method ApiType = 'cfx', -- 'cfx' or 'local' -- Notification system Notify = { name = 'onex-interaction', -- Notification resource Action = function(type, msg, color, duration, header, icon, src) -- Custom notification logic end }, -- Callbacks loadWalkingAnim = function() -- Called after appearance loads end, OpeningMenu = function() -- Called when clothing menu opens end, LoadOtherThingOnPed = function() -- Called when outfit is applied end, -- Database config(standalone/custom only) onex_db = { autoPath = true, db_path = "path/to/onexdb", db_outfit_name = "player_outfits", db_name = "onex_clothes" } }
Implementation Checklist#
Use this checklist to ensure complete integration:
- Set
onexFx.config.framework.fallback = 'custom'inonex-base/config/config.lua - Create
player_outfitsdatabase table with required schema - Create
modules/framework/server/custom.luawith Compat functions:-
Compat.SaveAppearance() -
Compat.GetAppearance() -
Compat.HasMoney() -
Compat.RemoveMoney()
-
- Implement CustomFx functions in
integrations/custom_framework/server.lua:-
CustomFx.GetPlayerOutfitData_FromDatabase() -
CustomFx.SaveNewOutfitData_InDatabase()(receives pre-encoded JSON) -
CustomFx.Delete_SelectedOutfit() -
CustomFx.UpdateExistingOutfitData_ByCode()(receives raw table) -
CustomFx.ImportOutfit_ByCode()
-
- Add client hooks in
integrations/custom_framework/client.lua - Test all outfit operations:
- Create and save new outfit
- Load saved outfits
- Delete outfit
- Update existing outfit
- Share outfit (get code)
- Import outfit by code
Troubleshooting#
Outfits Not Saving#
- Verify
GetPlayerIdentifier()returns a valid identifier - Check database table exists with correct schema
- Enable debug:
Config.Debug = true - Check server console for SQL errors
JSON Encoding Errors#
| Function | skindata Parameter | Action |
|---|---|---|
SaveNewOutfitData_InDatabase | PRE-ENCODED string | Do NOT encode |
UpdateExistingOutfitData_ByCode | RAW Lua table | MUST encode |
Import Not Working#
- Verify the share code exists in database
- Check player doesn't already own outfit with same name
- Verify
FilterOutfitForImportfunction is available - Check for console errors during import
Framework Not Detected#
- Ensure
onexFx.config.framework.fallback = 'custom'is set inonex-base/config/config.lua - Check resource start order in server.cfg (onex-base must start before onex-creation)
- Verify no other resource is overwriting the GlobalState
- Check that QBCore/ESX/QBX are not being detected (remove them if not needed)
API Reference#
Available Exports#
All standard exports work with custom frameworks:
Common Exports-- Open menus exports['onex-creation']:openCreationMenu('clothes') exports['onex-creation']:openCreationMenu('newchar') exports['onex-creation']:openCreationMenu('barber') -- Appearance management local appearance = exports['onex-creation']:FetchPlayerClothes() exports['onex-creation']:LoadPedSkin(appearance, ped) exports['onex-creation']:LoadPedModel(ped, model) -- Outfit management exports['onex-creation']:loadCustomOutfit(outfit, persist)