Initial commit — Southwest Roleplay FiveM

This commit is contained in:
jack
2026-06-20 13:05:34 +00:00
commit 3857de3d85
397 changed files with 30322 additions and 0 deletions

View 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 {}

View 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}.`);
}

View File

@@ -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;
};
};

View 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;
};

View 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;
};
};

View 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;
};
};

View 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;
};
};

View 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;
};
};

View 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();

View 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;
};
}

View 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();

View 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.');
}
}

View 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;

View File

@@ -0,0 +1,7 @@
export const BlackjackLogEvents = {
game: 'game',
tillOnline: 'tillOnline',
till: 'till',
} as const;
export type BlackjackLogEvent = keyof typeof BlackjackLogEvents;

View 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;

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,8 @@
export const DMVLogEvent = {
testing: 'testing',
passed: 'passed',
failed: 'failed',
renewed: 'renewed',
} as const;
export type DMVLogEvent = keyof typeof DMVLogEvent;

View File

@@ -0,0 +1,5 @@
export const DoorLogEvents = {
lock_status: 'lock_status',
} as const;
export type DoorLogEvent = keyof typeof DoorLogEvents;

View 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;

View 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;

View File

@@ -0,0 +1,8 @@
export const GraffitiLogEvents = {
destroyed: 'destroyed',
washed: 'washed',
sprayed: 'sprayed',
created: 'created',
} as const;
export type GraffitiLogEvent = keyof typeof GraffitiLogEvents;

View 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;

View File

@@ -0,0 +1,8 @@
export const LawLogEvents = {
jail: 'jail',
arsenal: 'arsenal',
loadgun: 'loadgun',
takegun: 'takegun',
};
export type LawLogEvent = keyof typeof LawLogEvents;

View File

@@ -0,0 +1,7 @@
export const MarkerLogEvents = {
expired: 'expired',
created: 'created',
permanent: 'permanent',
} as const;
export type MarkerLogEvent = keyof typeof MarkerLogEvents;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@@ -0,0 +1,7 @@
export const RouletteLogEvents = {
game: 'game',
tillOnline: 'tillOnline',
till: 'till',
} as const;
export type RouletteLogEvent = keyof typeof RouletteLogEvents;

View 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;

View File

@@ -0,0 +1,7 @@
export const WhitelistLogEvents = {
rejected: 'rejected',
removed: 'removed',
added: 'added',
};
export type WhitelistLogEvent = keyof typeof WhitelistLogEvents;

View 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
View 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
View 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
View 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]);
}
}

View 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();

View 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
View 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();

View 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));
}
});

View 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
View 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 {};

View 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();