How Integrations Work#
Air Hockey talks to the rest of your server through a handful of small, open files: server/custom/framework/ for money, permissions and names, and client/custom/framework.lua for notifications and the prompt. Everything else is detected on its own:
Layer | Supported | Without it |
|---|---|---|
Framework |
| Runs standalone: nobody is charged, and players are listed by their FiveM name. |
Money | Cash and bank on ESX, QBCore and Qbox | Every table is free to play. |
Custom currency |
| The framework's cash and bank. |
Permissions | ACE — | Nobody can open the creator. |
Notifications |
|
|
3D prompt |
| Built-in 3D text over the table. |
Map tables | Any | Only tables from the creator and the config. |
Languages | 41 locale files | English. |
Every optional integration degrades silently. A missing resource makes its part fall back to the built-in behaviour — nothing errors and nothing needs to be switched off by hand.
Frameworks#
The framework is picked from whichever of these is already started when Air Hockey starts:
Resource | Mode |
|---|---|
|
|
|
|
|
|
None of them |
|
Each mode has one file in server/custom/framework/ — esx.lua, qb.lua and standalone.lua — and only the one matching your server runs. Each defines the same four functions:
Function | Decides |
|---|---|
| Whether a player can pay a table's entry fee — checked before a game against the bot, when a challenge is sent and when it is accepted. |
| Takes the entry fee. |
| Who may open the creator and create, move or delete tables. |
| The name shown in the opponent list and in the challenge window: the character's name on ESX, QBCore and Qbox, the FiveM name standalone. |
method is the table's payment method from the creator — 'money' for cash or 'bank' — and maps to cash and the bank account on every framework.
A Custom Currency#
Entry fees can be paid in something other than money — casino chips, a VIP coin, arcade tokens. Define these two functions in config/config.lua and every framework file hands the decision to them, standalone included:
-- config/config.lua
Config.CanAfford = function(src, amount, method)
return exports.ox_inventory:GetItemCount(src, 'casino_chips') >= amount
end
Config.RemoveMoney = function(src, amount, method)
exports.ox_inventory:RemoveItem(src, 'casino_chips', amount)
endThey are only ever called on the server, and never for a free table. method is still passed, so you can use the table's Cash / Bank choice to pick between two currencies of your own.
Define both together. One without the other checks one currency and takes another. Air Hockey never pays anything out, so there is no third function to write.
Permissions#
HasPermission checks the ace in Config.Creator.permission — command.ahcreator unless you set your own. To let your framework's admin groups in as well, extend it in the matching file:
-- server/custom/framework/esx.lua
function HasPermission(src)
local xPlayer = ESX.GetPlayerFromId(src)
if xPlayer and (xPlayer.getGroup() == 'admin' or xPlayer.getGroup() == 'superadmin') then
return true
end
local perm = Config.Creator and Config.Creator.permission or 'command.ahcreator'
return IsPlayerAceAllowed(src, perm)
end-- server/custom/framework/qb.lua
function HasPermission(src)
if QBCore.Functions.HasPermission(src, 'admin') then
return true
end
local perm = Config.Creator and Config.Creator.permission or 'command.ahcreator'
return IsPlayerAceAllowed(src, perm)
endThe same check opens the creator, and the server runs it again for every create, move and delete — a modified client cannot change tables. The debug tools are the exception: they are gated only by Config.Debug.
Notifications and the Prompt#
Two functions in client/custom/framework.lua decide how Air Hockey looks next to the rest of your HUD:
Function | What it does | Default |
|---|---|---|
| Every message a player sees. |
|
| The [E] - Play Air Hockey prompt over a table. |
|
To use another notification system, replace the body of Notification. For example, with QBCore's own notifications:
-- client/custom/framework.lua
function Notification(msg, kind)
local types = { inform = 'primary', success = 'success', error = 'error' }
TriggerEvent('QBCore:Notify', msg, types[kind] or 'primary')
endAir Hockey Tables on Maps#
Many arcades and MLOs already have GTA V's air hockey table. With Config.AutoDetect = true, every player's game looks for the nearest one within Config.StreamDistance every two seconds and makes it playable — no placing, no config line. A see-through table, such as the creator's preview, is never picked up.
The models live in the config. If your map uses a different table, set it in Config.TableModel — the creator places the same model — and calibrate Config.Table for it as shown in Usage.
Config.TableModel = joaat('prop_airhockey_01')
-- Puck: tried in order, the first that exists in the game is used
Config.PuckModels = {
joaat('proair_hoc_puck'),
joaat('prop_air_hoc_puck_01'),
joaat('prop_air_hoc_puck'),
}
Config.PaddleModels = {
[1] = joaat('prop_air_hoc_paddle_01'), -- red (the solo player / the challenger)
[2] = joaat('prop_air_hoc_paddle_02'), -- blue (the bot / the challenged player)
}If none of the puck models exists in your game, the puck is invisible — /ahcheck shows which ones are valid. See Common Errors.
Languages#
41 translations ship in locales/: Arabic (ar), Bulgarian (bg), Bengali (bn), Catalan (ca), Czech (cs), Danish (da), German (de), Greek (el), English (en), Spanish (es), Estonian (et), Persian (fa), Finnish (fi), French (fr), Hebrew (he), Hindi (hi), Croatian (hr), Hungarian (hu), Indonesian (id), Italian (it), Japanese (ja), Korean (ko), Lithuanian (lt), Latvian (lv), Malay (ms), Dutch (nl), Norwegian (no), Polish (pl), Portuguese (pt), Romanian (ro), Russian (ru), Slovak (sk), Slovenian (sl), Swedish (sv), Swahili (sw), Thai (th), Turkish (tr), Ukrainian (uk), Urdu (ur), Vietnamese (vi) and Chinese (zh). Pick one with:
Config.Locale = 'es'One file translates the notifications and the whole interface, the creator included. To adjust a sentence, edit locales/<code>.json and restart the resource. Any key missing from your language falls back to English, and a code with no file falls back to English entirely, with a warning in the F8 console.



