X3D Main Menu
A server main menu: system cards, announcements, timed events, an in-game guidebook and per-player settings. No database, any framework.
Buy / Get freeConfig builderInstallation
- Unzip and rename the folder to
x3d_menu(other scripts call the exports by this name) - Drop it in
resources/ - Add to
server.cfg:
ensure x3d_menu
No database, no framework, no license. Works on ESX, QBCore and standalone.
Open it with F4 or /menu.
Systems (the "Systems" tab)
Cards that fire another script's command. They come from Config.Systems (seeded once) or from the RegisterSystem export at runtime:
| field | what it does |
|---|---|
system_id |
Unique key — used for favourites and as the id /menusystem edit|remove looks up |
label / description / icon |
Card text and its heroicons icon |
command |
The command fired on click — omit it and the card is not clickable |
resource |
The resource this card belongs to — the card is hidden while it is not started (unless always_show) |
always_show |
Show regardless of the resource state |
category |
'system' (default) renders in the full grid; 'qol' renders in its own "convenience" grid. Anything else is rejected |
sort_order |
Ascending |
The command actually executed comes from the client's cache of what the server just sent — a command string posted by the NUI is never trusted.
Shipped examples: x3d_mailbox and x3d_dailylogin cards (hidden unless those resources are installed) and a working qol card that clears your waypoint.
Favourites (the star on a card) are per player and stored client-side.
Announcements and events (the "Home" tab)
Config.Announcements — cards with title, content, image_url, color, is_active, sort_order.
Config.Events — the banner at the top, with start_at / end_at as unix seconds; omit one side for no bound. An event outside its window never renders even with is_active = true (the server filters before sending), and the banner reads "starting", "N days left" or "running" from the current time.
Both are seeded from config once and edited afterwards with /menuannouncement and /menuevent.
Guidebook (the "Guide" tab)
Config.Guide — rows with category, title, content, image_url, is_active, sort_order. The panel groups by category and adds a search box. Edit with /menuguide.
Settings (the "Settings" tab)
Config.Settings is the spec of which controls to draw — not the player's values. Another resource can add one at runtime:
exports['x3d_menu']:RegisterSetting('General', {
id = 'show_minimap', label = 'Show minimap', type = 'toggle', default_value = true,
})
exports['x3d_menu']:RegisterSetting('Graphics', {
id = 'quality', label = 'Effect quality', type = 'select', default_value = 'high',
options = { { value = 'low', label = 'Low' }, { value = 'high', label = 'High' } },
})
type is 'toggle', 'select' or 'slider' (min/max for a slider). A duplicate id is rejected, and the reserved ids theme, custom_color and favorites cannot be registered over. A newly registered spec is broadcast to clients immediately so its default becomes readable without the player opening the panel.
In the panel itself: th/en language (kept in the browser's localStorage — it is the panel's own language, it never goes through the server), the peach/jade themes and a custom accent colour. Every player gets all of them; no theme is locked behind an item.
Where the data lives — two places, two jobs
Per player (theme, custom colour, toggle/select/slider values, favourites) — client-side only, in resource KVP, republished to LocalPlayer.state under x3dmenu:<id>. Another resource reads it server-side through the GetSetting export or Player(src).state['x3dmenu:<id>'] directly. It is nil until that player has a value for that id.
Admin-managed content (systems, announcements, events, guidebook) — server-side through the storage adapter: one JSON file (Config.Storage.File, default data/menu.json), loaded once at resource start, written back every Config.Storage.FlushSeconds, on player drop and on a graceful stop. A corrupt file is preserved as data/menu.broken.json and the store starts empty. Config.Systems / Config.Events / Config.Announcements / Config.Guide are imported into storage only the first time the resource runs — editing config.lua afterwards changes nothing, use the admin commands.
To use MySQL for the admin content instead (player data stays in client KVP either way):
- Run
install.sqlthrough oxmysql - Open
addons/storage/server.lua, scroll to the bottom and copy the oxmysql block over the fourStorage.Get/Set/All/Flushfunctions above it
Exports
-- a new system card — returns false on a duplicate system_id, an incomplete def,
-- or a category that is not 'system' or 'qol'
exports['x3d_menu']:RegisterSystem({
system_id = 'my_shop', label = 'Shop', description = 'Buy supplies',
icon = 'i-heroicons-shopping-cart', command = 'openmyshop', resource = 'my_shop',
always_show = false, is_active = true, sort_order = 5, category = 'system',
})
exports['x3d_menu']:RegisterSetting(section, def) --> boolean
exports['x3d_menu']:GetSetting(src, 'show_hud') --> the value, or nil if never set
Hook — server-internal, not a net event
AddEventHandler('x3d_menu:onSetting', function(src, id, value)
if id == 'show_hud' then
print(('player %s set show_hud = %s'):format(src, tostring(value)))
end
end)
It fires whenever any player changes anything under x3dmenu:*, including theme, custom_color and favorites, not only the ids declared in Config.Settings.
Admin commands
Four collections (systems, events, announcements, guide). Always available from the server console; an in-game player needs the ace in Config.AdminAce (default x3d_menu.admin):
/menusystem add <json> -- json needs system_id and label
/menusystem edit <system_id> <json>
/menusystem remove <system_id>
/menusystem list
/menuevent add <json> -- json needs title
/menuevent edit <id> <json>
/menuevent remove <id>
/menuevent list
/menuannouncement add <json> -- json needs title
/menuannouncement edit <id> <json>
/menuannouncement remove <id>
/menuannouncement list
/menuguide add <json> -- json needs title, content and category
/menuguide edit <id> <json>
/menuguide remove <id>
/menuguide list
add and edit validate the fields the panel indexes — a row missing one is refused and nothing is written. edit cannot move a row onto an id that already exists. remove/edit on an unknown id answer "not found" instead of throwing. list prints the collection's raw JSON and mutates nothing.
Safety, backed by tests
- A card's command comes from the server's own cache, never from a string the NUI posted
- Every admin row is validated before it is written, on
addand on the merged result of anedit - The reserved ids (
theme,custom_color,favorites) cannot be registered or written over, server-side and client-side - Every request answers, even when refused — one call per second per player per action, and a player's throttle buckets are cleared the moment they disconnect
- The storage key is always a stable identifier, never a server id
pip install lupa
python tools/lua-test/x3d_menu_check.py
Config (config.lua)
| Key | What it does |
|---|---|
Config.Locale |
Default language of the Lua-side strings (th/en) — separate from the panel's own language |
Config.Command / Config.Key |
The open command (/menu) and the key binding (F4) |
Config.AdminAce |
The ace required for /menusystem, /menuevent, /menuannouncement, /menuguide |
Config.Storage.File / .FlushSeconds |
Where the admin content is written and how often |
Config.SocialLinks |
The social links on the home tab |
Config.Systems / Config.Events / Config.Announcements / Config.Guide |
Seed data, imported into storage once |
Config.Settings |
The spec of the controls in the settings tab |
Not ported from the original
This is a dependency-free port, so the original's Discord avatar and image uploads, its buff display, its PVP-shard toggle and its item-gated themes are not here — each of them needed a resource this product deliberately does not require.
Support
Discord: discord.gg/x3d-developer · Web: x3d-developer.com