Docs

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 builder

Installation

  1. Unzip and rename the folder to x3d_menu (other scripts call the exports by this name)
  2. Drop it in resources/
  3. 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):

  1. Run install.sql through oxmysql
  2. Open addons/storage/server.lua, scroll to the bottom and copy the oxmysql block over the four Storage.Get/Set/All/Flush functions 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 add and on the merged result of an edit
  • 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

View product page