How Integrations Work#
Nothing inside the protected core of Billiards knows which framework you run. Money, permissions, names, notifications, the table prompt and the menu all go through small, unencrypted bridge files you can edit:
Layer | Supported | Without it |
|---|---|---|
Framework |
| Standalone: every table is free, and bets are neither taken nor paid. |
Money | Cash and bank of the detected framework | Your own functions in the config — see A Custom Currency. |
Notifications |
| ox_lib's notifications — it is required anyway. |
Table prompt |
| The built-in floating text above the table. |
Menus |
| Required. |
Database |
| Required. The only one. |
Every optional integration degrades silently. A missing resource makes that part fall back to the built-in version — nothing errors, and nothing needs to be switched off by hand.
Frameworks#
The framework is detected when the resource starts, by looking for a running es_extended, qb-core or qbx_core. Each table charges in money (cash) or bank, and bets use Config.Bot.betMethod:
money is the player's cash (getMoney, removeMoney, addMoney); bank is the bank account (getAccount('bank'), removeAccountMoney, addAccountMoney). The name other players see comes from xPlayer.getName(). Bridge file: server/custom/framework/esx.lua.
money is cash and bank is bank, through Player.Functions.GetMoney, RemoveMoney and AddMoney. The name other players see is the character's first and last name. Bridge file: server/custom/framework/qb.lua.
Qbox runs through the same QBCore bridge, using the qb-core compatibility that qbx_core provides — there is nothing to configure. Keep qbx_core started before qs-billiards.
No framework found: CanAfford always answers yes and no money moves, so every table is free and bets are neither taken nor paid. Names are the players' FiveM names. To charge players anyway, define the three functions in A Custom Currency.
A Custom Currency#
Entry fees and bets only ever ask three things: can this player afford it, take it, give it. Define those three in config/config.lua and they replace the framework's money everywhere:
-- config/config.lua
-- method is what is being paid in: the table's payment method for an entry fee,
-- Config.Bot.betMethod for a bet. Both are 'money' or 'bank'.
Config.CanAfford = function(src, amount, method)
return exports['my-coins']:GetCoins(src) >= amount
end
Config.RemoveMoney = function(src, amount, method)
exports['my-coins']:RemoveCoins(src, amount)
end
Config.AddMoney = function(src, amount, method)
exports['my-coins']:AddCoins(src, amount)
endBecause method is passed in, one set of functions can treat fees and stakes differently — for example, leave your tables on money, set Config.Bot.betMethod = 'bank', and pay every 'bank' amount in casino chips inside your functions. They only ever run on the server.
Define all three together. A bet is taken with
RemoveMoneybefore the game and paid out withAddMoneywhen it ends; with only the first two, stakes are taken and winners are never paid.
Notifications, the Prompt and the Menu#
client/custom/framework.lua holds three functions. Replace the body of any of them to use your own resource:
Function | Called |
|---|---|
| For every message — fouls, results, payments, the creator. |
| Every frame the [E] prompt shows over a free table. Uses qs-textui when it is running, otherwise draws the text itself. |
| To open the Pool menu. |
-- client/custom/framework.lua — okokNotify instead of ox_lib
function Notification(msg, type)
local kind = (not type or type == 'inform' or type == 'info') and 'info' or type
exports['okokNotify']:Alert('Pool', msg, 3000, kind)
endThe status panel, the waiting panel, the power bar, Match Settings and the join invitation are part of Billiards' own interface. They follow the language you pick and do not depend on any other resource.
Permissions#
Who may open the creator is one function, HasPermission(src), in each file of server/custom/framework/. By default it checks the ACE named in Config.Creator.permission:
-- server/custom/framework/esx.lua
function HasPermission(src)
local perm = Config.Creator and Config.Creator.permission or 'command.poolcreator'
return IsPlayerAceAllowed(src, perm)
endNext to it, GetCharacterName(src) decides the name shown on the join invitation and in Waiting for <name>... — the character name on ESX and QBCore, and the FiveM name whenever it returns nothing.
Map Tables#
GTA V already has pool tables — prop_pooltable_02 and prop_pooltable_3b — and many maps and MLOs place them in bars and clubs. With Config.AutoDetect = true, every player looks for the nearest table of each model in Config.TableModels within 60 m, every 1.5 seconds, and registers it with the server. A room full of map tables fills in as players walk through it.
Config.AutoDetect = true
Config.TableModels = { `prop_pooltable_02`, `prop_pooltable_3b` } -- each needs a Config.Surfaces entryMap tables use the prop already in the map, charge Config.PlayPrice in cash and offer every enabled mode. They live in the server's memory only and come back each time a player loads in. To give one its own name, price or modes, open it in the creator and save it.
Maps often decorate their tables with a cue, a triangle or a loose cue ball. Config.HideWorldProps hides those models within 1.6 m of every table, so nothing sits on the cloth:
Config.HideWorldProps = {
enabled = true,
radius = 1.6,
models = { `prop_pool_cue`, `prop_pool_tri`, `prop_poolball_cue` },
}Adding a Table Model#
Any pool-table prop can be played on once Billiards knows where its cloth is. A model with no entry in Config.Surfaces is ignored everywhere — it never spawns and never becomes playable. If the model is not part of GTA V, stream it from your own resource first.
Describe its cloth
Add an entry to
Config.Surfaces.offsetis the distance from the prop's origin to the centre of the cloth;halfis half the width and half the length of the playable area, in metres. Start from the values of the shipped tables.lua Config.Surfaces = { [`prop_pooltable_02`] = { offset = vec3(-0.060, 0.130, 0.900), half = vec2(0.790, 1.380) }, [`prop_pooltable_3b`] = { offset = vec3(0.000, 0.000, 0.900), half = vec2(0.790, 1.380) }, [`my_pool_table`] = { offset = vec3(0.000, 0.000, 0.900), half = vec2(0.790, 1.380) }, }Offer it
Add it to
Config.Creator.modelsto see it in the creator, and toConfig.TableModelsif copies of it in your maps should be picked up.lua Config.Creator = { permission = 'command.poolcreator', models = { { hash = `prop_pooltable_02`, label = 'Classic Green Pool Table' }, { hash = `prop_pooltable_3b`, label = 'Classic Purple Pool Table' }, { hash = `my_pool_table`, label = 'Tournament Table' }, }, -- ... }Calibrate it
Place one, then follow Calibrating a Custom Table:
/pooleditand/pooltuneprint the exactoffsetandhalfto paste back in.
Languages#
Forty-one complete translations ship in locales/, and one setting picks the language of the notifications, the prompt and the whole interface — the creator, Match Settings and the join invitation:
-- ar, bg, bn, ca, cs, da, de, el, en, es, et, fa, fi, fr, he, hi, hr, hu, id, it, ja,
-- ko, lt, lv, ms, nl, no, pl, pt, ro, ru, sk, sl, sv, sw, th, tr, uk, ur, vi, zh
Config.Locale = 'en'Any text can be reworded in locales/<code>.json, under its billard section. A code that does not exist falls back to English, and so does any key missing from your file — the console names each one, so a partial translation is easy to finish.



