# Southwest Roleplay — FiveM Server This resource is a TypeScript FiveM port of the Southwest Roleplay game-server (originally written for RageMP). It shares the same database layer, REST API, and feature module structure as the original. The goal is a 1-to-1 feature parity port: one feature at a time, minimal API surface changes. --- ## Table of Contents 1. [Prerequisites](#prerequisites) 2. [Getting Started](#getting-started) 3. [Environment Variables](#environment-variables) 4. [Project Structure](#project-structure) 5. [Architecture Overview](#architecture-overview) 6. [Build System](#build-system) 7. [Boot Sequence](#boot-sequence) 8. [Key Systems](#key-systems) 9. [REST API](#rest-api) 10. [Porting a Feature](#porting-a-feature) 11. [RageMP → FiveM Cheatsheet](#ragemp--fivem-cheatsheet) 12. [Porting Status](#porting-status) --- ## Prerequisites | Tool | Version | |---|---| | Node.js | 18+ | | npm | 8+ | | MySQL | 8.0 | | Redis | 6+ | | FiveM server artifact | build 7290+ | --- ## Getting Started ```bash # 1. Install dependencies npm install # 2. Build both server and client bundles npm run build # 3. Drop the compiled resource into your FiveM server resources folder # and add `ensure swrp-fivem-server` to server.cfg ``` For active development, use watch mode — both bundles rebuild automatically on save: ```bash npm run watch ``` Lint the TypeScript source: ```bash npm run lint ``` --- ## Environment Variables All variables have sensible defaults for the dev environment. Override them in your FiveM `server.cfg` using `set` convars, or export them in your shell when running outside FiveM. | Variable | Default | Description | |---|---|---| | `DB_HOST` | `play.southwest-roleplay.dev` | MySQL host | | `DB_PORT` | `3306` | MySQL port | | `DB_USER` | `username` | MySQL user | | `DB_PASS` | *(see `database.ts`)* | MySQL password | | `DB_NAME` | `game` | MySQL database name | | `REDIS_HOST` | `swrp-redis` | Redis host | | `REDIS_PORT` | `6379` | Redis port | | `REDIS_URL` | *(unset)* | Full Redis URL (overrides host/port) | | `LSRAGE_MONGO_QUEUE` | `mongo:jobs` | Redis list key used for the Mongo write queue | | `SWRP_UCP_URL` | `http://127.0.0.1` | Comma-separated allowed origins for the REST API CORS policy | > **Note:** The database does not auto-migrate (`synchronize: false`). Schema migrations are managed separately. --- ## Project Structure ``` fivem-server/ ├── fxmanifest.lua # FiveM resource manifest ├── package.json ├── webpack.server.js # Server bundle config (Node target) ├── webpack.client.js # Client bundle config (browser target) ├── gulpfile.js # Orchestrates webpack via gulp ├── tsconfig.server.json ├── tsconfig.client.json ├── ormconfig.js # TypeORM CLI config (uses compiled entities) ├── PORTING.md # Feature porting checklist and guide │ ├── src/ │ ├── server/ │ │ ├── index.ts # Entry point — boot sequence │ │ ├── core/ │ │ │ ├── mp-shim.ts # Global `mp` object (RageMP compat layer) │ │ │ ├── database.ts # TypeORM / MySQL │ │ │ ├── redis.ts # Redis client │ │ │ ├── mongo.ts # Mongo write queue (via Redis list) │ │ │ ├── preload.ts # Eager DB cache (game entity types) │ │ │ ├── feature-system.ts # Base class for all feature modules │ │ │ ├── commands/ # Command system + guards │ │ │ ├── log/ # Structured logging + log events │ │ │ ├── rpc/ # Bidirectional client ↔ server RPC │ │ │ ├── player/ # PlayerWrapper + player registry │ │ │ └── vehicle/ # VehicleWrapper + vehicle registry │ │ ├── entities/ # TypeORM entities (207 tables) │ │ ├── features/ # Feature modules (one folder per feature) │ │ └── rest/ # Express REST API (external service hooks) │ │ │ ├── client/ │ │ ├── index.ts # Client entry point │ │ ├── core/ │ │ │ ├── mp-shim.ts # Client-side `mp` compat layer │ │ │ └── rpc/ # Client RPC (call server procs) │ │ └── features/ # Ported client features │ │ │ └── shared/ │ └── const/ # Constants shared between server and client │ ├── dist/ # Compiled output (gitignored, produced by build) │ ├── server/index.js │ └── client/index.js │ └── ui/ # Pre-built Vue NUI (served as the resource's ui_page) ``` --- ## Architecture Overview ``` ┌──────────────────────────────────────────────────┐ │ FiveM Server │ │ │ │ ┌──────────────┐ ┌──────────────────────────┐ │ │ │ mp shim │ │ FeatureSystem modules │ │ │ │ (global mp) │ │ (player, login, chat…) │ │ │ └──────┬───────┘ └──────────────────────────┘ │ │ │ │ │ ┌──────▼───────┐ ┌──────────────────────────┐ │ │ │ CommandSystem│ │ REST API (Express :3001) │ │ │ └──────────────┘ └──────────────────────────┘ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │ │ │ MySQL │ │ Redis │ │ Mongo queue │ │ │ │ (TypeORM)│ │ (cache) │ │ (via Redis) │ │ │ └──────────┘ └──────────┘ └────────────────┘ │ └──────────────────────────────────────────────────┘ ▲ net events / RPC ▼ ┌──────────────────────────────────────────────────┐ │ FiveM Client │ │ │ │ ┌──────────────┐ ┌────────────────────────┐ │ │ │ mp shim │ │ Feature modules │ │ │ │ (global mp) │ │ (login, vehicle, …) │ │ │ └──────────────┘ └────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ NUI (Vue — ui/index.html) │ │ │ └──────────────────────────────────────────┘ │ └──────────────────────────────────────────────────┘ ``` ### The `mp` compatibility shim Both server and client expose a global `mp` object that mirrors the RageMP API. This means ported feature files compile and run with minimal changes. Functions that have no FiveM equivalent are stubbed and log a `console.warn` so they are easy to find. Key mappings: | Concept | RageMP | FiveM (via shim) | |---|---|---| | Player state | `player.setVariable(k, v)` | FiveM StateBag | | Server → client event | `player.call('event', [...])` | `TriggerClientEvent` | | Client → server event | `mp.events.callRemote('event')` | `TriggerServerEvent` | | Server proc (RPC) | `mp.events.addProc('name', fn)` | `rpc.register('name', fn)` | | Config values | `mp.config.key` | `GetConvar('key', ...)` | --- ## Build System Webpack bundles the TypeScript source into two CommonJS files that FiveM loads at runtime: | Bundle | Entry | Output | Notes | |---|---|---|---| | Server | `src/server/index.ts` | `dist/server/index.js` | `target: node`; npm deps are external | | Client | `src/client/index.ts` | `dist/client/index.js` | Runs inside FiveM's V8 sandbox | npm packages are left as external `require()` calls in the server bundle — FiveM resolves them from the resource's `node_modules/` directory at runtime. Source maps are emitted alongside each bundle for stack-trace readability. --- ## Boot Sequence `src/server/index.ts` orchestrates startup in this order: 1. **`mp-shim`** — must be the first import; builds the global `mp` object before anything else. 2. **`databaseSystem.init()`** — opens the MySQL connection (retries up to 5 times). 3. **`mongoSystem.init()`** — verifies the Redis-backed Mongo write queue is available. 4. **`redisSystem.init()`** — connects to Redis and flushes all keys. 5. **`preloadSystem.init()`** — eager-loads game entity types from the DB into memory. 6. **Parallel features** — `featuresParallel` array; all `init()` calls run concurrently. 7. **Series features** — `featuresSeries` array; each `init()` runs in order (use for features with dependencies). 8. **`postInit` pass** — optional second-pass for cross-feature wiring. 9. **`commandSystem.init()`** — collects commands from all features and registers them with FiveM's `RegisterCommand()`. To add a newly-ported feature to the boot sequence, import it in `src/server/index.ts` and push it onto `featuresParallel` or `featuresSeries`. --- ## Key Systems ### FeatureSystem Every feature extends `FeatureSystem`: ```ts import { FeatureSystem } from '../../core/feature-system'; class MyFeature extends FeatureSystem { commands = [MyCommand]; // optional — registers with CommandSystem async init(): Promise { await super.init(); // set up event handlers, load DB data, etc. } async postInit(): Promise { // cross-feature wiring that requires other features to be initialised } } export const myFeature = new MyFeature(); ``` ### CommandSystem Commands are classes that extend `BaseCommand`. They declare their name, parameters, guards, and a `run()` method. The system validates parameters automatically and runs guards before `run()`. Guards live in `src/server/core/commands/guards/` and return `true` on success or an error string on failure. Available guards: - `PermissionsCommandGuard` — checks player permission group - `PlayerStateCommandGuard` — checks alive/dead state - `PrisonCommandGuard` — blocks commands while imprisoned - `NoSpectateCommandGuard` — blocks commands while spectating - `PhoneCommandGuard` — requires a phone - `VehicleCommandGuard` — requires the player to be in a vehicle ### RPC System Two-way remote procedure calls between server and client, replacing `mp.events.addProc` / `mp.events.callRemoteProc`. **Server registers a handler:** ```ts import { rpc } from '../core/rpc'; rpc.register('getPlayerInfo', async (player, ...args) => { return { name: player.name }; }); ``` **Client calls the server:** ```ts import { rpc } from '../core/rpc'; const info = await rpc.call('getPlayerInfo'); ``` **Server calls a client proc:** ```ts const result = await rpc.callClient(player, 'getPedPosition'); ``` The wire protocol uses two net events (`_rpc:call` / `_rpc:result`) and auto-times out client calls after 10 seconds. ### Player Registry `PlayerWrapper` wraps a FiveM net ID and exposes a RageMP-compatible player API (`.name`, `.id`, `.notify()`, `.call()`, `.setVariable()`, `.getVariable()`, etc.). The registry is maintained by the mp-shim on `playerConnecting` / `playerDropped`. --- ## REST API An Express server starts on port **3001** alongside the FiveM resource. It is used by external services (UCP, Discord bots, admin tooling) to interact with the live game server. All requests must include an `rpckey` header matching a key stored in the `rpc_keys` database table. Optionally the key can be IP-restricted to a CIDR range. ### Routes | Prefix | Module | Purpose | |---|---|---| | `/account` | AccountRpcModule | Bans, admin notes, session invalidation | | `/character` | CharacterRpcModule | Spawn items/money, refunds, in-game checks | | `/chat` | ChatRpcModule | Post messages to in-game chat channels | | `/company` | CompanyRpcModule | Manage company employees and metadata | | `/faction` | FactionRpcModule | Faction creation / kick events | | `/group` | GroupRpcModule | Permission group and frequency changes | | `/motd` | MotdRpcModule | Push new MOTD / force reload | | `/players` | PlayersRpcModule | Online player list / admin list | | `/property` | PropertyRpcModule | Reload property parameters | | `/vehicle` | VehicleRpcModule | Reload vehicle parameters, spawn checks | --- ## Porting a Feature See `PORTING.md` for the full guide and suggested porting order. The short version: ### Server feature 1. Copy `game-server/src/features//` → `fivem-server/src/server/features//` 2. Fix relative imports (`../../entities/`, `../../core/` paths stay the same). 3. Replace `mp.colshapes.*` with the polygon position-tracking system. 4. Replace `mp.objects.new` / `mp.peds.new` with FiveM `CreateObject` / `CreatePed` natives. 5. `player.call(...)` works unchanged via `PlayerWrapper.call()`. 6. Uncomment the import in `src/server/index.ts` and add to `featuresParallel` or `featuresSeries`. ### Client feature 1. Copy `game-client/src//` → `fivem-server/src/client/features//` 2. Fix imports. 3. Replace `mp.events.addProc('name', fn)` with `rpc.registerClientProc('name', fn)`. 4. Replace `mp.browsers.new(url).execute(js)` with `nui.send(type, data)` (PostMessage to Vue). 5. Replace `mp.game.invoke('0xHASH', ...)` with `Citizen.invokeNative('0xHASH', ...)` or the named native. 6. Uncomment the import in `src/client/index.ts`. --- ## RageMP → FiveM Cheatsheet | RageMP (server) | FiveM (server) | |---|---| | `mp.events.add('playerJoin', fn)` | Shim maps → `playerConnecting` | | `mp.events.add('playerQuit', fn)` | Shim maps → `playerDropped` | | `mp.events.add('myEvent', fn)` | Shim registers net event; player injected from `source` | | `player.call('event', [a, b])` | `TriggerClientEvent('event', netId, a, b)` | | `player.notify(msg)` | `TriggerClientEvent('_mp:notify', netId, msg)` | | `player.setVariable(k, v)` | FiveM StateBag: `Player(id).state.set(k, v, true)` | | `mp.vehicles.new(model, pos, opts)` | `vehicleRegistry.create(model, pos, opts)` | | `mp.events.addProc('name', fn)` | `rpc.register('name', fn)` | | `mp.colshapes.newSphere(...)` | Polygon position-tracking system | | `mp.players.broadcast(msg)` | `TriggerClientEvent('_mp:chatMessage', -1, msg)` | | RageMP (client) | FiveM (client) | |---|---| | `mp.events.add('render', fn)` | Shim adds to tick loop | | `mp.events.callRemote('event', ...args)` | `TriggerServerEvent('event', ...args)` | | `mp.events.addProc('name', fn)` | `rpc.registerClientProc('name', fn)` | | `mp.players.local.setComponentVariation(...)` | `SetPedComponentVariation(PlayerPedId(), ...)` | | `mp.game.invoke('0xHASH', ...args)` | `Citizen.invokeNative('0xHASH', ...args)` | | `mp.browsers.new(url)` | FiveM NUI (`ui/index.html`) | | `mp.gui.chat.push(msg)` | `TriggerEvent('chat:addMessage', { args: [msg] })` | --- ## Porting Status | Layer | Status | Notes | |---|---|---| | TypeORM entities (207) | Done | Copied verbatim, no changes needed | | Express REST API | Done | Copied verbatim, no changes needed | | Shared constants | Done | | | Core log / DB / Redis / Mongo | Done | | | `mp` server shim | Done | Events, players, vehicles, config | | `mp` client shim | Done | Events, game, gui, NUI | | RPC system | Done | Replaces `mp.events.addProc` | | Player wrapper | Done | Wraps FiveM net ID into `PlayerMp`-compatible object | | Vehicle wrapper | Done | Wraps FiveM entity handle | | CommandSystem | Done | Uses `RegisterCommand()` per command | | Server features (101) | Not started | See `PORTING.md` for suggested order | | Client features (~80) | Not started | See `PORTING.md` for suggested order | The recommended first features to port (in order) are: `time`, `weather`, `chat`, `login`, `session`, `player`, `permissions`, `money`, `inventory`, `vehicle`. Defer features with heavy colshape or server-object dependencies (`properties`, `furniture`, `drugs`, `graffiti`) until the polygon position-tracking system is fully integrated.