SHOPPING CART

Onex

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.cfg
ensure ox_lib ensure onex-base ensure onex-interaction ensure onex-creation

Architecture Overview#

The integration system has two main layers:

LayerLocationPurpose
Compat Interfacemodules/framework/server/custom.luaCore appearance operations (save/load/money)
CustomFx Interfaceintegrations/custom_framework/server.luaOutfit system operations (CRUD for outfits)

Script Loading Order#

Scripts load in this specific order (critical for proper initialization):

Server:

  1. modules/framework/server/compat.lua (creates Compat table)
  2. Framework modules (esx.lua, qb.lua, standalone.lua)
  3. integrations/custom_framework/server.lua (your custom code - loads last)

Client:

  1. modules/framework/client/global.lua
  2. Framework client modules
  3. 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.lua
onexFx.config.framework.fallback = 'custom'

Step 2: Database Setup#

Required Table: player_outfits#

SQL.sql
CREATE 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#

ColumnTypeDescription
identifierVARCHAR(255)Player identifier from your framework
outfitnameVARCHAR(255)Display name for the outfit
descriptionTEXTOptional description
skinLONGTEXTJSON-encoded clothing data
codeVARCHAR(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.lua
if 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.lua
if 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 Components
local 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.lua
if 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.lua
Framework = { -- 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' in onex-base/config/config.lua
  • Create player_outfits database table with required schema
  • Create modules/framework/server/custom.lua with 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#

  1. Verify GetPlayerIdentifier() returns a valid identifier
  2. Check database table exists with correct schema
  3. Enable debug: Config.Debug = true
  4. Check server console for SQL errors

JSON Encoding Errors#

Functionskindata ParameterAction
SaveNewOutfitData_InDatabasePRE-ENCODED stringDo NOT encode
UpdateExistingOutfitData_ByCodeRAW Lua tableMUST encode

Import Not Working#

  1. Verify the share code exists in database
  2. Check player doesn't already own outfit with same name
  3. Verify FilterOutfitForImport function is available
  4. Check for console errors during import

Framework Not Detected#

  1. Ensure onexFx.config.framework.fallback = 'custom' is set in onex-base/config/config.lua
  2. Check resource start order in server.cfg (onex-base must start before onex-creation)
  3. Verify no other resource is overwriting the GlobalState
  4. 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)

Next Steps#