Configuration
General Configuration
lib.locale('en')
cfg = {}
-- ============================================================================
-- rm_racing — server configuration
--
-- Sections (in order):
-- 1. Framework & dependencies — qbox/qb/esx/standalone, ox_*, etc.
-- 2. Tablet & UI — /racing command, item gating, map tile
-- 3. Economy — money type used for prizes / fees
-- 4. User tiers & permissions — racer/creator/master/god + flag map
-- 5. Player profile defaults — pictures, picker palettes, name change cost
-- 6. ELO rating — Arpad K-factor + display tiers
-- 7. Race engine — countdown, grid layout, DNF timeout
-- 8. Race types — sprint/circuit/drift/etc.
-- 9. Drift scoring — points-per-second, combo, penalties
-- 10. Tracks — creator rules + tags + placeable objects
-- 11. Competitions — auto schedule, prize pool, vehicle classes
-- 12. Pink slips (1v1 vehicle wager) — allowed types, redeem locations
-- 13. Bounties (time-trial chase) — interval, prize, target formula
-- 14. Item payouts — item rolls per finishing position
-- 15. 3D GPS route renderer — line/arrow style, lookahead, sizing
-- 16. Discord webhooks — notifications channel + colors
-- 17. Limits — page sizes / row caps
-- ============================================================================
-- ============================================================================
-- 1. FRAMEWORK & DEPENDENCIES
-- ============================================================================
---@type 'auto' | 'qbox' | 'qb' | 'esx' | 'standalone'
cfg.framework = 'auto'
-- Standalone fallback (no qbox/qb/esx). license identifiers become the
-- citizenid and money uses a virtual wallet in `rm_racing_wallet` instead
-- of a framework account.
cfg.standalone = {
initialBalance = 10000, ---@type number -- credits new players start with
currencyLabel = 'credits', ---@type string -- displayed in modals / tooltips (informational only)
-- DEV ONLY: when true, the standalone bridge suffixes the citizenid with
-- the player's server id so two FiveM clients on the same license get
-- distinct profiles. Each session is treated as a separate player — new
-- profile row, new wallet with initialBalance. TURN OFF in production
-- or every reconnect orphans the previous profile.
perSessionId = false, ---@type boolean
}
---@type 'auto' | 'ox_target' | 'qb-target' | false
cfg.target = 'auto'
---@type 'rm_minigames' | 'ox_lib' | 'qb-minigames' | 'ps-ui' | 'bl_ui' | false
cfg.minigame = 'ox_lib'
---@type 'ox_lib' | 'qb' | 'esx' | 'okokNotify' | 'ps-ui'
cfg.notification = 'ox_lib'
---@type 'rm_racing' | 'ox_lib' | 'esx' | 'qb' | 'okokTextUI' | 'jg-textui'
-- 'rm_racing' = built-in NUI hint (RaceHud-styled, auto-chips for [KEY]).
-- Switch to a third-party system if you want it to match the rest of your
-- server's UI instead.
cfg.textUI = 'rm_racing'
---@type 'ox_lib' | 'qb' | 'esx' | false
cfg.progressbar = 'ox_lib'
---@type 'auto' | false | 'qb-vehiclekeys' | 'wasabi_carlock' | 'qs-vehiclekeys' | 'cd_garage' | 'Renewed-Vehiclekeys' | 'okokGarage' | 't1ger_keys' | 'MrNewbVehicleKeys'
cfg.vehiclelock = 'auto'
---@type false | 'default_dispatch' | 'cd_dispatch' | 'qs-dispatch' | 'ps-dispatch' | 'rcore_dispatch' | 'sonoran_cad' | 'origen_police' | 'redutzu-mdt' | 'lb-tablet' | 'core_dispatch' | 'tk_dispatch' | 'l2s-dispatch'
cfg.dispatch = false
-- Visual style for the in-world `[E] interact` prompt (used by some bridges).
cfg.interaction = {
controlId = 38,
text = 'E',
colors = {
background = { r = 241, g = 93, b = 56, a = 255 },
text = { r = 226, g = 232, b = 240, a = 255 },
},
}
-- Police count gating (kept for parity with other resources; unused here unless > 0).
cfg.requiredPoliceCount = 0
cfg.enableOldMethodForPoliceCount = false
-- ============================================================================
-- 2. TABLET & UI
-- ============================================================================
-- Chat command that opens the racing tablet. `/racing` by default.
cfg.command = 'racing' ---@type string
-- Inventory item that opens the racing tablet when used. Set to `false` to
-- disable item-gating (the /racing command always works).
cfg.tabletItem = 'rm_racing_tablet' ---@type string|false
-- NUI-specific UI tweaks. The TrackInfoPage's Leaflet preview renders a
-- GTA V tile background. Default points at the Rockstar Social Club tile
-- server (stable, dark styled). Set `tileUrl = false` for a blank
-- background that only shows the track polyline.
cfg.ui = {
tileUrl = 'https://s.rsg.sc/sc/images/games/GTAV/map/game/{z}/{x}/{y}.jpg', ---@type string|false
}
-- ============================================================================
-- 3. ECONOMY
-- ============================================================================
-- Money type used for entry fees and prizes.
cfg.moneyType = 'bank' ---@type 'cash' | 'bank'
-- ============================================================================
-- 4. USER TIERS & PERMISSIONS
--
-- Tier hierarchy is by INDEX, NOT by ELO threshold. Index 1 = highest;
-- the last entry is the default for everyone without an override. ELO-based
-- auto-assignment uses `min` (highest tier whose min the player meets). A
-- tier override on profile.tier (set via /createracinguser) wins over ELO.
-- ============================================================================
---@type { name: string, min: number }[]
cfg.tiers = {
{ name = 'God', min = 99999 }, -- never auto-assigned, must be granted
{ name = 'Master', min = 1850 },
{ name = 'Creator', min = 1700 },
{ name = 'Racer', min = 0 },
}
-- For each capability, the MINIMUM tier required. Anything at or above the
-- listed tier in cfg.tiers (lower index = higher tier) passes.
cfg.permissions = {
join = 'Racer', ---@type string -- join races / lobbies
create = 'Racer', ---@type string -- start a race (lobby or solo)
createTrack = 'Creator', ---@type string -- save a new track via the editor
createCrew = 'Racer', ---@type string -- found a crew
control = 'Master', ---@type string -- moderate own creations (delete tracks, etc.)
controlAll = 'God', ---@type string -- moderate everyone's creations
handleBounties = 'Master', ---@type string -- re-roll / cancel active bounties
runAdminCommands = 'God', ---@type string -- /createracinguser, /removeallracetracks etc.
}
-- ============================================================================
-- 5. PLAYER PROFILE DEFAULTS
-- ============================================================================
cfg.profile = {
pictureCount = 8, ---@type number -- number of presets in /web/public/profiles/1..N.png
checkpointColors = { 'orange', 'red', 'blue', 'navy', 'green', 'yellow', 'purple', 'white' },
gpsColors = { 'orange', 'red', 'blue', 'navy', 'green', 'yellow', 'purple', 'white' },
-- 3D checkpoint visual styles. 'classic' = the original tall translucent
-- pillar + sphere top. 'highway' = highway-style billboard sign on a
-- vertical pole, distance label updating live. 'nav' = GPS-style nav
-- panel (distance + direction arrow + street name from
-- GetStreetNameAtCoord). Persisted on profile.checkpoint_style
-- (default 'classic').
checkpointStyles = { 'classic', 'highway', 'nav' },
-- Default for the "show my 3D PlayerCard above my head to other
-- players" privacy preference. Each player can flip it individually
-- in their profile; new players inherit this default. Saved on
-- profile.card_visible (1 = visible, 0 = hidden).
cardVisibleByDefault = true,
nameChangeCost = 1000,
}
-- Crew emblem allow-list. Must mirror `web/src/constants/crewEmblems.ts`
-- exactly — server validates the picked emblem against this list before
-- inserting the crew, so a new icon there needs a new entry here too.
cfg.crew = {
emblems = {
'flag', 'trophy', 'crown', 'bolt', 'flame',
'skull', 'shield', 'swords', 'target', 'anchor',
'compass', 'gem', 'ghost', 'rocket', 'star',
'wrench', 'cog', 'eye', 'heart', 'crosshair',
},
}
-- ============================================================================
-- PlayerCard — in-world 3D nameplates rendered above racers / nearby players
--
-- Powered by rm_stream (DUI projected onto a billboard marker). The same
-- React PlayerCard component used in the tablet renders standalone via
-- `build/index.html?card=1&...`; one stream per visible player, attached
-- to their vehicle (preferred) or ped fallback. Distance-faded.
-- ============================================================================
cfg.playerCard = {
enabled = true, ---@type boolean
duringRace = true, ---@type boolean -- show above each racer's vehicle in-race
outOfRace = true, ---@type boolean -- show above nearby players' peds in free-roam
-- Render & culling. renderDistance is the same value rm_stream uses to
-- skip drawing — keep it tight to limit DUI overdraw on busy servers.
renderDistance = 50.0, ---@type number -- metres
maxConcurrent = 10, ---@type integer -- hard cap; closest-N kept, rest hidden
scanIntervalMs = 1000, ---@type integer -- free-roam scan cadence
-- Server-side profile cache TTL (seconds). A nearby-scan returns up to
-- maxConcurrent player summaries; cache means repeated scans don't slam
-- the DB if the same players hang around.
profileCacheSec = 30, ---@type number
-- Vertical offset above the attach entity. Vehicle uses model height +
-- this; ped uses ~head height + this. Tune per server vehicle pool.
vehicleOffsetZ = 0.8, ---@type number
pedOffsetZ = 0.6, ---@type number -- on top of head bone (already ~head height)
-- Card billboard sizing (passed to rm_stream). Width/height drive the
-- DUI texture resolution; scale is the world-space size of the marker
-- (1.0 ≈ 1 metre tall before distance falloff).
width = 600, ---@type integer -- DUI texture px
height = 200, ---@type integer
scale = 0.8, ---@type number -- world units
}
-- ============================================================================
-- 6. ELO RATING
-- ============================================================================
cfg.elo = {
initial = 1500, ---@type number -- starting rating for new players
kFactor = 32, ---@type number -- rating volatility (chess uses 10-40)
minParticipants = 1, ---@type number -- minimum racers required for ELO to apply
-- Display tiers shown on profiles / leaderboards (separate from cfg.tiers
-- which gates permissions). Purely cosmetic ranks based on ELO bands.
tiers = { ---@type { name: string, min: number }[]
{ name = 'Racer', min = 0 },
{ name = 'Host', min = 1600 },
{ name = 'Alien', min = 1850 },
},
}
-- ============================================================================
-- 7. RACE ENGINE
-- ============================================================================
cfg.race = {
countdownSec = 5, ---@type number -- 3-2-1-go countdown length
dnfTimeoutSec = 100, ---@type number -- racer kicked if still running this long after first finish
checkpointArriveDistance = 3.5, ---@type number -- on-foot trigger distance; vehicles use cp.radius
-- Players must be within this distance of cp1 when /start fires, otherwise
-- the race rejects start with a "too far from grid" modal.
maxDistanceFromGrid = 20.0,
-- Starting-grid layout. Racers are placed in a `columns` × ceil(N/columns)
-- grid behind cp1, all facing toward cp2.
grid = {
columns = 2, ---@type number -- racers per row
columnSpacing = 4.0, ---@type number -- m between side-by-side slots
rowSpacing = 5.0, ---@type number -- m between rows
backOffset = 4.0, ---@type number -- m added to cp1.radius for the front row
},
}
-- ============================================================================
-- 8. RACE TYPES
--
-- circuit = true → loops, supports laps
-- drift = true → drift scoring is the win condition
-- timeTrial → solo runs only, no lobby
-- elimination → last-place dropped each lap
-- ============================================================================
---@type { id: string, label: string, circuit: boolean, drift: boolean, timeTrial?: boolean, elimination?: boolean }[]
cfg.raceTypes = {
{ id = 'sprint', label = 'Sprint', circuit = false, drift = false },
{ id = 'circuit', label = 'Circuit', circuit = true, drift = false },
{ id = 'drift', label = 'Drift', circuit = false, drift = true },
{ id = 'drift_challenge', label = 'Drift challenge', circuit = true, drift = true },
{ id = 'time_trial', label = 'Time trial', circuit = false, drift = false, timeTrial = true },
{ id = 'elimination', label = 'Elimination', circuit = true, drift = false, elimination = true },
}
-- ============================================================================
-- 9. DRIFT SCORING
-- Used by drift & drift_challenge race types.
-- ============================================================================
cfg.drift = {
pointsPerSecond = 8, -- baseline points for sustained drift
angleThreshold = 12.0, -- degrees; below this, no drift
minSpeedMps = 10.0, -- metres/sec; below this, no drift
wallHitPenalty = 500, -- points lost when hitting a wall/entity mid-drift
comboMultiplierStep = 0.25, -- +25% points per sustained second up to max
comboMultiplierMax = 3.0,
tickMs = 100, -- scoring tick interval
}
-- ============================================================================
-- 10. TRACKS
-- ============================================================================
-- Creator rules — bounds enforced server-side at saveTrack.
cfg.track = {
minCheckpoints = 2,
maxCheckpoints = 300,
maxObjects = 600,
checkpointDefaultRadius = 15.0,
checkpointMinRadius = 10.0,
checkpointMaxRadius = 50.0,
creatorMoneyCost = 5000,
nameMaxLen = 40,
descriptionMaxLen = 140,
objectPlacementDistance = 3.5, ---@type number -- m in front of player for object preview spawn (fallback when raycast misses)
objectYawStep = 15.0,
}
-- Track tags shown on track cards.
cfg.trackTags = {
newTrackDays = 7, ---@type number -- track <= N days old → 'New'
popularMinTimesLastWeek = 25, ---@type number -- ≥ N races last week → 'Popular'
legendaryMinTimesDriven = 200, ---@type number -- ≥ N total races → 'Legendary'
}
-- "Author" of seed-tracks.sql tracks (creator_cid = ''). Substituted
-- server-side in decorateTrack so the listing UI doesn't fall back to
-- "Unknown" + default avatar. picture accepts:
-- - a numeric preset id '1'..'8' (web/public/profiles/N.png)
-- - a relative file ending in .png/.webp (web/public/<file>, e.g.
-- 'eyescoloured.png' which ships alongside the NUI bundle)
-- - a full https:// URL (any image hostable to FiveM CEF)
cfg.seedTracks = {
creatorName = 'rm_racing',
creatorPicture = 'eyescoloured.png',
}
cfg.trackObjects = {
-- ============ Flags ============
{ id = 'flag_red', label = 'Red flag', model = 'prop_flagpole_2b' },
{ id = 'flag_blue', label = 'Blue flag', model = 'prop_flagpole_2c' },
{ id = 'flag_checkered', label = 'Checkered flag', model = 'prop_flagpole_2d' },
{ id = 'flag_beach_red', label = 'Beach flag (black)', model = 'prop_beachflag_01' },
{ id = 'flag_beach_blue', label = 'Beach flag (green)', model = 'prop_beachflag_02' },
-- ============ Tyres (single / pile) ============
{ id = 'tyre', label = 'Tyre', model = 'prop_wheel_tyre' },
{ id = 'tyre_stack', label = 'Tyre stack', model = 'prop_offroad_tyres02' },
{ id = 'tyre_offroad', label = 'Offroad tyre', model = 'prop_offroad_tyres01_tu' },
{ id = 'tyre_offroad_open', label = 'Offroad tyres (open)', model = 'prop_offroad_tyres01' },
{ id = 'tyre_snow', label = 'Snow tyre', model = 'prop_snow_tyre_01' },
{ id = 'tyre_industrial_1', label = 'Industrial tyre #1', model = 'v_ind_cm_tyre01' },
{ id = 'tyre_industrial_3', label = 'Industrial tyre #3', model = 'v_ind_cm_tyre03' },
{ id = 'tyre_industrial_6', label = 'Industrial tyre #6', model = 'v_ind_cm_tyre06' },
{ id = 'tyre_industrial_7', label = 'Industrial tyre #7', model = 'v_ind_cm_tyre07' },
{ id = 'tyre_procedural', label = 'Procedural tyre', model = 'ng_proc_tyre_01' },
{ id = 'tyre_junk_pile', label = 'Junk tyre cluster', model = 'v_46_cm_tyres' },
{ id = 'tyre_carware_pile', label = 'Carware tyre rack', model = 'imp_carwarecw_tyres' },
{ id = 'tyre_sea_pile', label = 'Sea tyre pile', model = 'ch1_12_sea_tyrepileb' },
{ id = 'tyre_shelves', label = 'Tyre shelves', model = 'v_71_tyreshelves' },
{ id = 'tyre_hoop_atomic', label = 'Atomic hoop tyre', model = 'stt_prop_hoop_tyre_01a' },
{ id = 'tyre_arrow', label = 'Arrow tyre', model = 'xs_prop_arrow_tyre_01a' },
{ id = 'tyre_gate_arch', label = 'Tyre gate arch', model = 'xs_prop_gate_tyre_01a_wl'},
-- ============ Tyre walls (basic) ============
{ id = 'tyre_wall', label = 'Tyre wall', model = 'prop_tyre_wall_03b' },
{ id = 'tyre_wall_short', label = 'Tyre wall (short)', model = 'prop_tyre_wall_01' },
{ id = 'tyre_wall_rolled', label = 'Tyre wall (rolled)', model = 'prop_tyre_wall_03' },
{ id = 'tyre_wall_mid', label = 'Tyre wall (mid)', model = 'prop_tyre_wall_03c' },
{ id = 'tyre_wall_yellow', label = 'Tyre wall (yellow)', model = 'prop_tyre_wall_04' },
{ id = 'tyre_wall_heavy', label = 'Tyre wall (heavy)', model = 'prop_tyre_wall_05' },
{ id = 'tyre_wall_curved', label = 'Tyre wall (curved)', model = 'xs_prop_wall_tyre_l_01a' },
{ id = 'tyre_wall_u_curve', label = 'Tyre wall (U-curve)', model = 'tr_prop_tr_tyre_wall_u_l'},
-- ============ Stunt tyre walls — straight (sponsor liveries) ============
{ id = 'stt_wall_basic', label = 'Stunt wall #1', model = 'stt_prop_tyre_wall_01' },
{ id = 'stt_wall_shitzu', label = 'Shitzu wall', model = 'stt_prop_tyre_wall_02' },
{ id = 'stt_wall_sprunk', label = 'Sprunk wall', model = 'stt_prop_tyre_wall_03' },
{ id = 'stt_wall_xero', label = 'Xero wall', model = 'stt_prop_tyre_wall_04' },
{ id = 'stt_wall_pisswasser', label = 'Pißwasser wall', model = 'stt_prop_tyre_wall_05' },
{ id = 'stt_wall_burgershot', label = 'Burger Shot wall', model = 'stt_prop_tyre_wall_06' },
{ id = 'stt_wall_cheetah', label = 'Cheetah wall', model = 'stt_prop_tyre_wall_07' },
{ id = 'stt_wall_globeoil', label = 'Globe Oil wall', model = 'stt_prop_tyre_wall_08' },
{ id = 'stt_wall_vapid', label = 'Vapid wall', model = 'stt_prop_tyre_wall_09' },
{ id = 'stt_wall_chevron', label = 'Chevron wall (black)', model = 'stt_prop_tyre_wall_010' },
{ id = 'stt_wall_patriot', label = 'Patriot wall', model = 'stt_prop_tyre_wall_011' },
{ id = 'stt_wall_pisswasser2', label = 'Pißwasser wall #2', model = 'stt_prop_tyre_wall_012' },
{ id = 'stt_wall_imponte_p', label = 'Imponte wall (purple)', model = 'stt_prop_tyre_wall_013' },
{ id = 'stt_wall_imponte_b', label = 'Imponte wall (black)', model = 'stt_prop_tyre_wall_014' },
{ id = 'stt_wall_fukaru_f', label = 'Fukaru F wall', model = 'stt_prop_tyre_wall_015' },
{ id = 'stt_wall_atomic_g', label = 'Atomic wall (green)', model = 'stt_prop_tyre_wall_016' },
{ id = 'stt_wall_pisswasser3', label = 'Pißwasser wall #3', model = 'stt_prop_tyre_wall_017' },
{ id = 'stt_wall_imponte_w', label = 'Imponte wall (white)', model = 'stt_prop_tyre_wall_018' },
-- ============ Stunt tyre walls — curved (L = left bend, R = right bend) ============
{ id = 'stt_curve_l3', label = 'Fukaru curve (L)', model = 'stt_prop_tyre_wall_0l3' },
{ id = 'stt_curve_r3', label = 'Fukaru curve (R)', model = 'stt_prop_tyre_wall_0r3' },
{ id = 'stt_curve_r1', label = 'Tyre wall curve (R)', model = 'stt_prop_tyre_wall_0r1' },
{ id = 'stt_curve_l2', label = 'Sprunk curve (L)', model = 'stt_prop_tyre_wall_0l2' },
{ id = 'stt_curve_l04', label = 'Xero curve (L)', model = 'stt_prop_tyre_wall_0l04' },
{ id = 'stt_curve_r04', label = 'Xero curve (R)', model = 'stt_prop_tyre_wall_0r04' },
{ id = 'stt_curve_l05', label = 'Sprunk curve (L)', model = 'stt_prop_tyre_wall_0l05' },
{ id = 'stt_curve_r05', label = 'Atomic curve (R)', model = 'stt_prop_tyre_wall_0r05' },
{ id = 'stt_curve_l06', label = 'Auto Exotic curve (L)', model = 'stt_prop_tyre_wall_0l06' },
{ id = 'stt_curve_r06', label = 'Pißwasser curve (R)', model = 'stt_prop_tyre_wall_0r06' },
{ id = 'stt_curve_l07', label = 'Fukaru curve (L)', model = 'stt_prop_tyre_wall_0l07' },
{ id = 'stt_curve_r07', label = 'Vapid curve (R)', model = 'stt_prop_tyre_wall_0r07' },
{ id = 'stt_curve_l08', label = 'Cheetah curve (L)', model = 'stt_prop_tyre_wall_0l08' },
{ id = 'stt_curve_r08', label = 'Cheetah curve (R)', model = 'stt_prop_tyre_wall_0r08' },
{ id = 'stt_curve_r09', label = 'Globe Oil curve (R)', model = 'stt_prop_tyre_wall_0r09' },
{ id = 'stt_curve_l010', label = 'Globe Oil curve (L)', model = 'stt_prop_tyre_wall_0l010'},
{ id = 'stt_curve_r010', label = 'Vapid curve (R)', model = 'stt_prop_tyre_wall_0r010'},
{ id = 'stt_curve_l012', label = 'Vapid curve (L)', model = 'stt_prop_tyre_wall_0l012'},
{ id = 'stt_curve_r012', label = 'Xero curve (R)', model = 'stt_prop_tyre_wall_0r012'},
{ id = 'stt_curve_l013', label = 'Junk Energy curve (L)', model = 'stt_prop_tyre_wall_0l013'},
{ id = 'stt_curve_r013', label = 'Nagasaki curve (R)', model = 'stt_prop_tyre_wall_0r013'},
{ id = 'stt_curve_l014', label = 'Cheetah curve (L)', model = 'stt_prop_tyre_wall_0l014'},
{ id = 'stt_curve_r014', label = 'Xero curve (R)', model = 'stt_prop_tyre_wall_0r014'},
{ id = 'stt_curve_l015', label = 'Nagasaki curve (L)', model = 'stt_prop_tyre_wall_0l015'},
{ id = 'stt_curve_r015', label = 'Burger Shot curve (R)', model = 'stt_prop_tyre_wall_0r015'},
{ id = 'stt_curve_l016', label = 'Atomic curve (L)', model = 'stt_prop_tyre_wall_0l016'},
{ id = 'stt_curve_l16', label = 'Fukaru green curve', model = 'stt_prop_tyre_wall_0l16' },
{ id = 'stt_curve_l017', label = 'Pißwasser curve (L)', model = 'stt_prop_tyre_wall_0l017'},
{ id = 'stt_curve_r017', label = 'Fukaru curve (R)', model = 'stt_prop_tyre_wall_0r017'},
{ id = 'stt_curve_l018', label = 'Imponte curve (L)', model = 'stt_prop_tyre_wall_0l018'},
{ id = 'stt_curve_r018', label = 'Nagasaki curve (R)', model = 'stt_prop_tyre_wall_0r018'},
{ id = 'stt_curve_l019', label = 'Fukaru curve (L)', model = 'stt_prop_tyre_wall_0l019'},
{ id = 'stt_curve_r019', label = 'Fukaru curve (R)', model = 'stt_prop_tyre_wall_0r019'},
{ id = 'stt_curve_l020', label = 'Fukaru F curve (L)', model = 'stt_prop_tyre_wall_0l020'},
-- ============ Chevron / direction walls (Tuner DLC arrows + lit panels) ============
{ id = 'chevron_lit', label = 'Chevron wall (lit)', model = 'sum_prop_ac_tyre_wall_lit_01' },
{ id = 'chevron_lit_l', label = 'Chevron wall lit (L)', model = 'sum_prop_ac_tyre_wall_lit_0l1' },
{ id = 'chevron_lit_r', label = 'Chevron wall lit (R)', model = 'sum_prop_ac_tyre_wall_lit_0r1' },
{ id = 'chevron_arrow_l', label = 'Chevron arrow (L)', model = 'sum_prop_ac_tyre_wall_u_l' },
{ id = 'chevron_arrow_r', label = 'Chevron arrow (R)', model = 'sum_prop_ac_tyre_wall_u_r' },
{ id = 'pit_arrow_l', label = 'PITS arrow (L)', model = 'sum_prop_ac_tyre_wall_pit_l' },
{ id = 'pit_arrow_r', label = 'PITS arrow (R)', model = 'sum_prop_ac_tyre_wall_pit_r' },
-- ============ Special ============
{ id = 'wall_nitrous', label = 'Nitrous boost wall', model = 'm24_2_prop_m42_tyre_wall_nos' },
-- ============ Cones / barriers / barrels / bales ============
{ id = 'cone', label = 'Traffic cone', model = 'prop_mp_cone_01' },
{ id = 'barrier', label = 'Barrier', model = 'prop_mp_barrier_02' },
{ id = 'barrel', label = 'Barrel', model = 'prop_barrel_02a' },
{ id = 'hay_bale', label = 'Hay bale', model = 'prop_offroad_bale01'},
}
-- ============================================================================
-- 11. COMPETITIONS
-- Auto-generated competition lobbies + manual competition rules.
-- ============================================================================
cfg.competition = {
-- Local server time HH:MM. Set to {} to disable the auto-scheduler.
autoSchedule = {
'12:00',
'16:00',
'19:00',
'22:00',
},
lobbyDurationSec = 900, ---@type number -- 15 min lobby before scheduler auto-starts (or auto-cancels if empty)
minRatingToHost = 1700, ---@type number -- min ELO to start a manual competition
prizePoolPerPlayer = 2500, ---@type number -- added to pool for each joined racer
-- Percent per finishing position; must sum to 100.
prizeDistribution = {
50, 25, 15, 10,
},
-- Vehicle classes pickable in lobby setup. id = -1 means "any class".
vehicleClasses = { ---@type { id: number, label: string }[]
{ id = -1, label = 'Any' },
{ id = 0, label = 'Compacts' },
{ id = 1, label = 'Sedans' },
{ id = 2, label = 'SUVs' },
{ id = 3, label = 'Coupes' },
{ id = 4, label = 'Muscle' },
{ id = 5, label = 'Sports Classics' },
{ id = 6, label = 'Sports' },
{ id = 7, label = 'Super' },
},
defaultMaxPlayers = 8,
phasingDefault = true,
-- Slipstream (drafting) — flips the engine's built-in slipstream
-- physics for the duration of the race via SetEnableVehicleSlipstreaming.
-- Global toggle (not per-vehicle), called once on start and once on end.
slipstreamDefault = false,
-- Private routing bucket — when on, race start moves every participant
-- (and their vehicle) into an isolated server bucket so non-racers in
-- the public world don't collide / desync with the race. Race end
-- restores everyone to bucket 0. Voice does NOT respect buckets —
-- proximity voice still bleeds through.
privateBucketDefault = false,
}
-- ============================================================================
-- 12. PINK SLIPS — wager your vehicle ownership in a 1v1 race.
--
-- The losing driver's vehicle gets transferred to the winner via the
-- framework bridge (qb/qbx → player_vehicles, esx → owned_vehicles).
-- Standalone has no inventory of vehicles, so the toggle is hidden in
-- the lobby UI when running standalone.
-- Claims are pending until the winner visits a redeemLocations entry and
-- interacts there — physical delivery, not a silent DB swap.
-- ============================================================================
cfg.pinkSlips = {
enabled = true,
-- Race types this flag CAN be applied to. Drift / elimination / TT are
-- excluded — pink slips need a clean position-based winner.
allowedTypes = { 'sprint', 'circuit' }, ---@type string[]
-- Forfeit window: if either player DNFs, the race still resolves and
-- the surviving racer claims. Set false to require both to finish.
awardOnDnf = true,
-- Where the winner picks up the vehicle. Each entry produces a blip
-- and a small interaction zone. Add as many as you like.
redeemLocations = {
{ x = 100.0, y = -1080.0, z = 29.2, label = 'Pink Slip Garage (Mission Row)' },
},
redeemBlip = {
sprite = 357, ---@type number
color = 1, ---@type number -- 1 = red
scale = 0.85,
label = 'Pink Slip Redeem',
},
}
-- ============================================================================
-- 13. BOUNTIES — time-trial chase prizes.
--
-- A scheduled job rolls one active bounty at a time. When a player runs
-- the bountied track in a Time Trial mode and finishes UNDER the
-- target_ms, they claim the prize. Bounties expire after `expireMin` if
-- unclaimed.
-- ============================================================================
cfg.bounties = {
enabled = true,
intervalMin = 30, ---@type number -- new roll every N minutes (also re-rolls if previous claimed/expired)
expireMin = 240, ---@type number -- auto-expire after N minutes (0 = never)
prize = 75000, ---@type number -- credits paid to the claimer
eligibleTrackTypes = { 'time_trial', 'sprint', 'circuit' }, ---@type string[]
requireRank = false, ---@type string|false -- e.g. 'Master' to gate claims; false = open
minResultsOnTrack = 0, ---@type number -- 0 = never block on race count (random track gets picked even on fresh DBs)
-- Target time formula. Bounty is computed as `best_ms * targetMult + random(extraMs)`.
-- 1.0 + small variance keeps it tight for top players; pad it (e.g. 1.10 →
-- 10% slower than best) for casual servers.
targetMult = 1.05,
extraMs = { min = 0, max = 3000 },
-- Fallback target for tracks with NO best time yet (fresh server / no
-- results on the picked track). Target = checkpoints_count × this value.
-- ~5 s per CP is a generous ballpark; tweak if your tracks are unusually
-- short or long.
fallbackMsPerCp = 5000,
}
-- ============================================================================
-- 14. ITEM PAYOUTS
--
-- Item rewards distributed alongside money prizes when a race finishes.
-- Each `lists.<name>` is a weighted pool: an entry's `weight` is the chance
-- relative to the sum of weights in its list; `count` is rolled uniformly
-- in [min, max]. payoutStyle picks WHO gets a roll:
-- 'all' → every finisher gets one roll
-- 'topThree' → positions 1..3 each roll
-- 'onlyOne' → only the winner rolls
-- 'custom' → uses `perPosition[pos]` to pick which list(s) to roll
-- ============================================================================
cfg.itemPayouts = {
enabled = true, ---@type boolean -- enable when item names below exist in your inventory
defaultList = 'standard', ---@type string -- list rolled for non-custom styles
payoutStyle = 'topThree', ---@type 'all'|'topThree'|'onlyOne'|'custom'
minRaceLengthSec = 10, ---@type number -- skip payouts for short fluke races
onlyCompetitions = false, ---@type boolean -- if true, only ranked competitions drop items
---@type table<string, { item: string, weight: number, count: { min: number, max: number } }[]>
lists = {
standard = {
{ item = 'water', weight = 8, count = { min = 1, max = 3 } },
{ item = 'sandwich', weight = 6, count = { min = 1, max = 2 } },
{ item = 'lockpick', weight = 2, count = { min = 1, max = 1 } },
},
rare = {
{ item = 'lockpick', weight = 4, count = { min = 1, max = 2 } },
{ item = 'advancedlockpick', weight = 1, count = { min = 1, max = 1 } },
},
},
-- Only used when payoutStyle = 'custom'.
---@type table<integer, string[]>
perPosition = {
[1] = { 'rare', 'standard' },
[2] = { 'standard' },
[3] = { 'standard' },
},
}
-- ============================================================================
-- 15. 3D GPS ROUTE RENDERER
--
-- During a race the upcoming checkpoint polyline is drawn in 3D world
-- space so the player can follow the route without taking their eyes off
-- the road to glance at the minimap. Color is per-player (profile.gps_color)
-- and style is per-player (profile.gps_route_type). Set `enabled = false`
-- to disable the entire renderer.
-- ============================================================================
cfg.gps3d = {
enabled = true,
-- Number of upcoming checkpoints to render ahead of the player. 1 = next
-- one only; higher = full path preview (more drawcalls per frame).
lookahead = 4,
-- Default style; each player can override via profile.gps_route_type:
-- 'off' → nothing drawn
-- 'line' / 'arrow'→ legacy DrawLine-based renderer (no YTD needed)
-- any preset id → textured ribbon using the asset in stream/ (chevrons.ytd)
defaultStyle = 'classic',
-- Lift the line a hair above the road so it doesn't z-fight on inclines
-- but stays close to the surface — the car effectively drives over it
-- instead of under a floating ribbon.
heightOffset = 0.1,
-- Chevron arrow spacing (m) and dimensions for the LEGACY DrawLine path.
arrowSpacing = 6.0,
arrowLength = 1.6,
arrowWidth = 0.9,
-- Textured ribbon (preset) parameters. The YTD ships in stream/ — its
-- internal texture-dictionary name is what RequestStreamedTextureDict
-- expects (usually 'chevrons' even if the .ytd file was renamed).
textureDict = 'routes',
ribbonWidth = 1.35, -- half-width of the ribbon (m), so total = 2.7m
ribbonLift = 0.05, -- extra Z offset on top of heightOffset
ribbonRepeatM = 4.0, -- texture repeats every N metres along the path
-- Preset list. `id` is what gets stored in profile.gps_route_type;
-- `texture` is the entry inside textureDict to sample. Order here drives
-- the order shown in the profile picker.
--
-- Per-preset `repeatM` overrides the global ribbonRepeatM. Per-preset
-- `tintable = false` skips the player's gps_color tint and draws the
-- texture with white modulation, preserving the colors baked into the
-- texture itself. Per-preset `alpha` overrides the default 230 — the
-- chevron_line textures have an opaque white background in their alpha
-- channel, so we drop alpha to fade the slab and keep the chevrons
-- the dominant element.
presets = {
{ id = 'classic', label = 'Classic', texture = 'chevrons', repeatM = 4.0, tintable = true },
{ id = 'chevron_line_06', label = 'Line 06', texture = 'chevron_line_06', repeatM = 0.8, tintable = false, alpha = 130 },
{ id = 'chevron_line_07', label = 'Line 07', texture = 'chevron_line_07', repeatM = 0.8, tintable = false, alpha = 130 },
{ id = 'chevron_line_08', label = 'Line 08', texture = 'chevron_line_08', repeatM = 0.8, tintable = false, alpha = 130 },
{ id = 'chevron_fire_01', label = 'Fire', texture = 'chevron_fire_01', repeatM = 4.0, tintable = true },
{ id = 'chevron_ice_01', label = 'Ice', texture = 'chevron_ice_01', repeatM = 4.0, tintable = true },
{ id = 'chevron_neon_01', label = 'Neon', texture = 'chevron_neon_01', repeatM = 4.0, tintable = true },
},
}
-- ============================================================================
-- 16. DISCORD WEBHOOKS — config moved to server/webhook.lua
--
-- The webhook URL is sensitive credential data and must NOT live in cfg.lua,
-- which is shipped/escrowed as part of the resource. server/webhook.lua is
-- listed in escrow_ignore so customers can edit the URL after install
-- without seeing the rest of the source. Set `WEBHOOK_URL` at the top of
-- that file.
-- ============================================================================
-- ============================================================================
-- 17. LIMITS — page sizes / row caps
-- ============================================================================
cfg.limits = {
tracksPerRequest = 500,
leaderboardSize = 100,
recentEventsCount = 10,
recentRacesCount = 100,
trackBestResults = 10,
}
Heist Configuration
Last updated