Initial commit — Southwest Roleplay FiveM
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.map
|
||||||
90
PORTING.md
Normal file
90
PORTING.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# FiveM Port — Feature Porting Guide
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
| Layer | Status | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| TypeORM entities (207) | ✅ Copied verbatim | Zero changes needed |
|
||||||
|
| Express REST API | ✅ Copied verbatim | Zero changes needed |
|
||||||
|
| Shared constants | ✅ Copied verbatim | |
|
||||||
|
| Core log/DB/Redis/Mongo | ✅ Copied verbatim | |
|
||||||
|
| `mp` server shim | ✅ Done | Covers events, players, vehicles, config |
|
||||||
|
| `mp` client shim | ✅ Done | Covers events, game, gui, browsers→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 order below |
|
||||||
|
| Client features (~80) | 🔲 Not started | See porting order below |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to port a server feature
|
||||||
|
|
||||||
|
1. **Copy** the feature folder from `game-server/src/features/<name>/` to `fivem-server/src/server/features/<name>/`
|
||||||
|
2. **Fix imports** — update relative paths (e.g. `../../entities/` → `../../entities/`, `../../core/` stays the same)
|
||||||
|
3. **Search for `mp.colshapes`** — FiveM has no server colshapes. Replace with the polygon position-tracking system.
|
||||||
|
4. **Search for `mp.objects.new` / `mp.peds.new`** — use FiveM `CreateObject` / `CreatePed` natives.
|
||||||
|
5. **Search for `player.call(`** — this works unchanged via `PlayerWrapper.call()`.
|
||||||
|
6. **Uncomment the import** in `src/server/index.ts` and add to `featuresParallel` or `featuresSeries`.
|
||||||
|
|
||||||
|
## How to port a client feature
|
||||||
|
|
||||||
|
1. **Copy** the feature folder from `game-client/src/<name>/` to `fivem-server/src/client/features/<name>/`
|
||||||
|
2. **Fix imports**
|
||||||
|
3. **Replace `mp.events.addProc`** with `rpc.registerClientProc` from `../core/rpc`
|
||||||
|
4. **Replace `mp.browsers.new(url).execute(js)`** with `nui.send(type, data)` (PostMessage to Vue)
|
||||||
|
5. **Replace `mp.game.invoke(hash, ...args)`** with direct native calls where the name is known, or keep `mp.game.invoke` (the shim passes through to `Citizen.invokeNative`)
|
||||||
|
6. **Uncomment the require** in `src/client/index.ts`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suggested porting order (server features)
|
||||||
|
|
||||||
|
Start with features that have minimal RageMP API surface and no colshape dependency:
|
||||||
|
|
||||||
|
1. `shared` — pure data sync
|
||||||
|
2. `time` — simple, no colshapes
|
||||||
|
3. `weather` — simple
|
||||||
|
4. `chat` — core comms, needed by everything
|
||||||
|
5. `login` — needed before players can do anything
|
||||||
|
6. `session` — depends on login
|
||||||
|
7. `player` — needed by commands
|
||||||
|
8. `permissions` — needed by guards
|
||||||
|
9. `money` — needed by jobs/most features
|
||||||
|
10. `inventory` — high-value, many features depend on it
|
||||||
|
11. `vehicle` — large but critical
|
||||||
|
12. `job/*` — already on feature/jobsystem branch, good to port early
|
||||||
|
|
||||||
|
Defer until late (complex colshape or streaming dependencies):
|
||||||
|
- `properties` (heavy colshape use → needs polygon system)
|
||||||
|
- `furniture` (server objects)
|
||||||
|
- `drugs` (colshapes + objects)
|
||||||
|
- `graffiti` (colshapes)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RageMP → FiveM cheatsheet
|
||||||
|
|
||||||
|
| RageMP (server) | FiveM (server) |
|
||||||
|
|---|---|
|
||||||
|
| `mp.events.add('playerJoin', (p) => ...)` | Auto-mapped by shim → `playerConnecting` |
|
||||||
|
| `mp.events.add('playerQuit', (p) => ...)` | Auto-mapped by shim → `playerDropped` |
|
||||||
|
| `mp.events.add('myEvent', (p, data) => ...)` | 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(...)` | Use 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 (declared in `fxmanifest.lua`, one page) |
|
||||||
|
| `mp.gui.chat.push(msg)` | `TriggerEvent('chat:addMessage', { args: [msg] })` |
|
||||||
382
README.md
Normal file
382
README.md
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
# 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<void> {
|
||||||
|
await super.init();
|
||||||
|
// set up event handlers, load DB data, etc.
|
||||||
|
}
|
||||||
|
|
||||||
|
async postInit(): Promise<void> {
|
||||||
|
// 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/<name>/` → `fivem-server/src/server/features/<name>/`
|
||||||
|
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/<name>/` → `fivem-server/src/client/features/<name>/`
|
||||||
|
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.
|
||||||
28
fxmanifest.lua
Normal file
28
fxmanifest.lua
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
fx_version 'cerulean'
|
||||||
|
game 'gta5'
|
||||||
|
|
||||||
|
description 'Southwest Roleplay'
|
||||||
|
version '1.0.0'
|
||||||
|
|
||||||
|
-- Server scripts (Node.js runtime, CommonJS bundles)
|
||||||
|
server_scripts {
|
||||||
|
'dist/server/index.js',
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Client scripts (single bundled file)
|
||||||
|
client_scripts {
|
||||||
|
'dist/client/index.js',
|
||||||
|
}
|
||||||
|
|
||||||
|
-- NUI page (same Vue UI from game-client)
|
||||||
|
ui_page 'ui/index.html'
|
||||||
|
|
||||||
|
files {
|
||||||
|
'ui/**',
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Dependencies
|
||||||
|
dependencies {
|
||||||
|
'/server:7290',
|
||||||
|
'/onesync',
|
||||||
|
}
|
||||||
40
gulpfile.js
Normal file
40
gulpfile.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const gulp = require('gulp');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
|
||||||
|
function buildServer(cb) {
|
||||||
|
exec('npx webpack --config webpack.server.js', (err, stdout, stderr) => {
|
||||||
|
if (stdout) process.stdout.write(stdout);
|
||||||
|
if (stderr) process.stderr.write(stderr);
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildClient(cb) {
|
||||||
|
exec('npx webpack --config webpack.client.js', (err, stdout, stderr) => {
|
||||||
|
if (stdout) process.stdout.write(stdout);
|
||||||
|
if (stderr) process.stderr.write(stderr);
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchServer(cb) {
|
||||||
|
exec('npx webpack --config webpack.server.js --watch', (err, stdout, stderr) => {
|
||||||
|
if (stdout) process.stdout.write(stdout);
|
||||||
|
if (stderr) process.stderr.write(stderr);
|
||||||
|
});
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchClient(cb) {
|
||||||
|
exec('npx webpack --config webpack.client.js --watch', (err, stdout, stderr) => {
|
||||||
|
if (stdout) process.stdout.write(stdout);
|
||||||
|
if (stderr) process.stderr.write(stderr);
|
||||||
|
});
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.buildServer = buildServer;
|
||||||
|
exports.buildClient = buildClient;
|
||||||
|
exports.build = gulp.parallel(buildServer, buildClient);
|
||||||
|
exports.watch = gulp.parallel(watchServer, watchClient);
|
||||||
|
exports.default = exports.build;
|
||||||
11
ormconfig.js
Normal file
11
ormconfig.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
type: 'mysql',
|
||||||
|
host: process.env.DB_HOST || 'play.southwest-roleplay.dev',
|
||||||
|
port: Number(process.env.DB_PORT) || 3306,
|
||||||
|
username: process.env.DB_USER || 'username',
|
||||||
|
password: process.env.DB_PASS || 'password',
|
||||||
|
database: process.env.DB_NAME || 'game',
|
||||||
|
logging: ['error', 'log'],
|
||||||
|
entities: ['./dist/server/entities/*.js'],
|
||||||
|
synchronize: false,
|
||||||
|
};
|
||||||
8476
package-lock.json
generated
Normal file
8476
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
65
package.json
Normal file
65
package.json
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"name": "swrp-fivem-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Southwest Roleplay - FiveM",
|
||||||
|
"license": "ISC",
|
||||||
|
"scripts": {
|
||||||
|
"watch": "npx gulp watch",
|
||||||
|
"build": "npx gulp build",
|
||||||
|
"lint": "eslint \"src/**/*.ts\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ansi_up": "^5.0.1",
|
||||||
|
"async-mutex": "^0.5.0",
|
||||||
|
"aws-sdk": "^2.1693.0",
|
||||||
|
"body-parser": "^1.20.0",
|
||||||
|
"colors": "^1.4.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dayjs": "^1.11.20",
|
||||||
|
"express": "^4.17.1",
|
||||||
|
"ip-cidr": "^2.1.4",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mongodb": "^3.7.4",
|
||||||
|
"mustache": "^4.2.0",
|
||||||
|
"mysql": "^2.18.1",
|
||||||
|
"node-fetch": "^2.6.1",
|
||||||
|
"node-schedule": "^2.1.1",
|
||||||
|
"nodemailer": "^8.0.5",
|
||||||
|
"redis": "^3.1.2",
|
||||||
|
"showdown": "^2.1.0",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"typeorm": "^0.2.45",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@citizenfx/client": "*",
|
||||||
|
"@citizenfx/server": "*",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@types/cors": "^2.8.10",
|
||||||
|
"@types/express": "^4.17.11",
|
||||||
|
"@types/lodash": "^4.14.168",
|
||||||
|
"@types/mongodb": "^4.0.6",
|
||||||
|
"@types/mustache": "^4.1.1",
|
||||||
|
"@types/node": "^18.0.0",
|
||||||
|
"@types/node-fetch": "^2.5.9",
|
||||||
|
"@types/node-schedule": "^2.1.8",
|
||||||
|
"@types/nodemailer": "^8.0.0",
|
||||||
|
"@types/redis": "^2.8.28",
|
||||||
|
"@types/showdown": "^2.0.6",
|
||||||
|
"@types/source-map-support": "^0.5.10",
|
||||||
|
"@types/uuid": "^8.3.0",
|
||||||
|
"del": "^8.0.1",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"globals": "^17.0.0",
|
||||||
|
"gulp": "^5.0.1",
|
||||||
|
"gulp-sourcemaps": "^3.0.0",
|
||||||
|
"gulp-typescript": "^6.0.0-alpha.1",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.20.0",
|
||||||
|
"webpack": "^5.0.0",
|
||||||
|
"webpack-cli": "^4.0.0",
|
||||||
|
"ts-loader": "^9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
266
src/client/core/mp-shim.ts
Normal file
266
src/client/core/mp-shim.ts
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
/**
|
||||||
|
* mp-shim.ts — RageMP compatibility layer for FiveM (client-side).
|
||||||
|
*
|
||||||
|
* Exposes a global `mp` object matching the RageMP client API so that ported
|
||||||
|
* client feature files compile and run with minimal changes.
|
||||||
|
*
|
||||||
|
* Key differences from the server shim:
|
||||||
|
* - No player pool (there's only the local player)
|
||||||
|
* - Native calls go through FiveM's global native functions
|
||||||
|
* - mp.game.* wraps native namespaces
|
||||||
|
* - Browsers are replaced by FiveM NUI
|
||||||
|
* - mp.events.addProc is handled by src/client/core/rpc/index.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { rpc } from './rpc/index';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Well-known RageMP client events → FiveM equivalents
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const RAGEMP_TO_FIVEM: Record<string, string> = {
|
||||||
|
render: 'FRAME_STARTED', // PARTIAL — see note below
|
||||||
|
entityStreamIn: 'entityCreating', // PARTIAL
|
||||||
|
entityStreamOut: 'entityRemoved', // PARTIAL
|
||||||
|
playerEnterVehicle: 'enteredVehicle', // via baseevents
|
||||||
|
playerExitVehicle: 'leftVehicle', // via baseevents
|
||||||
|
playerSpawn: 'playerSpawned',
|
||||||
|
};
|
||||||
|
|
||||||
|
// FiveM doesn't have a 'render' equivalent exactly — FRAME_STARTED doesn't
|
||||||
|
// exist either. For per-tick logic we use a persistent thread below.
|
||||||
|
const tickHandlers: Array<() => void> = [];
|
||||||
|
|
||||||
|
setTick(() => {
|
||||||
|
for (const handler of tickHandlers) {
|
||||||
|
try { handler(); } catch (e) { console.error('[mp-shim] tick handler error:', e); }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Events
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const events = {
|
||||||
|
// Register a client-side event handler.
|
||||||
|
// - 'render' → runs every tick
|
||||||
|
// - Net events sent from server → AddEventHandler + RegisterNetEvent
|
||||||
|
// - Game events (playerEnterVehicle etc.) → mapped to FiveM baseevents
|
||||||
|
add(eventOrMap: string | Record<string, Function>, handler?: Function): void {
|
||||||
|
if (typeof eventOrMap === 'object') {
|
||||||
|
for (const [name, fn] of Object.entries(eventOrMap)) {
|
||||||
|
events.add(name, fn);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = eventOrMap as string;
|
||||||
|
const fn = handler!;
|
||||||
|
|
||||||
|
if (name === 'render') {
|
||||||
|
tickHandlers.push(fn as () => void);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name in RAGEMP_TO_FIVEM) {
|
||||||
|
AddEventHandler(RAGEMP_TO_FIVEM[name], fn);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume it's a server→client net event
|
||||||
|
RegisterNetEvent(name);
|
||||||
|
AddEventHandler(name, fn);
|
||||||
|
},
|
||||||
|
|
||||||
|
// RageMP client proc — handled by custom RPC module
|
||||||
|
addProc(name: string, handler: (...args: any[]) => any): void {
|
||||||
|
rpc.registerClientProc(name, handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Client → Server event (replaces mp.events.callRemote)
|
||||||
|
callRemote(eventName: string, ...args: any[]): void {
|
||||||
|
TriggerServerEvent(eventName, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Local client event emit
|
||||||
|
call(eventName: string, ...args: any[]): void {
|
||||||
|
TriggerEvent(eventName, ...args);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Local player
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const localPlayer = {
|
||||||
|
get handle(): number { return PlayerPedId(); },
|
||||||
|
|
||||||
|
// Component variations (clothing)
|
||||||
|
setComponentVariation(componentId: number, drawableId: number, textureId: number, paletteId = 0): void {
|
||||||
|
SetPedComponentVariation(PlayerPedId(), componentId, drawableId, textureId, paletteId);
|
||||||
|
},
|
||||||
|
getComponentVariation(componentId: number): [number, number, number] {
|
||||||
|
return [
|
||||||
|
GetPedDrawableVariation(PlayerPedId(), componentId),
|
||||||
|
GetPedTextureVariation(PlayerPedId(), componentId),
|
||||||
|
GetPedPaletteVariation(PlayerPedId(), componentId),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
setPropIndex(propId: number, drawableId: number, textureId: number, attach: boolean): void {
|
||||||
|
SetPedPropIndex(PlayerPedId(), propId, drawableId, textureId, attach);
|
||||||
|
},
|
||||||
|
clearProp(propId: number): void {
|
||||||
|
ClearPedProp(PlayerPedId(), propId);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Head blend / facial features (character creator)
|
||||||
|
setHeadBlendData(
|
||||||
|
shapeFirst: number, shapeSecond: number, shapeThird: number,
|
||||||
|
skinFirst: number, skinSecond: number, skinThird: number,
|
||||||
|
shapeMix: number, skinMix: number, thirdMix: number, isParent: boolean,
|
||||||
|
): void {
|
||||||
|
SetPedHeadBlendData(PlayerPedId(), shapeFirst, shapeSecond, shapeThird, skinFirst, skinSecond, skinThird, shapeMix, skinMix, thirdMix, isParent);
|
||||||
|
},
|
||||||
|
setHeadOverlay(overlayId: number, index: number, opacity: number, firstColor?: number, secondColor?: number): void {
|
||||||
|
SetPedHeadOverlay(PlayerPedId(), overlayId, index, opacity);
|
||||||
|
if (firstColor !== undefined) SetPedHeadOverlayColor(PlayerPedId(), overlayId, 1, firstColor, secondColor ?? 0);
|
||||||
|
},
|
||||||
|
setFaceFeature(featureId: number, scale: number): void {
|
||||||
|
SetPedFaceFeature(PlayerPedId(), featureId, scale);
|
||||||
|
},
|
||||||
|
setHairColor(colorId: number, highlightColorId: number): void {
|
||||||
|
SetPedHairColor(PlayerPedId(), colorId, highlightColorId);
|
||||||
|
},
|
||||||
|
setEyeColor(index: number): void {
|
||||||
|
SetPedEyeColor(PlayerPedId(), index);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Config flags
|
||||||
|
setConfigFlag(flag: number, value: boolean): void {
|
||||||
|
SetPedConfigFlag(PlayerPedId(), flag, value);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Position / movement
|
||||||
|
get position(): { x: number; y: number; z: number } {
|
||||||
|
const [x, y, z] = GetEntityCoords(PlayerPedId(), true);
|
||||||
|
return { x, y, z };
|
||||||
|
},
|
||||||
|
|
||||||
|
isInVehicle(): boolean {
|
||||||
|
return IsPedInAnyVehicle(PlayerPedId(), false);
|
||||||
|
},
|
||||||
|
|
||||||
|
getCurrentVehicle(): number | null {
|
||||||
|
const v = GetVehiclePedIsIn(PlayerPedId(), false);
|
||||||
|
return v !== 0 ? v : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
// STUB — animation playback proxied to game.invoke or native wrappers
|
||||||
|
playAnimation(dict: string, name: string, blendInSpeed: number, blendOutSpeed: number, duration: number, flag: number, startOffset: number): void {
|
||||||
|
TaskPlayAnim(PlayerPedId(), dict, name, blendInSpeed, blendOutSpeed, duration, flag, startOffset, false, false, false);
|
||||||
|
},
|
||||||
|
stopAnimation(): void {
|
||||||
|
ClearPedTasks(PlayerPedId());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mp.game — wraps GTA native namespaces
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const game = {
|
||||||
|
audio: {
|
||||||
|
setAudioFlag(flag: string, value: boolean): void {
|
||||||
|
SetAudioFlag(flag, value);
|
||||||
|
},
|
||||||
|
playSound(soundId: number, soundName: string, soundSetName: string, isPedPersonalSound: boolean, p4: number, p5: boolean): void {
|
||||||
|
PlaySound(soundId, soundName, soundSetName, isPedPersonalSound, p4, p5);
|
||||||
|
},
|
||||||
|
playSoundFrontend(soundId: number, audioName: string, audioRef: string, p3: boolean): void {
|
||||||
|
PlaySoundFrontend(soundId, audioName, audioRef, p3);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
vehicle: {
|
||||||
|
set defaultEngineBehaviour(value: boolean) {
|
||||||
|
// SetVehicleDefaultEngineBehaviour doesn't exist; handled via config flags
|
||||||
|
},
|
||||||
|
setResetUnoccupiedSteerAngle(value: boolean): void {
|
||||||
|
// STUB — no direct FiveM equivalent
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Catch-all for raw native hash invocations (mp.game.invoke)
|
||||||
|
invoke(hash: string, ...args: any[]): any {
|
||||||
|
return (Citizen as any).invokeNative(hash, ...args);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mp.gui — chat and cursor
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const gui = {
|
||||||
|
chat: {
|
||||||
|
activate(value: boolean): void {
|
||||||
|
// FiveM chat activation is handled client-side via NUI events
|
||||||
|
TriggerEvent('chat:visible', value);
|
||||||
|
},
|
||||||
|
show(value: boolean): void {
|
||||||
|
TriggerEvent('chat:visible', value);
|
||||||
|
},
|
||||||
|
push(message: string): void {
|
||||||
|
TriggerEvent('chat:addMessage', { args: [message] });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
show(value: boolean, doLockCursor: boolean): void {
|
||||||
|
SetCursorLocation(0.5, 0.5);
|
||||||
|
// Full NUI cursor management handled per-feature
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mp.browsers — replaced by FiveM NUI
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const browsers = {
|
||||||
|
/**
|
||||||
|
* FiveM NUI replaces RageMP CEF browsers.
|
||||||
|
* The UI page is declared in fxmanifest.lua; only one exists at a time.
|
||||||
|
* Returns a stub object whose .execute() sends NUI messages.
|
||||||
|
*/
|
||||||
|
new(_url: string): { execute: (js: string) => void; active: boolean } {
|
||||||
|
return {
|
||||||
|
active: true,
|
||||||
|
execute(js: string) {
|
||||||
|
// Evaluate JS inside the NUI frame — use sparingly
|
||||||
|
SendNUIMessage({ type: '__eval__', code: js });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mp.nui — FiveM-native NUI helpers (no RageMP equivalent; new API)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export const nui = {
|
||||||
|
send(type: string, data?: any): void {
|
||||||
|
SendNUIMessage({ type, ...(data ?? {}) });
|
||||||
|
},
|
||||||
|
focus(hasFocus: boolean, hasCursor: boolean): void {
|
||||||
|
SetNuiFocus(hasFocus, hasCursor);
|
||||||
|
},
|
||||||
|
onCallback(name: string, handler: (data: any, cb: (result: any) => void) => void): void {
|
||||||
|
RegisterNuiCallback(name, handler);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Assemble global mp object
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
(global as any).mp = {
|
||||||
|
events,
|
||||||
|
players: {
|
||||||
|
local: localPlayer,
|
||||||
|
},
|
||||||
|
game,
|
||||||
|
gui,
|
||||||
|
browsers,
|
||||||
|
nui,
|
||||||
|
};
|
||||||
59
src/client/core/rpc/index.ts
Normal file
59
src/client/core/rpc/index.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Client-side RPC — replaces RageMP's mp.events.callRemoteProc / addProc.
|
||||||
|
*
|
||||||
|
* Call a server proc (registered with rpc.register on the server):
|
||||||
|
* const result = await rpc.call('getClosestTrunk', ...args);
|
||||||
|
*
|
||||||
|
* Register a client proc (called from server via rpc.callClient):
|
||||||
|
* rpc.registerClientProc('getVehicleClass', (vehicle) => { ... });
|
||||||
|
*/
|
||||||
|
|
||||||
|
let callCounter = 0;
|
||||||
|
const pending = new Map<string, { resolve: (v: any) => void; reject: (e: any) => void }>();
|
||||||
|
|
||||||
|
// Receive results from the server
|
||||||
|
RegisterNetEvent('_rpc:result');
|
||||||
|
AddEventHandler('_rpc:result', (callId: string, result: any, error: string | null) => {
|
||||||
|
const cb = pending.get(callId);
|
||||||
|
if (!cb) return;
|
||||||
|
pending.delete(callId);
|
||||||
|
if (error) cb.reject(new Error(error));
|
||||||
|
else cb.resolve(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Server → client proc invocations
|
||||||
|
RegisterNetEvent('_rpc:clientCall');
|
||||||
|
AddEventHandler('_rpc:clientCall', (callId: string, name: string, args: any[]) => {
|
||||||
|
const handler = clientProcs.get(name);
|
||||||
|
if (!handler) {
|
||||||
|
TriggerServerEvent('_rpc:clientResult', callId, null, `No client proc: ${name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => handler(...(args ?? [])))
|
||||||
|
.then((result) => TriggerServerEvent('_rpc:clientResult', callId, result, null))
|
||||||
|
.catch((err) => TriggerServerEvent('_rpc:clientResult', callId, null, err?.message ?? String(err)));
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientProcs = new Map<string, (...args: any[]) => any>();
|
||||||
|
|
||||||
|
export const rpc = {
|
||||||
|
call<T = any>(name: string, ...args: any[]): Promise<T> {
|
||||||
|
const id = `${Date.now()}_${callCounter++}`;
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
pending.set(id, { resolve, reject });
|
||||||
|
TriggerServerEvent('_rpc:call', id, name, args);
|
||||||
|
// Timeout after 10s
|
||||||
|
setTimeout(() => {
|
||||||
|
if (pending.has(id)) {
|
||||||
|
pending.delete(id);
|
||||||
|
reject(new Error(`RPC timeout: ${name}`));
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
registerClientProc(name: string, handler: (...args: any[]) => any): void {
|
||||||
|
clientProcs.set(name, handler);
|
||||||
|
},
|
||||||
|
};
|
||||||
238
src/client/features/login/index.ts
Normal file
238
src/client/features/login/index.ts
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
/**
|
||||||
|
* Login client — FiveM port of game-client/src/login/index.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
const loginCameras = [
|
||||||
|
{ pos: [2026.90, 3731.27, 50.40], rot: [0, 0, 71.62], fov: 45 },
|
||||||
|
{ pos: [-325.87, 4485.50, 106.75], rot: [0, 0, 117.66], fov: 45 },
|
||||||
|
{ pos: [-382.56, 4721.71, 309.94], rot: [0, 0, -28.25], fov: 45 },
|
||||||
|
];
|
||||||
|
const pickedCam = loginCameras[Math.floor(Math.random() * loginCameras.length)];
|
||||||
|
|
||||||
|
let loginCamHandle: number = -1;
|
||||||
|
let loginActive = false;
|
||||||
|
let overlayReady = false;
|
||||||
|
|
||||||
|
// Pending spawn data set by _mp:spawn / _mp:setModel before login:characterLoaded fires
|
||||||
|
let pendingSpawnPos: { x: number; y: number; z: number } | null = null;
|
||||||
|
let pendingModel = 'mp_m_freemode_01';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Register all server→client net events
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const NET_EVENTS = [
|
||||||
|
'login:show', 'login:hide', 'login:success', 'login:error',
|
||||||
|
'login:otp', 'login:characters', 'login:spawns', 'login:characterLoaded',
|
||||||
|
'login:characterError', '_mp:spawn', '_mp:setModel', '_mp:notify',
|
||||||
|
];
|
||||||
|
for (const e of NET_EVENTS) RegisterNetEvent(e);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// NUI bridge helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function nuiSend(eventName: string, ...args: any[]): void {
|
||||||
|
SendNUIMessage({ type: 'mp:event', eventName, args });
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisterNuiCallback('nuiEvent', (data: { eventName: string; args: any[] }, cb: Function) => {
|
||||||
|
const { eventName, args } = data;
|
||||||
|
TriggerEvent(eventName, ...(args ?? []));
|
||||||
|
cb({});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Camera helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function createLoginCamera(): void {
|
||||||
|
if (loginCamHandle !== -1) return;
|
||||||
|
|
||||||
|
const [px, py, pz] = pickedCam.pos;
|
||||||
|
const [rx, ry, rz] = pickedCam.rot;
|
||||||
|
|
||||||
|
loginCamHandle = CreateCam('DEFAULT_SCRIPTED_CAMERA', true);
|
||||||
|
SetCamCoord(loginCamHandle, px, py, pz);
|
||||||
|
SetCamRot(loginCamHandle, rx, ry, rz, 2);
|
||||||
|
SetCamFov(loginCamHandle, pickedCam.fov);
|
||||||
|
SetCamActive(loginCamHandle, true);
|
||||||
|
RenderScriptCams(true, false, 0, true, false);
|
||||||
|
|
||||||
|
const ped = PlayerPedId();
|
||||||
|
SetEntityVisible(ped, false, false);
|
||||||
|
FreezeEntityPosition(ped, true);
|
||||||
|
|
||||||
|
SetNuiFocus(true, true);
|
||||||
|
DisplayHud(false);
|
||||||
|
DisplayRadar(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyLoginCamera(): void {
|
||||||
|
if (loginCamHandle === -1) return;
|
||||||
|
|
||||||
|
RenderScriptCams(false, true, 500, true, false);
|
||||||
|
SetCamActive(loginCamHandle, false);
|
||||||
|
DestroyCam(loginCamHandle, false);
|
||||||
|
loginCamHandle = -1;
|
||||||
|
|
||||||
|
SetNuiFocus(false, false);
|
||||||
|
DisplayHud(true);
|
||||||
|
DisplayRadar(true);
|
||||||
|
loginActive = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Token storage
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function getStoredToken(): string {
|
||||||
|
return GetResourceKvpString('sw-rp:auth-token') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStoredToken(token: string): void {
|
||||||
|
SetResourceKvp('sw-rp:auth-token', token);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearStoredToken(): void {
|
||||||
|
DeleteResourceKvp('sw-rp:auth-token');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Boot
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function startLogin(): void {
|
||||||
|
if (loginActive) return;
|
||||||
|
loginActive = true;
|
||||||
|
overlayReady = true;
|
||||||
|
createLoginCamera();
|
||||||
|
TriggerServerEvent('login:init', getStoredToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
AddEventHandler('overlay:ready', startLogin);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!overlayReady) startLogin();
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Server → client events — forward to NUI
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
AddEventHandler('login:show', () => {
|
||||||
|
if (!loginActive) { loginActive = true; createLoginCamera(); }
|
||||||
|
nuiSend('login:show');
|
||||||
|
});
|
||||||
|
|
||||||
|
AddEventHandler('login:hide', () => { nuiSend('login:hide'); });
|
||||||
|
|
||||||
|
AddEventHandler('login:success', (token?: string) => {
|
||||||
|
if (token) setStoredToken(token);
|
||||||
|
nuiSend('login:success', token);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddEventHandler('login:error', (error: string) => { nuiSend('login:error', error); });
|
||||||
|
AddEventHandler('login:otp', (failure: boolean) => { nuiSend('login:otp', failure); });
|
||||||
|
AddEventHandler('login:characters', (characters: string) => { nuiSend('login:characters', characters); });
|
||||||
|
AddEventHandler('login:spawns', (spawns: string) => { nuiSend('login:spawns', spawns); });
|
||||||
|
AddEventHandler('login:characterError', (msg: string) => { nuiSend('login:error', msg); });
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Character loaded — run the full FiveM spawn sequence ourselves:
|
||||||
|
// model load → SetPlayerModel → NetworkResurrectLocalPlayer →
|
||||||
|
// collision wait → ShutdownLoadingScreen (clears "Awaiting scripts") →
|
||||||
|
// destroy login camera
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
AddEventHandler('login:characterLoaded', async () => {
|
||||||
|
nuiSend('login:hide');
|
||||||
|
|
||||||
|
const pos = pendingSpawnPos ?? { x: -1037.43, y: -2733.075, z: 13.7566 };
|
||||||
|
|
||||||
|
// 1. Load model by name (string form is safest across FiveM JS)
|
||||||
|
const model = pendingModel;
|
||||||
|
RequestModel(model as any);
|
||||||
|
let modelTries = 0;
|
||||||
|
while (!HasModelLoaded(model as any) && modelTries < 100) {
|
||||||
|
await new Promise<void>((r) => setTimeout(r, 50));
|
||||||
|
modelTries++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Swap model — SetPlayerModel creates a new ped; PlayerPedId() updates after
|
||||||
|
if (HasModelLoaded(model as any)) {
|
||||||
|
SetPlayerModel(PlayerId(), model as any);
|
||||||
|
SetModelAsNoLongerNeeded(model as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Always fetch ped AFTER SetPlayerModel — it creates a new ped handle
|
||||||
|
let ped = PlayerPedId();
|
||||||
|
|
||||||
|
// 4. Place player — NetworkResurrectLocalPlayer is what spawnmanager uses
|
||||||
|
RequestCollisionAtCoord(pos.x, pos.y, pos.z);
|
||||||
|
SetEntityCoordsNoOffset(ped, pos.x, pos.y, pos.z, false, false, false, true);
|
||||||
|
NetworkResurrectLocalPlayer(pos.x, pos.y, pos.z, 0.0, true, true, false);
|
||||||
|
|
||||||
|
// 5. Fetch ped AGAIN after resurrection (NetworkResurrectLocalPlayer can swap it)
|
||||||
|
ped = PlayerPedId();
|
||||||
|
SetEntityVisible(ped, true, false);
|
||||||
|
FreezeEntityPosition(ped, false);
|
||||||
|
|
||||||
|
// 6. Wait for collision (up to 5 s), then clear "Awaiting scripts"
|
||||||
|
const colStart = Date.now();
|
||||||
|
while (!HasCollisionLoadedAroundEntity(ped) && Date.now() - colStart < 5000) {
|
||||||
|
await new Promise<void>((r) => setTimeout(r, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
ShutdownLoadingScreen();
|
||||||
|
|
||||||
|
// 7. Hand camera back to game
|
||||||
|
destroyLoginCamera();
|
||||||
|
pendingSpawnPos = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// NUI → server events
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
AddEventHandler('login:submit', (username: string, password: string, otp = '', remember = false) => {
|
||||||
|
TriggerServerEvent('login:attempt', username, password, otp || null, Boolean(remember));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddEventHandler('login:characterSelect', (characterId: number) => {
|
||||||
|
TriggerServerEvent('login:character', Number(characterId));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddEventHandler('login:spawnSelect', (spawnId: number) => {
|
||||||
|
TriggerServerEvent('login:spawn', Number(spawnId));
|
||||||
|
});
|
||||||
|
|
||||||
|
AddEventHandler('login:reset', () => {
|
||||||
|
clearStoredToken();
|
||||||
|
TriggerServerEvent('login:init', '');
|
||||||
|
nuiSend('login:show');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// _mp:spawn — store position for spawnmanager call in login:characterLoaded
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
AddEventHandler('_mp:spawn', (pos: { x: number; y: number; z: number }) => {
|
||||||
|
pendingSpawnPos = pos;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// _mp:setModel — store model name for spawnmanager call in login:characterLoaded
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
AddEventHandler('_mp:setModel', (modelName: string) => {
|
||||||
|
pendingModel = modelName || 'mp_m_freemode_01';
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Version check on connect
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
AddEventHandler('connect', () => {
|
||||||
|
TriggerServerEvent('versionCheck', '1.0');
|
||||||
|
});
|
||||||
35
src/client/globals.d.ts
vendored
Normal file
35
src/client/globals.d.ts
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/// <reference path="../../node_modules/@citizenfx/client/index.d.ts" />
|
||||||
|
/// <reference path="../../node_modules/@citizenfx/client/natives_universal.d.ts" />
|
||||||
|
|
||||||
|
// FiveM client globals not covered by @citizenfx/client typings
|
||||||
|
declare function RegisterNetEvent(eventName: string, cb?: Function): void;
|
||||||
|
declare function AddEventHandler(eventName: string, handler: Function): void;
|
||||||
|
declare function TriggerServerEvent(eventName: string, ...args: any[]): void;
|
||||||
|
declare function TriggerEvent(eventName: string, ...args: any[]): void;
|
||||||
|
declare function setTick(handler: () => void): number;
|
||||||
|
declare function clearTick(tickId: number): void;
|
||||||
|
declare function SendNuiMessage(data: string): void;
|
||||||
|
declare function RegisterNuiCallbackType(name: string): void;
|
||||||
|
declare function SetNuiFocus(hasFocus: boolean, hasCursor: boolean): void;
|
||||||
|
declare function GetPlayerServerId(player: number): number;
|
||||||
|
declare function PlayerId(): number;
|
||||||
|
declare function PlayerPedId(): number;
|
||||||
|
declare function GetPlayerPed(player: number): number;
|
||||||
|
declare function NetworkGetPlayerIndexFromPed(ped: number): number;
|
||||||
|
declare function GetEntityCoords(entity: number, alive: boolean): [number, number, number];
|
||||||
|
declare function GetEntityHeading(entity: number): number;
|
||||||
|
declare function GetEntityModel(entity: number): number;
|
||||||
|
declare function GetPedInVehicleSeat(vehicle: number, seat: number): number;
|
||||||
|
declare function GetVehiclePedIsIn(ped: number, lastVehicle: boolean): number;
|
||||||
|
declare function GetHashKey(key: string): number;
|
||||||
|
declare function NetworkResurrectLocalPlayer(x: number, y: number, z: number, heading: number, p4: boolean, p5: boolean, p6: boolean): void;
|
||||||
|
declare function ShutdownLoadingScreen(): void;
|
||||||
|
declare function RequestCollisionAtCoord(x: number, y: number, z: number): void;
|
||||||
|
declare function HasCollisionLoadedAroundEntity(entity: number): boolean;
|
||||||
|
declare function SetEntityCoordsNoOffset(entity: number, x: number, y: number, z: number, xAxis: boolean, yAxis: boolean, zAxis: boolean, clearArea: boolean): void;
|
||||||
|
|
||||||
|
// exports — FiveM resource exports
|
||||||
|
declare const exports: Record<string, any>;
|
||||||
|
|
||||||
|
// mp global — set at runtime by core/mp-shim.ts
|
||||||
|
declare const mp: any;
|
||||||
26
src/client/index.ts
Normal file
26
src/client/index.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* FiveM client entry point — mirrors src/index.ts from game-client.
|
||||||
|
* The mp shim MUST be the very first import.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import './core/mp-shim';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Feature imports
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import './features/login';
|
||||||
|
|
||||||
|
// require('./vehicle');
|
||||||
|
// require('./weather');
|
||||||
|
// require('./destination-marker');
|
||||||
|
// require('./chat');
|
||||||
|
// require('./inventory');
|
||||||
|
// require('./phone');
|
||||||
|
// require('./player');
|
||||||
|
// ... add as features are ported
|
||||||
|
|
||||||
|
|
||||||
|
// Disable default FiveM chat UI (replaced by custom NUI)
|
||||||
|
mp.gui.chat.activate(false);
|
||||||
|
mp.gui.chat.show(false);
|
||||||
1
src/server/const/chat-colors.ts
Normal file
1
src/server/const/chat-colors.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { ChatColor, ChatColors, ChatColorsInt } from '../../shared/const/chat-colors';
|
||||||
18
src/server/core/commands/base.command.ts
Normal file
18
src/server/core/commands/base.command.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { CommandOptions } from './options';
|
||||||
|
import { ObjectLiteral } from 'typeorm';
|
||||||
|
|
||||||
|
export interface BaseCommand {
|
||||||
|
$options: CommandOptions<{
|
||||||
|
[key: string]: string;
|
||||||
|
}>;
|
||||||
|
run(
|
||||||
|
player: PlayerMp,
|
||||||
|
params?:
|
||||||
|
| {
|
||||||
|
[key: string]: string | number | null | PlayerMp | VehicleMp;
|
||||||
|
}
|
||||||
|
| ObjectLiteral,
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BaseCommand implements BaseCommand {}
|
||||||
91
src/server/core/commands/command-manifest.ts
Normal file
91
src/server/core/commands/command-manifest.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
|
||||||
|
import { commandSystem } from './index';
|
||||||
|
import { clientSideCommands } from '../../../shared/const/clientside-commands';
|
||||||
|
import { logSystem } from '../log';
|
||||||
|
|
||||||
|
type GuardWithPermissions = CommandGuard & {
|
||||||
|
__permissions?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatValidator(validator: CommandParamValidator): string {
|
||||||
|
if (Array.isArray(validator)) {
|
||||||
|
return `one-of(${validator.map((value) => String(value)).join('|')})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validator instanceof RegExp) {
|
||||||
|
return `regex(${validator.toString()})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildParamString(params?: CommandParamMetaData<any>): string {
|
||||||
|
if (!params || !Object.keys(params).length) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.entries(params)
|
||||||
|
.map(([key, meta]) => {
|
||||||
|
const optional = meta.default !== undefined;
|
||||||
|
const optionalHint = optional ? ` optional default=${String(meta.default)}` : ' required';
|
||||||
|
return `${key} <${formatValidator(meta.validator)}> (${meta.hint};${optionalHint})`;
|
||||||
|
})
|
||||||
|
.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPermissions(guards?: CommandGuard[]): string[] {
|
||||||
|
if (!guards?.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = guards
|
||||||
|
.flatMap((guard) => {
|
||||||
|
const typedGuard = guard as GuardWithPermissions;
|
||||||
|
return typedGuard.__permissions ?? [];
|
||||||
|
})
|
||||||
|
.filter((permission, index, all) => permission && all.indexOf(permission) === index);
|
||||||
|
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeCommandManifestTxt(): Promise<void> {
|
||||||
|
const lines: string[] = [];
|
||||||
|
lines.push('Southwest Roleplay Command Manifest');
|
||||||
|
lines.push(`Generated: ${new Date().toISOString()}`);
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
const commandEntries = Array.from(commandSystem.commands.entries()).sort(([left], [right]) => left.localeCompare(right));
|
||||||
|
|
||||||
|
lines.push(`Server Commands (${commandEntries.length})`);
|
||||||
|
lines.push('----------------------------------------');
|
||||||
|
|
||||||
|
for (const [commandName, command] of commandEntries) {
|
||||||
|
const params = buildParamString(command.$options.params);
|
||||||
|
const permissions = extractPermissions(command.$options.guards);
|
||||||
|
|
||||||
|
lines.push(`/${commandName}`);
|
||||||
|
lines.push(` params: ${params}`);
|
||||||
|
lines.push(` required_permissions: ${permissions.length ? permissions.join(', ') : 'none'}`);
|
||||||
|
lines.push(` grouped: ${command.$options.grouped ?? 'none'}`);
|
||||||
|
lines.push(` description: ${command.$options.description ?? 'none'}`);
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueClientCommands = [...new Set(clientSideCommands)].sort((left, right) => left.localeCompare(right));
|
||||||
|
lines.push(`Client-side Commands (${uniqueClientCommands.length})`);
|
||||||
|
lines.push('----------------------------------------');
|
||||||
|
|
||||||
|
for (const commandName of uniqueClientCommands) {
|
||||||
|
lines.push(`/${commandName}`);
|
||||||
|
lines.push(' params: n/a (handled client-side)');
|
||||||
|
lines.push(' required_permissions: n/a (handled client-side)');
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPath = path.resolve(process.cwd(), 'command-manifest.txt');
|
||||||
|
await fs.writeFile(outputPath, `${lines.join('\n')}\n`, 'utf8');
|
||||||
|
|
||||||
|
logSystem.initLog('Command Manifest', `Wrote ${commandEntries.length} server command entries to ${outputPath}.`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export const NoSpectateCommandGuard = (): CommandGuard => {
|
||||||
|
return (player: PlayerMp) => {
|
||||||
|
if (player.state?.spectating?.target) {
|
||||||
|
return `You can't use this command while spectating.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
21
src/server/core/commands/guards/permissions.command.guard.ts
Normal file
21
src/server/core/commands/guards/permissions.command.guard.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { permissionSystem } from '../../../features/permission';
|
||||||
|
|
||||||
|
export const PermissionCommandGuard = (...permissionNames: string[]): CommandGuard => {
|
||||||
|
const permission = ((player: PlayerMp) => {
|
||||||
|
for (const permissionName of permissionNames) {
|
||||||
|
if (permissionSystem.doesPlayerHavePermission(player, permissionName)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissionNames.includes('aduty') && player.getVariable('adminDuty')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `You're not allowed to use this command`;
|
||||||
|
}) as CommandGuard & { __permissions?: string[] };
|
||||||
|
|
||||||
|
permission.__permissions = [...permissionNames];
|
||||||
|
|
||||||
|
return permission;
|
||||||
|
};
|
||||||
36
src/server/core/commands/guards/phone.command.guard.ts
Normal file
36
src/server/core/commands/guards/phone.command.guard.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { phoneSystem } from '../../../features/phone';
|
||||||
|
|
||||||
|
type PhoneCommandGuardOptions = {
|
||||||
|
on?: boolean;
|
||||||
|
unlocked?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PhoneCommandGuard = (options?: PhoneCommandGuardOptions): CommandGuard => {
|
||||||
|
return (player: PlayerMp) => {
|
||||||
|
if (!options) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!player.character?.phone || !player.character.phone.item) {
|
||||||
|
return `You must have a phone.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.on !== undefined) {
|
||||||
|
if (options.on === true && !phoneSystem.isPhoneOn(player.character.phone.item)) {
|
||||||
|
return `Your phone must be turned on.`;
|
||||||
|
} else if (options.on === false && phoneSystem.isPhoneOn(player.character.phone.item)) {
|
||||||
|
return `Your phone must be turned off.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.unlocked !== undefined) {
|
||||||
|
if (options.unlocked === true && !phoneSystem.isPlayerPhoneLocked(player as PlayerMpLoggedIn)) {
|
||||||
|
return `Your phone must be locked.`;
|
||||||
|
} else if (options.unlocked === false && phoneSystem.isPlayerPhoneLocked(player as PlayerMpLoggedIn)) {
|
||||||
|
return `Your phone must be unlocked.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
24
src/server/core/commands/guards/prisoned.command.guard.ts
Normal file
24
src/server/core/commands/guards/prisoned.command.guard.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
type PrisonCommandGuardOptions = {
|
||||||
|
prison?: boolean;
|
||||||
|
jail?: boolean;
|
||||||
|
adminJail?: boolean;
|
||||||
|
all?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PrisonCommandGuard = (options: PrisonCommandGuardOptions): CommandGuard => {
|
||||||
|
return (player: PlayerMp) => {
|
||||||
|
if ((options.prison || options.all) && player.character?.prison_cell_id) {
|
||||||
|
return `You can't use this while imprisoned.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((options.jail || options.all) && player.state.jailed) {
|
||||||
|
return `You can't use this while jailed.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((options.adminJail || options.all) && player.state.adminJail?.releaseTime) {
|
||||||
|
return `You can't use this while admin jailed.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
46
src/server/core/commands/guards/state.command.guard.ts
Normal file
46
src/server/core/commands/guards/state.command.guard.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
type PlayerStateCommandGuardOption = {
|
||||||
|
alive?: boolean;
|
||||||
|
wounded?: boolean;
|
||||||
|
knocked?: boolean;
|
||||||
|
dead?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PlayerStateCommandGuard = (options?: PlayerStateCommandGuardOption): CommandGuard => {
|
||||||
|
return (player: PlayerMp) => {
|
||||||
|
if (!options) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.alive !== undefined) {
|
||||||
|
if (options.alive && player.state.deathState !== 'ALIVE') {
|
||||||
|
return `You must be alive.`;
|
||||||
|
} else if (!options.alive && player.state.deathState === 'ALIVE') {
|
||||||
|
return `You must not be alive.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.wounded !== undefined) {
|
||||||
|
if (options.wounded && player.state.deathState !== 'WOUNDED') {
|
||||||
|
return `You aren't brutally wounded.`;
|
||||||
|
} else if (!options.wounded && player.state.deathState === 'WOUNDED') {
|
||||||
|
return `You're brutally wounded.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.knocked !== undefined) {
|
||||||
|
if (!options.knocked && player.state.deathState === 'KNOCKED') {
|
||||||
|
return `You're knocked out.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.dead !== undefined) {
|
||||||
|
if (options.dead && player.state.deathState !== 'DEAD') {
|
||||||
|
return `You aren't dead.`;
|
||||||
|
} else if (!options.dead && player.state.deathState === 'DEAD') {
|
||||||
|
return `You're dead.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
45
src/server/core/commands/guards/vehicle.command.guard.ts
Normal file
45
src/server/core/commands/guards/vehicle.command.guard.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { VehicleSystem } from '../../../features/vehicle';
|
||||||
|
|
||||||
|
const DRIVER_SEAT = -1; // GTA/FiveM convention: -1 = driver seat
|
||||||
|
|
||||||
|
type VehicleCommandGuardOptions = {
|
||||||
|
driver?: boolean;
|
||||||
|
owner?: boolean;
|
||||||
|
engine?: 'on' | 'off';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VehicleCommandGuard = (options?: VehicleCommandGuardOptions): CommandGuard => {
|
||||||
|
return (player: PlayerMp) => {
|
||||||
|
if (!player.vehicle) {
|
||||||
|
return `You aren't in a vehicle`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.engine !== undefined) {
|
||||||
|
if (VehicleSystem.GetVehicleClassName(player.vehicle) !== 'CYCLES') {
|
||||||
|
if (options.engine === 'on' && !player.vehicle.engine) {
|
||||||
|
return `The engine must be turned on`;
|
||||||
|
} else if (options.engine === 'off' && player.vehicle.engine) {
|
||||||
|
return `The engine must be turned off`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.driver !== undefined) {
|
||||||
|
if (options.driver && player.seat !== DRIVER_SEAT) {
|
||||||
|
return `You're not driving a vehicle`;
|
||||||
|
} else if (!options.driver && player.seat === DRIVER_SEAT) {
|
||||||
|
return `You're not a passenger`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.owner !== undefined) {
|
||||||
|
if (options.owner && !VehicleSystem.DoesPlayerOwnVehicle(player, player.vehicle)) {
|
||||||
|
return `You don't own the vehicle`;
|
||||||
|
} else if (!options.owner && VehicleSystem.DoesPlayerOwnVehicle(player, player.vehicle)) {
|
||||||
|
return `You can't own the vehicle`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
};
|
||||||
364
src/server/core/commands/index.ts
Normal file
364
src/server/core/commands/index.ts
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
/**
|
||||||
|
* CommandSystem — FiveM port.
|
||||||
|
*
|
||||||
|
* Key difference from RageMP: each command is registered individually via
|
||||||
|
* RegisterCommand() rather than through a single 'playerCommand' event.
|
||||||
|
* The rest of the validation/dispatch logic is unchanged.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BaseCommand } from './base.command';
|
||||||
|
import { LogSystem, logSystem } from '../log';
|
||||||
|
import { ObjectLiteral } from 'typeorm';
|
||||||
|
import { PlayerSystem } from '../../features/player';
|
||||||
|
import { ChatSystem } from '../../features/chat';
|
||||||
|
import { clientSideCommands } from '../../../shared/const/clientside-commands';
|
||||||
|
import { AnimationEntity } from '../../entities/animation.entity';
|
||||||
|
import { animationSystem } from '../../features/animations';
|
||||||
|
import { PlayerStateCommandGuard } from './guards/state.command.guard';
|
||||||
|
import { portableRadioSystem } from '../../features/portable-radio';
|
||||||
|
import { propertySystem } from '../../features/properties';
|
||||||
|
import { PropertyEntity } from '../../entities/property.entity';
|
||||||
|
import { ItemEntity } from '../../entities/item.entity';
|
||||||
|
import { PrisonCommandGuard } from './guards/prisoned.command.guard';
|
||||||
|
import { staffSystem } from '../../features/staff';
|
||||||
|
import { ContainsInvalidCharacters } from '../../features/utility/ContainsInvalidCharacters';
|
||||||
|
import { Sanitize } from '../../features/utility/Sanitize';
|
||||||
|
import { PlayerLogEvents } from '../log/events/player.log.events';
|
||||||
|
import { companySystem } from '../../features/company';
|
||||||
|
import { CompanyEntity } from '../../entities/company.entity';
|
||||||
|
import { factionSystem } from '../../features/faction';
|
||||||
|
import { FactionEntity } from '../../entities/faction.entity';
|
||||||
|
import { LargeDataServer } from '../../features/utility/SendLargeData';
|
||||||
|
import { punishmentSystem } from '../../features/admin/punishment';
|
||||||
|
import { LoginSystem } from '../../features/login';
|
||||||
|
import { playerRegistry, PlayerWrapper } from '../player/player-registry';
|
||||||
|
|
||||||
|
type CommandList = {
|
||||||
|
name: string[];
|
||||||
|
params: { hint: string; default?: string }[];
|
||||||
|
guards?: CommandGuard[];
|
||||||
|
grouped?: string;
|
||||||
|
description?: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export class CommandSystem {
|
||||||
|
commands: Map<string, BaseCommand> = new Map();
|
||||||
|
commandList: CommandList = [];
|
||||||
|
|
||||||
|
init(features: { commands?: (typeof BaseCommand)[] }[]): void {
|
||||||
|
features
|
||||||
|
.filter((f) => f.commands)
|
||||||
|
.forEach((f) => {
|
||||||
|
f.commands!.forEach((FeatureCommand: typeof BaseCommand) => {
|
||||||
|
const command = new FeatureCommand();
|
||||||
|
if (this.areCommandParamsValid(command)) {
|
||||||
|
this.addCommandToMap(this.commands, command);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.generateCommandList();
|
||||||
|
this.registerAllWithFiveM();
|
||||||
|
|
||||||
|
// Send command list when client requests it
|
||||||
|
RegisterNetEvent('requestCommandList');
|
||||||
|
AddEventHandler('requestCommandList', () => {
|
||||||
|
const src = (global as any).source as number;
|
||||||
|
const player = playerRegistry.get(src);
|
||||||
|
if (player) this.requestCommandList(player);
|
||||||
|
});
|
||||||
|
|
||||||
|
logSystem.initLog('Command System', `Loaded ${this.commands.size} commands.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerAllWithFiveM(): void {
|
||||||
|
for (const [name] of this.commands) {
|
||||||
|
this.registerWithFiveM(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerWithFiveM(name: string): void {
|
||||||
|
RegisterCommand(name, async (source: number, args: string[], rawCommand: string) => {
|
||||||
|
const player = playerRegistry.get(source);
|
||||||
|
if (!player) return;
|
||||||
|
// Re-assemble the full command text the way RageMP provided it
|
||||||
|
const fullText = args.length ? `${name} ${args.join(' ')}` : name;
|
||||||
|
await this.playerCommand(player, fullText);
|
||||||
|
}, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
initAnimCommand(anim: AnimationEntity): void {
|
||||||
|
if (this.commands.has(anim.command)) {
|
||||||
|
LogSystem.Error('Animation', 'initAnimCommand', `Duplicate command /${anim.command}`, anim);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeCommand: BaseCommand = {
|
||||||
|
$options: { name: anim.command, grouped: 'animation' },
|
||||||
|
run: async (player: PlayerMp) => {
|
||||||
|
animationSystem.playAnim(player as PlayerMpLoggedIn, anim.command);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addCommandToMap(this.commands, writeCommand);
|
||||||
|
this.registerWithFiveM(anim.command);
|
||||||
|
}
|
||||||
|
|
||||||
|
initRadioCommands(): void {
|
||||||
|
for (let index = 1; index <= 20; index++) {
|
||||||
|
const makeGuards = () => [
|
||||||
|
PlayerStateCommandGuard({ alive: true }),
|
||||||
|
PrisonCommandGuard({ all: true }),
|
||||||
|
];
|
||||||
|
|
||||||
|
const radioCommands: BaseCommand[] = [
|
||||||
|
{
|
||||||
|
$options: {
|
||||||
|
name: `r${index}`,
|
||||||
|
params: { message: { hint: 'Message', validator: 'string' } },
|
||||||
|
guards: makeGuards(),
|
||||||
|
grouped: 'Portable Radio',
|
||||||
|
description: `Talk in slot ${index} of your radio`,
|
||||||
|
},
|
||||||
|
run: async (player: PlayerMpLoggedIn, params: { message: string }) => {
|
||||||
|
if (punishmentSystem.checkMuted(player, 'ic')) return;
|
||||||
|
if (!portableRadioSystem.hasRadio(player)) return ChatSystem.Error(player, "You don't have a radio.");
|
||||||
|
if (!portableRadioSystem.isRadioOn(player)) return ChatSystem.Error(player, 'Turn on your radio to use it.');
|
||||||
|
if (!portableRadioSystem.getSlotFrequency(player, index)) return ChatSystem.Error(player, `You don't have a frequency in slot #${index}.`);
|
||||||
|
portableRadioSystem.sendRadioMessage(player, params.message, index, false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// r{n}low, r{n}me, r{n}do — same structure, abbreviated for brevity
|
||||||
|
// TODO: port the remaining radio sub-commands from the original CommandSystem
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const cmd of radioCommands) {
|
||||||
|
this.addCommandToMap(this.commands, cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addCommandToMap(map: Map<any, any>, command: BaseCommand): void {
|
||||||
|
const names = Array.isArray(command.$options.name) ? command.$options.name : [command.$options.name];
|
||||||
|
for (const n of names) {
|
||||||
|
map.set(n.toLowerCase(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
areCommandParamsValid(command: BaseCommand): boolean {
|
||||||
|
if (!command.$options.params) return true;
|
||||||
|
|
||||||
|
const paramKeys = Object.keys(command.$options.params);
|
||||||
|
const invalid = paramKeys
|
||||||
|
.map((pk) => command.$options.params![pk])
|
||||||
|
.find((param, index) => {
|
||||||
|
if (param.validator === 'string' && index !== paramKeys.length - 1) {
|
||||||
|
console.error(`Skipping /${command.$options.name}: 'string' param must be last`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (param.default !== undefined) {
|
||||||
|
if (paramKeys.slice(index).find((pk) => command.$options.params![pk].default === undefined)) {
|
||||||
|
console.error(`Skipping /${command.$options.name}: optional param before required param`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return !invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCommand(name: string): BaseCommand | undefined {
|
||||||
|
return this.commands.get(name.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
async runCommand(player: PlayerMp, command: BaseCommand, params?: ObjectLiteral): Promise<boolean> {
|
||||||
|
await command.run(player, params);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseParams(params: any, words: string[]) {
|
||||||
|
const paramEntries = Object.entries(params) as any;
|
||||||
|
|
||||||
|
for (const [index, paramEntry] of paramEntries.entries()) {
|
||||||
|
if (!paramEntry?.[1]?.overrides) continue;
|
||||||
|
if (paramEntry[1].overrides[words[index]]) {
|
||||||
|
const overrideEntries = Object.entries(paramEntry[1].overrides[words[index]]) as any;
|
||||||
|
for (const [overrideIndex, overrideEntry] of overrideEntries) {
|
||||||
|
paramEntries[overrideIndex] = [overrideEntry._propertyName, overrideEntry];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramsParsed: any = {};
|
||||||
|
for (const entry of paramEntries) {
|
||||||
|
if (entry) paramsParsed[entry[0]] = entry[1];
|
||||||
|
}
|
||||||
|
return paramsParsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
async playerCommand(player: PlayerWrapper, fullText: string): Promise<void> {
|
||||||
|
if (!LoginSystem.IsLoggedIn(player)) return;
|
||||||
|
|
||||||
|
if (ContainsInvalidCharacters(fullText)) {
|
||||||
|
LogSystem.PlayerEvent(player, PlayerLogEvents.invalid_input, 'Attempted command with invalid characters', { command: fullText });
|
||||||
|
return ChatSystem.Error(player, 'Your command contains invalid characters.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const split = fullText.trim().split(' ');
|
||||||
|
const commandName = split.shift();
|
||||||
|
if (!commandName) return ChatSystem.Error(player, 'Well give us a command will you!');
|
||||||
|
|
||||||
|
const command = this.getCommand(commandName);
|
||||||
|
|
||||||
|
LogSystem.CommandEvent(
|
||||||
|
player,
|
||||||
|
command?.$options.name
|
||||||
|
? (Array.isArray(command.$options.name) ? command.$options.name[0] : command.$options.name).toLowerCase()
|
||||||
|
: null,
|
||||||
|
`/${fullText}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
if (!clientSideCommands.includes(commandName)) {
|
||||||
|
return ChatSystem.Error(player, `${commandName} is not a valid command!`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command.$options.guards?.length) {
|
||||||
|
for (const guard of command.$options.guards) {
|
||||||
|
const passed = guard(player);
|
||||||
|
if (passed !== true) return ChatSystem.Error(player, passed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!command.$options.params) {
|
||||||
|
await this.runCommand(player, command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramsTemplate = this.parseParams(command.$options.params, split);
|
||||||
|
const params: Record<string, any> = {};
|
||||||
|
const paramNames = Object.keys(paramsTemplate);
|
||||||
|
const mandatoryCount = paramNames.filter((pk) => paramsTemplate[pk].default === undefined).length;
|
||||||
|
|
||||||
|
if (split.length < mandatoryCount) {
|
||||||
|
const missing = paramNames
|
||||||
|
.slice(split.length)
|
||||||
|
.map((p) => `${paramsTemplate[p].hint}${paramsTemplate[p].default !== undefined ? ` (default: ${paramsTemplate[p].default})` : ''}`);
|
||||||
|
return ChatSystem.Error(player, `Missing parameter${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors: [string, string][] = [];
|
||||||
|
|
||||||
|
paramNames.forEach((paramName, index) => {
|
||||||
|
const meta = paramsTemplate[paramName];
|
||||||
|
const { validator, hint } = meta;
|
||||||
|
const value = split[index];
|
||||||
|
|
||||||
|
if (value === undefined && meta.default !== undefined) {
|
||||||
|
params[paramName] = meta.default;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validator === 'number') {
|
||||||
|
if (!/^[+-]?([0-9]*[.])?[0-9]+$/.test(value)) return void errors.push([hint, 'has to be a number']);
|
||||||
|
params[paramName] = Number(value);
|
||||||
|
} else if (validator === 'integer') {
|
||||||
|
if (!/^[+-]?[0-9]+$/.test(value)) return void errors.push([hint, 'has to be a whole number']);
|
||||||
|
params[paramName] = Number(value);
|
||||||
|
} else if (validator === 'string') {
|
||||||
|
const input = split.slice(index).join(' ');
|
||||||
|
if (meta.length && input.length > meta.length) return void errors.push([hint, `longer than ${meta.length} symbols`]);
|
||||||
|
params[paramName] = input;
|
||||||
|
} else if (validator === 'vehicle') {
|
||||||
|
const vehicle = mp.vehicles.at(Number(value));
|
||||||
|
if (!vehicle) return void errors.push([hint, 'invalid ID']);
|
||||||
|
params[paramName] = vehicle;
|
||||||
|
} else if (validator === 'character') {
|
||||||
|
params[paramName] = PlayerSystem.GetPlayerFromString(value, true, false, false);
|
||||||
|
if (!params[paramName]) return void errors.push([hint, 'invalid character name']);
|
||||||
|
} else if (validator === 'account') {
|
||||||
|
params[paramName] = PlayerSystem.GetPlayerFromString(value, false, true, false);
|
||||||
|
if (!params[paramName]) return void errors.push([hint, 'invalid account name']);
|
||||||
|
} else if (validator === 'player') {
|
||||||
|
params[paramName] = PlayerSystem.GetPlayerFromString(value, true, true, true, false, staffSystem.isOnAdminDuty(player));
|
||||||
|
if (!params[paramName]) return void errors.push([hint, 'invalid character/account name']);
|
||||||
|
} else if (validator === 'company') {
|
||||||
|
if (!/^[0-9]+$/.test(value)) return void errors.push([hint, 'invalid company ID']);
|
||||||
|
params[paramName] = companySystem.getCompanyById(Number(value)) ?? null;
|
||||||
|
if (!params[paramName]) return void errors.push([hint, 'invalid company ID']);
|
||||||
|
} else if (validator === 'faction') {
|
||||||
|
if (!/^[0-9]+$/.test(value)) return void errors.push([hint, 'invalid faction ID']);
|
||||||
|
params[paramName] = factionSystem.getFactionFromId(Number(value)) ?? null;
|
||||||
|
if (!params[paramName]) return void errors.push([hint, 'invalid faction ID']);
|
||||||
|
} else if (validator === 'property') {
|
||||||
|
const prop = propertySystem.getPropertyByNetId(Number(value));
|
||||||
|
if (!prop) return void errors.push([hint, 'invalid property ID']);
|
||||||
|
params[paramName] = prop;
|
||||||
|
} else if (validator === 'word') {
|
||||||
|
if (!value) return void errors.push([hint, 'required']);
|
||||||
|
params[paramName] = value;
|
||||||
|
} else if (Array.isArray(validator)) {
|
||||||
|
if (!validator.includes(value)) return void errors.push([hint, `must be one of: ${validator.join(', ')}`]);
|
||||||
|
params[paramName] = value;
|
||||||
|
} else if (validator instanceof RegExp) {
|
||||||
|
if (!validator.test(value)) return void errors.push([hint, 'invalid format']);
|
||||||
|
params[paramName] = value;
|
||||||
|
} else {
|
||||||
|
params[paramName] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return ChatSystem.Error(player, `Invalid: ${errors.map(([h, e]) => `${h} ${e}`).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.runCommand(player, command, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
requestCommandList(player: PlayerWrapper): void {
|
||||||
|
if (!LoginSystem.IsLoggedIn(player)) return;
|
||||||
|
|
||||||
|
let list = this.commandList.filter((cmd) => {
|
||||||
|
if (!cmd.guards) return true;
|
||||||
|
for (const guard of cmd.guards) {
|
||||||
|
if (guard.name !== 'permission' && guard.name !== 'staff') continue;
|
||||||
|
if (guard(player) !== true) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
list = list.map((cmd) =>
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(cmd).filter(
|
||||||
|
([_, v]) => (Array.isArray(v) && v.length > 0 && !(v.length === 1 && v[0] === null)) || (!Array.isArray(v) && v != null),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) as CommandList;
|
||||||
|
|
||||||
|
LargeDataServer.Send(player, 'commandList', JSON.stringify(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
generateCommandList(): CommandList {
|
||||||
|
this.commandList = Array.from(this.commands.values()).map((cmd) => {
|
||||||
|
const params = cmd.$options.params ?? {};
|
||||||
|
return {
|
||||||
|
name: Array.isArray(cmd.$options.name) ? cmd.$options.name : [cmd.$options.name],
|
||||||
|
params: Object.keys(params).map((pk) => ({
|
||||||
|
hint: params[pk].hint,
|
||||||
|
...(params[pk].default !== undefined ? { default: String(params[pk].default) } : {}),
|
||||||
|
...(params[pk].overrides ? { overrides: params[pk].overrides } : {}),
|
||||||
|
})),
|
||||||
|
guards: cmd.$options.guards,
|
||||||
|
grouped: cmd.$options.grouped,
|
||||||
|
description: cmd.$options.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.commandList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const commandSystem = new CommandSystem();
|
||||||
15
src/server/core/commands/options.ts
Normal file
15
src/server/core/commands/options.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { BaseCommand } from './base.command';
|
||||||
|
|
||||||
|
export type CommandOptions<T> = {
|
||||||
|
name: string | string[];
|
||||||
|
params?: CommandParamMetaData<T>;
|
||||||
|
guards?: CommandGuard[];
|
||||||
|
grouped?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Command<T>(options: CommandOptions<T>) {
|
||||||
|
return function (constructor: typeof BaseCommand): void {
|
||||||
|
constructor.prototype.$options = options;
|
||||||
|
};
|
||||||
|
}
|
||||||
47
src/server/core/database.ts
Normal file
47
src/server/core/database.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Connection, createConnection, ConnectionOptions } from 'typeorm';
|
||||||
|
import { ALL_ENTITIES } from '../entities/index';
|
||||||
|
import { logSystem } from './log';
|
||||||
|
|
||||||
|
function buildConfig(): ConnectionOptions {
|
||||||
|
return {
|
||||||
|
type: 'mysql',
|
||||||
|
host: process.env.DB_HOST || 'play.southwest-roleplay.dev',
|
||||||
|
port: Number(process.env.DB_PORT) || 3306,
|
||||||
|
username: process.env.DB_USER || 'username',
|
||||||
|
password: process.env.DB_PASS || 'password',
|
||||||
|
database: process.env.DB_NAME || 'game',
|
||||||
|
logging: ['error', 'log'],
|
||||||
|
entities: ALL_ENTITIES as unknown as any[],
|
||||||
|
synchronize: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DatabaseSystem {
|
||||||
|
connection?: Connection | void;
|
||||||
|
attempts = 0;
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
const config = buildConfig();
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
if (this.connection) {
|
||||||
|
await this.connection.connect();
|
||||||
|
} else {
|
||||||
|
this.connection = await createConnection(config);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.attempts < 5) {
|
||||||
|
console.error(`[Bootstrap] Database connection unsuccessful, retrying ...`, error);
|
||||||
|
this.attempts++;
|
||||||
|
} else {
|
||||||
|
console.error(`[Bootstrap] Database connection unsuccessful`, error);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (!this.connection);
|
||||||
|
|
||||||
|
logSystem.initLog('Database', 'Connection successful');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const databaseSystem = new DatabaseSystem();
|
||||||
14
src/server/core/feature-system.ts
Normal file
14
src/server/core/feature-system.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { BaseCommand } from "./commands/base.command";
|
||||||
|
import { logSystem } from "./log";
|
||||||
|
|
||||||
|
export abstract class FeatureSystem {
|
||||||
|
public commands: typeof BaseCommand[];
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
postInit?(): Promise<void>;
|
||||||
|
|
||||||
|
public async init(message?: string): Promise<void> {
|
||||||
|
logSystem.initLog(this.constructor.name, message ?? 'Initialized.');
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/server/core/log/events/admin.log.events.ts
Normal file
85
src/server/core/log/events/admin.log.events.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
export const AdminLogEvents = {
|
||||||
|
spectating: 'spectating',
|
||||||
|
stop_spectating: 'stop_spectating',
|
||||||
|
kick: 'kick',
|
||||||
|
ajail: 'ajail',
|
||||||
|
ban: 'ban',
|
||||||
|
unjail: 'unjail',
|
||||||
|
teleport: 'teleport',
|
||||||
|
watch_pm: 'watch_pm',
|
||||||
|
watch_radio: 'watch_radio',
|
||||||
|
mute: 'mute',
|
||||||
|
get_health: 'get_health',
|
||||||
|
set_health: 'set_health',
|
||||||
|
set_armor: 'set_armor',
|
||||||
|
froze: 'froze',
|
||||||
|
slap: 'slap',
|
||||||
|
respawn_car: 'respawn_car',
|
||||||
|
spawn_car: 'spawn_car',
|
||||||
|
park_car: 'park_car',
|
||||||
|
respawn: 'respawn',
|
||||||
|
setstat: 'setstat',
|
||||||
|
anti_cheat: 'anti_cheat',
|
||||||
|
removed_ad: 'removed_ad',
|
||||||
|
viewed_items: 'viewed_items',
|
||||||
|
decrypt_mask: 'decrypt_mask',
|
||||||
|
give_money: 'give_money',
|
||||||
|
vehicle_livery: 'vehicle_livery',
|
||||||
|
noclip: 'noclip',
|
||||||
|
throw_off: 'throw_off',
|
||||||
|
anote_issued: 'anote_issued',
|
||||||
|
anote_deleted: 'anote_deleted',
|
||||||
|
anote_edited: 'anote_edited',
|
||||||
|
anote_expired: 'anote_expired',
|
||||||
|
report: 'report',
|
||||||
|
set_ptp: 'set_ptp',
|
||||||
|
watch_faction: 'watch_faction',
|
||||||
|
create_company: 'create_company',
|
||||||
|
set_company: 'set_company',
|
||||||
|
revived: 'revived',
|
||||||
|
create_faction: 'create_faction',
|
||||||
|
set_faction: 'set_faction',
|
||||||
|
spawn_item: 'spawn_item',
|
||||||
|
set_license: 'set_license',
|
||||||
|
atm: 'atm',
|
||||||
|
payday: 'payday',
|
||||||
|
portable_radio: 'portable_radio',
|
||||||
|
prison: 'prison',
|
||||||
|
set_property: 'set_property',
|
||||||
|
create_property: 'create_property',
|
||||||
|
create_car: 'create_car',
|
||||||
|
set_car: 'set_car',
|
||||||
|
delete_car: 'delete_car',
|
||||||
|
vehicle: 'vehicle',
|
||||||
|
impound_lot: 'impound_lot',
|
||||||
|
weather: 'weather',
|
||||||
|
set_time: 'set_time',
|
||||||
|
set_weather: 'set_weather',
|
||||||
|
override_weather: 'override_weather',
|
||||||
|
trucking: 'trucking',
|
||||||
|
item_type_groups: 'item_type_groups',
|
||||||
|
property_stock: 'property_stock',
|
||||||
|
mission: 'mission',
|
||||||
|
managejob: 'managejob',
|
||||||
|
soberup: 'soberup',
|
||||||
|
deletescene: 'deletescene',
|
||||||
|
gotoscene: 'gotoscene',
|
||||||
|
expirescene: 'expirescene',
|
||||||
|
trip: 'trip',
|
||||||
|
canceltow: 'cancel_tow',
|
||||||
|
graffiti: 'graffiti',
|
||||||
|
pet: 'pet',
|
||||||
|
loan_defaulted: 'loan_defaulted',
|
||||||
|
set_pet: 'set_pet',
|
||||||
|
goto_pet: 'goto_pet',
|
||||||
|
get_pet: 'get_pet',
|
||||||
|
force_tutorial: 'force_tutorial',
|
||||||
|
updated_tutorial: 'updated_tutorial',
|
||||||
|
vehicle_mods: 'vehicle_mods',
|
||||||
|
ipl: 'ipl',
|
||||||
|
roadblock: 'roadblock',
|
||||||
|
find_prop_inventories: 'find_prop_inventories',
|
||||||
|
radio_station: 'radio_station',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type AdminLogEvent = keyof typeof AdminLogEvents;
|
||||||
7
src/server/core/log/events/blackjack.log.events.ts
Normal file
7
src/server/core/log/events/blackjack.log.events.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const BlackjackLogEvents = {
|
||||||
|
game: 'game',
|
||||||
|
tillOnline: 'tillOnline',
|
||||||
|
till: 'till',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type BlackjackLogEvent = keyof typeof BlackjackLogEvents;
|
||||||
12
src/server/core/log/events/chat.log.events.ts
Normal file
12
src/server/core/log/events/chat.log.events.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export const ChatLogEvents = {
|
||||||
|
ic: 'ic',
|
||||||
|
ic_quiet: 'ic_low',
|
||||||
|
ic_loud: 'ic_loud',
|
||||||
|
ic_whisper: 'ic_whisper',
|
||||||
|
ooc: 'ooc',
|
||||||
|
ooc_aooc: 'ooc_aooc',
|
||||||
|
pm: 'pm',
|
||||||
|
vicinity: 'vicinity',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ChatLogEvent = keyof typeof ChatLogEvents;
|
||||||
19
src/server/core/log/events/company.log.events.ts
Normal file
19
src/server/core/log/events/company.log.events.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
export const CompanyLogEvents = {
|
||||||
|
created: 'created',
|
||||||
|
primary_property_change: 'primary_property_change',
|
||||||
|
paycheck_reward: 'paycheck_reward',
|
||||||
|
position_created: 'position_created',
|
||||||
|
position_modified: 'position_modified',
|
||||||
|
position_deleted: 'position_deleted',
|
||||||
|
pos_payment: 'pos_payment',
|
||||||
|
type_changed: 'type_changed',
|
||||||
|
protect_changed: 'protect_changed',
|
||||||
|
employee_added: 'employee_added',
|
||||||
|
employee_fired: 'employee_fired',
|
||||||
|
respawn_car: 'respawn_car',
|
||||||
|
tow_cars: 'tow_cars',
|
||||||
|
set_company: 'set_company',
|
||||||
|
vehicle: 'vehicle',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type CompanyLogEvent = keyof typeof CompanyLogEvents;
|
||||||
16
src/server/core/log/events/corpse.log.event.ts
Normal file
16
src/server/core/log/events/corpse.log.event.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export const CorpseLogEvents = {
|
||||||
|
created: 'created',
|
||||||
|
spawned: 'spawned',
|
||||||
|
despawned: 'despawned',
|
||||||
|
moved_veh: 'moved_veh',
|
||||||
|
moved_morgue: 'moved_morgue',
|
||||||
|
drag: 'drag',
|
||||||
|
stop_drag: 'stop_drag',
|
||||||
|
request_id: 'request_id',
|
||||||
|
identification: 'identification',
|
||||||
|
buried: 'buried',
|
||||||
|
exhumed: 'exhumed',
|
||||||
|
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type CorpseLogEvent = keyof typeof CorpseLogEvents;
|
||||||
7
src/server/core/log/events/darknet.log.events.ts
Normal file
7
src/server/core/log/events/darknet.log.events.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const DarkNetLogEvents = {
|
||||||
|
order_placed: 'order_placed',
|
||||||
|
delivery_failed: 'delivery_failed',
|
||||||
|
delivered: 'delivered',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type DarkNetLogEvent = keyof typeof DarkNetLogEvents;
|
||||||
8
src/server/core/log/events/dmv.log.events.ts
Normal file
8
src/server/core/log/events/dmv.log.events.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const DMVLogEvent = {
|
||||||
|
testing: 'testing',
|
||||||
|
passed: 'passed',
|
||||||
|
failed: 'failed',
|
||||||
|
renewed: 'renewed',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type DMVLogEvent = keyof typeof DMVLogEvent;
|
||||||
5
src/server/core/log/events/door.log.events.ts
Normal file
5
src/server/core/log/events/door.log.events.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const DoorLogEvents = {
|
||||||
|
lock_status: 'lock_status',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type DoorLogEvent = keyof typeof DoorLogEvents;
|
||||||
27
src/server/core/log/events/faction.log.events.ts
Normal file
27
src/server/core/log/events/faction.log.events.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export const FactionLogEvents = {
|
||||||
|
created: 'created',
|
||||||
|
name_change: 'name_change',
|
||||||
|
abbrev_change: 'abbrev_change',
|
||||||
|
type_change: 'type_change',
|
||||||
|
faction_property: 'faction_property',
|
||||||
|
chat_color_change: 'chat_color_change',
|
||||||
|
primary_property_change: 'primary_property_change',
|
||||||
|
paycheck_reward: 'paycheck_reward',
|
||||||
|
permit_revoke: 'permit_revoke',
|
||||||
|
permit_issue: 'permit_issue',
|
||||||
|
rank_created: 'rank_created',
|
||||||
|
rank_modified: 'rank_modified',
|
||||||
|
rank_deleted: 'rank_deleted',
|
||||||
|
mdc: 'mdc',
|
||||||
|
faction_invite: 'faction_invite',
|
||||||
|
set_faction: 'set_faction',
|
||||||
|
player_ranks: 'player_ranks',
|
||||||
|
faction_uninvite: 'faction_uninvite',
|
||||||
|
prison: 'prison',
|
||||||
|
respawn_car: 'respawn_car',
|
||||||
|
tow_cars: 'tow_cars',
|
||||||
|
pet: 'pet',
|
||||||
|
cut_power: 'cut_power',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type FactionLogEvent = keyof typeof FactionLogEvents;
|
||||||
16
src/server/core/log/events/furniture.log.events.ts
Normal file
16
src/server/core/log/events/furniture.log.events.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export const FurnitureLogEvents = {
|
||||||
|
created: 'created',
|
||||||
|
modified: 'modified',
|
||||||
|
duplicate: 'duplicate',
|
||||||
|
sold: 'sold',
|
||||||
|
created_inventory: 'created_inventory',
|
||||||
|
tagged: 'tagged',
|
||||||
|
entered_object_editor: 'entered_object_editor',
|
||||||
|
named: 'named',
|
||||||
|
purged_furniture: 'purged_furniture',
|
||||||
|
replaced: 'replaced',
|
||||||
|
lock_status: 'lock_status',
|
||||||
|
created_tv: 'created_tv',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type FurnitureLogEvent = keyof typeof FurnitureLogEvents;
|
||||||
8
src/server/core/log/events/graffiti.log.events.ts
Normal file
8
src/server/core/log/events/graffiti.log.events.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const GraffitiLogEvents = {
|
||||||
|
destroyed: 'destroyed',
|
||||||
|
washed: 'washed',
|
||||||
|
sprayed: 'sprayed',
|
||||||
|
created: 'created',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type GraffitiLogEvent = keyof typeof GraffitiLogEvents;
|
||||||
18
src/server/core/log/events/item.log.events.ts
Normal file
18
src/server/core/log/events/item.log.events.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const ItemLogEvents = {
|
||||||
|
created: 'created',
|
||||||
|
transaction: 'transaction',
|
||||||
|
dropped: 'dropped',
|
||||||
|
world_place: 'world_place',
|
||||||
|
world_pickup: 'world_pickup',
|
||||||
|
world_moved: 'world_moved',
|
||||||
|
vehicle_world_place: 'vehicle_world_place',
|
||||||
|
vehicle_world_pickup: 'vehicle_world_pickup',
|
||||||
|
vehicle_world_moved: 'vehicle_world_moved',
|
||||||
|
data: 'data',
|
||||||
|
deleted: 'deleted',
|
||||||
|
confiscated: 'confiscate',
|
||||||
|
rename: 'rename',
|
||||||
|
merged: 'merged',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ItemLogEvent = keyof typeof ItemLogEvents;
|
||||||
8
src/server/core/log/events/law.log.events.ts
Normal file
8
src/server/core/log/events/law.log.events.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export const LawLogEvents = {
|
||||||
|
jail: 'jail',
|
||||||
|
arsenal: 'arsenal',
|
||||||
|
loadgun: 'loadgun',
|
||||||
|
takegun: 'takegun',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LawLogEvent = keyof typeof LawLogEvents;
|
||||||
7
src/server/core/log/events/marker.log.events.ts
Normal file
7
src/server/core/log/events/marker.log.events.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const MarkerLogEvents = {
|
||||||
|
expired: 'expired',
|
||||||
|
created: 'created',
|
||||||
|
permanent: 'permanent',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type MarkerLogEvent = keyof typeof MarkerLogEvents;
|
||||||
18
src/server/core/log/events/mdc.log.events.ts
Normal file
18
src/server/core/log/events/mdc.log.events.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export const MDCLogEvents = {
|
||||||
|
opened: 'opened',
|
||||||
|
searched: 'searched',
|
||||||
|
trace: 'trace',
|
||||||
|
executable: 'executable',
|
||||||
|
prescription: 'prescription',
|
||||||
|
fine: 'fine',
|
||||||
|
mugshot: 'mugshot',
|
||||||
|
notice: 'notice',
|
||||||
|
medical: 'medical',
|
||||||
|
driver: 'driver',
|
||||||
|
weapon: 'weapon',
|
||||||
|
permit_issue: 'permit_issue',
|
||||||
|
permit_revoke: 'permit_revoke',
|
||||||
|
corpse_id: 'corpse_id',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type MDCLogEvent = keyof typeof MDCLogEvents;
|
||||||
13
src/server/core/log/events/pet.log.events.ts
Normal file
13
src/server/core/log/events/pet.log.events.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export const PetLogEvents = {
|
||||||
|
created: 'created',
|
||||||
|
removed: 'removed',
|
||||||
|
spawned: 'spawned',
|
||||||
|
despawned: 'despawned',
|
||||||
|
ordered: 'order',
|
||||||
|
name_change: 'name_change',
|
||||||
|
type_change: 'type_change',
|
||||||
|
faction_change: 'faction_change',
|
||||||
|
character_change: 'character_change',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PetLogEvent = keyof typeof PetLogEvents;
|
||||||
98
src/server/core/log/events/player.log.events.ts
Normal file
98
src/server/core/log/events/player.log.events.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
export const PlayerLogEvents = {
|
||||||
|
join: 'join',
|
||||||
|
login: 'login',
|
||||||
|
disconnect: 'disconnect',
|
||||||
|
login_fail: 'login_fail',
|
||||||
|
login_groups: 'login_groups',
|
||||||
|
character_loaded: 'character_loaded',
|
||||||
|
phone_created: 'phone_created',
|
||||||
|
character_create: 'character_create',
|
||||||
|
jailed: 'jailed',
|
||||||
|
kick: 'kick',
|
||||||
|
ajail: 'ajail',
|
||||||
|
muted: 'muted',
|
||||||
|
banned: 'banned',
|
||||||
|
towcars: 'towcars',
|
||||||
|
anti_cheat: 'anti_cheat',
|
||||||
|
transaction_flag: 'transaction_flagged',
|
||||||
|
showing_atp_alerts: 'showing_atp_alerts',
|
||||||
|
read_atp_alerts: 'read_atp_alerts',
|
||||||
|
entered_vehicle: 'entered_vehicle',
|
||||||
|
exited_vehicle: 'exited_vehicle',
|
||||||
|
vehicle_purchase: 'vehicle_purchase',
|
||||||
|
vehicle_modshop: 'vehicle_modshop',
|
||||||
|
canceled_vehicle_purchase: 'canceled_vehicle_purchase',
|
||||||
|
freeze: 'freeze',
|
||||||
|
thawed: 'thawed',
|
||||||
|
unjailed: 'unjailed',
|
||||||
|
report: 'report',
|
||||||
|
anotes_expired: 'anotes_expired',
|
||||||
|
afk: 'afk',
|
||||||
|
animation: 'animation',
|
||||||
|
barber_shop: 'barber_shop',
|
||||||
|
ads: 'ads',
|
||||||
|
employment: 'employment',
|
||||||
|
damage: 'damage',
|
||||||
|
carrying: 'carrying',
|
||||||
|
helpup: 'helpup',
|
||||||
|
weapons: 'weapons',
|
||||||
|
darknet: 'darknet',
|
||||||
|
dmv: 'dmv',
|
||||||
|
door: 'door',
|
||||||
|
duty: 'duty',
|
||||||
|
menu: 'menu',
|
||||||
|
property: 'property',
|
||||||
|
company: 'company',
|
||||||
|
faction: 'faction',
|
||||||
|
vehicle: 'vehicle',
|
||||||
|
mdc: 'mdc',
|
||||||
|
medic: 'medic',
|
||||||
|
restraints: 'restraints',
|
||||||
|
fishing: 'fishing',
|
||||||
|
gambling: 'gambling',
|
||||||
|
object: 'object',
|
||||||
|
inventory: 'inventory',
|
||||||
|
money: 'money',
|
||||||
|
offer: 'offer',
|
||||||
|
permissions: 'permissions',
|
||||||
|
phone: 'phone',
|
||||||
|
level: 'level',
|
||||||
|
spawned: 'spawned',
|
||||||
|
frisk: 'frisk',
|
||||||
|
mask: 'mask',
|
||||||
|
portable_radio: 'portable_radio',
|
||||||
|
prison: 'prison',
|
||||||
|
xm_radio: 'xm_radio',
|
||||||
|
store: 'store',
|
||||||
|
fine: 'fine',
|
||||||
|
left_company: 'left_company',
|
||||||
|
mission: 'mission',
|
||||||
|
job: 'job',
|
||||||
|
invalid_input: 'invalid_input',
|
||||||
|
confiscate: 'confiscate',
|
||||||
|
reset_position: 'reset_position',
|
||||||
|
rent: 'rent',
|
||||||
|
manually_unfroze: 'manually_unfroze',
|
||||||
|
phone_alert: 'phone_alert',
|
||||||
|
graffiti: 'graffiti',
|
||||||
|
camera: 'camera',
|
||||||
|
outfit: 'outfit',
|
||||||
|
clean_cash: 'clean_cash',
|
||||||
|
pawn: 'pawn',
|
||||||
|
pet: 'pet',
|
||||||
|
loan: 'loan',
|
||||||
|
transfer_property: 'transfer_property',
|
||||||
|
expression: 'expression',
|
||||||
|
cinematic: 'cinematic',
|
||||||
|
coroner: 'coroner',
|
||||||
|
scratch_weapon: 'scratch_weapon',
|
||||||
|
convert_appearance: 'convert_appearance',
|
||||||
|
force_tutorial: 'force_tutorial',
|
||||||
|
crawling: 'crawling',
|
||||||
|
corpse: 'corpse',
|
||||||
|
uncaughtException: 'uncaughtException',
|
||||||
|
share: 'share',
|
||||||
|
surgery: 'surgery',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PlayerLogEvent = keyof typeof PlayerLogEvents;
|
||||||
57
src/server/core/log/events/property.log.events.ts
Normal file
57
src/server/core/log/events/property.log.events.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
export const PropertyLogEvents = {
|
||||||
|
created: 'created',
|
||||||
|
renamed: 'renamed',
|
||||||
|
exterior_change: 'exterior_change',
|
||||||
|
interior_change: 'interior_change',
|
||||||
|
type_change: 'type_change',
|
||||||
|
parent_change: 'parent_change',
|
||||||
|
faction_change: 'faction_change',
|
||||||
|
company_change: 'company_change',
|
||||||
|
character_change: 'character_change',
|
||||||
|
furniture_slots: 'furniture_slots',
|
||||||
|
furniture_loading: 'furniture_loading',
|
||||||
|
furniture_unloading: 'furniture_unloading',
|
||||||
|
security_loading: 'security_loading',
|
||||||
|
security_unloading: 'security_unloading',
|
||||||
|
settings_change: 'settings_change',
|
||||||
|
permit_revoke: 'permit_revoke',
|
||||||
|
permit_issue: 'permit_issue',
|
||||||
|
access_key: 'access_key',
|
||||||
|
lease: 'lease',
|
||||||
|
player_entered: 'player_entered',
|
||||||
|
player_exited: 'plaeyr_exited',
|
||||||
|
locked: 'locked',
|
||||||
|
rent: 'rent',
|
||||||
|
interior_props: 'interior_props',
|
||||||
|
interior_ipls: 'interior_ipls',
|
||||||
|
areas: 'areas',
|
||||||
|
address: 'address',
|
||||||
|
master_change: 'master_change',
|
||||||
|
name_change: 'name_change',
|
||||||
|
xm_radio: 'xm_radio',
|
||||||
|
local_time: 'local_time',
|
||||||
|
local_weather: 'local_weather',
|
||||||
|
blip: 'blip',
|
||||||
|
reload: 'reload',
|
||||||
|
stock: 'stock',
|
||||||
|
shop: 'shop',
|
||||||
|
freeze: 'freeze',
|
||||||
|
enter_reward: 'enter_reward',
|
||||||
|
visitor_cap: 'visitor_cap',
|
||||||
|
collection: 'collection',
|
||||||
|
eviction: 'eviction',
|
||||||
|
hospital_price: 'hospital_price',
|
||||||
|
eviction_inactive: 'eviction_inactive',
|
||||||
|
clean_cash: 'clean_cash',
|
||||||
|
pawn: 'pawn',
|
||||||
|
purchased: 'purchased',
|
||||||
|
transfer: 'transfer',
|
||||||
|
lockpick: 'lockpick',
|
||||||
|
security_subscribe: 'security_subscribe',
|
||||||
|
mission: 'mission',
|
||||||
|
power: 'power',
|
||||||
|
metal_detector: 'metal_detector',
|
||||||
|
elevator: 'elevator',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropertyLogEvent = keyof typeof PropertyLogEvents;
|
||||||
9
src/server/core/log/events/ptp.log.events.ts
Normal file
9
src/server/core/log/events/ptp.log.events.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export const PointToPointLogEvents = {
|
||||||
|
created: 'created',
|
||||||
|
entered: 'entered',
|
||||||
|
exited: 'exited',
|
||||||
|
set_ptp: 'set_ptp',
|
||||||
|
lock: 'lock',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type PointToPointLogEvent = keyof typeof PointToPointLogEvents;
|
||||||
7
src/server/core/log/events/roulette.log.event.ts
Normal file
7
src/server/core/log/events/roulette.log.event.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const RouletteLogEvents = {
|
||||||
|
game: 'game',
|
||||||
|
tillOnline: 'tillOnline',
|
||||||
|
till: 'till',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type RouletteLogEvent = keyof typeof RouletteLogEvents;
|
||||||
65
src/server/core/log/events/vehicle.log.events.ts
Normal file
65
src/server/core/log/events/vehicle.log.events.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
export const VehicleLogEvents = {
|
||||||
|
created: 'created',
|
||||||
|
deleted: 'deleted',
|
||||||
|
spawned: 'spawned',
|
||||||
|
respawned: 'respawned',
|
||||||
|
engine: 'engine',
|
||||||
|
parked: 'parked',
|
||||||
|
rent: 'rent',
|
||||||
|
rent_extend: 'rent_extend',
|
||||||
|
rent_stop: 'rent_stop',
|
||||||
|
plate: 'plate',
|
||||||
|
registration: 'registration',
|
||||||
|
player_entered: 'player_entered',
|
||||||
|
player_exited: 'player_exited',
|
||||||
|
canceled_dealership: 'canceled_dealership',
|
||||||
|
successful_dealership: 'successful_dealership',
|
||||||
|
livery: 'livery',
|
||||||
|
slap: 'slap',
|
||||||
|
ejected: 'ejected',
|
||||||
|
weapons: 'weapons',
|
||||||
|
access_key: 'access_key',
|
||||||
|
helo: 'helo',
|
||||||
|
mechanic: 'mechanic',
|
||||||
|
xm_radio: 'xm_radio',
|
||||||
|
fine: 'fine',
|
||||||
|
extras: 'extras',
|
||||||
|
fuel: 'fuel',
|
||||||
|
flipped: 'flipped',
|
||||||
|
keyless: 'keyless',
|
||||||
|
repair: 'repair',
|
||||||
|
color_change: 'color_change',
|
||||||
|
character_change: 'character_change',
|
||||||
|
faction_change: 'faction_change',
|
||||||
|
company_change: 'company_change',
|
||||||
|
spawn_change: 'spawn_change',
|
||||||
|
wheel_change: 'wheel_change',
|
||||||
|
tint_change: 'tint_change',
|
||||||
|
suspension_change: 'suspension_change',
|
||||||
|
dirt_change: 'dirt_change',
|
||||||
|
manual_change: 'manual_change',
|
||||||
|
protected_change: 'protected_change',
|
||||||
|
engine_change: 'engine_change',
|
||||||
|
model_change: 'model_change',
|
||||||
|
upgrades: 'upgrades',
|
||||||
|
raw: 'raw',
|
||||||
|
transfer_owner: 'transfer_owner',
|
||||||
|
trunk: 'trunk',
|
||||||
|
hood: 'hood',
|
||||||
|
wash: 'wash',
|
||||||
|
impound: 'impound',
|
||||||
|
tow: 'tow',
|
||||||
|
personaltow: 'personaltow',
|
||||||
|
lock: 'lock',
|
||||||
|
alarm: 'alarm',
|
||||||
|
stolen: 'stolen',
|
||||||
|
modshop: 'modshop',
|
||||||
|
pet_entered: 'pet_entered',
|
||||||
|
pet_exited: 'pet_exited',
|
||||||
|
pet_searched: 'pet_searched',
|
||||||
|
insurance: 'insurance',
|
||||||
|
mods_change: 'mods_change',
|
||||||
|
mods_copy: 'mods_copy',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type VehicleLogEvent = keyof typeof VehicleLogEvents;
|
||||||
7
src/server/core/log/events/whitelist.log.events.ts
Normal file
7
src/server/core/log/events/whitelist.log.events.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const WhitelistLogEvents = {
|
||||||
|
rejected: 'rejected',
|
||||||
|
removed: 'removed',
|
||||||
|
added: 'added',
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WhitelistLogEvent = keyof typeof WhitelistLogEvents;
|
||||||
744
src/server/core/log/index.ts
Normal file
744
src/server/core/log/index.ts
Normal file
@@ -0,0 +1,744 @@
|
|||||||
|
import * as colors from 'colors';
|
||||||
|
import { mongoSystem } from '../mongo';
|
||||||
|
import { PlayerLogEvent, PlayerLogEvents } from './events/player.log.events';
|
||||||
|
import { ChatLogEvent, ChatLogEvents } from './events/chat.log.events';
|
||||||
|
import { PropertyEntity } from '../../entities/property.entity';
|
||||||
|
import { PropertyLogEvent, PropertyLogEvents } from './events/property.log.events';
|
||||||
|
import { FactionEntity } from '../../entities/faction.entity';
|
||||||
|
import { FactionLogEvent, FactionLogEvents } from './events/faction.log.events';
|
||||||
|
import { WhitelistLogEvent } from './events/whitelist.log.events';
|
||||||
|
import { AdminLogEvent } from './events/admin.log.events';
|
||||||
|
import { ItemEntity } from '../../entities/item.entity';
|
||||||
|
import { ItemLogEvent } from './events/item.log.events';
|
||||||
|
import { JSONStringifyRecursive } from '../../features/utility/JSONStringifyNoRecursive';
|
||||||
|
import { LawLogEvent } from './events/law.log.events';
|
||||||
|
import { VehicleLogEvent } from './events/vehicle.log.events';
|
||||||
|
import { VehicleSystem } from '../../features/vehicle';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { CompanyEntity } from '../../entities/company.entity';
|
||||||
|
import { CompanyLogEvent } from './events/company.log.events';
|
||||||
|
import { VehicleEntity } from '../../entities/vehicle.entity';
|
||||||
|
import { ObjectLiteral } from 'typeorm';
|
||||||
|
import { DarkNetLogEvent } from './events/darknet.log.events';
|
||||||
|
import { DoorEntity } from '../../entities/door.entity';
|
||||||
|
import { DoorLogEvent } from './events/door.log.events';
|
||||||
|
import { PointToPointEntity } from '../../entities/point-to-point.entity';
|
||||||
|
import { PointToPointLogEvent } from './events/ptp.log.events';
|
||||||
|
import { FurnitureLogEvent } from './events/furniture.log.events';
|
||||||
|
import { FurnitureItemEntity } from '../../entities/furniture-item.entity';
|
||||||
|
import { mdcSystem } from '../../features/mdc';
|
||||||
|
import { MDCLogEvent } from './events/mdc.log.events';
|
||||||
|
import { DarknetOrderEntity } from '../../entities/darknet-order.entity';
|
||||||
|
import { GraffitiLogEvent } from './events/graffiti.log.events';
|
||||||
|
import { GraffitiEntity } from '../../entities/graffiti.entity';
|
||||||
|
import { PetLogEvent } from './events/pet.log.events';
|
||||||
|
import { PetEntity } from '../../entities/pet.entity';
|
||||||
|
import { BlackjackLogEvent } from './events/blackjack.log.events';
|
||||||
|
import { CorpseLogEvent } from './events/corpse.log.event';
|
||||||
|
import { MarkerLogEvent } from './events/marker.log.events';
|
||||||
|
import { InfoMarkerEntity } from '../../entities/info-markers.entity';
|
||||||
|
import { RouletteLogEvent } from './events/roulette.log.event';
|
||||||
|
|
||||||
|
colors.enable();
|
||||||
|
|
||||||
|
interface LogCollection {
|
||||||
|
name: string;
|
||||||
|
permission: string;
|
||||||
|
hasEvents?: boolean;
|
||||||
|
fields: {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
trace?: string;
|
||||||
|
default?: boolean;
|
||||||
|
required?: boolean;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DarknetOrderData = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
faction: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DarknetItems = {
|
||||||
|
type: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
price: number;
|
||||||
|
oocPrice: number;
|
||||||
|
amount: number;
|
||||||
|
unit: string;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
export class LogSystem {
|
||||||
|
lastInitTime: number;
|
||||||
|
firstInitTime: number;
|
||||||
|
|
||||||
|
static Collections: LogCollection[] = [
|
||||||
|
{
|
||||||
|
name: 'players',
|
||||||
|
permission: 'LOG_VIEW_PLAYERS',
|
||||||
|
hasEvents: true,
|
||||||
|
fields: [
|
||||||
|
{ name: 'player', type: 'player', default: true, required: true },
|
||||||
|
{ name: 'ip', trace: 'player.ip', type: 'string' },
|
||||||
|
{ name: 'log', type: 'string', default: true },
|
||||||
|
{ name: 'target', type: 'player' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
this.lastInitTime = +new Date();
|
||||||
|
this.firstInitTime = +new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
initLog(feature: string, content: string): void {
|
||||||
|
const now = +new Date();
|
||||||
|
const time = ((now - this.lastInitTime) / 1000).toFixed(1).padStart(4, ' ') + 's';
|
||||||
|
const totalTime = ((now - this.firstInitTime) / 1000).toFixed(1).padEnd(4, ' ') + 's';
|
||||||
|
LogSystem.Collect(`${(totalTime + ' | ' + time).grey} [init:${feature}]`.green + `: ${content.yellow}`);
|
||||||
|
this.lastInitTime = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Log(feature: string, content: string): void {
|
||||||
|
LogSystem.Collect(`[${feature}]`.green + `: ${content.reset}`);
|
||||||
|
// LogSystem.Collect(`LogSystem.Log is for debugging only, all relevant data needs to be stored using event functions`.red);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComposePlayerObject(player?: PlayerMp, options?: { state?: boolean }): Record<string, any> | undefined {
|
||||||
|
if (!player) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerObject: {
|
||||||
|
gameId: number;
|
||||||
|
ip: string;
|
||||||
|
sessionId?: number;
|
||||||
|
accountId?: number;
|
||||||
|
accountName?: string;
|
||||||
|
characterId?: number;
|
||||||
|
characterName?: string;
|
||||||
|
state?: PlayerState;
|
||||||
|
} = {
|
||||||
|
gameId: player.id,
|
||||||
|
ip: player.ip,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (player.session) {
|
||||||
|
playerObject.sessionId = player.session.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.account) {
|
||||||
|
playerObject.accountId = player.account.id;
|
||||||
|
playerObject.accountName = player.account.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (player.character) {
|
||||||
|
playerObject.characterId = player.character.id;
|
||||||
|
playerObject.characterName = player.character.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.state) {
|
||||||
|
playerObject.state = player.state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(JSONStringifyRecursive(playerObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComposeGraffitiEntityObject(graffiti: GraffitiEntity): Record<string, any> | undefined {
|
||||||
|
const graffitiObject: {
|
||||||
|
id: number;
|
||||||
|
content: string;
|
||||||
|
font: string;
|
||||||
|
colour: string;
|
||||||
|
opacity: number;
|
||||||
|
destroyed: Date;
|
||||||
|
creator_id: number;
|
||||||
|
} = {
|
||||||
|
id: graffiti.id,
|
||||||
|
content: graffiti.content,
|
||||||
|
font: graffiti.font,
|
||||||
|
colour: graffiti.colour,
|
||||||
|
opacity: graffiti.opacity,
|
||||||
|
destroyed: graffiti.destroyed,
|
||||||
|
creator_id: graffiti.creator_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.parse(JSONStringifyRecursive(graffitiObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComposeDoorEntityObject(door: DoorEntity): Record<string, any> | undefined {
|
||||||
|
const doorObject: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
permission: string;
|
||||||
|
lockType: string;
|
||||||
|
property: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
} = {
|
||||||
|
id: door.id,
|
||||||
|
name: door.name,
|
||||||
|
permission: door.permission_name || 'null',
|
||||||
|
lockType: door.lock_type,
|
||||||
|
property: door.property
|
||||||
|
? {
|
||||||
|
id: door.property.id,
|
||||||
|
name: door.property.getName(),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.parse(JSONStringifyRecursive(doorObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComposePTPEntityObject(ptp: PointToPointEntity): Record<string, any> | undefined {
|
||||||
|
const ptpObject: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
permission: string | null;
|
||||||
|
lockStatus: boolean;
|
||||||
|
} = {
|
||||||
|
id: ptp.id,
|
||||||
|
name: ptp.name,
|
||||||
|
permission: ptp.permission,
|
||||||
|
lockStatus: ptp.locked,
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.parse(JSONStringifyRecursive(ptpObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComposePlayerPosition(player: PlayerMp): { x: number; y: number; z: number; dimension: number } {
|
||||||
|
return {
|
||||||
|
x: Math.round(player.position.x * 1000) / 1000,
|
||||||
|
y: Math.round(player.position.y * 1000) / 1000,
|
||||||
|
z: Math.round(player.position.z * 1000) / 1000,
|
||||||
|
dimension: player.dimension,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComposePlayerName(player: PlayerMp): string {
|
||||||
|
if (!mp.players.exists(player)) {
|
||||||
|
return 'Unknown Player';
|
||||||
|
}
|
||||||
|
let name = player.name;
|
||||||
|
if (player.account) {
|
||||||
|
name = player.account.name;
|
||||||
|
if (player.character) {
|
||||||
|
name = `${name} - ${player.character.name}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name = `${name} [${player.id}]`;
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComposeFurnitureItemObject(item: FurnitureItemEntity): Record<string, any> {
|
||||||
|
const itemObject = {
|
||||||
|
id: item.id,
|
||||||
|
created: mdcSystem.formatDate(item.created),
|
||||||
|
creator: item.created_by_character,
|
||||||
|
price: item.price,
|
||||||
|
name: item.name,
|
||||||
|
tag: item.tag,
|
||||||
|
inventory:
|
||||||
|
item.has_inventory && item.inventory
|
||||||
|
? {
|
||||||
|
inventory_id: item.inventory.id,
|
||||||
|
items: item.inventory.items.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.id,
|
||||||
|
type: item.type.name,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: false,
|
||||||
|
prison_cell: item.prison_cell_id,
|
||||||
|
property_id: item.property_id,
|
||||||
|
position: {
|
||||||
|
x: item.x,
|
||||||
|
y: item.y,
|
||||||
|
z: item.z,
|
||||||
|
rx: item.rx,
|
||||||
|
ry: item.ry,
|
||||||
|
rz: item.rz,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.parse(JSONStringifyRecursive(itemObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComposeVehicleObject(vehicle?: VehicleMp): Record<string, any> {
|
||||||
|
if (!vehicle) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const speed = Math.floor(VehicleSystem.GetVehicleMph(vehicle));
|
||||||
|
const vehicleObject: VehicleLogComposition = {
|
||||||
|
gameId: vehicle.id,
|
||||||
|
model: VehicleSystem.GetVehicleNameFromHash(vehicle.model) || String(vehicle.model),
|
||||||
|
plate: vehicle.numberPlate,
|
||||||
|
bodyHealth: vehicle.bodyHealth,
|
||||||
|
engineHealth: vehicle.engineHealth,
|
||||||
|
occupants: LogSystem.ComposeVehicleOccupants(vehicle),
|
||||||
|
speedInMph: speed,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (vehicle.vehicleEntity) {
|
||||||
|
vehicleObject.id = vehicle.vehicleEntity.id;
|
||||||
|
if (vehicle.vehicleEntity.owner) {
|
||||||
|
vehicleObject.owner = vehicle.vehicleEntity.owner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(JSONStringifyRecursive(vehicleObject));
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComposeVehiclePosition(vehicle: VehicleMp): { x: number; y: number; z: number; heading: number; dimension: number } {
|
||||||
|
return {
|
||||||
|
x: Math.round(vehicle.position.x * 1000) / 1000,
|
||||||
|
y: Math.round(vehicle.position.z * 1000) / 1000,
|
||||||
|
z: Math.round(vehicle.position.z * 1000) / 1000,
|
||||||
|
heading: Math.round(vehicle.heading),
|
||||||
|
dimension: vehicle.dimension,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComposeVehicleOccupants(vehicle: VehicleMp): any {
|
||||||
|
const occupants: {
|
||||||
|
seatId: number;
|
||||||
|
player: any;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
vehicle.getVehicleOccupants().forEach((player) => {
|
||||||
|
const composed = LogSystem.ComposePlayerObject(player);
|
||||||
|
occupants.push({
|
||||||
|
seatId: player.seat,
|
||||||
|
player: composed,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return JSON.parse(JSONStringifyRecursive(occupants));
|
||||||
|
}
|
||||||
|
|
||||||
|
static ComposeVehicleName(vehicle: VehicleMp | VehicleEntity): string {
|
||||||
|
let name = VehicleSystem.GetVehicleName(vehicle);
|
||||||
|
if (!(vehicle instanceof VehicleEntity) && vehicle.vehicleEntity) {
|
||||||
|
name += ` (#${vehicle.vehicleEntity.id}`;
|
||||||
|
if (vehicle.vehicleEntity.owner) {
|
||||||
|
name += `, ${vehicle.vehicleEntity.owner})`;
|
||||||
|
} else {
|
||||||
|
name += ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
name += ` [${vehicle.id}]`;
|
||||||
|
} else if (vehicle instanceof VehicleEntity) {
|
||||||
|
name += ` (Entity #${vehicle.id})`;
|
||||||
|
} else {
|
||||||
|
name += ` [${vehicle.id}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async PlayerEvent(player: PlayerMp, event: PlayerLogEvent, log: string, data?: ObjectLiteral): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'players', {
|
||||||
|
date: new Date(),
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
position: LogSystem.ComposePlayerPosition(player),
|
||||||
|
data: data !== undefined ? JSON.parse(JSONStringifyRecursive(data)) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
LogSystem.Collect(`[Player:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async GraffitiEvent(graffiti: GraffitiEntity, event: GraffitiLogEvent, log: string, data?: ObjectLiteral): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'graffiti', {
|
||||||
|
date: new Date(),
|
||||||
|
graffiti: graffiti ? LogSystem.ComposeGraffitiEntityObject(graffiti) : undefined,
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
data: data !== undefined ? JSON.parse(JSONStringifyRecursive(data)) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
LogSystem.Collect(`[Graffiti:${event}]`.green + ` ${log.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async VehicleEvent(vehicle: VehicleMp | VehicleEntity, event: VehicleLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'vehicles', {
|
||||||
|
date: new Date(),
|
||||||
|
vehicle: vehicle instanceof VehicleEntity ? { id: vehicle.id } : LogSystem.ComposeVehicleObject(vehicle),
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
position: vehicle instanceof VehicleEntity ? null : LogSystem.ComposeVehiclePosition(vehicle),
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
});
|
||||||
|
|
||||||
|
LogSystem.Collect(`[Vehicle:${event}]`.green + `: ${LogSystem.ComposeVehicleName(vehicle).yellow} ${log.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async ChatEvent(player: PlayerMp, event: ChatLogEvent, log: string, target?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'chat', {
|
||||||
|
date: new Date(),
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
position: LogSystem.ComposePlayerPosition(player),
|
||||||
|
target: LogSystem.ComposePlayerObject(target),
|
||||||
|
});
|
||||||
|
|
||||||
|
LogSystem.Collect(`[Chat:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async BlackjackEvent(event: BlackjackLogEvent, tableId: number, roundId: string, log: string, player?: PlayerMp) {
|
||||||
|
await mongoSystem.insertOne('logs', 'blackjack', {
|
||||||
|
date: new Date(),
|
||||||
|
tableId,
|
||||||
|
roundId,
|
||||||
|
log,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
LogSystem.Collect(`[Blackjack:${event}]`.green + `: ${tableId}/${roundId} ${log.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async RouletteEvent(event: RouletteLogEvent, tableId: number, gameId: string, log: string, player?: PlayerMp) {
|
||||||
|
await mongoSystem.insertOne('logs', 'roulette', {
|
||||||
|
date: new Date(),
|
||||||
|
tableId,
|
||||||
|
gameId,
|
||||||
|
log,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
LogSystem.Collect(`[Roulette:${event}]`.green + `: ${tableId}/${gameId} ${log.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async CorpseEvent(event: CorpseLogEvent, corpseId: number, log: string, player?: PlayerMp) {
|
||||||
|
await mongoSystem.insertOne('logs', 'corpses', {
|
||||||
|
date: new Date(),
|
||||||
|
corpseId,
|
||||||
|
log,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
LogSystem.Collect(`[Corpse:${event}]`.green + `: ${corpseId} ${log.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async MarkerEvent(event: MarkerLogEvent, marker: InfoMarkerEntity, log: string, player?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'marker', {
|
||||||
|
date: new Date(),
|
||||||
|
marker,
|
||||||
|
log,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
LogSystem.Collect(`[Marker:${event}]`.green + `: ${marker.id} ${log.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async ItemEvent(item: ItemEntity, event: ItemLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'items', {
|
||||||
|
date: new Date(),
|
||||||
|
item,
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
LogSystem.Collect(`[Item:${event}]`.green + `: ${item.inventory_id}/${item.id} ${log.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async CommandEvent(player: PlayerMp, command: string | null, log: string): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'commands', {
|
||||||
|
date: new Date(),
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
command,
|
||||||
|
log,
|
||||||
|
position: LogSystem.ComposePlayerPosition(player),
|
||||||
|
});
|
||||||
|
|
||||||
|
LogSystem.Collect(`[Command:${command}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async PropertyEvent(property: PropertyEntity, event: PropertyLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'properties', {
|
||||||
|
date: new Date(),
|
||||||
|
property: {
|
||||||
|
id: property.id,
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
});
|
||||||
|
|
||||||
|
let eventLog = `[Property(${property.id}):${event}]`.green + `: ${log}`;
|
||||||
|
if (player) {
|
||||||
|
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async PetEvent(pet: PetEntity, event: PetLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'pets', {
|
||||||
|
date: new Date(),
|
||||||
|
pet: {
|
||||||
|
id: pet.id,
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
});
|
||||||
|
|
||||||
|
let eventLog = `[Pet(${pet.id}):${event}]`.green + `: ${log}`;
|
||||||
|
if (player) {
|
||||||
|
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async MDCEvent(player: PlayerMp, event: MDCLogEvent, log: string, data?: any): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'mdc', {
|
||||||
|
date: new Date(),
|
||||||
|
event,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
faction: player.character?.faction
|
||||||
|
? {
|
||||||
|
id: player.character.faction.id,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
log,
|
||||||
|
data: !data ? undefined : JSON.parse(JSONStringifyRecursive(data)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventLog = `[MDC:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`;
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async FurnitureEvent(
|
||||||
|
player: PlayerMp,
|
||||||
|
event: FurnitureLogEvent,
|
||||||
|
dimension: number,
|
||||||
|
log: string,
|
||||||
|
furnitureItem?: FurnitureItemEntity,
|
||||||
|
property?: PropertyEntity,
|
||||||
|
): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'furniture', {
|
||||||
|
date: new Date(),
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
dimension,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
item: furnitureItem ? LogSystem.ComposeFurnitureItemObject(furnitureItem) : undefined,
|
||||||
|
property: property
|
||||||
|
? {
|
||||||
|
id: property.id,
|
||||||
|
currentFurnitureCount: property.furnitureItems ? property.furnitureItems.length : 0,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventLog = `[Furniture:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`;
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async FactionEvent(faction: FactionEntity, event: FactionLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'factions', {
|
||||||
|
date: new Date(),
|
||||||
|
faction: {
|
||||||
|
id: faction.id,
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
});
|
||||||
|
|
||||||
|
let eventLog = `[Faction(${faction.id}):${event}]`.green + `: ${log}`;
|
||||||
|
if (player) {
|
||||||
|
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async CompanyEvent(company: CompanyEntity, event: CompanyLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'companies', {
|
||||||
|
date: new Date(),
|
||||||
|
company: {
|
||||||
|
id: company.id,
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
});
|
||||||
|
|
||||||
|
let eventLog = `[Company(${company.id}):${event}]`.green + `: ${log}`;
|
||||||
|
if (player) {
|
||||||
|
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Error(system: string, event: string, log: string, trace: any, player?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'errors', {
|
||||||
|
date: new Date(),
|
||||||
|
system,
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
trace: JSONStringifyRecursive(trace),
|
||||||
|
player: LogSystem.ComposePlayerObject(player, { state: true }),
|
||||||
|
});
|
||||||
|
LogSystem.Collect(`[Error:${system}/${event}]`.red + ` ${log.red}`);
|
||||||
|
console.error(trace);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async WhitelistEvent(event: WhitelistLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'whitelist', {
|
||||||
|
date: new Date(),
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
});
|
||||||
|
|
||||||
|
let eventLog = `[Whitelist:${event}]`.green + `: ${log}`;
|
||||||
|
if (player) {
|
||||||
|
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async AdminLog(event: AdminLogEvent, log: string, player?: PlayerMp, target?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'admin', {
|
||||||
|
date: new Date(),
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
target: LogSystem.ComposePlayerObject(target),
|
||||||
|
});
|
||||||
|
|
||||||
|
let eventLog = `[Admin:${event}]`.green + `: ${log}`;
|
||||||
|
if (player) {
|
||||||
|
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||||
|
}
|
||||||
|
if (target) {
|
||||||
|
eventLog += ` (${LogSystem.ComposePlayerName(target)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async DarkNetLog(event: DarkNetLogEvent, log: string, metadata?: DarknetOrderData, order?: DarknetOrderEntity, item?: DarknetItems): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'darknet', {
|
||||||
|
date: new Date(),
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
metadata,
|
||||||
|
order,
|
||||||
|
item,
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventLog = `[DarkNet:${event}]`.green + `: ${log}`;
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async DoorLog(door: DoorEntity, event: DoorLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'door', {
|
||||||
|
date: new Date(),
|
||||||
|
door: LogSystem.ComposeDoorEntityObject(door),
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
});
|
||||||
|
|
||||||
|
let eventLog = `[DoorSystem:${event}]`.green + `: Door #${door.id}`.yellow + ` ${log.reset}`;
|
||||||
|
|
||||||
|
if (player) {
|
||||||
|
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async PTPLog(ptp: PointToPointEntity, event: PointToPointLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'point_to_point', {
|
||||||
|
date: new Date(),
|
||||||
|
ptp: LogSystem.ComposePTPEntityObject(ptp),
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
});
|
||||||
|
|
||||||
|
let eventLog = `[PointToPoint:${event}]`.green + `: PTP #${ptp.id}`.yellow + ` ${log.reset}`;
|
||||||
|
|
||||||
|
if (player) {
|
||||||
|
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async LawLog(event: LawLogEvent, log: string, player?: PlayerMp, target?: PlayerMp): Promise<void> {
|
||||||
|
await mongoSystem.insertOne('logs', 'law', {
|
||||||
|
date: new Date(),
|
||||||
|
player: LogSystem.ComposePlayerObject(player),
|
||||||
|
event,
|
||||||
|
log,
|
||||||
|
target: LogSystem.ComposePlayerObject(target),
|
||||||
|
});
|
||||||
|
|
||||||
|
let eventLog = `[Law:${event}]`.green + `: ${log}`;
|
||||||
|
if (player) {
|
||||||
|
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||||
|
}
|
||||||
|
if (target) {
|
||||||
|
eventLog += ` (${LogSystem.ComposePlayerName(target)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
LogSystem.Collect(eventLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
static GetLogEvents(): {
|
||||||
|
players: string[];
|
||||||
|
properties: string[];
|
||||||
|
chat: string[];
|
||||||
|
factions: string[];
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
players: Object.keys(PlayerLogEvents),
|
||||||
|
properties: Object.keys(PropertyLogEvents),
|
||||||
|
chat: Object.keys(ChatLogEvents),
|
||||||
|
factions: Object.keys(FactionLogEvents),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Collect(raw: string): void {
|
||||||
|
const now = dayjs(new Date());
|
||||||
|
const timeFormat = now.format('hh:mm:ss');
|
||||||
|
|
||||||
|
console.log('['.white + `${timeFormat}`.grey + '] '.white + raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('uncaughtException', (error: Error, origin: string) => {
|
||||||
|
LogSystem.Error('Node.js', 'uncaughtException', 'Uncaught Exception', { origin, error });
|
||||||
|
});
|
||||||
|
|
||||||
|
export const logSystem = new LogSystem();
|
||||||
85
src/server/core/mongo.ts
Normal file
85
src/server/core/mongo.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { randomBytes } from 'crypto';
|
||||||
|
import { logSystem } from './log';
|
||||||
|
import { redisSystem } from './redis';
|
||||||
|
|
||||||
|
const createJobId = (): string => {
|
||||||
|
const bytes = randomBytes(16);
|
||||||
|
|
||||||
|
// RFC 4122 UUID v4 variant/version bits
|
||||||
|
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||||
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||||
|
|
||||||
|
const hex = bytes.toString('hex');
|
||||||
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MongoJobPayload =
|
||||||
|
| {
|
||||||
|
op: 'insertOne';
|
||||||
|
db: string;
|
||||||
|
collection: string;
|
||||||
|
document: unknown;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
op: 'updateOne';
|
||||||
|
db: string;
|
||||||
|
collection: string;
|
||||||
|
filter: Record<string, unknown>;
|
||||||
|
update: Record<string, unknown>;
|
||||||
|
options?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MongoJob = MongoJobPayload & {
|
||||||
|
id: string;
|
||||||
|
ts: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class MongoSystem {
|
||||||
|
private queueKey = process.env.LSRAGE_MONGO_QUEUE || 'mongo:jobs';
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
// No direct Mongo connection in game-server anymore.
|
||||||
|
// Only verifies Redis path is available.
|
||||||
|
const redis: any = (redisSystem as any).client ?? (redisSystem as any).redis ?? redisSystem;
|
||||||
|
|
||||||
|
if (!redis) {
|
||||||
|
throw new Error('Redis client is not available for Mongo queue publishing.');
|
||||||
|
}
|
||||||
|
|
||||||
|
logSystem.initLog('MongoQueue', `Enabled (queue=${this.queueKey})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async publish(job: MongoJobPayload): Promise<void> {
|
||||||
|
const redis: any = (redisSystem as any).client ?? (redisSystem as any).redis ?? redisSystem;
|
||||||
|
const payload: MongoJob = {
|
||||||
|
id: createJobId(),
|
||||||
|
ts: Date.now(),
|
||||||
|
...job,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Support both node-redis (lPush) and ioredis (lpush)
|
||||||
|
if (typeof redis.lPush === 'function') {
|
||||||
|
await redis.lPush(this.queueKey, JSON.stringify(payload));
|
||||||
|
} else if (typeof redis.lpush === 'function') {
|
||||||
|
await redis.lpush(this.queueKey, JSON.stringify(payload));
|
||||||
|
} else {
|
||||||
|
throw new Error('Redis client does not support LPUSH.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async insertOne(db: string, collection: string, document: unknown): Promise<void> {
|
||||||
|
await this.publish({ op: 'insertOne', db, collection, document });
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOne(
|
||||||
|
db: string,
|
||||||
|
collection: string,
|
||||||
|
filter: Record<string, unknown>,
|
||||||
|
update: Record<string, unknown>,
|
||||||
|
options?: Record<string, unknown>,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.publish({ op: 'updateOne', db, collection, filter, update, options });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mongoSystem = new MongoSystem();
|
||||||
297
src/server/core/mp-shim.ts
Normal file
297
src/server/core/mp-shim.ts
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
/**
|
||||||
|
* mp-shim.ts — RageMP compatibility layer for FiveM (server-side).
|
||||||
|
*
|
||||||
|
* Exposes a global `mp` object that mirrors the RageMP server API so that
|
||||||
|
* ported feature files compile and run with minimal changes.
|
||||||
|
*
|
||||||
|
* Migration status markers used in this file:
|
||||||
|
* COMPAT — fully implemented equivalent
|
||||||
|
* PARTIAL — works for most cases, edge cases may differ
|
||||||
|
* STUB — not yet implemented; logs a warning when called
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { playerRegistry, PlayerWrapper } from './player/player-registry';
|
||||||
|
import { vehicleRegistry, VehicleWrapper } from './vehicle/vehicle-wrapper';
|
||||||
|
import { rpc } from './rpc/index';
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Well-known RageMP server events → FiveM event names
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const RAGEMP_TO_FIVEM: Record<string, string> = {
|
||||||
|
playerJoin: 'playerConnecting', // COMPAT (slightly different — no char load yet)
|
||||||
|
playerQuit: 'playerDropped', // COMPAT
|
||||||
|
playerReady: 'playerSpawned', // PARTIAL — FiveM fires after first spawn
|
||||||
|
};
|
||||||
|
|
||||||
|
// Events where the handler's first argument should be a PlayerWrapper
|
||||||
|
const PLAYER_SOURCE_EVENTS = new Set([
|
||||||
|
'playerJoin', 'playerQuit', 'playerReady', 'playerDeath',
|
||||||
|
'playerCommand', 'requestCommandList',
|
||||||
|
// All custom client→server events are also player-sourced
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Event bus
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const localEventEmitter = new (require('events').EventEmitter)();
|
||||||
|
|
||||||
|
const events = {
|
||||||
|
delayInitialization: false,
|
||||||
|
|
||||||
|
// COMPAT — primary event registration
|
||||||
|
add(eventOrMap: string | Record<string, Function>, handler?: Function): void {
|
||||||
|
if (typeof eventOrMap === 'object') {
|
||||||
|
for (const [name, fn] of Object.entries(eventOrMap)) {
|
||||||
|
events.add(name, fn);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = eventOrMap as string;
|
||||||
|
const fn = handler!;
|
||||||
|
|
||||||
|
if (name in RAGEMP_TO_FIVEM) {
|
||||||
|
// Map to FiveM built-in event
|
||||||
|
const fivemName = RAGEMP_TO_FIVEM[name];
|
||||||
|
AddEventHandler(fivemName, function (...args: any[]) {
|
||||||
|
const src = (global as any).source as number;
|
||||||
|
if (src > 0) {
|
||||||
|
const player = playerRegistry.getOrCreate(src);
|
||||||
|
fn(player, ...args);
|
||||||
|
} else {
|
||||||
|
fn(...args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Custom event: register as net event (player-sourced) AND as local event
|
||||||
|
RegisterNetEvent(name);
|
||||||
|
AddEventHandler(name, function (...args: any[]) {
|
||||||
|
const src = (global as any).source as number;
|
||||||
|
if (src > 0) {
|
||||||
|
const player = playerRegistry.getOrCreate(src);
|
||||||
|
fn(player, ...args);
|
||||||
|
} else {
|
||||||
|
fn(...args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Local event bus (for mp.events.call)
|
||||||
|
localEventEmitter.on(name, fn);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// COMPAT — replaces mp.events.addProc (server-side RPC handler)
|
||||||
|
addProc(name: string, handler: (...args: any[]) => any): void {
|
||||||
|
rpc.register(name, handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
// COMPAT — fire a local server-side event (no player source).
|
||||||
|
// Use only the JS EventEmitter — TriggerEvent would msgpack-serialize
|
||||||
|
// complex objects (e.g. PlayerWrapper) and strip their prototype getters.
|
||||||
|
call(eventName: string, ...args: any[]): void {
|
||||||
|
localEventEmitter.emit(eventName, ...args);
|
||||||
|
},
|
||||||
|
|
||||||
|
// PARTIAL — call remote on all players (no per-player targeting here)
|
||||||
|
callRemote(eventName: string, ...args: any[]): void {
|
||||||
|
TriggerClientEvent(eventName, -1, ...args);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Player pool
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const players = {
|
||||||
|
// COMPAT
|
||||||
|
at(id: number): PlayerWrapper | undefined {
|
||||||
|
return playerRegistry.get(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
// COMPAT
|
||||||
|
toArray(): PlayerWrapper[] {
|
||||||
|
return playerRegistry.toArray();
|
||||||
|
},
|
||||||
|
|
||||||
|
toArrayFast(): PlayerWrapper[] {
|
||||||
|
return playerRegistry.toArray();
|
||||||
|
},
|
||||||
|
|
||||||
|
forEach(fn: (player: PlayerWrapper) => void): void {
|
||||||
|
playerRegistry.forEach(fn);
|
||||||
|
},
|
||||||
|
|
||||||
|
forEachFast(fn: (player: PlayerWrapper) => void): void {
|
||||||
|
playerRegistry.forEach(fn);
|
||||||
|
},
|
||||||
|
|
||||||
|
// COMPAT — broadcast chat message to all
|
||||||
|
broadcast(message: string): void {
|
||||||
|
TriggerClientEvent('_mp:chatMessage', -1, message);
|
||||||
|
},
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return playerRegistry.toArray().length;
|
||||||
|
},
|
||||||
|
|
||||||
|
exists(player: PlayerWrapper): boolean {
|
||||||
|
return playerRegistry.get(player.id) !== undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Vehicle pool
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const vehicles = {
|
||||||
|
// COMPAT — replaces mp.vehicles.new(model, position, options)
|
||||||
|
new(model: string | number, position: Vector3, options?: any): VehicleWrapper {
|
||||||
|
return vehicleRegistry.create(model, position, options);
|
||||||
|
},
|
||||||
|
|
||||||
|
at(handle: number): VehicleWrapper | undefined {
|
||||||
|
return vehicleRegistry.get(handle);
|
||||||
|
},
|
||||||
|
|
||||||
|
toArray(): VehicleWrapper[] {
|
||||||
|
return vehicleRegistry.toArray();
|
||||||
|
},
|
||||||
|
|
||||||
|
forEach(fn: (vehicle: VehicleWrapper) => void): void {
|
||||||
|
vehicleRegistry.forEach(fn);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Config (replaces mp.config — reads FiveM ConVars set in server.cfg)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const config = new Proxy({} as Record<string, any>, {
|
||||||
|
get(_target, prop: string) {
|
||||||
|
const val = GetConvar(prop, '__undefined__');
|
||||||
|
if (val === '__undefined__') return undefined;
|
||||||
|
if (val === 'true') return true;
|
||||||
|
if (val === 'false') return false;
|
||||||
|
const num = Number(val);
|
||||||
|
return isNaN(num) ? val : num;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Objects pool — STUB (RageMP server objects = FiveM server-side peds/objects)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const objects = {
|
||||||
|
new(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.objects.new() not yet implemented');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
at(id: number): any { return null; },
|
||||||
|
toArray(): any[] { return []; },
|
||||||
|
forEach(_fn: any): void {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Peds pool — STUB
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const peds = {
|
||||||
|
new(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.peds.new() not yet implemented');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
at(id: number): any { return null; },
|
||||||
|
toArray(): any[] { return []; },
|
||||||
|
forEach(_fn: any): void {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Markers / blips / labels — client-side constructs in FiveM; server sends
|
||||||
|
// creation events to clients. Stubs here so code compiles.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const markers = {
|
||||||
|
new(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.markers.new() — use client-side equivalent');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const blips = {
|
||||||
|
new(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.blips.new() — use client-side equivalent');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
new(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.labels.new() — use client-side equivalent');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkpoints = {
|
||||||
|
new(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.checkpoints.new() — use client-side equivalent');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Colshapes are client-side in FiveM (or use server polygons with player position tracking)
|
||||||
|
const colshapes = {
|
||||||
|
newSphere(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
newCuboid(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
newCircle(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
newRectangle(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
newTube(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
newPolygon(...args: any[]): any {
|
||||||
|
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Vector3 constructor (replaces new mp.Vector3(x, y, z))
|
||||||
|
class Vector3Impl {
|
||||||
|
constructor(public x: number, public y: number, public z: number) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Assemble the global mp object
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
(global as any).mp = {
|
||||||
|
events,
|
||||||
|
players,
|
||||||
|
vehicles,
|
||||||
|
objects,
|
||||||
|
peds,
|
||||||
|
markers,
|
||||||
|
blips,
|
||||||
|
labels,
|
||||||
|
checkpoints,
|
||||||
|
colshapes,
|
||||||
|
config,
|
||||||
|
Vector3: Vector3Impl,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Player lifecycle — build/tear down PlayerWrapper on connect/disconnect
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
AddEventHandler('playerConnecting', (name: string, _setKickReason: Function, deferrals: any) => {
|
||||||
|
const netId = (global as any).source as number;
|
||||||
|
playerRegistry.add(netId);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddEventHandler('playerDropped', (reason: string) => {
|
||||||
|
const netId = (global as any).source as number;
|
||||||
|
playerRegistry.remove(netId);
|
||||||
|
});
|
||||||
69
src/server/core/notify.ts
Normal file
69
src/server/core/notify.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
export type NotifyType = 'info' | 'success' | 'warning' | 'error';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A button shown on an action toast. When clicked, `event` is fired as a
|
||||||
|
* remote event on the server with the supplied `args`.
|
||||||
|
*/
|
||||||
|
export interface NotifyAction {
|
||||||
|
label: string;
|
||||||
|
event: string;
|
||||||
|
args?: (string | number | boolean)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server-side helper for sending toast notifications to players via the
|
||||||
|
* custom overlay notify component. Replaces raw player.notify() calls.
|
||||||
|
*
|
||||||
|
* Standard toasts:
|
||||||
|
* NotifySystem.Info(player, 'Heading to depot...')
|
||||||
|
* NotifySystem.Success(player, 'Package delivered!')
|
||||||
|
* NotifySystem.Warning(player, 'Van not at depot yet.')
|
||||||
|
* NotifySystem.Error(player, 'You must be in a company vehicle.')
|
||||||
|
*
|
||||||
|
* Action toasts (persistent, with buttons):
|
||||||
|
* NotifySystem.Action(player, 'ride_1', 'Taxi request 1.2km away', 'info', [
|
||||||
|
* { label: 'Accept', event: 'taxiDuty:accept', args: [1] },
|
||||||
|
* { label: 'Decline', event: 'taxiDuty:decline', args: [1] },
|
||||||
|
* ]);
|
||||||
|
*
|
||||||
|
* Dismiss a specific action toast by key:
|
||||||
|
* NotifySystem.Dismiss(player, 'ride_1');
|
||||||
|
*/
|
||||||
|
export class NotifySystem {
|
||||||
|
static Show(player: PlayerMp, message: string, type: NotifyType = 'info', duration = 5000): void {
|
||||||
|
player.call('notify:show', [message, type, duration]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Info(player: PlayerMp, message: string, duration?: number): void {
|
||||||
|
this.Show(player, message, 'info', duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Success(player: PlayerMp, message: string, duration?: number): void {
|
||||||
|
this.Show(player, message, 'success', duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Warning(player: PlayerMp, message: string, duration?: number): void {
|
||||||
|
this.Show(player, message, 'warning', duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Error(player: PlayerMp, message: string, duration?: number): void {
|
||||||
|
this.Show(player, message, 'error', duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a persistent toast with action buttons. The toast stays on screen
|
||||||
|
* until the player clicks a button, closes it manually, or the server
|
||||||
|
* calls Dismiss() with the same key.
|
||||||
|
*/
|
||||||
|
static Action(player: PlayerMp, key: string, message: string, type: NotifyType = 'info', actions: NotifyAction[]): void {
|
||||||
|
player.call('notify:action', [key, message, type, JSON.stringify(actions)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a specific action toast from a player's screen by key.
|
||||||
|
* Safe to call even if the toast has already been dismissed.
|
||||||
|
*/
|
||||||
|
static Dismiss(player: PlayerMp, key: string): void {
|
||||||
|
player.call('notify:dismiss', [key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
218
src/server/core/player/player-registry.ts
Normal file
218
src/server/core/player/player-registry.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import { CharacterEntity } from '../../entities/character.entity';
|
||||||
|
import { SessionEntity } from '../../entities/session.entity';
|
||||||
|
import { AccountDto } from '../../features/sw-rp-api/index';
|
||||||
|
|
||||||
|
export class PlayerWrapper {
|
||||||
|
readonly id: number; // FiveM net ID (= RageMP player.id)
|
||||||
|
|
||||||
|
state: PlayerState;
|
||||||
|
character?: CharacterEntity;
|
||||||
|
account?: AccountDto;
|
||||||
|
session?: SessionEntity;
|
||||||
|
groups: number[] = [];
|
||||||
|
permissions: EntityPermission[] = [];
|
||||||
|
|
||||||
|
constructor(netId: number) {
|
||||||
|
this.id = netId;
|
||||||
|
this.state = createDefaultPlayerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Identity ---
|
||||||
|
get name(): string {
|
||||||
|
return GetPlayerName(String(this.id)) ?? `Player${this.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
get ip(): string {
|
||||||
|
const endpoint = GetPlayerEndpoint(String(this.id)) ?? '';
|
||||||
|
const colonIdx = endpoint.lastIndexOf(':');
|
||||||
|
return colonIdx > 0 ? endpoint.slice(0, colonIdx) : endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
get socialClub(): string {
|
||||||
|
return GetPlayerIdentifierByType(String(this.id), 'steam') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Position (server-side via ped) ---
|
||||||
|
get position(): Vector3 {
|
||||||
|
const ped = GetPlayerPed(String(this.id));
|
||||||
|
const [x, y, z] = GetEntityCoords(ped);
|
||||||
|
return { x, y, z } as Vector3;
|
||||||
|
}
|
||||||
|
|
||||||
|
get heading(): number {
|
||||||
|
return GetEntityHeading(GetPlayerPed(String(this.id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Health ---
|
||||||
|
get health(): number {
|
||||||
|
return GetEntityHealth(GetPlayerPed(String(this.id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
set health(value: number) {
|
||||||
|
SetEntityHealth(GetPlayerPed(String(this.id)), value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dimension (stored server-side, synced via state bag) ---
|
||||||
|
get dimension(): number {
|
||||||
|
return (Player(this.id).state.dimension as number) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDimension(dimension: number): void {
|
||||||
|
Player(this.id).state.set('dimension', dimension, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Server → Client event (replaces player.call) ---
|
||||||
|
// RageMP: player.call('eventName', [arg1, arg2])
|
||||||
|
// FiveM: TriggerClientEvent('eventName', netId, arg1, arg2)
|
||||||
|
call(eventName: string, args: any[] = []): void {
|
||||||
|
TriggerClientEvent(eventName, this.id, ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Notifications (RageMP player.notify) ---
|
||||||
|
notify(message: string): void {
|
||||||
|
TriggerClientEvent('_mp:notify', this.id, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
outputChatBox(message: string): void {
|
||||||
|
TriggerClientEvent('_mp:chatMessage', this.id, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Synced variables (replaces player.setVariable / getVariable) ---
|
||||||
|
// Non-serialisable values (timers, promises, entity refs) must stay in .state;
|
||||||
|
// only primitives & plain objects should go into state bags.
|
||||||
|
setVariable(key: string, value: any): void {
|
||||||
|
try {
|
||||||
|
Player(this.id).state.set(key, value, true);
|
||||||
|
} catch {
|
||||||
|
// State bags can only hold serialisable values; silently ignore others.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getVariable<T = any>(key: string): T {
|
||||||
|
return Player(this.id).state[key] as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOwnVariable(key: string, value: any): void {
|
||||||
|
Player(this.id).state.set(key, value, false); // not broadcast to other clients
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Player management ---
|
||||||
|
kick(reason: string): void {
|
||||||
|
DropPlayer(String(this.id), reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
kickSilent(): void {
|
||||||
|
DropPlayer(String(this.id), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
ban(reason: string): void {
|
||||||
|
// Record the ban then kick — ban storage handled by admin system
|
||||||
|
this.kick(`[Banned] ${reason}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Appearance helper (sends to client for ped apply) ---
|
||||||
|
spawn(position: Vector3): void {
|
||||||
|
TriggerClientEvent('_mp:spawn', this.id, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Vehicle (replaces RageMP player.vehicle / player.seat) ---
|
||||||
|
get vehicle(): import('../vehicle/vehicle-wrapper').VehicleWrapper | null {
|
||||||
|
const { vehicleRegistry } = require('../vehicle/vehicle-wrapper');
|
||||||
|
const ped = GetPlayerPed(String(this.id));
|
||||||
|
const handle = GetVehiclePedIsIn(ped, false);
|
||||||
|
if (handle === 0) return null;
|
||||||
|
return vehicleRegistry.get(handle) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seat number: -1 = driver, 0+ = passenger (matches RageMP/GTA convention)
|
||||||
|
get seat(): number {
|
||||||
|
const ped = GetPlayerPed(String(this.id));
|
||||||
|
const handle = GetVehiclePedIsIn(ped, false);
|
||||||
|
if (handle === 0) return -2;
|
||||||
|
if (GetPedInVehicleSeat(handle, -1) === ped) return -1;
|
||||||
|
for (let i = 0; i < 8; i++) {
|
||||||
|
if (GetPedInVehicleSeat(handle, i) === ped) return i;
|
||||||
|
}
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Distance ---
|
||||||
|
dist(other: Vector3): number {
|
||||||
|
const p = this.position;
|
||||||
|
return Math.sqrt((p.x - other.x) ** 2 + (p.y - other.y) ** 2 + (p.z - other.z) ** 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
distSquared(other: Vector3): number {
|
||||||
|
const p = this.position;
|
||||||
|
return (p.x - other.x) ** 2 + (p.y - other.y) ** 2 + (p.z - other.z) ** 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Type guard helpers (mirrors RageMP pattern) ---
|
||||||
|
get isLoggedIn(): boolean {
|
||||||
|
return !!(this.character && this.account && this.session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDefaultPlayerState(): PlayerState {
|
||||||
|
return {
|
||||||
|
reconnecting: false,
|
||||||
|
phoneAnim: { currentStatus: 'out' },
|
||||||
|
currentHairSlot: 0,
|
||||||
|
togLevel: false,
|
||||||
|
loginAttempts: 0,
|
||||||
|
damageHistory: [],
|
||||||
|
deathState: 'alive' as DeathState,
|
||||||
|
falling: false,
|
||||||
|
pmWatchers: [],
|
||||||
|
pmWatched: [],
|
||||||
|
freqWatched: [],
|
||||||
|
factionWatched: [],
|
||||||
|
companyWatched: [],
|
||||||
|
report: [],
|
||||||
|
props: [],
|
||||||
|
trackedDutyIds: [],
|
||||||
|
wikiBookmarks: [],
|
||||||
|
paymentMutex: null as any, // initialised by MoneySystem
|
||||||
|
outfitOverrides: [],
|
||||||
|
serverWeapons: {},
|
||||||
|
actionLock: 0,
|
||||||
|
maxHealth: 200,
|
||||||
|
dutyHandcuffs: 0,
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PlayerRegistry {
|
||||||
|
private players: Map<number, PlayerWrapper> = new Map();
|
||||||
|
|
||||||
|
add(netId: number): PlayerWrapper {
|
||||||
|
const wrapper = new PlayerWrapper(netId);
|
||||||
|
this.players.set(netId, wrapper);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(netId: number): void {
|
||||||
|
this.players.delete(netId);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(netId: number): PlayerWrapper | undefined {
|
||||||
|
return this.players.get(netId);
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrCreate(netId: number): PlayerWrapper {
|
||||||
|
return this.players.get(netId) ?? this.add(netId);
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): PlayerWrapper[] {
|
||||||
|
return [...this.players.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
toArrayFast(): PlayerWrapper[] {
|
||||||
|
return this.toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(fn: (player: PlayerWrapper) => void): void {
|
||||||
|
this.players.forEach(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const playerRegistry = new PlayerRegistry();
|
||||||
28
src/server/core/preload.ts
Normal file
28
src/server/core/preload.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { getRepository } from 'typeorm';
|
||||||
|
import { GameEntityTypeEntity } from '../entities/game-entity-type.entity';
|
||||||
|
import { logSystem } from './log';
|
||||||
|
|
||||||
|
export class PreloadSystem {
|
||||||
|
private gameEntityTypesMap: {
|
||||||
|
[key: string]: number;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
const gameEntities = await getRepository(GameEntityTypeEntity).find();
|
||||||
|
for (const gameEntity of gameEntities) {
|
||||||
|
this.gameEntityTypesMap[gameEntity.name] = gameEntity.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
logSystem.initLog('Preload System', `Loaded ${gameEntities.length} game entity types.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
getGameEntityTypeId(name: string): number {
|
||||||
|
return this.gameEntityTypesMap[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
getGameEntityTypeById(id: number): string {
|
||||||
|
return Object.entries(this.gameEntityTypesMap).find((type) => type[1] === id)?.[0] ?? 'unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const preloadSystem = new PreloadSystem();
|
||||||
87
src/server/core/redis.ts
Normal file
87
src/server/core/redis.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import * as redis from 'redis';
|
||||||
|
import { LogSystem, logSystem } from './log';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
const REDIS_HOST = process.env.REDIS_HOST || 'swrp-redis';
|
||||||
|
const REDIS_PORT = Number(process.env.REDIS_PORT || 6379);
|
||||||
|
const REDIS_URL = process.env.REDIS_URL;
|
||||||
|
|
||||||
|
export class RedisSystem {
|
||||||
|
client: redis.RedisClient = REDIS_URL ? redis.createClient(REDIS_URL) : redis.createClient(REDIS_PORT, REDIS_HOST);
|
||||||
|
|
||||||
|
async init(): Promise<void> {
|
||||||
|
this.client.on('error', this.error.bind(this));
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.client.flushall((error: any) => {
|
||||||
|
if (!error) {
|
||||||
|
logSystem.initLog('Redis', 'Connection Ready');
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
LogSystem.Error('Redis', 'error', 'Failed to flush', error);
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(key: string): Promise<string | null> {
|
||||||
|
return promisify(this.client.get).call(this.client, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async hget(key: string, field: string): Promise<string> {
|
||||||
|
return promisify(this.client.hget).call(this.client, key, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
async hdel(key: string, field: string): Promise<number> {
|
||||||
|
// @ts-ignore
|
||||||
|
return promisify(this.client.hdel).call(this.client, key, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
async hgetall(key: string): Promise<any> {
|
||||||
|
return promisify(this.client.hgetall).call(this.client, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async hset(key: string, field: string, value: string): Promise<number> {
|
||||||
|
// @ts-ignore
|
||||||
|
return promisify(this.client.hset).call(this.client, key, field, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async smembers(key: string): Promise<string[]> {
|
||||||
|
return promisify(this.client.smembers).call(this.client, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sinter(keys: string[]): Promise<string[]> {
|
||||||
|
// @ts-ignore
|
||||||
|
return promisify(this.client.sinter).call(this.client, keys);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sismemebr(key: string, member: string): Promise<number> {
|
||||||
|
return promisify(this.client.sismember).call(this.client, key, member);
|
||||||
|
}
|
||||||
|
|
||||||
|
async scard(key: string): Promise<number> {
|
||||||
|
return promisify(this.client.scard).call(this.client, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async del(key: string): Promise<number> {
|
||||||
|
// @ts-ignore
|
||||||
|
return promisify(this.client.del).call(this.client, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
async sadd(key: string, member: string | string[]): Promise<number> {
|
||||||
|
// @ts-ignore
|
||||||
|
return promisify(this.client.sadd).call(this.client, key, member);
|
||||||
|
}
|
||||||
|
|
||||||
|
async srem(key: string, member: string): Promise<number> {
|
||||||
|
// @ts-ignore
|
||||||
|
return promisify(this.client.srem).call(this.client, key, member);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(error: string): void {
|
||||||
|
LogSystem.Error('Redis', 'error', error, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const redisSystem = new RedisSystem();
|
||||||
73
src/server/core/rpc/index.ts
Normal file
73
src/server/core/rpc/index.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Server-side RPC — replaces RageMP's mp.events.addProc / mp.events.callRemoteProc.
|
||||||
|
*
|
||||||
|
* Server registers a proc:
|
||||||
|
* rpc.register('getClosestTrunk', async (player, ...args) => { return result; });
|
||||||
|
*
|
||||||
|
* Client calls it via the client RPC module (see src/client/core/rpc/index.ts):
|
||||||
|
* const result = await rpc.call('getClosestTrunk', ...args);
|
||||||
|
*
|
||||||
|
* The wire protocol uses two net events:
|
||||||
|
* _rpc:call player → server { id, name, args }
|
||||||
|
* _rpc:result server → player { id, result, error }
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { playerRegistry } from '../player/player-registry';
|
||||||
|
|
||||||
|
type ProcHandler = (player: import('../player/player-registry').PlayerWrapper, ...args: any[]) => any;
|
||||||
|
|
||||||
|
const handlers = new Map<string, ProcHandler>();
|
||||||
|
|
||||||
|
let clientCallCounter = 0;
|
||||||
|
const clientPending = new Map<string, { resolve: (v: any) => void; reject: (e: any) => void }>();
|
||||||
|
|
||||||
|
RegisterNetEvent('_rpc:clientResult');
|
||||||
|
AddEventHandler('_rpc:clientResult', (callId: string, result: any, error: string | null) => {
|
||||||
|
const cb = clientPending.get(callId);
|
||||||
|
if (!cb) return;
|
||||||
|
clientPending.delete(callId);
|
||||||
|
if (error) cb.reject(new Error(error));
|
||||||
|
else cb.resolve(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rpc = {
|
||||||
|
register(name: string, handler: ProcHandler): void {
|
||||||
|
handlers.set(name, handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Server → client proc call (replaces mp.events.callRemoteProc / player.callProc)
|
||||||
|
callClient<T = any>(player: import('../player/player-registry').PlayerWrapper, name: string, ...args: any[]): Promise<T> {
|
||||||
|
const id = `${Date.now()}_${clientCallCounter++}`;
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
clientPending.set(id, { resolve, reject });
|
||||||
|
TriggerClientEvent('_rpc:clientCall', player.id, id, name, args);
|
||||||
|
setTimeout(() => {
|
||||||
|
if (clientPending.has(id)) {
|
||||||
|
clientPending.delete(id);
|
||||||
|
reject(new Error(`Client RPC timeout: ${name}`));
|
||||||
|
}
|
||||||
|
}, 10000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
RegisterNetEvent('_rpc:call');
|
||||||
|
AddEventHandler('_rpc:call', async (callId: string, name: string, args: any[]) => {
|
||||||
|
const netId = (global as any).source as number;
|
||||||
|
const player = playerRegistry.get(netId);
|
||||||
|
|
||||||
|
if (!player) return;
|
||||||
|
|
||||||
|
const handler = handlers.get(name);
|
||||||
|
if (!handler) {
|
||||||
|
TriggerClientEvent('_rpc:result', netId, callId, null, `No proc registered: ${name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await handler(player, ...(args ?? []));
|
||||||
|
TriggerClientEvent('_rpc:result', netId, callId, result, null);
|
||||||
|
} catch (err: any) {
|
||||||
|
TriggerClientEvent('_rpc:result', netId, callId, null, err?.message ?? String(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
41
src/server/core/session-runners.ts
Normal file
41
src/server/core/session-runners.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class SessionRunnerSystem {
|
||||||
|
pendingRunners: {
|
||||||
|
player: PlayerMp;
|
||||||
|
runner: QueryRunner;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
mp.events.add('playerQuit', this.playerQuit.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
register(player: PlayerMp, runner: QueryRunner): void {
|
||||||
|
this.pendingRunners.push({ player, runner });
|
||||||
|
console.log(`Registered runner for ${player.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister(player: PlayerMp, runner: QueryRunner): void {
|
||||||
|
this.pendingRunners = this.pendingRunners.filter((pr) => !(pr.player === player && pr.runner === runner));
|
||||||
|
console.log('unregistering runner');
|
||||||
|
}
|
||||||
|
|
||||||
|
playerQuit(player: PlayerMp): void {
|
||||||
|
const playerRunners = this.pendingRunners.filter((entry) => entry.player === player);
|
||||||
|
this.pendingRunners = this.pendingRunners.filter((entry) => entry.player !== player);
|
||||||
|
|
||||||
|
playerRunners.forEach((pendingRunner) => {
|
||||||
|
if (pendingRunner.runner.isTransactionActive) {
|
||||||
|
pendingRunner.runner.rollbackTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pendingRunner.runner.isReleased) {
|
||||||
|
pendingRunner.runner.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('removing runner on quit');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sessionRunnerSystem = new SessionRunnerSystem();
|
||||||
423
src/server/core/types.d.ts
vendored
Normal file
423
src/server/core/types.d.ts
vendored
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
/**
|
||||||
|
* Global type augmentations for the FiveM port.
|
||||||
|
*
|
||||||
|
* PlayerMp / VehicleMp etc. are aliased to our wrapper classes so that
|
||||||
|
* existing feature code that references these types compiles without changes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { PlayerWrapper } from './player/player-registry';
|
||||||
|
import { VehicleWrapper } from './vehicle/vehicle-wrapper';
|
||||||
|
import { CharacterEntity } from '../entities/character.entity';
|
||||||
|
import { AccountDto } from '../features/sw-rp-api/index';
|
||||||
|
import { SessionEntity } from '../entities/session.entity';
|
||||||
|
|
||||||
|
// Re-export wrappers under the RageMP type names so existing code just works
|
||||||
|
declare global {
|
||||||
|
type PlayerMp = PlayerWrapper;
|
||||||
|
type VehicleMp = VehicleWrapper;
|
||||||
|
|
||||||
|
// Logged-in subtype (mirrors original)
|
||||||
|
interface PlayerMpLoggedIn extends PlayerWrapper {
|
||||||
|
character: CharacterEntity;
|
||||||
|
account: AccountDto;
|
||||||
|
session: SessionEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectMp — FiveM server-side objects (peds, props); stub for now
|
||||||
|
interface ObjectMp {
|
||||||
|
id: number;
|
||||||
|
handle: number;
|
||||||
|
setVariable(key: string, value: any): void;
|
||||||
|
getVariable<T = any>(key: string): T;
|
||||||
|
furnitureOfProperty?: import('../entities/property.entity').PropertyEntity;
|
||||||
|
inventory?: import('../entities/inventory.entity').InventoryEntity;
|
||||||
|
itemLock: boolean;
|
||||||
|
bluetoothConnectedTo?: PlayerWrapper;
|
||||||
|
notifyStreaming?: boolean;
|
||||||
|
createdBy?: string;
|
||||||
|
createdOn?: string;
|
||||||
|
doorLocked?: boolean;
|
||||||
|
doorAjar?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vector3 — matches RageMP's Vector3 shape
|
||||||
|
interface Vector3 {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
z: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Array4d = [number, number, number, number];
|
||||||
|
|
||||||
|
// ColshapeMp stub
|
||||||
|
interface ColshapeMp {
|
||||||
|
vehicleDealership?: import('../entities/vehicle-dealership.entity').VehicleDealershipEntity;
|
||||||
|
property?: import('../entities/property.entity').PropertyEntity;
|
||||||
|
forProperty?: import('../entities/property.entity').PropertyEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PedMp stub
|
||||||
|
interface PedMp {
|
||||||
|
entity?: import('../entities/corpse.entity').CorpseEntity;
|
||||||
|
handle: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EntityPermission {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
repositoryId: number;
|
||||||
|
repositoryType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// VehicleMpDatabase — vehicle with a guaranteed DB entity
|
||||||
|
interface VehicleMpDatabase extends VehicleWrapper {
|
||||||
|
vehicleEntity: import('../entities/vehicle.entity').VehicleEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- PlayerState -------------------------------------------------------
|
||||||
|
// Copied verbatim from the original types.d.ts — nothing in here references
|
||||||
|
// RageMP APIs directly so it's portable as-is.
|
||||||
|
interface PlayerState {
|
||||||
|
diedToFall?: boolean;
|
||||||
|
reconnecting: boolean;
|
||||||
|
lastPlayerDeath?: number;
|
||||||
|
isOnPhone?: boolean;
|
||||||
|
phoneAnim: {
|
||||||
|
currentStatus: 'out' | 'call' | 'text';
|
||||||
|
lastDict?: string;
|
||||||
|
lastAnim?: string;
|
||||||
|
};
|
||||||
|
companyChat?: import('../entities/company.entity').CompanyEntity;
|
||||||
|
interviewing?: PlayerWrapper;
|
||||||
|
interviewedBy?: PlayerWrapper;
|
||||||
|
shift?: { companyId: number; startTime: Date; paycheckBuffer: number; paydays: number };
|
||||||
|
currentHairSlot: number;
|
||||||
|
metroDuty?: string;
|
||||||
|
sentenceId?: number | null;
|
||||||
|
prisoned?: Date | null;
|
||||||
|
jailed?: Date | null;
|
||||||
|
jailedFor?: number | null;
|
||||||
|
jailCellId?: number | null;
|
||||||
|
paroled?: Date | null;
|
||||||
|
segregated?: Date | null;
|
||||||
|
segregatedFor?: number | null;
|
||||||
|
segregatedUntil?: Date | null;
|
||||||
|
sentencedUntil?: Date | null;
|
||||||
|
remanded?: Date | null;
|
||||||
|
paroleEligible?: number | null;
|
||||||
|
noticeServed?: { release: boolean; parole: boolean };
|
||||||
|
togLevel: boolean;
|
||||||
|
togXChat?: boolean;
|
||||||
|
phoneCall?: {
|
||||||
|
id: number; number: string; party?: PlayerMpLoggedIn; objectParty?: ObjectMp;
|
||||||
|
automatedParty?: string; outgoing: boolean; status?: string; start?: number;
|
||||||
|
active: boolean; dialTimeout?: NodeJS.Timeout; timeoutTimeout?: NodeJS.Timeout;
|
||||||
|
clearingTimeout?: NodeJS.Timeout; loudSpeaker?: boolean; muted?: boolean;
|
||||||
|
};
|
||||||
|
posResetUnlock: number;
|
||||||
|
propertyInteractionUnlock: number;
|
||||||
|
vehicleInteraction: boolean;
|
||||||
|
destinationMarker?: Vector3;
|
||||||
|
emergencyDispatch?: any;
|
||||||
|
nonEmergencyDispatch?: any;
|
||||||
|
menuCompany?: import('../entities/company.entity').CompanyEntity;
|
||||||
|
phoneCaller?: PlayerMpLoggedIn;
|
||||||
|
phoneDialTimeout?: NodeJS.Timeout;
|
||||||
|
clothing: import('../features/clothing/index').StatefulClothingItem[];
|
||||||
|
characterLoadedAt?: number;
|
||||||
|
inProperty?: import('../entities/property.entity').PropertyEntity;
|
||||||
|
inPropertyArea?: any;
|
||||||
|
mechanicOperation?: VehicleMechanicOperation;
|
||||||
|
atPropertyCol?: import('../entities/property.entity').PropertyEntity;
|
||||||
|
atBackdoorCol?: import('../entities/property.entity').PropertyEntity;
|
||||||
|
jailTimer?: NodeJS.Timeout;
|
||||||
|
insideJail?: import('../entities/jail-cell.entity').JailCellEntity;
|
||||||
|
jailReleaseTime?: number;
|
||||||
|
paymentMutex: import('async-mutex').MutexInterface;
|
||||||
|
mission?: import('../features/mission/mission.model').Mission;
|
||||||
|
navDestCallback?: (...args: any[]) => void;
|
||||||
|
adjustingWeapon?: number;
|
||||||
|
objectEditor?: any;
|
||||||
|
objectSelector?: any;
|
||||||
|
vehicleTask?: any;
|
||||||
|
aimControls?: any;
|
||||||
|
furnishing?: any;
|
||||||
|
furnitureEditor?: any;
|
||||||
|
dmvTest?: any;
|
||||||
|
dmvTmpKnowledge?: boolean;
|
||||||
|
dmvTesting?: boolean;
|
||||||
|
dmvWaivedPayments?: number;
|
||||||
|
report: any[];
|
||||||
|
wakeUpTimer?: NodeJS.Timeout;
|
||||||
|
spectating?: any;
|
||||||
|
itemOffer?: any;
|
||||||
|
bluetoothConnectedTo?: ObjectMp;
|
||||||
|
localStation?: any;
|
||||||
|
localStationVolume?: number;
|
||||||
|
killEvent?: any;
|
||||||
|
killTime?: number;
|
||||||
|
damageHistory: any[];
|
||||||
|
deathState: DeathState;
|
||||||
|
falling: boolean;
|
||||||
|
lastMissclick?: number;
|
||||||
|
drugVisualEffects?: any[];
|
||||||
|
drugCameraEffect?: any;
|
||||||
|
smoking?: any;
|
||||||
|
drinking?: any;
|
||||||
|
offer?: PlayerOffer;
|
||||||
|
mdcInitialized?: boolean;
|
||||||
|
loginTimeOut?: NodeJS.Timeout;
|
||||||
|
loginAttempts: number;
|
||||||
|
afkTimeout?: NodeJS.Timeout;
|
||||||
|
trackedDutyIds: number[];
|
||||||
|
afkStartTime?: number;
|
||||||
|
afkDutyStartTime?: number;
|
||||||
|
afkDutyEndTime?: number;
|
||||||
|
tabStartTime?: number;
|
||||||
|
tabEndTime?: number;
|
||||||
|
adminDutyStartTime?: number;
|
||||||
|
testerDutyStartTime?: number;
|
||||||
|
quietModeStartTime?: number;
|
||||||
|
pmWatchers: PlayerMpLoggedIn[];
|
||||||
|
pmWatched: PlayerMpLoggedIn[];
|
||||||
|
freqWatched: number[];
|
||||||
|
factionWatched: any[];
|
||||||
|
companyWatched: any[];
|
||||||
|
muted?: any;
|
||||||
|
lastKillAlert?: number;
|
||||||
|
adminJail?: any;
|
||||||
|
characterCreatorCharacterId?: number;
|
||||||
|
helpmeRequest?: any;
|
||||||
|
wikiBookmarks: any[];
|
||||||
|
weapons?: any[];
|
||||||
|
atm?: any;
|
||||||
|
atmAccountUsed?: any;
|
||||||
|
healthTracker?: NodeJS.Timer;
|
||||||
|
actionLock: number;
|
||||||
|
polygonEditor?: any;
|
||||||
|
duty?: DutyState;
|
||||||
|
serverWeapons: Record<string, number>;
|
||||||
|
dutyTimeOverall?: number;
|
||||||
|
specialDutyTimeOverall?: number;
|
||||||
|
specialDuty?: number;
|
||||||
|
lastWound?: number;
|
||||||
|
maxHealth: number;
|
||||||
|
regenTick?: number;
|
||||||
|
deptFrequency?: any;
|
||||||
|
friskRequests?: PlayerMpLoggedIn[];
|
||||||
|
frisksApproved?: PlayerMpLoggedIn[];
|
||||||
|
fishing?: any;
|
||||||
|
barberShop?: any;
|
||||||
|
adCooldown?: number;
|
||||||
|
lastLessLethalHit?: number;
|
||||||
|
lastPaintballHit?: number;
|
||||||
|
lastPaintbillRuleHit?: number;
|
||||||
|
paintballRuleHits?: number;
|
||||||
|
attachingPlate?: any;
|
||||||
|
removingPlate?: any;
|
||||||
|
blockPm?: any;
|
||||||
|
prisonCell?: any;
|
||||||
|
propertyRadioColshapes?: any;
|
||||||
|
vehicleLockBreakin?: VehicleWrapper;
|
||||||
|
vehicleHotwire?: VehicleWrapper;
|
||||||
|
bleedOut?: any;
|
||||||
|
propertyBreakin?: number;
|
||||||
|
dutyHandcuffs: number;
|
||||||
|
applyingMakeup?: any;
|
||||||
|
staticWeather?: string;
|
||||||
|
staticTime?: number;
|
||||||
|
pendingVehicle?: boolean;
|
||||||
|
oocChatDisabled?: boolean;
|
||||||
|
staffMessagesDisabled?: boolean;
|
||||||
|
processingMissionKey?: boolean;
|
||||||
|
showJoinMessages?: boolean;
|
||||||
|
nametagDamageBlipTimeout?: NodeJS.Timeout;
|
||||||
|
outfitOverrides: any[];
|
||||||
|
lastWeaponShot?: any;
|
||||||
|
autoLow?: any;
|
||||||
|
vehicleModShop?: any;
|
||||||
|
tvAccess?: number;
|
||||||
|
lastPMSender?: PlayerMpLoggedIn;
|
||||||
|
handlingCorpse?: boolean;
|
||||||
|
tutorialCooldown?: number;
|
||||||
|
mdcExtraData?: any;
|
||||||
|
currentCallMenu?: string;
|
||||||
|
radioFreqOrder?: any;
|
||||||
|
lastEquippedBulletWeapon?: string;
|
||||||
|
lastEquippedMeleeWeapon?: string;
|
||||||
|
holdingObject?: boolean;
|
||||||
|
tackleCooldown?: number;
|
||||||
|
tackled?: boolean;
|
||||||
|
changeCharacterCooldown?: number;
|
||||||
|
corpseDigging?: boolean;
|
||||||
|
chatHighlight?: number[];
|
||||||
|
pushingGurney?: ObjectMp;
|
||||||
|
onGurney?: ObjectMp;
|
||||||
|
processingPhonePower?: boolean;
|
||||||
|
lastTeleport?: any;
|
||||||
|
fakeCall?: boolean;
|
||||||
|
props: [number, number][];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Misc types from original types.d.ts -------------------------------
|
||||||
|
type DeathState = 'ALIVE' | 'WOUNDED' | 'KNOCKED' | 'DEAD';
|
||||||
|
|
||||||
|
interface PlayerOffer {
|
||||||
|
target: PlayerWrapper;
|
||||||
|
autoReject: NodeJS.Timeout;
|
||||||
|
proximity?: number;
|
||||||
|
resolve: (value: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VehicleMechanicOperation {
|
||||||
|
name: string;
|
||||||
|
mechanic: PlayerMpLoggedIn;
|
||||||
|
customer: PlayerMpLoggedIn;
|
||||||
|
vehicle: VehicleWrapper;
|
||||||
|
companyId: number;
|
||||||
|
vehicleEntity: import('../entities/vehicle.entity').VehicleEntity;
|
||||||
|
activity: 'paint' | 'wheels' | 'tint' | 'upgrade' | 'repair' | 'suspension' | 'engine' | 'brakes';
|
||||||
|
data: Record<string, any>;
|
||||||
|
timer?: NodeJS.Timeout;
|
||||||
|
timeReady: number;
|
||||||
|
invoiceId: number;
|
||||||
|
mechanicReward: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VehicleAttachment {
|
||||||
|
attachment: any;
|
||||||
|
bone: string;
|
||||||
|
collision: boolean;
|
||||||
|
x: number; y: number; z: number;
|
||||||
|
rx: number; ry: number; rz: number;
|
||||||
|
opacity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VehicleLabel {
|
||||||
|
text: string; font: number; color: number; size: number;
|
||||||
|
outline: boolean; bone: string; offset: Vector3; opacity: number; distance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandParamValidator =
|
||||||
|
| 'player' | 'character' | 'account' | 'number' | 'integer'
|
||||||
|
| 'string' | 'word' | 'vehicle' | 'property' | 'item'
|
||||||
|
| 'company' | 'faction' | 'ped'
|
||||||
|
| readonly any[] | any[] | RegExp;
|
||||||
|
|
||||||
|
type CommandGuard = (player: PlayerWrapper) => true | string;
|
||||||
|
|
||||||
|
type DutyState = { callsign: string; start: number; weapons: any[] };
|
||||||
|
type BoundingBox = [number, number, number, number, number, number];
|
||||||
|
type FineEntities = 'person' | 'vehicle' | 'company' | 'faction' | 'property';
|
||||||
|
type EngineData = [string, string, number, number, number, number];
|
||||||
|
type KeyValueCollection = Record<string, any>;
|
||||||
|
type EventMpThis = any;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// DamageHistory / KillEntry — from features/damage/state.d.ts
|
||||||
|
type DamageHistory = {
|
||||||
|
id: number;
|
||||||
|
damager: { id: number; name: string; position: Vector3; dimension: number; health: number; armor: number };
|
||||||
|
weapon: { gameId: number; weaponName: string; ammoType: string; currentWeaponAmmo: number };
|
||||||
|
boneIndex: number; damage: number; time: number; position: Vector3; dimension: number;
|
||||||
|
armorHit: boolean; distance: number; health: number; armor: number;
|
||||||
|
bleeding: number | null; postMortem?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface KillEntry {
|
||||||
|
_id: string; victim_id: number; victim_name: string; victim_damages: DamageHistory[];
|
||||||
|
killer_id: number | null; killer_name: string | null;
|
||||||
|
kill_type: 'WOUNDED' | 'KNOCKED'; weapon: string; time: string;
|
||||||
|
executed_time: string | null; executed_by_id: number | null;
|
||||||
|
executed_by_name: string | null; executed_by_weapon: string | null;
|
||||||
|
revived: string | null; respawned: string | null; accepted_death: string | null;
|
||||||
|
rescued: string | null; rescued_by: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// mp global — set at runtime by mp-shim.ts, declared here for TypeScript
|
||||||
|
const mp: {
|
||||||
|
events: {
|
||||||
|
add(event: string, handler: Function): void;
|
||||||
|
add(events: Record<string, Function>): void;
|
||||||
|
addProc(name: string, handler: Function): void;
|
||||||
|
call(event: string, ...args: any[]): void;
|
||||||
|
callRemote(event: string, ...args: any[]): void;
|
||||||
|
delayInitialization: boolean;
|
||||||
|
};
|
||||||
|
players: {
|
||||||
|
at(id: number): PlayerWrapper | undefined;
|
||||||
|
toArray(): PlayerWrapper[];
|
||||||
|
toArrayFast(): PlayerWrapper[];
|
||||||
|
forEach(fn: (player: PlayerWrapper) => void): void;
|
||||||
|
broadcast(message: string): void;
|
||||||
|
length: number;
|
||||||
|
exists(player: PlayerWrapper): boolean;
|
||||||
|
};
|
||||||
|
Vector3: new (x: number, y: number, z: number) => Vector3;
|
||||||
|
vehicles: {
|
||||||
|
new(model: string | number, position: Vector3, options?: any): import('./vehicle/vehicle-wrapper').VehicleWrapper;
|
||||||
|
at(id: number): import('./vehicle/vehicle-wrapper').VehicleWrapper | undefined;
|
||||||
|
toArray(): import('./vehicle/vehicle-wrapper').VehicleWrapper[];
|
||||||
|
forEach(fn: (vehicle: import('./vehicle/vehicle-wrapper').VehicleWrapper) => void): void;
|
||||||
|
};
|
||||||
|
config: Record<string, any>;
|
||||||
|
objects: { new(...args: any[]): any; at(id: number): any; toArray(): any[]; forEach(fn: any): void };
|
||||||
|
peds: { new(...args: any[]): any; at(id: number): any; toArray(): any[]; forEach(fn: any): void };
|
||||||
|
markers: { new(...args: any[]): any };
|
||||||
|
blips: { new(...args: any[]): any };
|
||||||
|
labels: { new(...args: any[]): any };
|
||||||
|
colshapes: {
|
||||||
|
newSphere(...args: any[]): any;
|
||||||
|
newCuboid(...args: any[]): any;
|
||||||
|
newCircle(...args: any[]): any;
|
||||||
|
newRectangle(...args: any[]): any;
|
||||||
|
newTube(...args: any[]): any;
|
||||||
|
newPolygon(...args: any[]): any;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type CommandParamMetaData<T> = {
|
||||||
|
[K in keyof T]: {
|
||||||
|
hint: string;
|
||||||
|
validator: CommandParamValidator;
|
||||||
|
default?: any;
|
||||||
|
length?: number;
|
||||||
|
overrides?: Record<string, Record<number, CommandParamMetaData<any>>>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface VehicleLogComposition {
|
||||||
|
id?: number;
|
||||||
|
gameId: number;
|
||||||
|
owner?: string;
|
||||||
|
model: string;
|
||||||
|
plate: string;
|
||||||
|
occupants: string;
|
||||||
|
bodyHealth: number;
|
||||||
|
engineHealth: number;
|
||||||
|
speedInMph: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client-side map object stubs (server never creates these directly in FiveM)
|
||||||
|
interface BlipMp { destroy(): void; }
|
||||||
|
interface MarkerMp { destroy(): void; }
|
||||||
|
interface LabelMp { destroy(): void; }
|
||||||
|
interface CheckpointMp { destroy(): void; }
|
||||||
|
interface TextLabelMp { destroy(): void; text: string; }
|
||||||
|
|
||||||
|
// PedCustomization — from original types.d.ts
|
||||||
|
interface PedCustomization {
|
||||||
|
headBlend: { shapes: number[]; skins: number[]; shapeMix: number; skinMix: number; thirdMix: number };
|
||||||
|
headOverlay: { overlayId: number; data: Array4d }[];
|
||||||
|
faceFeatures: { featureId: number; data: number }[];
|
||||||
|
hairData: { drawable: number; color1: number; color2: number };
|
||||||
|
outfitData: { component: number; drawable: number[] }[];
|
||||||
|
propData: { propIndex: number; prop: [number, number] }[];
|
||||||
|
vehicle?: { status: boolean; vehID: number; seat: number };
|
||||||
|
weapon?: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
129
src/server/core/vehicle/vehicle-wrapper.ts
Normal file
129
src/server/core/vehicle/vehicle-wrapper.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { VehicleEntity } from '../../entities/vehicle.entity';
|
||||||
|
import { ActiveJob } from '../../features/job/job.interface';
|
||||||
|
|
||||||
|
export class VehicleWrapper {
|
||||||
|
readonly handle: number; // FiveM server entity handle
|
||||||
|
|
||||||
|
vehicleEntity?: VehicleEntity;
|
||||||
|
job?: ActiveJob;
|
||||||
|
dmvTest?: any;
|
||||||
|
extraBitmap: number = 0;
|
||||||
|
fuelLastPosition: Vector3 = { x: 0, y: 0, z: 0 } as Vector3;
|
||||||
|
alarmTimer?: NodeJS.Timeout;
|
||||||
|
keyInIgnition: boolean = false;
|
||||||
|
engine: boolean = false; // tracked via client→server sync event
|
||||||
|
bodyHealth: number = 1000; // synced from client
|
||||||
|
engineHealth: number = 1000; // synced from client
|
||||||
|
dimension: number = 0; // stored server-side
|
||||||
|
mechanicOperation?: VehicleMechanicOperation;
|
||||||
|
attachments?: VehicleAttachment[];
|
||||||
|
labels?: VehicleLabel[];
|
||||||
|
|
||||||
|
constructor(handle: number) {
|
||||||
|
this.handle = handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// RageMP compat: vehicle.id maps to network ID (what clients use to identify the vehicle)
|
||||||
|
get id(): number {
|
||||||
|
return NetworkGetNetworkIdFromEntity(this.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
get model(): number {
|
||||||
|
return GetEntityModel(this.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
get position(): Vector3 {
|
||||||
|
const [x, y, z] = GetEntityCoords(this.handle);
|
||||||
|
return { x, y, z } as Vector3;
|
||||||
|
}
|
||||||
|
|
||||||
|
set position(v: Vector3) {
|
||||||
|
SetEntityCoords(this.handle, v.x, v.y, v.z, false, false, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
get heading(): number {
|
||||||
|
return GetEntityHeading(this.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
set heading(h: number) {
|
||||||
|
SetEntityHeading(this.handle, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
get numberPlate(): string {
|
||||||
|
return GetVehicleNumberPlateText(this.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
set numberPlate(plate: string) {
|
||||||
|
SetVehicleNumberPlateText(this.handle, plate);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
vehicleRegistry.remove(this.handle);
|
||||||
|
DeleteEntity(this.handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
getVehicleOccupants(): import('../player/player-registry').PlayerWrapper[] {
|
||||||
|
const { playerRegistry } = require('../player/player-registry');
|
||||||
|
return playerRegistry.toArray().filter((p: any) => {
|
||||||
|
const ped = GetPlayerPed(String(p.id));
|
||||||
|
return GetVehiclePedIsIn(ped, false) === this.handle;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VehicleRegistry {
|
||||||
|
private vehicles: Map<number, VehicleWrapper> = new Map();
|
||||||
|
|
||||||
|
create(model: string | number, position: Vector3, options?: {
|
||||||
|
heading?: number;
|
||||||
|
numberPlate?: string;
|
||||||
|
dimension?: number;
|
||||||
|
color?: [number, number];
|
||||||
|
}): VehicleWrapper {
|
||||||
|
const modelHash = typeof model === 'string' ? GetHashKey(model) : model;
|
||||||
|
const heading = options?.heading ?? 0;
|
||||||
|
|
||||||
|
const handle = CreateVehicle(
|
||||||
|
modelHash,
|
||||||
|
position.x, position.y, position.z,
|
||||||
|
heading,
|
||||||
|
true, // isNetwork
|
||||||
|
false, // netMissionEntity
|
||||||
|
);
|
||||||
|
|
||||||
|
if (options?.numberPlate) {
|
||||||
|
SetVehicleNumberPlateText(handle, options.numberPlate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.color) {
|
||||||
|
SetVehicleColours(handle, options.color[0], options.color[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = new VehicleWrapper(handle);
|
||||||
|
this.vehicles.set(handle, wrapper);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(handle: number): void {
|
||||||
|
this.vehicles.delete(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(handle: number): VehicleWrapper | undefined {
|
||||||
|
return this.vehicles.get(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
getByNetId(netId: number): VehicleWrapper | undefined {
|
||||||
|
const handle = NetworkGetEntityFromNetworkId(netId);
|
||||||
|
return this.vehicles.get(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): VehicleWrapper[] {
|
||||||
|
return [...this.vehicles.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
forEach(fn: (vehicle: VehicleWrapper) => void): void {
|
||||||
|
this.vehicles.forEach(fn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const vehicleRegistry = new VehicleRegistry();
|
||||||
14
src/server/entities/acknowledge-message.entity.ts
Normal file
14
src/server/entities/acknowledge-message.entity.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('acknowledge_messages')
|
||||||
|
export class AcknowledgeMessageEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'int', nullable: true }) character_id: number;
|
||||||
|
@Column({ type: 'int', nullable: true }) account_id: number;
|
||||||
|
@Column({ type: 'longtext' }) content: string;
|
||||||
|
@Column({ type: 'varchar', length: 128 }) author: string;
|
||||||
|
@Column({ type: 'int', nullable: true }) author_account_id: number;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) created: Date;
|
||||||
|
@Column({ type: 'datetime', nullable: true }) acknowledged: Date | null;
|
||||||
|
@Column({ type: 'datetime', nullable: true, default: null }) deleted: Date | null;
|
||||||
|
}
|
||||||
27
src/server/entities/admin-note.entity.ts
Normal file
27
src/server/entities/admin-note.entity.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CharacterEntity } from './character.entity';
|
||||||
|
|
||||||
|
@Entity('admin_notes')
|
||||||
|
export class AdminNoteEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() character_id: number;
|
||||||
|
@Column() account_id: number;
|
||||||
|
@Column({ type: 'varchar', length: 34 }) account_name: string;
|
||||||
|
|
||||||
|
@Column() admin_account_id: number;
|
||||||
|
@Column({ type: 'varchar', length: 34 }) admin_account_name: string;
|
||||||
|
|
||||||
|
@Column({ type: 'longtext' }) note: string;
|
||||||
|
@Column() alert_type: number;
|
||||||
|
|
||||||
|
@Column({ type: 'datetime' }) expiration: string;
|
||||||
|
@Column({ type: 'datetime', default: null, nullable: true }) expired: string | null;
|
||||||
|
@Column({ type: 'datetime' }) created: string;
|
||||||
|
|
||||||
|
@OneToOne(() => CharacterEntity)
|
||||||
|
@JoinColumn({
|
||||||
|
name: 'character_id',
|
||||||
|
referencedColumnName: 'id',
|
||||||
|
})
|
||||||
|
character?: CharacterEntity;
|
||||||
|
}
|
||||||
28
src/server/entities/ajail.entity.ts
Normal file
28
src/server/entities/ajail.entity.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CharacterEntity } from './character.entity';
|
||||||
|
|
||||||
|
@Entity('ajails')
|
||||||
|
export class AdminJailEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
account_id: number;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) account_name: string;
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
character_id: number;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) character_name: string;
|
||||||
|
@Column({ type: 'varchar', length: 16 }) player_ip: string;
|
||||||
|
@Column({ type: 'int', nullable: true }) ajailer_account_id: number | null;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) ajailer_name: string;
|
||||||
|
@Column({ type: 'varchar', length: 256 }) reason: string;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) date: string;
|
||||||
|
@Column() length: number;
|
||||||
|
@Column() remaining_length: number;
|
||||||
|
@Column({ type: 'datetime', default: null }) served: string | null;
|
||||||
|
@Column({ type: 'datetime', default: null }) voided: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => CharacterEntity)
|
||||||
|
@JoinColumn({ name: 'character_id' })
|
||||||
|
character?: CharacterEntity;
|
||||||
|
}
|
||||||
30
src/server/entities/animation.entity.ts
Normal file
30
src/server/entities/animation.entity.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('animations')
|
||||||
|
export class AnimationEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', length: 50 }) category: string;
|
||||||
|
@Column({ type: 'varchar', length: 30, unique: true }) command: string;
|
||||||
|
@Column({ type: 'varchar', length: 130 }) dict: string;
|
||||||
|
@Column({ type: 'varchar', length: 130 }) name: string;
|
||||||
|
@Column({ type: 'int', default: 1 }) speed: number;
|
||||||
|
@Column({ type: 'int' }) flag: number;
|
||||||
|
@Column({ type: 'varchar', length: 100 }) description: string;
|
||||||
|
@Column({ type: 'int', default: 0 }) allowed_in_vehicle: number;
|
||||||
|
@Column({ type: 'varchar', default: null, nullable: true }) attached_object_model: string;
|
||||||
|
@Column({ type: 'int', default: null, nullable: true }) attached_object_bone: number;
|
||||||
|
@Column({ type: 'float', default: null, nullable: true }) attached_object_pos_x: number;
|
||||||
|
@Column({ type: 'float', default: null, nullable: true }) attached_object_pos_y: number;
|
||||||
|
@Column({ type: 'float', default: null, nullable: true }) attached_object_pos_z: number;
|
||||||
|
@Column({ type: 'float', default: null, nullable: true }) attached_object_rot_x: number;
|
||||||
|
@Column({ type: 'float', default: null, nullable: true }) attached_object_rot_y: number;
|
||||||
|
@Column({ type: 'float', default: null, nullable: true }) attached_object_rot_z: number;
|
||||||
|
|
||||||
|
get attachedObjectPos(): Vector3 {
|
||||||
|
return new mp.Vector3(this.attached_object_pos_x, this.attached_object_pos_y, this.attached_object_pos_z);
|
||||||
|
}
|
||||||
|
|
||||||
|
get attachedObjectRot(): Vector3 {
|
||||||
|
return new mp.Vector3(this.attached_object_rot_x, this.attached_object_rot_y, this.attached_object_rot_z);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/server/entities/appearance-presets.entity.ts
Normal file
21
src/server/entities/appearance-presets.entity.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Column, Entity, getRepository, PrimaryColumn } from 'typeorm';
|
||||||
|
|
||||||
|
export type DrawableData = {
|
||||||
|
componentId: number;
|
||||||
|
drawableId: number;
|
||||||
|
texture: number;
|
||||||
|
palette?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Entity('appearance_presets')
|
||||||
|
export class AppearancePresetEntity {
|
||||||
|
@PrimaryColumn({ type: 'varchar', length: 32, nullable: false }) name: string;
|
||||||
|
@Column({ type: 'int', nullable: false }) character_id: number;
|
||||||
|
@Column({ type: 'json' }) clothing_data: DrawableData[];
|
||||||
|
@Column({ type: 'json' }) prop_data: DrawableData[];
|
||||||
|
|
||||||
|
static async SaveAppearance(name: string, clothingData: DrawableData[], propData: DrawableData[], characterId = -1) {
|
||||||
|
const res = await getRepository(this).save({ name, character_id: characterId, clothing_data: clothingData, prop_data: propData });
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/server/entities/atm.entity.ts
Normal file
43
src/server/entities/atm.entity.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { BankEntity } from './bank.entity';
|
||||||
|
import { CompanyEntity } from './company.entity';
|
||||||
|
|
||||||
|
@Entity('atms')
|
||||||
|
export class AtmEntity {
|
||||||
|
static MAX_CASH = 100_000;
|
||||||
|
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
bank_id: number;
|
||||||
|
@Column() posx: number;
|
||||||
|
@Column() posy: number;
|
||||||
|
@Column() posz: number;
|
||||||
|
@Column() rotx: number;
|
||||||
|
@Column() roty: number;
|
||||||
|
@Column() rotz: number;
|
||||||
|
@Column({ type: 'varchar', length: 20, default: null, nullable: true }) model: string | null;
|
||||||
|
@Column('datetime') created: string;
|
||||||
|
@Column() dimension: number;
|
||||||
|
@Index()
|
||||||
|
@Column({ type: 'int', nullable: true })
|
||||||
|
operator_id: number | null;
|
||||||
|
@Column() fee: number;
|
||||||
|
|
||||||
|
operatedBy?: PlayerMpLoggedIn;
|
||||||
|
object?: ObjectMp;
|
||||||
|
|
||||||
|
@ManyToOne(() => BankEntity, (bank) => bank.accounts)
|
||||||
|
@JoinColumn({ name: 'bank_id' })
|
||||||
|
bank?: BankEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => CompanyEntity)
|
||||||
|
@JoinColumn({ name: 'operator_id' })
|
||||||
|
operator?: CompanyEntity;
|
||||||
|
|
||||||
|
availableCash = AtmEntity.MAX_CASH;
|
||||||
|
|
||||||
|
position() {
|
||||||
|
return new mp.Vector3(this.posx, this.posy, this.posz);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/server/entities/auth-token.entity.ts
Normal file
16
src/server/entities/auth-token.entity.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('auth_tokens')
|
||||||
|
export class AuthTokenEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() account_id: number;
|
||||||
|
@Column({ type: 'varchar', length: 32 }) account_username: string;
|
||||||
|
@Column({ type: 'varchar', length: 128 }) token: string;
|
||||||
|
@Column({ type: 'datetime' }) expires: Date;
|
||||||
|
@Column({ type: 'tinyint' }) persistent: boolean;
|
||||||
|
@Column({ type: 'varchar', length: 128 }) serial: string;
|
||||||
|
@Column({ type: 'varchar', length: 32 }) ip: string;
|
||||||
|
@Column({ type: 'varchar', length: 24 }) socialclub_id: string;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) created: string;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) last_used: string;
|
||||||
|
}
|
||||||
162
src/server/entities/bank-account.entity.ts
Normal file
162
src/server/entities/bank-account.entity.ts
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import { AfterLoad, Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { getRepository } from 'typeorm';
|
||||||
|
import { BankTransactionEntity } from './bank-transaction.entity';
|
||||||
|
import { BankEntity } from './bank.entity';
|
||||||
|
import { CharacterEntity } from './character.entity';
|
||||||
|
import { FactionEntity } from './faction.entity';
|
||||||
|
import { CompanyEntity } from './company.entity';
|
||||||
|
import { companySystem } from '../features/company';
|
||||||
|
import { LoanEntity } from './loan.entity';
|
||||||
|
import { MazePayAccountEntity } from './maze-pay-account.entity';
|
||||||
|
|
||||||
|
@Entity('bank_accounts')
|
||||||
|
export class BankAccountEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ unique: true, length: 10 }) number: string;
|
||||||
|
@Column({ type: 'int' }) bank_id: number;
|
||||||
|
@Column({ type: 'int', default: 0 }) balance: number;
|
||||||
|
@Column({ type: 'int', default: null }) character_id: number | null;
|
||||||
|
@Column({ type: 'int', default: null }) company_id: number | null;
|
||||||
|
@Column({ type: 'int', default: null }) faction_id: number | null;
|
||||||
|
@Column({ type: 'datetime' }) created: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => BankTransactionEntity, (transaction) => transaction.bankAccount)
|
||||||
|
transactions: BankTransactionEntity[];
|
||||||
|
|
||||||
|
@ManyToOne(() => BankEntity, (bank) => bank.accounts)
|
||||||
|
@JoinColumn({ name: 'bank_id' })
|
||||||
|
bank: BankEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => CompanyEntity, (company) => company.bankAccounts)
|
||||||
|
@JoinColumn({ name: 'company_id' })
|
||||||
|
company: CompanyEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => CharacterEntity, (character) => character.bankAccounts)
|
||||||
|
@JoinColumn({ name: 'character_id' })
|
||||||
|
character: CharacterEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => FactionEntity, (faction) => faction.bankAccounts)
|
||||||
|
@JoinColumn({ name: 'faction_id' })
|
||||||
|
faction: FactionEntity;
|
||||||
|
|
||||||
|
@OneToMany(() => LoanEntity, (loan) => loan.bankAccount)
|
||||||
|
loans: LoanEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => MazePayAccountEntity, (mpae) => mpae.bankAccount)
|
||||||
|
mazePayAccounts: MazePayAccountEntity[];
|
||||||
|
|
||||||
|
iban?: string;
|
||||||
|
|
||||||
|
historyForPreview: number[] = [];
|
||||||
|
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@AfterLoad()
|
||||||
|
afterLoad(): void {
|
||||||
|
this.iban = this.getIban(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateHistoryForPreview(): Promise<void> {
|
||||||
|
const recentTransactions = await getRepository(BankTransactionEntity)
|
||||||
|
.createQueryBuilder()
|
||||||
|
.where('DATE(`date`) > DATE_SUB(NOW(), INTERVAL 10 DAY) AND bank_account_id = :id', {
|
||||||
|
id: this.id,
|
||||||
|
})
|
||||||
|
.orderBy({
|
||||||
|
date: 'DESC',
|
||||||
|
})
|
||||||
|
.getMany();
|
||||||
|
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
this.historyForPreview = [this.balance];
|
||||||
|
let currentBalance = this.balance;
|
||||||
|
|
||||||
|
for (let i = 0; i < 9; i++) {
|
||||||
|
const amountInDate = recentTransactions
|
||||||
|
.filter((rt) => {
|
||||||
|
return new Date(rt.date).toDateString() === date.toDateString();
|
||||||
|
})
|
||||||
|
.reduce((total, rt) => total - rt.amount, currentBalance);
|
||||||
|
|
||||||
|
this.historyForPreview.unshift(amountInDate);
|
||||||
|
|
||||||
|
currentBalance = amountInDate;
|
||||||
|
date.setDate(date.getDate() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getIban(pretty: boolean, bold?: boolean): string {
|
||||||
|
const statePrefix = 'LS';
|
||||||
|
let accountType = '10'; // personal
|
||||||
|
|
||||||
|
if (this.faction_id) {
|
||||||
|
accountType = '20';
|
||||||
|
} else if (this.company_id) {
|
||||||
|
accountType = '30';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pretty) {
|
||||||
|
if (bold) {
|
||||||
|
return `${statePrefix}${accountType} <strong>${this.bank_id}</strong> 0000 00<strong>${this.number.slice(0, 2)} ${this.number.slice(
|
||||||
|
2,
|
||||||
|
6,
|
||||||
|
)} ${this.number.slice(6)}</strong>`;
|
||||||
|
}
|
||||||
|
return `${statePrefix}${accountType} ${this.bank_id} 0000 00${this.number.slice(0, 2)} ${this.number.slice(2, 6)} ${this.number.slice(6)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${statePrefix}${accountType}${this.bank_id}000000${this.number}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async cacheName(): Promise<void> {
|
||||||
|
this.name = await this.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getName(): Promise<string> {
|
||||||
|
if (this.character_id) {
|
||||||
|
const character = await getRepository(CharacterEntity).findOne({
|
||||||
|
id: this.character_id,
|
||||||
|
});
|
||||||
|
if (character) {
|
||||||
|
return character.getIcName(false);
|
||||||
|
}
|
||||||
|
return this.getIban(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.faction_id) {
|
||||||
|
const faction = await getRepository(FactionEntity).findOne({
|
||||||
|
id: this.faction_id,
|
||||||
|
});
|
||||||
|
if (faction) {
|
||||||
|
return faction.name;
|
||||||
|
}
|
||||||
|
return this.getIban(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.company_id) {
|
||||||
|
const company = companySystem.companies.find((c) => c.id === this.company_id);
|
||||||
|
if (company) {
|
||||||
|
return company.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getIban(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTypeName(): string {
|
||||||
|
if (this.character_id) {
|
||||||
|
return 'Personal Account';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.company_id) {
|
||||||
|
return 'Business Account';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.faction_id) {
|
||||||
|
return 'Agency Account';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown Account';
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/server/entities/bank-transaction.entity.ts
Normal file
29
src/server/entities/bank-transaction.entity.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { BankAccountEntity } from './bank-account.entity';
|
||||||
|
import { PaydayRewardEntity } from './payday-reward.entity';
|
||||||
|
|
||||||
|
@Entity('bank_transactions')
|
||||||
|
export class BankTransactionEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() bank_account_id: number;
|
||||||
|
@Column({ type: 'int', nullable: true }) counter_bank_account_id: number | null;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) counter_name: string;
|
||||||
|
@Column() amount: number;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) reason: string;
|
||||||
|
@Column({ type: 'varchar', length: 128 }) description: string;
|
||||||
|
@Column({ type: 'varchar', length: 16 }) type: string;
|
||||||
|
@Column({ type: 'varchar', length: 32 }) category: string;
|
||||||
|
@Column({ type: 'binary', length: 32 }) ref: string;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) date: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => BankAccountEntity, (bankAccount) => bankAccount.transactions)
|
||||||
|
@JoinColumn({ name: 'bank_account_id' })
|
||||||
|
bankAccount: BankAccountEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => BankAccountEntity, (bankAccount) => bankAccount.transactions)
|
||||||
|
@JoinColumn({ name: 'counter_bank_account_id' })
|
||||||
|
counterBankAccount: BankAccountEntity;
|
||||||
|
|
||||||
|
@OneToMany(() => PaydayRewardEntity, (reward) => reward.bankTransaction)
|
||||||
|
paydayRewards: PaydayRewardEntity[];
|
||||||
|
}
|
||||||
11
src/server/entities/bank.entity.ts
Normal file
11
src/server/entities/bank.entity.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { BankAccountEntity } from './bank-account.entity';
|
||||||
|
|
||||||
|
@Entity('banks')
|
||||||
|
export class BankEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) name: string;
|
||||||
|
|
||||||
|
@OneToMany(() => BankAccountEntity, (account) => account.bank)
|
||||||
|
accounts: BankAccountEntity[];
|
||||||
|
}
|
||||||
10
src/server/entities/barber-shop-seats.entity.ts
Normal file
10
src/server/entities/barber-shop-seats.entity.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('barber_shop_seat')
|
||||||
|
export class BarberShopSeatEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'float' }) x: number;
|
||||||
|
@Column({ type: 'float' }) y: number;
|
||||||
|
@Column({ type: 'float' }) z: number;
|
||||||
|
@Column({ type: 'float' }) heading: number;
|
||||||
|
}
|
||||||
31
src/server/entities/business-stock-level.entity.ts
Normal file
31
src/server/entities/business-stock-level.entity.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||||
|
import { PropertyEntity } from './property.entity';
|
||||||
|
import { ItemTypeEntity } from './item-type.entity';
|
||||||
|
|
||||||
|
@Entity('business_stock_levels')
|
||||||
|
@Unique(['property_id', 'item_type_id'])
|
||||||
|
export class BusinessStockLevelEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
property_id: number;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
item_type_id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 0 })
|
||||||
|
quantity: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 0 })
|
||||||
|
min_threshold: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => PropertyEntity, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'property_id' })
|
||||||
|
property: PropertyEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => ItemTypeEntity)
|
||||||
|
@JoinColumn({ name: 'item_type_id' })
|
||||||
|
itemType: ItemTypeEntity;
|
||||||
|
}
|
||||||
16
src/server/entities/changing-room.entity.ts
Normal file
16
src/server/entities/changing-room.entity.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CompanyEntity } from './company.entity';
|
||||||
|
|
||||||
|
@Entity('changing_rooms')
|
||||||
|
export class ChangingRoomEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() company_id: number;
|
||||||
|
@Column({ type: 'float' }) x: number;
|
||||||
|
@Column({ type: 'float' }) y: number;
|
||||||
|
@Column({ type: 'float' }) z: number;
|
||||||
|
@Column() dimension: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => CompanyEntity, (company) => company.changingRooms)
|
||||||
|
@JoinColumn({ name: 'company_id' })
|
||||||
|
company: CompanyEntity;
|
||||||
|
}
|
||||||
58
src/server/entities/character-appearance.entity.ts
Normal file
58
src/server/entities/character-appearance.entity.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { Column, Entity, Index, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CharacterEntity } from './character.entity';
|
||||||
|
|
||||||
|
export interface CharacterShape {
|
||||||
|
shapeFirstId: number;
|
||||||
|
shapeSecondId: number;
|
||||||
|
shapeThirdId: number;
|
||||||
|
shapeMix: number;
|
||||||
|
thirdMix: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharacterSkin {
|
||||||
|
skinFirstId: number;
|
||||||
|
skinSecondId: number;
|
||||||
|
skinThirdId: number;
|
||||||
|
skinMix: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharacterOverlays {
|
||||||
|
overlayId: number;
|
||||||
|
index: number;
|
||||||
|
opacity: number;
|
||||||
|
firstColor?: number;
|
||||||
|
secondColor?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharacterFaceFeatures {
|
||||||
|
index: number;
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharacterMiscFeatures {
|
||||||
|
hairStyle: number;
|
||||||
|
hairColor: number;
|
||||||
|
hairColorSecondary: number;
|
||||||
|
eyeColor: number;
|
||||||
|
microPigmentation: boolean;
|
||||||
|
microPigmentationOne: number;
|
||||||
|
microPigmentationTwo: number;
|
||||||
|
microPigmentationThree: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('character_appearances')
|
||||||
|
export class CharacterAppearanceEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
character_id: number;
|
||||||
|
@Column({ type: 'json' }) shape: CharacterShape;
|
||||||
|
@Column({ type: 'json' }) skin: CharacterSkin;
|
||||||
|
@Column({ type: 'json' }) overlays: CharacterOverlays[];
|
||||||
|
@Column({ type: 'json' }) face: CharacterFaceFeatures[];
|
||||||
|
@Column({ type: 'json' }) misc: CharacterMiscFeatures;
|
||||||
|
|
||||||
|
@OneToOne(() => CharacterEntity)
|
||||||
|
@JoinColumn({ name: 'character_id' })
|
||||||
|
character?: CharacterEntity;
|
||||||
|
}
|
||||||
8
src/server/entities/character-attributes.entity.ts
Normal file
8
src/server/entities/character-attributes.entity.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('attributes')
|
||||||
|
export class AttributesEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ unique: true }) character_name: string;
|
||||||
|
@Column({ length: 94 }) attributes: string;
|
||||||
|
}
|
||||||
16
src/server/entities/character-cache.entity.ts
Normal file
16
src/server/entities/character-cache.entity.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('character_cache')
|
||||||
|
export class CharacterCacheEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', length: 24, unique: true }) name: string;
|
||||||
|
@Column() gender: 0 | 1;
|
||||||
|
@Column({ type: 'varchar', length: 10 }) date_of_birth: string;
|
||||||
|
@Column({ type: 'int', nullable: true }) ssn: number | null;
|
||||||
|
@Column({ type: 'text', nullable: true }) appearance: string | null;
|
||||||
|
@Column({ type: 'int', nullable: true }) jail_id: number | null;
|
||||||
|
@Column({ type: 'int', nullable: true }) jail_time: number | null;
|
||||||
|
@Column({ type: 'int', nullable: true }) prison_minutes: number | null;
|
||||||
|
@Column({ type: 'datetime', nullable: true }) prison_until: Date | null;
|
||||||
|
@Column({ type: 'int', nullable: true }) prison_seg_minutes: number | null;
|
||||||
|
}
|
||||||
27
src/server/entities/character-company-positions.entity.ts
Normal file
27
src/server/entities/character-company-positions.entity.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CompanyEntity } from './company.entity';
|
||||||
|
import { CompanyPositionEntity } from './company-position.entity';
|
||||||
|
|
||||||
|
@Entity('character_company_positions')
|
||||||
|
export class CharacterCompanyPositionEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'int' }) character_id: number;
|
||||||
|
@Column({ type: 'int' }) company_id: number;
|
||||||
|
@Column({ type: 'int' }) company_position_id: number;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) created: Date;
|
||||||
|
@Column({ type: 'int' }) created_by: number;
|
||||||
|
|
||||||
|
@OneToOne(() => CompanyEntity)
|
||||||
|
@JoinColumn({
|
||||||
|
name: 'company_id',
|
||||||
|
referencedColumnName: 'id',
|
||||||
|
})
|
||||||
|
company: CompanyEntity;
|
||||||
|
|
||||||
|
@OneToOne(() => CompanyPositionEntity)
|
||||||
|
@JoinColumn({
|
||||||
|
name: 'company_position_id',
|
||||||
|
referencedColumnName: 'id',
|
||||||
|
})
|
||||||
|
companyPosition: CompanyPositionEntity;
|
||||||
|
}
|
||||||
21
src/server/entities/character-name-changes.entity.ts
Normal file
21
src/server/entities/character-name-changes.entity.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Column, Entity, JoinColumn, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CharacterEntity } from './character.entity';
|
||||||
|
|
||||||
|
@Entity('character_name_changes')
|
||||||
|
export class CharacterNameChangesEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'int' }) account_id: number;
|
||||||
|
@Column({ type: 'int' }) character_id: number;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) from: string;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) to: string;
|
||||||
|
@Column({ type: 'datetime' }) date: Date;
|
||||||
|
@Column({ type: 'tinyint' }) is_ck: boolean;
|
||||||
|
@Column({ type: 'int' }) changed_by_account_id: number;
|
||||||
|
|
||||||
|
@OneToOne(() => CharacterEntity)
|
||||||
|
@JoinColumn({
|
||||||
|
name: 'character_id',
|
||||||
|
referencedColumnName: 'id',
|
||||||
|
})
|
||||||
|
character: CharacterEntity;
|
||||||
|
}
|
||||||
78
src/server/entities/character-outfit.entity.ts
Normal file
78
src/server/entities/character-outfit.entity.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { Gender } from '../../shared/const/gender.const';
|
||||||
|
import { CharacterEntity } from './character.entity';
|
||||||
|
|
||||||
|
export type CharacterOutfitStackPart = [number, number] | null;
|
||||||
|
export type CharacterOutfitStack = [
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
];
|
||||||
|
|
||||||
|
export type CharacterOutfitStackProps = [
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
CharacterOutfitStackPart,
|
||||||
|
];
|
||||||
|
|
||||||
|
export type CharacterOutfitStackState = [
|
||||||
|
boolean,
|
||||||
|
boolean,
|
||||||
|
boolean,
|
||||||
|
boolean,
|
||||||
|
boolean,
|
||||||
|
boolean,
|
||||||
|
boolean,
|
||||||
|
boolean,
|
||||||
|
boolean,
|
||||||
|
boolean,
|
||||||
|
boolean,
|
||||||
|
boolean,
|
||||||
|
];
|
||||||
|
|
||||||
|
export enum CharacterOutfitOverrideLayer {
|
||||||
|
FactionUniform,
|
||||||
|
CompanyUniform,
|
||||||
|
PrisonUniform,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CharacterOutfitOverrideKey {
|
||||||
|
FactionUniform = 'Faction Uniform',
|
||||||
|
CompanyUniform = 'Company Uniform',
|
||||||
|
PrisonUniform = 'Prison Uniform',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CharacterOutfitOverride {
|
||||||
|
components: CharacterOutfitStack;
|
||||||
|
props?: CharacterOutfitStackProps;
|
||||||
|
resumeOnReconnect?: number;
|
||||||
|
key: CharacterOutfitOverrideKey;
|
||||||
|
layer: CharacterOutfitOverrideLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('character_outfits')
|
||||||
|
export class CharacterOutfitEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() character_id: number;
|
||||||
|
@Column({ type: 'int' }) character_gender: Gender;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) name: string;
|
||||||
|
@Column({ type: 'json' }) components: CharacterOutfitStack;
|
||||||
|
|
||||||
|
@ManyToOne(() => CharacterEntity, (character) => character.outfits)
|
||||||
|
@JoinColumn({ name: 'character_id' })
|
||||||
|
character: CharacterEntity;
|
||||||
|
}
|
||||||
23
src/server/entities/character-overlay-preset.entity.ts
Normal file
23
src/server/entities/character-overlay-preset.entity.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CharacterEntity } from './character.entity';
|
||||||
|
|
||||||
|
export interface CharacterOverlay {
|
||||||
|
overlayId: number;
|
||||||
|
index: number;
|
||||||
|
opacity: number;
|
||||||
|
colors: [number, number];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('character_overlay_presets')
|
||||||
|
export class CharacterOverlayPresetEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
character_id: number;
|
||||||
|
@Column({ type: 'varchar', length: 128 }) name: string;
|
||||||
|
@Column({ type: 'json' }) overlays: CharacterOverlay[];
|
||||||
|
|
||||||
|
@ManyToOne(() => CharacterEntity)
|
||||||
|
@JoinColumn({ name: 'character_id' })
|
||||||
|
character?: CharacterEntity;
|
||||||
|
}
|
||||||
26
src/server/entities/character-tattoo.entity.ts
Normal file
26
src/server/entities/character-tattoo.entity.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CharacterEntity } from './character.entity';
|
||||||
|
import { TattooEntity } from './tattoo.entity';
|
||||||
|
|
||||||
|
@Entity('character_tattoos')
|
||||||
|
export class CharacterTattooEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() character_id: number;
|
||||||
|
@Column({ type: 'varchar', length: 32 }) character_name: string;
|
||||||
|
@Column() tattoo_id: number;
|
||||||
|
@Column() opacity: number;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) created: Date;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) modified: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => CharacterEntity, (character) => character.tattoos)
|
||||||
|
@JoinColumn({
|
||||||
|
name: 'character_id',
|
||||||
|
})
|
||||||
|
character: CharacterEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => TattooEntity, (tattoo) => tattoo.characterTattoos)
|
||||||
|
@JoinColumn({
|
||||||
|
name: 'tattoo_id',
|
||||||
|
})
|
||||||
|
tattoo: TattooEntity;
|
||||||
|
}
|
||||||
283
src/server/entities/character.entity.ts
Normal file
283
src/server/entities/character.entity.ts
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
import { Column, Entity, getRepository, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { InventoryEntity } from './inventory.entity';
|
||||||
|
import { FactionEntity } from './faction.entity';
|
||||||
|
import { PhoneEntity } from './phone.entity';
|
||||||
|
import { CompanyEntity } from './company.entity';
|
||||||
|
import { CompanyPositionEntity } from './company-position.entity';
|
||||||
|
import { SessionEntity } from './session.entity';
|
||||||
|
import { FactionRankEntity } from './factionrank.entity';
|
||||||
|
import { PlayerSettings } from '../features/settings';
|
||||||
|
import { DriversLicenseEntity } from './drivers-license.entity';
|
||||||
|
import { WeaponLicenseEntity } from './weapon-license.entity';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { BankAccountEntity } from './bank-account.entity';
|
||||||
|
import { PropertyKeyEntity } from './property-key.entity';
|
||||||
|
import { VehicleOrderEntity } from './vehicle-order.entity';
|
||||||
|
import { VehicleEntity } from './vehicle.entity';
|
||||||
|
import { CommercialDriversLicenseEntity } from './commercial-drivers-license.entity';
|
||||||
|
import { TaxiLicenseEntity } from './taxi-license.entity';
|
||||||
|
import { PrisonCellEntity } from './prison-cell.entity';
|
||||||
|
import { CharacterOutfitEntity, CharacterOutfitStack, CharacterOutfitStackState } from './character-outfit.entity';
|
||||||
|
import { CharacterTattooEntity } from './character-tattoo.entity';
|
||||||
|
import { SceneEntity } from './scene.entity';
|
||||||
|
import { GraffitiEntity } from './graffiti.entity';
|
||||||
|
import { PetEntity } from './pet.entity';
|
||||||
|
import { WeaponAttachmentEntity } from './weapon-attachment.entity';
|
||||||
|
import { colourableChats } from '../features/chat/const/chat-color-channels';
|
||||||
|
import { CharacterOverlay } from './character-overlay-preset.entity';
|
||||||
|
import { CharacterAppearanceEntity } from './character-appearance.entity';
|
||||||
|
import { aconfigSettings } from '../features/admin/types';
|
||||||
|
import { Gender } from '../../shared/const/gender.const';
|
||||||
|
|
||||||
|
@Entity('characters')
|
||||||
|
export class CharacterEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', length: 24 }) name: string;
|
||||||
|
@Column({ default: 500 }) cash: number;
|
||||||
|
@Column({ type: 'varchar', width: 10, default: '2003-12-01' }) date_of_birth: string;
|
||||||
|
@Column() gender: Gender;
|
||||||
|
@Column({ type: 'int', default: null, nullable: true }) faction_id: number | null;
|
||||||
|
@Column({ type: 'int', default: null, nullable: true }) faction_rank: number | null;
|
||||||
|
@Column() account_id: number;
|
||||||
|
@Column() level: number;
|
||||||
|
@Column() play_time: number;
|
||||||
|
@Column({ type: 'json', default: '{}' }) joined_job_play_times: { [companyId: number]: number };
|
||||||
|
@Column('datetime') created: Date;
|
||||||
|
@Column({ type: 'datetime', nullable: true }) last_online: Date | null;
|
||||||
|
@Column({ type: 'int', nullable: true }) jail_time: number | null;
|
||||||
|
@Column({ type: 'varchar', length: 32 }) spawn: string | null;
|
||||||
|
@Column({ type: 'int', nullable: true }) ssn: number | null;
|
||||||
|
@Column({ type: 'json' }) settings: Partial<PlayerSettings>;
|
||||||
|
@Column({ type: 'varchar', length: 40, nullable: true, default: null }) walkstyle: string | null;
|
||||||
|
@Column({ type: 'varchar', length: 30, default: 'mood_normal_1' }) expression: string;
|
||||||
|
@Column({ type: 'int', nullable: true }) prison_cell_id: number | null;
|
||||||
|
@Column({ type: 'int', nullable: true }) prison_minutes: number | null;
|
||||||
|
@Column({ type: 'int', nullable: true }) prison_seg_minutes: number | null;
|
||||||
|
@Column({ type: 'varchar', length: 24, nullable: true, default: null }) signature: string | null;
|
||||||
|
@Column({ type: 'varchar', length: 32, nullable: true, default: null }) signature_font: string | null;
|
||||||
|
@Column() payday_duty_buffer: number;
|
||||||
|
@Column() welfare_buffer: number;
|
||||||
|
@Column({ type: 'tinyint', width: 1, default: 0 }) killed: boolean;
|
||||||
|
@Column({ type: 'json' }) outfit: CharacterOutfitStack;
|
||||||
|
@Column({ type: 'json' }) overlay: CharacterOverlay[] | null;
|
||||||
|
@Column({ type: 'json' }) outfit_state: CharacterOutfitStackState;
|
||||||
|
@Column({ type: 'int', default: 0 }) reward_stack: number;
|
||||||
|
|
||||||
|
@Column({ type: 'float' }) last_x: number;
|
||||||
|
@Column({ type: 'float' }) last_y: number;
|
||||||
|
@Column({ type: 'float' }) last_z: number;
|
||||||
|
@Column({ type: 'int' }) last_d: number;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 1 }) max_companies: number;
|
||||||
|
@Column({ type: 'int', default: 0 }) join_rewards: number;
|
||||||
|
|
||||||
|
@Column({ type: 'json', default: '[0,0,0]' }) hairs: [number, number, number];
|
||||||
|
@Column({ type: 'datetime', nullable: true }) lockpick_reset: Date | null;
|
||||||
|
@Column({ type: 'datetime', nullable: true }) weaponfile_reset: Date | null;
|
||||||
|
@Column({ type: 'datetime', nullable: true }) surgeon_reset: Date | null;
|
||||||
|
|
||||||
|
@Column({ type: 'json' }) chat_colours: { [colour in keyof typeof colourableChats]: string } | Record<string, never> | null;
|
||||||
|
@Column({ type: 'json' }) admin_config: { [setting in keyof typeof aconfigSettings]: boolean | undefined };
|
||||||
|
|
||||||
|
@Column({ type: 'int', nullable: true }) nail_style_id: number | null;
|
||||||
|
@Column({ type: 'tinyint', width: 1, default: 0 }) tutorial: boolean;
|
||||||
|
|
||||||
|
@ManyToOne(() => PrisonCellEntity, (prisonCell) => prisonCell.residents)
|
||||||
|
@JoinColumn({ name: 'prison_cell_id' })
|
||||||
|
prisonCell: PrisonCellEntity;
|
||||||
|
|
||||||
|
// @ManyToOne(() => CompanyEntity, (company) => company.employees)
|
||||||
|
// @JoinColumn({ name: 'company_id' })
|
||||||
|
// company?: CompanyEntity;
|
||||||
|
|
||||||
|
@ManyToMany(() => CompanyEntity, {
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
@JoinTable({
|
||||||
|
name: 'character_company_positions',
|
||||||
|
joinColumn: { name: 'character_id' },
|
||||||
|
inverseJoinColumn: { name: 'company_id' },
|
||||||
|
})
|
||||||
|
companies: CompanyEntity[];
|
||||||
|
|
||||||
|
@ManyToMany(() => CompanyPositionEntity, {
|
||||||
|
eager: true,
|
||||||
|
})
|
||||||
|
@JoinTable({
|
||||||
|
name: 'character_company_positions',
|
||||||
|
joinColumn: { name: 'character_id' },
|
||||||
|
inverseJoinColumn: { name: 'company_position_id' },
|
||||||
|
})
|
||||||
|
companyPositions: CompanyPositionEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => BankAccountEntity, (bankAccount) => bankAccount.character)
|
||||||
|
bankAccounts: BankAccountEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => SessionEntity, (session) => session.character)
|
||||||
|
@JoinColumn({ name: 'id' })
|
||||||
|
sessions: SessionEntity;
|
||||||
|
|
||||||
|
@OneToMany(() => GraffitiEntity, (graffiti) => graffiti.creator_id)
|
||||||
|
graffitis: GraffitiEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => PetEntity, (pet) => pet.character_id)
|
||||||
|
pets: PetEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => VehicleEntity, (vehicle) => vehicle.character)
|
||||||
|
vehicles: VehicleEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => PhoneEntity, (phone) => phone.character)
|
||||||
|
phones: PhoneEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => SceneEntity, (scene) => scene.character)
|
||||||
|
scenes: SceneEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => WeaponAttachmentEntity, (attachment) => attachment.character_id)
|
||||||
|
weaponAttachments: WeaponAttachmentEntity[];
|
||||||
|
|
||||||
|
phone?: PhoneEntity;
|
||||||
|
|
||||||
|
inventory?: InventoryEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => FactionEntity, (faction) => faction.members)
|
||||||
|
@JoinColumn({ name: 'faction_id' })
|
||||||
|
faction?: FactionEntity;
|
||||||
|
|
||||||
|
factionRank?: FactionRankEntity;
|
||||||
|
|
||||||
|
@OneToMany(() => CompanyEntity, (company) => company.owner)
|
||||||
|
ownedCompanies: CompanyEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => PropertyKeyEntity, (pke) => pke.character)
|
||||||
|
propertyKeys: PropertyKeyEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => VehicleOrderEntity, (voe) => voe.character)
|
||||||
|
vehicleOrders: VehicleOrderEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => CharacterOutfitEntity, (coe) => coe.character)
|
||||||
|
outfits: CharacterOutfitEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => CharacterTattooEntity, (cte) => cte.character)
|
||||||
|
tattoos: CharacterTattooEntity[];
|
||||||
|
|
||||||
|
@OneToOne(() => CharacterAppearanceEntity)
|
||||||
|
@JoinColumn({
|
||||||
|
name: 'id',
|
||||||
|
referencedColumnName: 'character_id',
|
||||||
|
})
|
||||||
|
appearanceData?: CharacterAppearanceEntity;
|
||||||
|
|
||||||
|
masked: boolean;
|
||||||
|
maskedName: string;
|
||||||
|
|
||||||
|
driverLicense?: DriversLicenseEntity;
|
||||||
|
weaponLicense?: WeaponLicenseEntity;
|
||||||
|
|
||||||
|
bankAccount?: BankAccountEntity;
|
||||||
|
|
||||||
|
cdl?: CommercialDriversLicenseEntity;
|
||||||
|
|
||||||
|
taxiLicense?: TaxiLicenseEntity;
|
||||||
|
|
||||||
|
attributes?: string;
|
||||||
|
|
||||||
|
async save(): Promise<CharacterEntity> {
|
||||||
|
return await getRepository(CharacterEntity).save(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
getIcName(includeMaskId = true): string {
|
||||||
|
if (includeMaskId && this.masked) {
|
||||||
|
return this.maskedName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.name.replace('_', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
getFactionName(includeMaskId = false): string {
|
||||||
|
const parts = [];
|
||||||
|
if (this.factionRank) {
|
||||||
|
parts.push(this.factionRank.name);
|
||||||
|
}
|
||||||
|
parts.push(this.getIcName(includeMaskId));
|
||||||
|
return parts.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstName(): string {
|
||||||
|
return this.name.split('_')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
getAge(): number {
|
||||||
|
return dayjs().diff(dayjs(this.date_of_birth), 'years');
|
||||||
|
}
|
||||||
|
|
||||||
|
getLastName(withInitial = false): string {
|
||||||
|
const parts = this.name.split('_');
|
||||||
|
if (withInitial) {
|
||||||
|
return `${parts[0][0]}. ${parts[1]}`;
|
||||||
|
}
|
||||||
|
return parts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
getName(): string {
|
||||||
|
return this.getIcName(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSSN(): string | undefined {
|
||||||
|
if (!this.ssn) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const s = String(this.ssn);
|
||||||
|
return [s.slice(0, 3), s.slice(3, 5), s.slice(5)].join('-');
|
||||||
|
}
|
||||||
|
|
||||||
|
getSignatureStyle() {
|
||||||
|
return this.signature || 'full';
|
||||||
|
}
|
||||||
|
|
||||||
|
getSignatureFont() {
|
||||||
|
return this.signature_font || 'Alex Brush';
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntityType(): 'character' {
|
||||||
|
return 'character';
|
||||||
|
}
|
||||||
|
|
||||||
|
getSignature() {
|
||||||
|
const style = this.getSignatureStyle();
|
||||||
|
|
||||||
|
if (style === 'full') {
|
||||||
|
return this.name.replace('_', ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = this.name.split('_');
|
||||||
|
|
||||||
|
if (style === 'firsti') {
|
||||||
|
return `${parts[0][0]}. ${parts[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style === 'firstin') {
|
||||||
|
return `${parts[0][0]} ${parts[1]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style === 'lasti') {
|
||||||
|
return `${parts[0]} ${parts[1][0]}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style === 'lastin') {
|
||||||
|
return `${parts[0]} ${parts[1][0]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style === 'i') {
|
||||||
|
return `${parts[0][0]}. ${parts[1][0]}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style === 'in') {
|
||||||
|
return `${parts[0][0]} ${parts[1][0]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style === 'last') {
|
||||||
|
return `${parts[1]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/server/entities/charge.entity.ts
Normal file
15
src/server/entities/charge.entity.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('charges')
|
||||||
|
export class ChargeEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'int' }) character_id: number;
|
||||||
|
@Column({ type: 'varchar', width: 32 }) character_name: string;
|
||||||
|
@Column({ type: 'int' }) account_id: number;
|
||||||
|
@Column({ type: 'int' }) by_character_id: number;
|
||||||
|
@Column({ type: 'varchar', width: 50 }) by_name: string;
|
||||||
|
@Column({ type: 'varchar', width: 256 }) reason: string;
|
||||||
|
@Column({ type: 'text', default: null, nullable: true }) statement: string | null;
|
||||||
|
@Column({ type: 'varchar', width: 20 }) status: 'charge' | 'prison' | 'manual-entry' | 'update';
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP()' }) time: Date;
|
||||||
|
}
|
||||||
17
src/server/entities/chat-ads-entity.ts
Normal file
17
src/server/entities/chat-ads-entity.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('chat_ads')
|
||||||
|
export class ChatAdEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', width: 40 }) character_name: string;
|
||||||
|
@Column({ type: 'int' }) character_id: number;
|
||||||
|
@Column({ type: 'int' }) account_id: number;
|
||||||
|
@Column({ type: 'varchar', width: 296 }) advert: string;
|
||||||
|
@Column({ type: 'datetime' }) time: Date;
|
||||||
|
@Column({ type: 'int' }) paid: number;
|
||||||
|
@Column({ type: 'datetime', default: null, nullable: true }) aired: Date | null;
|
||||||
|
@Column({ type: 'datetime', default: null, nullable: true }) removed: Date | null;
|
||||||
|
@Column({ type: 'varchar', width: 40, default: null, nullable: true }) removed_by: string | null;
|
||||||
|
|
||||||
|
@Column({ type: 'boolean', default: false }) premium: boolean;
|
||||||
|
}
|
||||||
21
src/server/entities/clothing-item.entity.ts
Normal file
21
src/server/entities/clothing-item.entity.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
export type ClothingItemType = 'jacket' | 'shirt';
|
||||||
|
|
||||||
|
export interface ClothingItemState {
|
||||||
|
type: string;
|
||||||
|
variations: {
|
||||||
|
drawable: number;
|
||||||
|
texture: number;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('clothing_items')
|
||||||
|
export class ClothingItemEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', length: 64, nullable: false }) name: string;
|
||||||
|
@Column({ type: 'varchar', length: 16, nullable: false }) type: ClothingItemType;
|
||||||
|
@Column({ type: 'int', width: 11, nullable: false }) drawable: string;
|
||||||
|
@Column({ type: 'int', width: 11, nullable: false }) texture: string;
|
||||||
|
@Column({ type: 'json', nullable: true }) states?: ClothingItemState[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CommercialDriversLicenseEntity } from './commercial-drivers-license.entity';
|
||||||
|
|
||||||
|
@Entity('commercial_drivers_license_types')
|
||||||
|
export class CommercialDriversLicenseTypeEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) name: string;
|
||||||
|
@Column() max_load: number;
|
||||||
|
@Column() required_xp: number;
|
||||||
|
@Column() level: number;
|
||||||
|
@Column() reward_multiplier: number;
|
||||||
|
|
||||||
|
@OneToMany(() => CommercialDriversLicenseEntity, (cdle) => cdle.type)
|
||||||
|
cdls: CommercialDriversLicenseEntity[];
|
||||||
|
}
|
||||||
17
src/server/entities/commercial-drivers-license.entity.ts
Normal file
17
src/server/entities/commercial-drivers-license.entity.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CommercialDriversLicenseTypeEntity } from './commercial-drivers-license-type.entity';
|
||||||
|
|
||||||
|
@Entity('commercial_drivers_licenses')
|
||||||
|
export class CommercialDriversLicenseEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'int' }) character_id: number | null;
|
||||||
|
@Column({ type: 'varchar', length: 32 }) character_name: string | null;
|
||||||
|
@Column() commercial_drivers_license_type_id: number;
|
||||||
|
@Column({ type: 'int', default: 0 }) xp: number;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) created: Date;
|
||||||
|
@Column({ type: 'datetime' }) expires: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => CommercialDriversLicenseTypeEntity, (cdlte) => cdlte.cdls)
|
||||||
|
@JoinColumn({ name: 'commercial_drivers_license_type_id' })
|
||||||
|
type: CommercialDriversLicenseTypeEntity;
|
||||||
|
}
|
||||||
11
src/server/entities/commercial-vehicle.ts
Normal file
11
src/server/entities/commercial-vehicle.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('commercial_vehicles')
|
||||||
|
export class CommercialVehicleEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) model: string;
|
||||||
|
@Column({ type: 'json' }) cargo_space: BoundingBox;
|
||||||
|
@Column() capacity_weight: number;
|
||||||
|
@Column() min_cdl_level: number;
|
||||||
|
@Column({ type: 'boolean', default: false }) requires_trailer: boolean;
|
||||||
|
}
|
||||||
26
src/server/entities/company-contracts.entity.ts
Normal file
26
src/server/entities/company-contracts.entity.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CompanyEntity } from './company.entity';
|
||||||
|
import { GovernmentContractEntity } from './government-contract.entity';
|
||||||
|
|
||||||
|
@Entity('company_contracts')
|
||||||
|
export class CompanyContractEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
company_id: number;
|
||||||
|
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
contract_id: number;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) signed: Date;
|
||||||
|
@Column({ type: 'datetime', nullable: true, default: null }) expiration: Date | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => CompanyEntity)
|
||||||
|
@JoinColumn({ name: 'company_id' })
|
||||||
|
company?: CompanyEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => GovernmentContractEntity)
|
||||||
|
@JoinColumn({ name: 'contract_id' })
|
||||||
|
contract?: GovernmentContractEntity;
|
||||||
|
}
|
||||||
41
src/server/entities/company-employment-point.entity.ts
Normal file
41
src/server/entities/company-employment-point.entity.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CompanyEntity } from './company.entity';
|
||||||
|
import { CompanyPositionEntity } from './company-position.entity';
|
||||||
|
|
||||||
|
@Entity('company_employment_points')
|
||||||
|
export class CompanyEmploymentPointEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
company_id: number;
|
||||||
|
@Index()
|
||||||
|
@Column()
|
||||||
|
company_position_id: number;
|
||||||
|
@Column({ type: 'int', nullable: true }) blip: number | null;
|
||||||
|
@Column({ type: 'int', nullable: true }) marker: number | null;
|
||||||
|
|
||||||
|
@Column({ type: 'float' }) x: number;
|
||||||
|
@Column({ type: 'float' }) y: number;
|
||||||
|
@Column({ type: 'float' }) z: number;
|
||||||
|
@Column() dimension: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 32 }) image: string;
|
||||||
|
@Column({ type: 'text' }) description: string;
|
||||||
|
|
||||||
|
@Column({ type: 'tinyint', default: 0 }) requires_drivers_license: boolean;
|
||||||
|
@Column({ type: 'tinyint', default: 0 }) requires_cdl: boolean;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 64, nullable: true }) wiki_link_title: string | null;
|
||||||
|
|
||||||
|
@ManyToOne(() => CompanyEntity, (company) => company.employmentPoints)
|
||||||
|
@JoinColumn({ name: 'company_id' })
|
||||||
|
company: CompanyEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => CompanyPositionEntity)
|
||||||
|
@JoinColumn({ name: 'company_position_id' })
|
||||||
|
position: CompanyPositionEntity;
|
||||||
|
|
||||||
|
blipEntity?: BlipMp;
|
||||||
|
markerEntity?: MarkerMp;
|
||||||
|
colShapeEntity?: ColshapeMp;
|
||||||
|
}
|
||||||
32
src/server/entities/company-position.entity.ts
Normal file
32
src/server/entities/company-position.entity.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CompanyEntity } from './company.entity';
|
||||||
|
import { CharacterEntity } from './character.entity';
|
||||||
|
|
||||||
|
@Entity('company_positions')
|
||||||
|
export class CompanyPositionEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() company_id: number;
|
||||||
|
@Column() sort_id: number;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) name: string;
|
||||||
|
@Column() modify_power: number;
|
||||||
|
@Column() needed_modify_power: number;
|
||||||
|
@Column() hourly_pay: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => CompanyEntity, (company) => company.positions)
|
||||||
|
@JoinColumn({ name: 'company_id' })
|
||||||
|
company: CompanyEntity;
|
||||||
|
|
||||||
|
@ManyToMany(() => CharacterEntity, (character) => character.companyPositions)
|
||||||
|
@JoinTable({
|
||||||
|
name: 'character_company_positions',
|
||||||
|
joinColumn: { name: 'company_position_id' },
|
||||||
|
inverseJoinColumn: { name: 'character_id' },
|
||||||
|
})
|
||||||
|
employees: CharacterEntity[];
|
||||||
|
|
||||||
|
permissions: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}[] = [];
|
||||||
|
}
|
||||||
10
src/server/entities/company-type.entity.ts
Normal file
10
src/server/entities/company-type.entity.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('company_types')
|
||||||
|
export class CompanyTypeEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) name: string;
|
||||||
|
@Column({ type: 'json' }) suffixes: string[];
|
||||||
|
@Column({ type: 'int' }) max_members: number;
|
||||||
|
@Column({ type: 'decimal' }) income_tax: number;
|
||||||
|
}
|
||||||
15
src/server/entities/company-uniform.entity.ts
Normal file
15
src/server/entities/company-uniform.entity.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CompanyEntity } from './company.entity';
|
||||||
|
|
||||||
|
@Entity('company_uniforms')
|
||||||
|
export class CompanyUniformEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() company_id: number;
|
||||||
|
@Column({ type: 'int', nullable: true }) position_id: number | null;
|
||||||
|
@Column({ type: 'varchar', length: 32 }) name: string;
|
||||||
|
@Column({ type: 'json' }) clothing_items: number[];
|
||||||
|
|
||||||
|
@ManyToOne(() => CompanyEntity, (company) => company.uniforms)
|
||||||
|
@JoinColumn({ name: 'company_id' })
|
||||||
|
company: CompanyEntity;
|
||||||
|
}
|
||||||
89
src/server/entities/company.entity.ts
Normal file
89
src/server/entities/company.entity.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Column, Entity, JoinColumn, JoinTable, ManyToMany, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { CompanyPositionEntity } from './company-position.entity';
|
||||||
|
import { CompanyEmploymentPointEntity } from './company-employment-point.entity';
|
||||||
|
import { ChangingRoomEntity } from './changing-room.entity';
|
||||||
|
import { CompanyUniformEntity } from './company-uniform.entity';
|
||||||
|
import { CharacterEntity } from './character.entity';
|
||||||
|
import { GovernmentContractEntity } from './government-contract.entity';
|
||||||
|
import { VehicleEntity } from './vehicle.entity';
|
||||||
|
import { BankAccountEntity } from './bank-account.entity';
|
||||||
|
import { PhoneEntity } from './phone.entity';
|
||||||
|
import { VehicleOrderEntity } from './vehicle-order.entity';
|
||||||
|
|
||||||
|
@Entity('companies')
|
||||||
|
export class CompanyEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) name: string;
|
||||||
|
@Column({ type: 'int' }) type: number;
|
||||||
|
@Column({ type: 'varchar', length: 64, nullable: true }) stamp: string | null;
|
||||||
|
@Column({ type: 'int', nullable: true }) owner_character_id: number | null;
|
||||||
|
@Column({ type: 'tinyint' }) protected: boolean;
|
||||||
|
@Column({ type: 'varchar', length: 6 }) chat_color: string;
|
||||||
|
@Column({ type: 'int', default: 3 }) max_on_shift: number;
|
||||||
|
|
||||||
|
// type should be type_id so we can add relations to companytypeentity
|
||||||
|
|
||||||
|
@OneToMany(() => CompanyPositionEntity, (position) => position.company)
|
||||||
|
positions: CompanyPositionEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => CompanyEmploymentPointEntity, (point) => point.company)
|
||||||
|
employmentPoints: CompanyEmploymentPointEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => ChangingRoomEntity, (changingRoom) => changingRoom.company)
|
||||||
|
changingRooms: ChangingRoomEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => CompanyUniformEntity, (uniform) => uniform.company)
|
||||||
|
uniforms: CompanyUniformEntity[];
|
||||||
|
|
||||||
|
@ManyToMany(() => CharacterEntity, (character) => character.companies)
|
||||||
|
@JoinTable({
|
||||||
|
name: 'character_company_positions',
|
||||||
|
joinColumn: { name: 'company_id' },
|
||||||
|
inverseJoinColumn: { name: 'character_id' },
|
||||||
|
})
|
||||||
|
employees: CharacterEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => VehicleEntity, (vehicle) => vehicle.company)
|
||||||
|
vehicles: VehicleEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => BankAccountEntity, (bankAccount) => bankAccount.company)
|
||||||
|
bankAccounts: BankAccountEntity[];
|
||||||
|
|
||||||
|
@OneToMany(() => PhoneEntity, (phone) => phone.company)
|
||||||
|
phones: PhoneEntity[];
|
||||||
|
|
||||||
|
@ManyToMany(() => GovernmentContractEntity, (contract) => contract.companies)
|
||||||
|
@JoinTable({
|
||||||
|
name: 'company_contracts',
|
||||||
|
joinColumn: { name: 'company_id' },
|
||||||
|
inverseJoinColumn: { name: 'contract_id' },
|
||||||
|
})
|
||||||
|
contracts: GovernmentContractEntity[];
|
||||||
|
|
||||||
|
@ManyToOne(() => CharacterEntity, (character) => character.ownedCompanies)
|
||||||
|
@JoinColumn({ name: 'owner_character_id' })
|
||||||
|
owner?: CharacterEntity;
|
||||||
|
|
||||||
|
@OneToMany(() => VehicleOrderEntity, (voe) => voe.character)
|
||||||
|
vehicleOrders: VehicleOrderEntity[];
|
||||||
|
|
||||||
|
positionPermissionAccess: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
getRegNo() {
|
||||||
|
return String(this.id).padStart(6, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return this.name ?? `Company #${this.getRegNo()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntityType(): 'company' {
|
||||||
|
return 'company';
|
||||||
|
}
|
||||||
|
|
||||||
|
chatDisabled = false;
|
||||||
|
}
|
||||||
22
src/server/entities/contract-party.entity.ts
Normal file
22
src/server/entities/contract-party.entity.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { ContractEntity } from './contract.entity';
|
||||||
|
|
||||||
|
@Entity('contract_parties')
|
||||||
|
export class ContractPartyEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() contract_id: number;
|
||||||
|
@Column() entity_type_id: number;
|
||||||
|
@Column() entity_id: number;
|
||||||
|
@Column({ type: 'datetime', nullable: true }) signed: Date | null;
|
||||||
|
@Column({ type: 'varchar', length: 128, nullable: true }) entity_name: string | null;
|
||||||
|
@Column({ type: 'varchar', length: 128, nullable: true }) entity_note: string | null;
|
||||||
|
@Column({ type: 'varchar', length: 64, nullable: true }) stamp: string | null;
|
||||||
|
@Column({ type: 'varchar', length: 24, nullable: true }) signature: string | null;
|
||||||
|
@Column({ type: 'varchar', length: 32, nullable: true }) signature_font: string | null;
|
||||||
|
@Column({ type: 'int', nullable: true }) character_id: number | null;
|
||||||
|
@Column({ type: 'varchar', length: 64, nullable: false }) name: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => ContractEntity, (contract) => contract.parties)
|
||||||
|
@JoinColumn({ name: 'contract_id' })
|
||||||
|
contract: ContractEntity;
|
||||||
|
}
|
||||||
9
src/server/entities/contract-template.entity.ts
Normal file
9
src/server/entities/contract-template.entity.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('contract_templates')
|
||||||
|
export class ContractTemplateEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', length: 64 }) key: string;
|
||||||
|
@Column({ type: 'varchar', length: 128 }) name: string;
|
||||||
|
@Column({ type: 'longtext' }) markdown: string;
|
||||||
|
}
|
||||||
15
src/server/entities/contract.entity.ts
Normal file
15
src/server/entities/contract.entity.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { ContractPartyEntity } from './contract-party.entity';
|
||||||
|
|
||||||
|
@Entity('contracts2')
|
||||||
|
export class ContractEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'varchar', length: 128 }) name: string;
|
||||||
|
@Column({ type: 'longtext' }) html: string;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) created: Date;
|
||||||
|
@Column({ type: 'datetime', nullable: true }) validated: Date;
|
||||||
|
@Column({ type: 'datetime', nullable: true }) invalidated: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => ContractPartyEntity, (cpe) => cpe.contract)
|
||||||
|
parties: ContractPartyEntity[];
|
||||||
|
}
|
||||||
33
src/server/entities/corpse.entity.ts
Normal file
33
src/server/entities/corpse.entity.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { ColumnDecimalTransformer } from '../features/utility/ColumnDecimalTransformer';
|
||||||
|
import { InventoryEntity } from './inventory.entity';
|
||||||
|
|
||||||
|
@Entity('corpses')
|
||||||
|
export class CorpseEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column({ type: 'int' }) character_id: number;
|
||||||
|
@Column({ type: 'json' }) appearance: PedCustomization;
|
||||||
|
@Column({ type: 'json' }) damages: DamageHistory[];
|
||||||
|
@Column({ type: 'varchar', length: 32 }) model: string;
|
||||||
|
@Column({ type: 'decimal', precision: 17, scale: 12, transformer: new ColumnDecimalTransformer() }) x: number;
|
||||||
|
@Column({ type: 'decimal', precision: 17, scale: 12, transformer: new ColumnDecimalTransformer() }) y: number;
|
||||||
|
@Column({ type: 'decimal', precision: 17, scale: 12, transformer: new ColumnDecimalTransformer() }) z: number;
|
||||||
|
@Column({ type: 'int', nullable: false }) dimension: number;
|
||||||
|
@Column({ type: 'boolean', default: false }) at_morgue: boolean;
|
||||||
|
@Column({ type: 'boolean', default: false }) identified: boolean;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP()' }) created: Date;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP()' }) last_interaction: Date;
|
||||||
|
@Column({ type: 'datetime', nullable: true, default: null }) stored: Date | null;
|
||||||
|
@Column({ type: 'datetime', nullable: true, default: null }) requested_id: Date | null;
|
||||||
|
@Column({ type: 'boolean', default: false }) buried: boolean;
|
||||||
|
@Column({ type: 'datetime', nullable: true, default: null }) buried_date: Date | null;
|
||||||
|
|
||||||
|
inventory?: InventoryEntity;
|
||||||
|
spawned = false;
|
||||||
|
in_vehicle?: number | undefined;
|
||||||
|
being_buried = false;
|
||||||
|
|
||||||
|
get position(): Vector3 {
|
||||||
|
return new mp.Vector3(this.x, this.y, this.z);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/server/entities/darknet-entity-access.entity.ts
Normal file
19
src/server/entities/darknet-entity-access.entity.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { ItemTypeEntity } from './item-type.entity';
|
||||||
|
|
||||||
|
@Entity('darknet_entity_access')
|
||||||
|
export class DarknetEntityAccessEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() entity_type_id: number;
|
||||||
|
@Column() entity_id: number;
|
||||||
|
@Column() item_type_id: number;
|
||||||
|
@Column({ default: 1 }) amount: number;
|
||||||
|
@Column() price: number;
|
||||||
|
@Column() ooc_price: number;
|
||||||
|
@Column() created: Date;
|
||||||
|
@Column() created_by: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => ItemTypeEntity, (type) => type.items)
|
||||||
|
@JoinColumn({ name: 'item_type_id' })
|
||||||
|
itemType: ItemTypeEntity;
|
||||||
|
}
|
||||||
20
src/server/entities/darknet-order-item.entity.ts
Normal file
20
src/server/entities/darknet-order-item.entity.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { DarknetOrderEntity } from './darknet-order.entity';
|
||||||
|
import { ItemTypeEntity } from './item-type.entity';
|
||||||
|
|
||||||
|
@Entity('darknet_order_items')
|
||||||
|
export class DarknetOrderItemEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() order_id: number;
|
||||||
|
@Column() item_type_id: number;
|
||||||
|
@Column() amount: number;
|
||||||
|
@Column() unit_amount: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => DarknetOrderEntity, (order) => order.items)
|
||||||
|
@JoinColumn({ name: 'order_id' })
|
||||||
|
order: DarknetOrderEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => ItemTypeEntity, (itemType) => itemType.darknetOrderItems)
|
||||||
|
@JoinColumn({ name: 'item_type_id' })
|
||||||
|
itemType: ItemTypeEntity;
|
||||||
|
}
|
||||||
27
src/server/entities/darknet-order.entity.ts
Normal file
27
src/server/entities/darknet-order.entity.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { DarknetOrderItemEntity } from './darknet-order-item.entity';
|
||||||
|
import { PropertyEntity } from './property.entity';
|
||||||
|
import { InventoryEntity } from './inventory.entity';
|
||||||
|
|
||||||
|
@Entity('darknet_orders')
|
||||||
|
export class DarknetOrderEntity {
|
||||||
|
@PrimaryGeneratedColumn() id: number;
|
||||||
|
@Column() character_id: number;
|
||||||
|
@Column() property_id: number;
|
||||||
|
@Column() inventory_id: number;
|
||||||
|
@Column() delivery_attempts: number;
|
||||||
|
@Column({ type: 'datetime', default: () => 'CURRENT_TIMESTAMP' }) created: Date;
|
||||||
|
@Column({ type: 'datetime', nullable: true }) delivered: Date | null;
|
||||||
|
@Column({ type: 'datetime' }) deliver_after: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => DarknetOrderItemEntity, (item) => item.order)
|
||||||
|
items: DarknetOrderItemEntity[];
|
||||||
|
|
||||||
|
@ManyToOne(() => PropertyEntity, (property) => property.darknetOrders)
|
||||||
|
@JoinColumn({ name: 'property_id' })
|
||||||
|
property: PropertyEntity;
|
||||||
|
|
||||||
|
@ManyToOne(() => InventoryEntity, (inventory) => inventory.darknetOrders)
|
||||||
|
@JoinColumn({ name: 'inventory_id' })
|
||||||
|
inventory: InventoryEntity;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user