Developer Guide
For scripters extending the resource or adapting it to a framework fork.
On the RedM Escrow edition the editable surface is config/, locales/, server/frameworks/ and the web overlay source — which is enough for everything on this page except reading the protected Lua. The Open Source edition ships the full codebase.
Architecture
One sentence per piece:
| Piece | Job |
|---|---|
shared/protocol.lua | Every event name, RPC name, refusal code and NUI message shape. Both sides load the same file, so a name cannot drift |
server/bridge.lua | The only file that touches a framework; picks the flavour, provides the RPC layer, notices, inventory verbs, webhooks |
server/frameworks/*.lua | One adapter per framework, all exposing the same surface |
server/registry.lua | Deck records and the database |
server/warden.lua | Authority ("does this deck answer to you?") and presence ("are you actually there?") |
server/turntable.lua | The single authority on sessions and queues. Every session carries a serial, so stale reports can't touch the track that replaced theirs |
server/librarian.lua | oEmbed title lookup (cached) and thumbnails |
server/boot.lua | Wires the above to the net; every handler runs exists → near → obeys → act |
client/atlas.lua | The client's copy of the deck map |
client/streamer.lua | Spawns and removes cabinet props by distance |
client/jukebox.lua | Mirrors sessions to the overlay; 4 Hz distance mixer |
client/prompts.lua · rigger.lua · panel.lua | Prompts, the carry-and-place mode, and the one file that owns NUI focus |
web/src/ | React overlay. audio/engine.js does the actual streaming, outside the React tree so closing the panel never kills audio |
The design rule worth keeping if you extend it
The client never decides anything. Its authority checks are cosmetic — which prompts light up — and the server re-judges everything from scratch.
The RPC layer
The resource ships its own ask/answer mechanism so it works identically on every framework:
- The client sends
(ticket, ...)on the RPC's event name and blocks on a promise. - The server replies once on the shared answer event with the same ticket.
- An unanswered ask resolves to
nilafter 10 seconds instead of hanging.
See Util.ask on the client and Bridge.rpc on the server.
Adapting a framework fork
server/frameworks/ contains one file per framework. Each registers a factory whose build() returns this surface — and build() is the only place framework exports are touched:
character(src) -> { identifier, charId, job, firstname, lastname } | nil
notify(src, text, ms)
holding(src, item) -> number
roomFor(src, item, amount) -> boolean
take(src, item, amount) -> boolean -- false when they don't have it
give(src, item, amount)
usable(item, handler) -> false when the hook can't be reached
(the bridge then binds the place command)If your fork renamed an export or an event, the flavour file for your framework is the only file to touch.
Returning false from usable — or letting it error — makes the bridge fall back to the /phonograph command automatically.
Rebuilding the overlay
Only needed if you change anything under web/src/:
cd web
npm install
npm run buildThe build lands in web/dist/, which is what fxmanifest.lua serves. Requires Node 18+.
Re-skinning
Panel art lives in web/src/art/:
| File | What it is |
|---|---|
panel-bg.png | The leather cabinet background. Stretched 100%/100%, so keep the frame near the edges |
vinyl.png | The idle disc. Square, disc filling the frame, circular alpha |
vinyl-brass.png | The playing disc, crossfaded in over the idle one |
tonearm.png | Transparent cutout. The pivot ball must sit near the top — CSS rotates around 50% 8% |
emblem.png | The round badge in the header |
Fonts are in web/src/fonts/ — Cinzel and IM Fell English, both OFL-licensed. Colours and layout are CSS variables at the top of web/src/styles.css.
Swap files under the same names and rebuild. Different formats work if you update the imports in styles.css / Deck.jsx.
Adding records
- Register a new inventory item in your framework.
- Add an entry under the same name in
config/records.lua. - Drop a
<item_name>.pngicon into your inventory's image folder.
No code changes — the shelf, queue, webhooks and validation pick the new record up automatically.
Adding cabinets
Add an entry to config/cabinets.lua with its own item, prop and carry offsets, register the item as usable, and it works everywhere the brass one does.
Cabinet keys are permanent
The config key is stored in the database per placed deck. Renaming one orphans every deck already placed with it.
NUI protocol
If you modify the overlay: every message between Lua and React carries the envelope
{ channel = 'rm_phonograph', kind, body }The channel filter lets the overlay share a window with other resources.
The full vocabulary — kinds, callback names, panel ops and payload shapes — is documented in shared/protocol.lua, which is the file to update first when you extend the wire.