Skip to content

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:

PieceJob
shared/protocol.luaEvery event name, RPC name, refusal code and NUI message shape. Both sides load the same file, so a name cannot drift
server/bridge.luaThe only file that touches a framework; picks the flavour, provides the RPC layer, notices, inventory verbs, webhooks
server/frameworks/*.luaOne adapter per framework, all exposing the same surface
server/registry.luaDeck records and the database
server/warden.luaAuthority ("does this deck answer to you?") and presence ("are you actually there?")
server/turntable.luaThe single authority on sessions and queues. Every session carries a serial, so stale reports can't touch the track that replaced theirs
server/librarian.luaoEmbed title lookup (cached) and thumbnails
server/boot.luaWires the above to the net; every handler runs exists → near → obeys → act
client/atlas.luaThe client's copy of the deck map
client/streamer.luaSpawns and removes cabinet props by distance
client/jukebox.luaMirrors sessions to the overlay; 4 Hz distance mixer
client/prompts.lua · rigger.lua · panel.luaPrompts, 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 nil after 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:

lua
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/:

bash
cd web
npm install
npm run build

The build lands in web/dist/, which is what fxmanifest.lua serves. Requires Node 18+.

Re-skinning

Panel art lives in web/src/art/:

FileWhat it is
panel-bg.pngThe leather cabinet background. Stretched 100%/100%, so keep the frame near the edges
vinyl.pngThe idle disc. Square, disc filling the frame, circular alpha
vinyl-brass.pngThe playing disc, crossfaded in over the idle one
tonearm.pngTransparent cutout. The pivot ball must sit near the top — CSS rotates around 50% 8%
emblem.pngThe 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

  1. Register a new inventory item in your framework.
  2. Add an entry under the same name in config/records.lua.
  3. Drop a <item_name>.png icon 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

js
{ 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.

Documentation for RedMorrow. Scripts are licensed per server — redistribution is not permitted.