F5 StudioF5 Studio
Skip to main content

Framework Compatibility

F5 Combat HUD talks to your framework through a bridge. Everything framework-specific lives in bridge/; the rest of the resource only ever calls Bridge.*. The bridge asks your framework for exactly five things:

WhatUsed for
The core objectReaching everything below
Who the player isThe key every setting, profile and K/D row is saved under
Whether the character is in the worldLoading the HUD at the right moment
Which group a player belongs toMarketplace moderation and per-group section overrides
The name of a weaponThe weapon label in the kill feed

Nothing else in the resource touches your framework.

The bridge is open

bridge/ ships as editable source — it is not protected. The custom adapter described below is the no-code path; for anything more exotic you can edit the bridge files directly. See Editing the Bridge Directly.

Supported Frameworks

FrameworkDetection resourcemode value
QBox Coreqbx_core'qbx'
QBCoreqb-core'qb'
ESXes_extended'esx'
A fork of any of the three'custom'

Auto-Detection

config/main.lua
F5Cfg.Framework = {
mode = 'auto', -- 'auto' | 'qb' | 'qbx' | 'esx' | 'custom'
}

With mode = 'auto' the bridge takes the first started resource in this order:

  1. qbx_coreqbx
  2. qb-coreqb
  3. es_extendedesx

A resource counts as running when its state is started or starting.

Why QBox wins the tie

Many QBox servers still keep qb-core around for older scripts. Checking qbx_core first means such a server is read as QBox rather than QBCore — which is what you want, because the two cores expose different APIs.

If nothing matches, the resource does not run:

[f5_combathud][bridge] no supported framework detected (qbx_core, qb-core, es_extended) — set F5Cfg.Framework.mode in config/main.lua
[f5_combathud] no supported framework detected — see above for details

Forcing a Framework

Set mode explicitly when the guess is wrong:

config/main.lua
F5Cfg.Framework = { mode = 'qb' }

Two things to know about a forced mode:

  • An unknown value is rejected with F5Cfg.Framework.mode = "..." is not one of: auto, qb, qbx, esx, custom (config/main.lua), and the resource refuses to start.

  • If the named framework's resource is not running, the bridge prints a warning and still uses that adapter:

    [f5_combathud][bridge] F5Cfg.Framework.mode = "esx", but resource es_extended is not running — start it before f5_combathud, or set mode = 'auto'

    Every call then fails to reach a core, which surfaces as cannot reach the es_extended core object — make sure that resource starts before f5_combathud.

The core is resolved lazily

The bridge does not grab the core object at startup — it resolves it on first use and caches it. A framework that finishes starting after f5_combathud still works, as long as it is up before the first player loads.

Per-Framework Behaviour

ConcernQBCore (qb)QBox (qbx)ESX (esx)
Core objectexports['qb-core']:GetCoreObject()exports.qbx_coreexports['es_extended']:getSharedObject()
Player objectCore.Functions.GetPlayer(src)core:GetPlayer(src)ESX.GetPlayerFromId(src)
Character idPlayerData.citizenidPlayerData.citizenidxPlayer.identifier
Display namecharinfo.firstname + lastnamecharinfo.firstname + lastnamexPlayer.getName()
Character loaded (client)LocalPlayer.state.isLoggedIn, else citizenidLocalPlayer.state.isLoggedIn, else citizenidESX.IsPlayerLoaded(), else ESX.PlayerLoaded
Loaded / unload eventsQBCore:Client:OnPlayerLoaded / QBCore:Client:OnPlayerUnloadQBCore:Client:OnPlayerLoaded / QBCore:Client:OnPlayerUnloadesx:playerLoaded / esx:onPlayerLogout
Group checkACE group.<name>ACE group.<name>xPlayer.getGroup(), then ACE group.<name>
Weapon labelCore.Shared.Weapons[hash]core:GetWeapons() (cached once)ESX.GetWeaponFromHash(hash)

When the framework has no entry for a weapon, the kill feed still works: the icon comes from config/weapons.lua, and the weapon text falls back to the weapon's spawn name. With the shipped defaults that difference is invisible — the feed shows the icon and no text unless a player turns on Show weapon name, or the icon PNG is missing.

QBox caches the weapon list

The QBox adapter calls GetWeapons() once and keeps the result for the lifetime of the client session. If you add weapons to QBox at runtime, a player has to reconnect (or you restart the resource) before the kill feed knows their labels.

The Save Identifier

Every setting, profile, K/D row, marketplace listing and ban is stored under the player's native FiveM license, not a framework field:

license:110000112345678

The bridge takes license and falls back to license2; only if the client has neither does it fall back to the framework's own field (PlayerData.license on QB/QBox, xPlayer.license then xPlayer.identifier on ESX).

That has three consequences worth knowing:

  • Nothing moves when you switch framework. The same player keeps their crosshair, profiles and K/D on QBCore, QBox and ESX alike.
  • One HUD per account. All of a player's characters share the same crosshair, the same profiles and the same K/D — a crosshair is a client preference, not roleplay property.
  • A player without a license identifier cannot be served. Every save and load resolves the player through the bridge first and returns early when it comes back empty.
ESX character id is only ever printed

xPlayer.identifier (and citizenid on QB/QBox) is carried as charId and used only in Discord webhook messages, never as a database key.

Custom Framework Adapter

mode = 'custom' is for a fork of one of the three supported cores — a renamed core with the same semantics. You name the framework yours derives from, then override only what your fork actually changed. Anything left nil keeps the base's behaviour, and most forks rename nothing but their core resource — that is getCore on its own.

config/main.lua
F5Cfg.Framework = {
mode = 'custom',
custom = {
base = 'qb', -- REQUIRED: 'qb' | 'qbx' | 'esx'

getCore = nil, -- function() return exports['my-core']:GetCoreObject() end

-- Server
getPlayer = nil,
identifierOf = nil,
charIdOf = nil,
nameOf = nil,
hasGroup = nil,

-- Client
isPlayerLoaded = nil,
weaponInfo = nil,

-- Lifecycle
playerLoadedEvent = nil,
playerUnloadEvent = nil,
onPlayerLoaded = nil,
onPlayerUnload = nil,
},
}
getCore runs on both sides

config/main.lua is a shared script, so every function you write in custom is compiled into the client and the server. getCore is called by both — a version that uses a server-only export or native will break the HUD on the client while the server looks fine. getPlayer, identifierOf, charIdOf, nameOf and hasGroup are only ever called on the server; isPlayerLoaded and weaponInfo only on the client.

base is mandatory

[f5_combathud][bridge] F5Cfg.Framework.custom.base = "nil" is not one of: qb, qbx, esx — a custom framework must name the one it derives from (config/main.lua)

Without a valid base there is no framework at all and the resource refuses to start. base decides which adapter file loads, which events are wired, and how a player object is read.

The shipped config already carries base = 'qb', so this error only appears if you delete the line. Switching to mode = 'custom' for an ESX or QBox fork and forgetting to change base inherits QBCore behaviour silently — check it first.

The contract

FieldSideSignatureLeft nil
getCorebothfunction() -> coreThe base's own core export is used. A function that errors or returns nil logs F5Cfg.Framework.custom.getCore failed or returned nil, and does not fall back to the base getter
getPlayerserverfunction(src) -> raw player, or nil when not loadedThe base's player lookup is used
identifierOfserverfunction(raw, src) -> stringNative license → license2 → the base's field. This is the database key
charIdOfserverfunction(raw, src) -> stringThe base's character id. Only ever printed to Discord
nameOfserverfunction(raw, src) -> stringThe base's character name. Shown on marketplace listings
hasGroupserverfunction(src, group) -> booleanThe base's group check. Drives ModeratorAces and SectionOverrides
isPlayerLoadedclientfunction() -> booleanThe base's check — true once the character is in the world
weaponInfoclientfunction(hash) -> { label = , name = } or nilThe base's weapon lookup. label is the kill feed text, name picks the icon
playerLoadedEventclientstringThe base's event name is registered
playerUnloadEventclientstringThe base's event name is registered
onPlayerLoadedclientfunction(emit)Not used — the event above is registered instead
onPlayerUnloadclientfunction(emit)Not used — the event above is registered instead

The return value of getPlayer is fed through the base's normalizer, so a fork that keeps PlayerData.charinfo gets charId and name for free; only what your fork renamed needs its own function. Whatever identifierOf returns is trusted as-is, and when it returns nil the bridge still falls back to the native license — a fork can never accidentally save under an empty key.

Lifecycle

A fork that only renamed the two client events gives their names:

config/main.lua
custom = {
base = 'qb',
playerLoadedEvent = 'mycore:client:OnPlayerLoaded',
playerUnloadEvent = 'mycore:client:OnPlayerUnload',
}

A fork that also changed what those events pass wires the handler itself and calls emit():

config/main.lua
custom = {
base = 'qb',
onPlayerLoaded = function(emit)
AddEventHandler('mycore:characterSpawned', function(_, _) emit() end)
end,
onPlayerUnload = function(emit)
AddEventHandler('mycore:characterDespawned', function() emit() end)
end,
}

When onPlayerLoaded / onPlayerUnload are given, the base registers nothing — the event name field for that side is ignored. Both hooks are client-side only.

Examples

A fork that only renamed its core resource
config/main.lua
F5Cfg.Framework = {
mode = 'custom',
custom = {
base = 'qb',
getCore = function() return exports['my-core']:GetCoreObject() end,
},
}
A fork with its own permission system
config/main.lua
F5Cfg.Framework = {
mode = 'custom',
custom = {
base = 'qbx',
getCore = function() return exports['my-core'] end,
hasGroup = function(src, group)
return exports['my-perms']:PlayerHasGroup(src, group)
end,
},
}

hasGroup fully replaces the base check — the ACE fallback is gone, so return true for every group you want to count, including your own ACEs.

An ESX fork that stores identity elsewhere
config/main.lua
F5Cfg.Framework = {
mode = 'custom',
custom = {
base = 'esx',
getPlayer = function(src) return exports['my-core']:GetPlayer(src) end,
identifierOf = function(raw, src) return raw.licenseId end,
nameOf = function(raw) return raw.displayName end,
},
}
Changing identifierOf re-keys everything

The identifier is the primary key of every table. Point it at a different field and every existing player looks brand new — old settings, profiles, K/D, listings and bans stay in the database under the old key and are never read again. Decide this before launch.

Groups and Permissions

Bridge.HasGroup(src, group) is what backs F5Cfg.ModeratorAces and F5Cfg.SectionOverrides:

  • QBCore / QBox — the ACE group.<name> only.
  • ESX — the group on the character first (xPlayer.getGroup() == name), then the ACE group.<name>.

Moderation additionally accepts the bare ACE itself (command.combathud_admin), so the shipped list works on all three frameworks without editing. See Permissions.

The Callback Layer

Client and server talk over the bridge's own request/response pair — f5_combathud:bridge:cbReq and f5_combathud:bridge:cbRes — rather than a framework's callback API. That is why the resource needs no QBCore.Functions.CreateCallback equivalent on ESX.

A request that gets no answer within 60 seconds is dropped and logged:

[f5_combathud][bridge] server callback timed out after 60s: f5_combathud:server:getData

In practice this only shows up when the server side stopped (an unfinished database migration, a stopped resource) — the menu will look like it is hanging on load.

Editing the Bridge Directly

bridge/ is yours. The layout:

FileContains
bridge/init.luaDetection, the callback layer, lifecycle plumbing, shared helpers
bridge/client/qb.lua, qbx.lua, esx.luaClient adapters — one guard line, then the framework calls
bridge/server/qb.lua, qbx.lua, esx.luaServer adapters — player normalization and group checks
bridge/client/custom.lua, bridge/server/custom.luaApply the custom overrides on top of the base

Each adapter begins with if Bridge.Base ~= 'qb' then return end, so only the one matching your framework ever runs. If you edit an adapter, keep the shape of what it returns — GetPlayer must return a table with src, identifier, charId, name and raw, and GetWeaponInfo a table with label and name (or nil).

Prefer the config adapter

An edited bridge file is yours to re-apply after every update. Everything in the table above can be expressed through F5Cfg.Framework.custom instead, which survives updates untouched.

See Also