> For the complete documentation index, see [llms.txt](https://project-07.gitbook.io/project_07-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://project-07.gitbook.io/project_07-docs/scripts/project07-characters/configuration.md).

# Configuration

Config Files

{% tabs %}
{% tab title="Config.lua" %}

```lua
--[[
    Project07_Characters — config.lua

    This is the ONE file for every setting that isn't a color. Colors live
    in config.json (so they're easy to hand a designer without touching
    Lua). Everything else — branding text, sound, framework/housing/spawn/
    appearance selection, character slots, validation ranges, starter
    items, ped scaling, admin commands, preview scenes
    — is here.
]]

Config = {}

-- ============================== FRAMEWORK ================================
-- 'auto' picks the first one below that's actually running. Set it to an
-- exact resource name ('qbx_core' | 'qb-core' | 'es_extended') to force it.
Config.framework = {
    mode = 'auto',
    priority = { 'qbx_core', 'qb-core', 'es_extended' },
}

-- =============================== DATABASE =================================
-- Auto-creates/repairs the SQL this resource needs on start:
--   - p07_character_slots (all frameworks — backs /addcharslot)
--   - players.last_logged_out column (qbx_core / qb-core)
--   - p07_characters table (es_extended only)
-- Set to false if you'd rather run the .sql files under sql/ by hand.
Config.database = {
    autoMigrate = true,
}

-- =============================== HOUSING =================================
-- Apartment/property assignment for BRAND-NEW characters (see
-- Config.startingApartment above). qbx_properties/qbx_apartments/
-- qb-apartments/ps-housing/0r-apartment all fire (or are wrapped to fire)
-- the same `apartments:client:setupSpawnUI` event — see bridge/housing.lua.
Config.housing = {
    mode = 'auto',
    priority = {
        'qbx_properties', 'qbx_apartments', 'qb-apartments', 'qb-houses',
        'qs-housing', 'ps-housing', 'bcs_housing', '0r-apartment', 'esx_property',
    },
}

-- ============================== INVENTORY =================================
-- Used only for giving starter items (Config.starterItems). 'auto' picks
-- whichever is running; falls back to ESX's own built-in inventory when
-- the framework is es_extended and none of these are found.
Config.inventory = {
    mode = 'auto',
    priority = { 'ox_inventory', 'qb-inventory', 'qs-inventory', 'esx-inventory' },
}

-- ================================ SPAWN ===================================
-- Spawn-point selection for RETURNING characters (and new characters when
-- Config.startingApartment is disabled/unavailable) — see bridge/housing.lua.
Config.spawn = {
    mode = 'auto',
    priority = { 'qbx_spawn', 'qb-spawn', 'um-spawn', 'okokSpawnSelector', 'vms_spawnselector', 'default' },
}

-- ============================== APPEARANCE ================================
Config.appearance = {
    mode = 'auto',
    priority = {
        'illenium-appearance', 'fivem-appearance', 'rcore_appearance', 'rcore_clothing',
        'bl_appearance', 'crm-appearance', 'tgiann-clothing', '0r-clothingv2', '0r-clothing',
        'origen_clothing', 'qb-clothing',
    },
}

-- ============================== BRANDING ================================
Config.branding = {
    serverName = 'Project07',
    welcomeTitle = 'Welcome',
    welcomeSubtitle = 'to PROJECT07',
    welcomeReadyText = 'Ready to start',
    welcomeButtonText = 'Start',
    createCharacterTitle = 'Create Character',
    logo = 'assets/brand/logo.png', -- relative to web/build/ — false to disable
    -- Optional welcome-screen background image, relative to web/build/.
    -- false to keep the plain gradient background instead.
    backgroundImage = false, -- e.g. 'assets/brand/welcome-bg.jpg'
    -- How long (ms) the loading bar runs after pressing Start, before the
    -- multichar screen actually appears.
    welcomeLoadingMs = 7000,
    showWelcomeScreen = true,
    -- Shown in the "About" panel from the character-select menu. Put
    -- whatever you'd like players to see — Discord link, rules summary,
    -- credits, version number, etc.
    aboutText = 'Project07 is a FiveM roleplay community. Add your own server description here (Config.branding.aboutText in config.lua).',

    -- Scrolling movie-style credits shown under the about text. Add/remove
    -- rows freely — role is the small label, name is the bigger line.
    credits = {
        { role = 'Owner', name = 'Your Name' },
        { role = 'Lead Developer', name = 'Pehesara' },
        -- { role = 'Staff', name = 'Someone' },
    },
}

-- ================================ SOUND ==================================
Config.sound = {
    enabled = true,
    defaultVolume = 0.4,
    musicByDefault = false,
    -- File names only — actual audio lives in web/src/lib/assets/sounds/.
    -- Swap the files (same names) to reskin; no need to touch this table.

    -- Optional global default background music (server-side, applies to
    -- every player who hasn't set their own link). A YouTube video/watch
    -- URL, or a direct https link ending in .mp3/.ogg — leave blank ('')
    -- to use the local ambient.wav loop instead. Players can override
    -- this for themselves from the in-game Settings menu; their choice is
    -- stored locally to their own client and never touches the server.
    -- (SoundCloud links aren't supported — that needs SoundCloud's own
    -- widget/API, not just a URL.)
    musicUrl = '',
}



-- =========================== STARTING APARTMENT ============================
-- true  -> brand new characters go through the apartment-assignment flow
--          of whichever housing resource is detected (Config.activeHousing).
-- false -> brand new characters skip apartments entirely and are placed at
--          customSpawn below instead.
Config.startingApartment = {
    enabled = true,
    customSpawn = { x = -540.58, y = -212.02, z = 37.65, w = 208.88 },
}

-- ============================== NAME FILTER ================================
-- Basic profanity/toxic-word blocklist for first/last name fields.
-- Add your own words to Config.nameFilter.blockedWords (case-insensitive,
-- substring match).
Config.nameFilter = {
    enabled = true,
    blockedWords = {
        -- add words here, e.g. 'admin', 'fuck', 'nigger', ...
    },
}

-- ================================= LOGS ===================================
-- Discord webhook logging. Leave a URL blank ('') to disable logging for
-- that specific event; Config.logs.enabled is the master switch.
Config.logs = {
    enabled = false,
    webhooks = {
        playgame = '',
        logout = '',
        exploit = '',
        createcharacter = '',
        deletecharacter = '',
    },
}

-- ============================== ANTI-EXPLOIT ================================
Config.antiExploit = {
    enabled = true,
    -- Minimum ms between repeated calls to sensitive callbacks from the
    -- same player, per callback name. Anything faster is dropped + logged.
    callbackCooldownMs = 800,
}



-- ============================== CHARACTERS =================================
Config.characters = {
    defaultSlots = 1,

    -- Static per-license overrides. Slots granted at runtime via the
    -- /addcharslot command are stored in the p07_character_slots table
    -- instead (see sql/p07_character_slots.sql) and take priority over
    -- this static list.
    ---@type table<string, number>
    slotOverridesByLicense = {
        -- ['license2:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'] = 10,
    },

    allowDeleteCharacter = true,
    exitAction = 'disconnect', -- 'disconnect' | 'quit'

    -- When a character is deleted, also DELETE FROM each of these tables
    -- WHERE <column> = <citizenid>. `player_vehicles` (citizenid) is the
    -- standard QB/QBX vehicle-ownership table and is on by default.
    -- Housing/garage/etc table names vary a LOT between scripts, so add
    -- your own here rather than guessing — e.g. for qb-houses-style
    -- ownership: { table = 'houselocations', column = 'citizenid' }.
    -- Every entry is wrapped safely — a table that doesn't exist on your
    -- server is skipped, not an error.
    deleteCascade = {
        enabled = true,
        tables = {
            { table = 'player_vehicles', column = 'citizenid' },
            { table = 'player_houses', column = 'citizenid' },
            { table = 'apartments', column = 'citizenid' },
            { table = 'player_gangs', column = 'citizenid' },
            { table = 'player_contacts', column = 'citizenid' },
            { table = 'player_mails', column = 'citizenid' },
            { table = 'player_outfits', column = 'citizenid' },
            { table = 'playerskins', column = 'citizenid' },
            { table = 'bank_accounts_characters', column = 'citizenid' },
            -- Add more tables here if your server has them — any table
            -- that doesn't exist is safely skipped (no error).
        },
    },
}

-- =============================== VALIDATION =================================
Config.validation = {
    minNameLength = 2,
    maxNameLength = 16,
    minHeight = 140,
    maxHeight = 220,
    maxBackstoryLength = 300,
    minBirthYear = 1940,
    maxBirthYear = 2006,
    namePattern = "^[%a%s%-']+$",
}

-- ============================== ADMIN COMMANDS ===============================
-- Both commands below share ONE ACE permission — grant it to your admin
-- group in server.cfg and both /addcharslot and targeted /logout <id> work:
--   add_ace group.admin project07.character allow
--   add_principal identifier.YOUR_IDENTIFIER_HERE group.admin

-- /addcharslot [id] — grants that player one extra character slot.
Config.addCharSlotCommand = {
    enabled = true,
    commandName = 'addcharslot',
}

-- /logout [id] — sends a player back to the character-select screen.
-- Plain "/logout" (no id) always works for everyone, on themselves only.
-- Supplying an id (to log out someone else) is gated by targetPermission:
--   'admin' — only players with the "project07.character" ACE permission (see above).
--   'all'   — any player can supply an id to log another player out too.
Config.logoutCommand = {
    enabled = true,
    commandName = 'logout',
    targetPermission = 'admin', -- 'admin' | 'all'
}

-- /charadmin — opens the admin tablet: browse every character in the
-- database, delete a character, grant a character slot, or change the
-- multichar screen's theme color — with every action logged to
-- p07_admin_logs. Same "project07.character" ACE permission as the other
-- admin commands.
Config.adminPanelCommand = {
    enabled = true,
    commandName = 'charadmin',
    -- Set to false to hide the Locations tab from the admin tablet
    -- entirely (e.g. if you're distributing this script and don't want
    -- buyers to add their own locations). The Locations tab code is still
    -- present in the build but gated behind this flag — it's not exposed
    -- to non-admins regardless.
    showLocationsTab = true,
}

-- =============================== SPAWN COORDS ================================
Config.spawnCoords = { x = -540.58, y = -212.02, z = 37.65, w = 208.88 }

-- When true, player's saved location preference is ignored and a random
-- scene from Config.previewScenes + admin-captured locations is picked
-- each time the character screen opens. When false, the player's
-- Settings → Locations choice is remembered via KVP.
Config.randomSpawnLocation = true

-- =============================== PREVIEW CAMERA ================================
Config.loadingModelsTimeout = 20000

-- Applied uniformly on top of every scene's camCoords/pedCoords (see
-- config/preview_scenes.lua), so you can tune the framing once instead of
-- re-placing every scene. Defaults frame an upper-body "portrait" shot.
Config.previewCamera = {
    fov = 28.0,          -- lower = more zoomed in (was a full-body-friendly 40.0)
    heightOffset = 0.55, -- raises the camera itself above each scene's camCoords.z
    lookAtHeight = 1.55, -- where the camera points, relative to pedCoords.z (was +0.5, ankle/knee height)

    -- Depth of field — the cinematic blurred-background look behind the
    -- character. Set enabled = false for a fully sharp scene with no blur.
    dof = {
        enabled = true,
        nearDof = 0.5,   -- distance (m) where the sharp zone starts
        farDof = 3.5,    -- distance (m) where the sharp zone ends / blur begins
        strength = 0.45, -- 0.0–1.0, how strong the background blur is
    },
}

-- The rest of the config lives in its own files under config/ — see:
--   config/weather.lua         Config.multicharWeather
--   config/random_peds.lua     Config.randomPeds
--   config/starter_items.lua   Config.starterItems
--   config/preview_scenes.lua  Config.previewScenes
--   config/qbx.lua / qbcore.lua / esx.lua   per-framework overrides

```

{% endtab %}

{% tab title="Config.json" %}

```jsonc
{
  "_comment": "themePreset picks a FULL theme (accent + every surface/border/muted-text color together). Valid ids: crimson (red, default), midnight (blue), emerald (green), violet (purple), amber, monochrome (white/silver). IMPORTANT: the admin /charadmin panel's Theme tab saves to the database and that ALWAYS wins over this file while a saved value exists - if you change themePreset here and don't see it in-game, an admin (maybe during testing) already saved something from the panel. Fix: open /charadmin -> Theme -> 'Reset to default', or run: DELETE FROM p07_settings WHERE `key` = 'theme_override'; -- then restart the resource. Set themePreset to null to always use whatever's saved from the panel (or crimson if nothing's saved) instead of this file. In 'theme' below: primarySurface/primaryText = selected character card + PLAY/CREATE buttons, cardIdleSurface = unselected/'New Character' card background - all optional, omit to use the preset/admin-panel value.",
  "themePreset": "emerald",

  "theme": {
    "white": "#ffffff",
    "success": "#22c55e",
    "danger": "#ef4444",
    "cardIdleSurface": "#181818",

    "glassSurfaceOpacity": 0.88,
    "glassAccentOpacity": 0.06,
    "glassBorderOpacity": 0.25,
    "glassBlur": "18px"
  }
}

```

{% endtab %}

{% tab title="esx.lua" %}

```lua
--[[
    Project07_Characters — config/esx.lua

    Framework-specific overrides layered on top of config.lua, applied
    AFTER framework detection (so it only runs when es_extended is
    actually the active framework).
]]

if Config.activeFramework ~= 'es_extended' then return end

-- config.lua's default Config.characters.deleteCascade targets
-- `player_vehicles` (citizenid) — that's a QBX/QB-Core table and doesn't
-- exist on ESX. Standard ESX vehicle ownership also isn't per-character:
-- `owned_vehicles` is keyed by `owner` (the player's license/identifier),
-- which is shared across every p07 character on that license — see the
-- ESX note in README.md. So there's nothing safe to auto-delete by
-- citizenid here; clear the QB-shaped default instead of running a query
-- against a table that doesn't exist.
Config.characters.deleteCascade.tables = {
    -- If you're using a custom vehicle/property system that DOES store
    -- your own per-character citizenid (this resource's p07_characters
    -- table does), add it here the same way config.lua's default does:
    -- { table = 'your_table', column = 'citizenid' },
}

```

{% endtab %}

{% tab title="qbcore.lua" %}

```lua
--[[
    Project07_Characters — config/qbcore.lua

    Framework-specific overrides layered on top of config.lua, applied
    AFTER framework detection (so it only runs when qb-core is actually
    the active framework). See config/qbx.lua for more notes.
]]

if Config.activeFramework ~= 'qb-core' then return end

-- Example: qb-core-only starter item.
-- table.insert(Config.starterItems, { name = 'qb_specific_item', amount = 1 })

```

{% endtab %}

{% tab title="qbx.lua" %}

```lua
--[[
    Project07_Characters — config/qbx.lua

    Framework-specific overrides layered on top of config.lua, applied
    AFTER framework detection (so it only runs when qbx_core is actually
    the active framework). Most QBX servers are fine with config.lua's
    defaults as-is — this file is here for the handful of things that
    genuinely differ per framework, and as a template for your own tweaks.

    Runs as a shared_script, same as config.lua — anything you set here
    can be read from both client and server code.
]]

if Config.activeFramework ~= 'qbx_core' then return end

-- Example: give QBX players an extra default character slot.
-- Config.characters.defaultSlots = 2

-- Example: QBX-only starter item.
-- table.insert(Config.starterItems, { name = 'qbx_specific_item', amount = 1 })

```

{% endtab %}

{% tab title="preview\_scenes.lua" %}

```lua
--[[
    Project07_Characters — config/preview_scenes.lua

    Interior locations the multichar camera can randomly pick between for
    the character-preview background — and that players can jump between
    directly from Settings → Locations. One is chosen at random each time
    the character screen opens (see client/main.lua). See
    Config.previewCamera in config.lua for framing/blur settings that
    apply uniformly on top of every scene here.

    Only 4 real, verified interior coordinate sets ship by default — add
    more entries here (with your own found pedCoords/camCoords) for a
    bigger Locations list; making up coordinates for places like "Bunker
    Entrance" or "IAA Office" without checking them in-game risks putting
    the ped underground/in a wall, so this intentionally doesn't guess at
    ones that weren't verified.
]]

Config.previewScenes = {
    {
        id = 'penthouse',
        label = 'Penthouse',
        pedCoords = vec4(969.25, 72.61, 116.18, 276.55),
        camCoords = vec4(972.2, 72.9, 116.68, 97.27),
        ipls = { 'vw_casino_penthouse' },
    },
    {
        id = 'luxury-garage',
        label = 'Luxury Garage Showcase',
        pedCoords = vec4(1104.49, 195.9, -49.44, 44.22),
        camCoords = vec4(1102.29, 198.14, -48.86, 225.07),
        ipls = { 'vw_casino_main', 'vw_casino_garage', 'vw_casino_carpark' },
    },
    {
        id = 'apartment',
        label = 'City Center',
        pedCoords = vec4(-996.71, -68.07, -99.0, 57.61),
        camCoords = vec4(-999.90, -66.30, -98.45, 241.68),
        ipls = { 'apa_mp_h_01_int', 'apa_mp_h_01' },
    },
    {
        id = 'nightclub',
        label = 'Nightclub',
        pedCoords = vec4(-1023.45, -418.42, 67.66, 205.69),
        camCoords = vec4(-1021.8, -421.7, 68.14, 27.11),
    },
}

```

{% endtab %}

{% tab title="random\_peds.lua" %}

```lua
--[[
    Project07_Characters — config/random_peds.lua

    Default appearances used for the character-creation preview ped when
    the active appearance system uses the generic illenium-shaped JSON
    (illenium-appearance / fivem-appearance / rcore_appearance) — see
    Config.activeAppearance in bridge/detect.lua. Add more entries here
    (or edit the two defaults) to change what a brand-new character looks
    like before the player customizes it.
]]

Config.randomPeds = {
    {
        model = `mp_m_freemode_01`,
        headOverlays = {
            beard = { color = 0, style = 0, secondColor = 0, opacity = 1 },
            complexion = { color = 0, style = 0, secondColor = 0, opacity = 0 },
            bodyBlemishes = { color = 0, style = 0, secondColor = 0, opacity = 0 },
            blush = { color = 0, style = 0, secondColor = 0, opacity = 0 },
            lipstick = { color = 0, style = 0, secondColor = 0, opacity = 0 },
            blemishes = { color = 0, style = 0, secondColor = 0, opacity = 0 },
            eyebrows = { color = 0, style = 0, secondColor = 0, opacity = 1 },
            makeUp = { color = 0, style = 0, secondColor = 0, opacity = 0 },
            sunDamage = { color = 0, style = 0, secondColor = 0, opacity = 0 },
            moleAndFreckles = { color = 0, style = 0, secondColor = 0, opacity = 0 },
            chestHair = { color = 0, style = 0, secondColor = 0, opacity = 1 },
            ageing = { color = 0, style = 0, secondColor = 0, opacity = 1 },
        },
        components = {
            { texture = 0, drawable = 0, component_id = 0 },
            { texture = 0, drawable = 0, component_id = 1 },
            { texture = 0, drawable = 0, component_id = 2 },
            { texture = 0, drawable = 0, component_id = 5 },
            { texture = 0, drawable = 0, component_id = 7 },
            { texture = 0, drawable = 0, component_id = 9 },
            { texture = 0, drawable = 0, component_id = 10 },
            { texture = 0, drawable = 15, component_id = 11 },
            { texture = 0, drawable = 15, component_id = 8 },
            { texture = 0, drawable = 15, component_id = 3 },
            { texture = 0, drawable = 34, component_id = 6 },
            { texture = 0, drawable = 61, component_id = 4 },
        },
        props = {
            { prop_id = 0, drawable = -1, texture = -1 },
            { prop_id = 1, drawable = -1, texture = -1 },
            { prop_id = 2, drawable = -1, texture = -1 },
            { prop_id = 6, drawable = -1, texture = -1 },
            { prop_id = 7, drawable = -1, texture = -1 },
        },
    },
    {
        model = `mp_f_freemode_01`,
        headBlend = {
            shapeMix = 0.3,
            skinFirst = 0,
            shapeFirst = 31,
            skinSecond = 0,
            shapeSecond = 0,
            skinMix = 0,
            thirdMix = 0,
            shapeThird = 0,
            skinThird = 0,
        },
        hair = {
            color = 0,
            style = 15,
            texture = 0,
            highlight = 0,
        },
        headOverlays = {
            chestHair = { secondColor = 0, opacity = 0, color = 0, style = 0 },
            bodyBlemishes = { secondColor = 0, opacity = 0, color = 0, style = 0 },
            beard = { secondColor = 0, opacity = 0, color = 0, style = 0 },
            lipstick = { secondColor = 0, opacity = 0, color = 0, style = 0 },
            complexion = { secondColor = 0, opacity = 0, color = 0, style = 0 },
            blemishes = { secondColor = 0, opacity = 0, color = 0, style = 0 },
            moleAndFreckles = { secondColor = 0, opacity = 0, color = 0, style = 0 },
            makeUp = { secondColor = 0, opacity = 0, color = 0, style = 0 },
            ageing = { secondColor = 0, opacity = 1, color = 0, style = 0 },
            eyebrows = { secondColor = 0, opacity = 1, color = 0, style = 0 },
            blush = { secondColor = 0, opacity = 0, color = 0, style = 0 },
            sunDamage = { secondColor = 0, opacity = 0, color = 0, style = 0 },
        },
        components = {
            { drawable = 0, component_id = 0, texture = 0 },
            { drawable = 0, component_id = 1, texture = 0 },
            { drawable = 0, component_id = 2, texture = 0 },
            { drawable = 0, component_id = 5, texture = 0 },
            { drawable = 0, component_id = 7, texture = 0 },
            { drawable = 0, component_id = 9, texture = 0 },
            { drawable = 0, component_id = 10, texture = 0 },
            { drawable = 15, component_id = 3, texture = 0 },
            { drawable = 15, component_id = 11, texture = 3 },
            { drawable = 14, component_id = 8, texture = 0 },
            { drawable = 15, component_id = 4, texture = 3 },
            { drawable = 35, component_id = 6, texture = 0 },
        },
        props = {
            { prop_id = 0, drawable = -1, texture = -1 },
            { prop_id = 1, drawable = -1, texture = -1 },
            { prop_id = 2, drawable = -1, texture = -1 },
            { prop_id = 6, drawable = -1, texture = -1 },
            { prop_id = 7, drawable = -1, texture = -1 },
        },
    },
}

```

{% endtab %}

{% tab title="scene\_filters.lua" %}

```lua
--[[
    Project07_Characters — config/scene_filters.lua

    Optional cosmetic filters for the multichar-screen camera (Settings ->
    Game Filters), applied via GTA's own SetTimecycleModifier native.
    Purely visual/client-side, same scope as the clock.

    IMPORTANT: the `modifier` strings below are REAL, verified GTA V
    timecycle modifier names (cross-checked against
    https://wiki.rage.mp/wiki/Timecycle_Modifiers — a maintained dump of
    every valid name in the game). The previous version of this file used
    invented names (CAMERA_SECURITY, PROLOGUE_FLASHBACK,
    SwitchHUD_MissionFailed, SP_FBI3_gasfumes, trevor4_trip1/2) that don't
    exist in the game at all, which is exactly why "Game Filters" visibly
    did nothing — SetTimecycleModifier silently no-ops on an unknown name.
    If you want to add more, only use names from that wiki page (or
    https://forge.plebmasters.de/timecyclemods), not invented ones.
]]

Config.sceneFilters = {
    { id = 'default', label = 'Default', modifier = nil }, -- nil = clear/no filter
    { id = 'camera', label = 'Camera', modifier = 'CAMERA_secuirity' },
    { id = 'bw', label = 'Black & White', modifier = 'blackNwhite' },
    { id = 'cinema', label = 'Cinematic', modifier = 'cinema' },
    { id = 'stoned', label = 'Stoned', modifier = 'stoned' },
    { id = 'nightvision', label = 'Night Vision', modifier = 'nightvision' },
    { id = 'drunk', label = 'Drunk', modifier = 'Drunk' },
    { id = 'dark', label = 'Dark', modifier = 'superDARK' },
    { id = 'bloom', label = 'Bright', modifier = 'BloomLight' },
}

```

{% endtab %}

{% tab title="starter\_items.lua" %}

```lua
--[[
    Project07_Characters — config/starter_items.lua

    Items given to a player right after they finish creating a new
    character (see server/starter.lua). Handed out through whichever
    inventory Config.inventory resolves to (bridge/inventory.lua).
]]

Config.starterItems = {
    { name = 'phone', amount = 1 },
    { name = 'bank_card', amount = 1 },

    {
        name = 'id_card',
        amount = 1,
        -- Returns nil (no metadata) gracefully on frameworks/servers that
        -- don't run qbx_idcard, instead of hard-erroring.
        metadata = function(source)
            if GetResourceState('qbx_idcard') ~= 'started' then return nil end
            local ok, meta = pcall(function()
                return exports.qbx_idcard:GetMetaLicense(source, { 'id_card' })
            end)
            return ok and meta or nil
        end,
    },
    {
        name = 'driver_license',
        amount = 1,
        metadata = function(source)
            if GetResourceState('qbx_idcard') ~= 'started' then return nil end
            local ok, meta = pcall(function()
                return exports.qbx_idcard:GetMetaLicense(source, { 'driver_license' })
            end)
            return ok and meta or nil
        end,
    },
}

```

{% endtab %}

{% tab title="theme\_presets.lua" %}

```lua
--[[
    Project07_Characters — config/theme_presets.lua (shared)

    Named admin-panel theme presets. Each one re-tints the whole UI
    (surfaces/border/muted text), not just the accent color — that's what
    was missing before: the admin panel could only ever override
    Config.theme.accent, so picking anything other than the red default
    still showed the same red panels underneath with a different button
    color.

    IMPORTANT: keep this in sync with web/src/lib/theme.ts's THEME_PRESETS
    (same ids, same hex values) — the web side applies these instantly for
    a snappy admin preview, and this file is what the server actually
    trusts/validates and broadcasts to every other player. If the two
    drift, the admin's own screen and everyone else's screen will
    momentarily disagree.
]]

Config.themePresets = {
    crimson = {
        label = 'Red',
        accent = '#e63946',
        accentHover = '#c62838',
        surface = '#120b0c',
        surfaceRaised = '#1a1011',
        surfaceOverlay = '#221415',
        border = '#3a1f21',
        textMuted = '#c9a3a5',
    },
    midnight = {
        label = 'Blue',
        accent = '#3aa0ff',
        accentHover = '#2b84d9',
        surface = '#0a0f14',
        surfaceRaised = '#101823',
        surfaceOverlay = '#152030',
        border = '#1f3350',
        textMuted = '#a7b8cc',
    },
    emerald = {
        label = 'Green',
        accent = '#22c55e',
        accentHover = '#1a9c48',
        surface = '#0a120d',
        surfaceRaised = '#101a13',
        surfaceOverlay = '#16241a',
        border = '#1f3a29',
        textMuted = '#a6c9b3',
    },
    violet = {
        label = 'Purple',
        accent = '#a855f7',
        accentHover = '#8b3ddb',
        surface = '#100a17',
        surfaceRaised = '#180f22',
        surfaceOverlay = '#20142d',
        border = '#3a2350',
        textMuted = '#c3a9d9',
    },
    amber = {
        label = 'Amber',
        accent = '#f59e0b',
        accentHover = '#cc8009',
        surface = '#140f08',
        surfaceRaised = '#1e160c',
        surfaceOverlay = '#281e10',
        border = '#463218',
        textMuted = '#d3bfa0',
    },
    monochrome = {
        label = 'White',
        accent = '#f5f5f7',
        accentHover = '#c7c7cc',
        surface = '#111113',
        surfaceRaised = '#1a1a1d',
        surfaceOverlay = '#232326',
        border = '#3a3a3f',
        textMuted = '#c6c6cc',
    },
}

Config.defaultThemePresetId = 'crimson'

```

{% endtab %}

{% tab title="weather.lua" %}

```lua
--[[
    Project07_Characters — config/weather.lua

    Time of day shown while the multichar camera is active. Players can
    change it from the in-UI Settings menu if allowPlayerChange is true —
    this only affects what THEY see while on the character screen, never
    the server's real time (see bridge/weather.lua).

    The weather-TYPE picker that used to live here has been intentionally
    removed (being handled separately) — this file/table name is kept as
    "multicharWeather" to avoid touching every reference across the
    codebase, but it only controls time now.
]]

Config.multicharWeather = {
    enabled = true,
    hour = 21, -- 24h, night by default
    allowPlayerChange = true,

    -- If your server's weather-sync resource isn't one of the ones
    -- already known about in bridge/weather.lua (qb-weathersync,
    -- qb-weather, cd_easytime, renewed-weathersync, ps-weather,
    -- T1GER_weathersync) and time still flickers/keeps snapping back to
    -- the server's real time, set these to that resource's own
    -- disable/enable-sync event names (check its docs/source):
    externalSyncDisableEvent = nil, -- e.g. 'my-weather:client:disable'
    externalSyncEnableEvent = nil,  -- e.g. 'my-weather:client:enable'
}

```

{% endtab %}
{% endtabs %}
