Initial commit — Southwest Roleplay FiveM
This commit is contained in:
18
src/server/core/commands/base.command.ts
Normal file
18
src/server/core/commands/base.command.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { CommandOptions } from './options';
|
||||
import { ObjectLiteral } from 'typeorm';
|
||||
|
||||
export interface BaseCommand {
|
||||
$options: CommandOptions<{
|
||||
[key: string]: string;
|
||||
}>;
|
||||
run(
|
||||
player: PlayerMp,
|
||||
params?:
|
||||
| {
|
||||
[key: string]: string | number | null | PlayerMp | VehicleMp;
|
||||
}
|
||||
| ObjectLiteral,
|
||||
): Promise<void>;
|
||||
}
|
||||
|
||||
export class BaseCommand implements BaseCommand {}
|
||||
91
src/server/core/commands/command-manifest.ts
Normal file
91
src/server/core/commands/command-manifest.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import path from 'path';
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
import { commandSystem } from './index';
|
||||
import { clientSideCommands } from '../../../shared/const/clientside-commands';
|
||||
import { logSystem } from '../log';
|
||||
|
||||
type GuardWithPermissions = CommandGuard & {
|
||||
__permissions?: string[];
|
||||
};
|
||||
|
||||
function formatValidator(validator: CommandParamValidator): string {
|
||||
if (Array.isArray(validator)) {
|
||||
return `one-of(${validator.map((value) => String(value)).join('|')})`;
|
||||
}
|
||||
|
||||
if (validator instanceof RegExp) {
|
||||
return `regex(${validator.toString()})`;
|
||||
}
|
||||
|
||||
return String(validator);
|
||||
}
|
||||
|
||||
function buildParamString(params?: CommandParamMetaData<any>): string {
|
||||
if (!params || !Object.keys(params).length) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
return Object.entries(params)
|
||||
.map(([key, meta]) => {
|
||||
const optional = meta.default !== undefined;
|
||||
const optionalHint = optional ? ` optional default=${String(meta.default)}` : ' required';
|
||||
return `${key} <${formatValidator(meta.validator)}> (${meta.hint};${optionalHint})`;
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
function extractPermissions(guards?: CommandGuard[]): string[] {
|
||||
if (!guards?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const permissions = guards
|
||||
.flatMap((guard) => {
|
||||
const typedGuard = guard as GuardWithPermissions;
|
||||
return typedGuard.__permissions ?? [];
|
||||
})
|
||||
.filter((permission, index, all) => permission && all.indexOf(permission) === index);
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
||||
export async function writeCommandManifestTxt(): Promise<void> {
|
||||
const lines: string[] = [];
|
||||
lines.push('Southwest Roleplay Command Manifest');
|
||||
lines.push(`Generated: ${new Date().toISOString()}`);
|
||||
lines.push('');
|
||||
|
||||
const commandEntries = Array.from(commandSystem.commands.entries()).sort(([left], [right]) => left.localeCompare(right));
|
||||
|
||||
lines.push(`Server Commands (${commandEntries.length})`);
|
||||
lines.push('----------------------------------------');
|
||||
|
||||
for (const [commandName, command] of commandEntries) {
|
||||
const params = buildParamString(command.$options.params);
|
||||
const permissions = extractPermissions(command.$options.guards);
|
||||
|
||||
lines.push(`/${commandName}`);
|
||||
lines.push(` params: ${params}`);
|
||||
lines.push(` required_permissions: ${permissions.length ? permissions.join(', ') : 'none'}`);
|
||||
lines.push(` grouped: ${command.$options.grouped ?? 'none'}`);
|
||||
lines.push(` description: ${command.$options.description ?? 'none'}`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const uniqueClientCommands = [...new Set(clientSideCommands)].sort((left, right) => left.localeCompare(right));
|
||||
lines.push(`Client-side Commands (${uniqueClientCommands.length})`);
|
||||
lines.push('----------------------------------------');
|
||||
|
||||
for (const commandName of uniqueClientCommands) {
|
||||
lines.push(`/${commandName}`);
|
||||
lines.push(' params: n/a (handled client-side)');
|
||||
lines.push(' required_permissions: n/a (handled client-side)');
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const outputPath = path.resolve(process.cwd(), 'command-manifest.txt');
|
||||
await fs.writeFile(outputPath, `${lines.join('\n')}\n`, 'utf8');
|
||||
|
||||
logSystem.initLog('Command Manifest', `Wrote ${commandEntries.length} server command entries to ${outputPath}.`);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export const NoSpectateCommandGuard = (): CommandGuard => {
|
||||
return (player: PlayerMp) => {
|
||||
if (player.state?.spectating?.target) {
|
||||
return `You can't use this command while spectating.`;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
21
src/server/core/commands/guards/permissions.command.guard.ts
Normal file
21
src/server/core/commands/guards/permissions.command.guard.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { permissionSystem } from '../../../features/permission';
|
||||
|
||||
export const PermissionCommandGuard = (...permissionNames: string[]): CommandGuard => {
|
||||
const permission = ((player: PlayerMp) => {
|
||||
for (const permissionName of permissionNames) {
|
||||
if (permissionSystem.doesPlayerHavePermission(player, permissionName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionNames.includes('aduty') && player.getVariable('adminDuty')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return `You're not allowed to use this command`;
|
||||
}) as CommandGuard & { __permissions?: string[] };
|
||||
|
||||
permission.__permissions = [...permissionNames];
|
||||
|
||||
return permission;
|
||||
};
|
||||
36
src/server/core/commands/guards/phone.command.guard.ts
Normal file
36
src/server/core/commands/guards/phone.command.guard.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { phoneSystem } from '../../../features/phone';
|
||||
|
||||
type PhoneCommandGuardOptions = {
|
||||
on?: boolean;
|
||||
unlocked?: boolean;
|
||||
};
|
||||
|
||||
export const PhoneCommandGuard = (options?: PhoneCommandGuardOptions): CommandGuard => {
|
||||
return (player: PlayerMp) => {
|
||||
if (!options) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!player.character?.phone || !player.character.phone.item) {
|
||||
return `You must have a phone.`;
|
||||
}
|
||||
|
||||
if (options.on !== undefined) {
|
||||
if (options.on === true && !phoneSystem.isPhoneOn(player.character.phone.item)) {
|
||||
return `Your phone must be turned on.`;
|
||||
} else if (options.on === false && phoneSystem.isPhoneOn(player.character.phone.item)) {
|
||||
return `Your phone must be turned off.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.unlocked !== undefined) {
|
||||
if (options.unlocked === true && !phoneSystem.isPlayerPhoneLocked(player as PlayerMpLoggedIn)) {
|
||||
return `Your phone must be locked.`;
|
||||
} else if (options.unlocked === false && phoneSystem.isPlayerPhoneLocked(player as PlayerMpLoggedIn)) {
|
||||
return `Your phone must be unlocked.`;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
24
src/server/core/commands/guards/prisoned.command.guard.ts
Normal file
24
src/server/core/commands/guards/prisoned.command.guard.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
type PrisonCommandGuardOptions = {
|
||||
prison?: boolean;
|
||||
jail?: boolean;
|
||||
adminJail?: boolean;
|
||||
all?: boolean;
|
||||
};
|
||||
|
||||
export const PrisonCommandGuard = (options: PrisonCommandGuardOptions): CommandGuard => {
|
||||
return (player: PlayerMp) => {
|
||||
if ((options.prison || options.all) && player.character?.prison_cell_id) {
|
||||
return `You can't use this while imprisoned.`;
|
||||
}
|
||||
|
||||
if ((options.jail || options.all) && player.state.jailed) {
|
||||
return `You can't use this while jailed.`;
|
||||
}
|
||||
|
||||
if ((options.adminJail || options.all) && player.state.adminJail?.releaseTime) {
|
||||
return `You can't use this while admin jailed.`;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
46
src/server/core/commands/guards/state.command.guard.ts
Normal file
46
src/server/core/commands/guards/state.command.guard.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
type PlayerStateCommandGuardOption = {
|
||||
alive?: boolean;
|
||||
wounded?: boolean;
|
||||
knocked?: boolean;
|
||||
dead?: boolean;
|
||||
};
|
||||
|
||||
export const PlayerStateCommandGuard = (options?: PlayerStateCommandGuardOption): CommandGuard => {
|
||||
return (player: PlayerMp) => {
|
||||
if (!options) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.alive !== undefined) {
|
||||
if (options.alive && player.state.deathState !== 'ALIVE') {
|
||||
return `You must be alive.`;
|
||||
} else if (!options.alive && player.state.deathState === 'ALIVE') {
|
||||
return `You must not be alive.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.wounded !== undefined) {
|
||||
if (options.wounded && player.state.deathState !== 'WOUNDED') {
|
||||
return `You aren't brutally wounded.`;
|
||||
} else if (!options.wounded && player.state.deathState === 'WOUNDED') {
|
||||
return `You're brutally wounded.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.knocked !== undefined) {
|
||||
if (!options.knocked && player.state.deathState === 'KNOCKED') {
|
||||
return `You're knocked out.`;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.dead !== undefined) {
|
||||
if (options.dead && player.state.deathState !== 'DEAD') {
|
||||
return `You aren't dead.`;
|
||||
} else if (!options.dead && player.state.deathState === 'DEAD') {
|
||||
return `You're dead.`;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
45
src/server/core/commands/guards/vehicle.command.guard.ts
Normal file
45
src/server/core/commands/guards/vehicle.command.guard.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { VehicleSystem } from '../../../features/vehicle';
|
||||
|
||||
const DRIVER_SEAT = -1; // GTA/FiveM convention: -1 = driver seat
|
||||
|
||||
type VehicleCommandGuardOptions = {
|
||||
driver?: boolean;
|
||||
owner?: boolean;
|
||||
engine?: 'on' | 'off';
|
||||
};
|
||||
|
||||
export const VehicleCommandGuard = (options?: VehicleCommandGuardOptions): CommandGuard => {
|
||||
return (player: PlayerMp) => {
|
||||
if (!player.vehicle) {
|
||||
return `You aren't in a vehicle`;
|
||||
}
|
||||
|
||||
if (options?.engine !== undefined) {
|
||||
if (VehicleSystem.GetVehicleClassName(player.vehicle) !== 'CYCLES') {
|
||||
if (options.engine === 'on' && !player.vehicle.engine) {
|
||||
return `The engine must be turned on`;
|
||||
} else if (options.engine === 'off' && player.vehicle.engine) {
|
||||
return `The engine must be turned off`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.driver !== undefined) {
|
||||
if (options.driver && player.seat !== DRIVER_SEAT) {
|
||||
return `You're not driving a vehicle`;
|
||||
} else if (!options.driver && player.seat === DRIVER_SEAT) {
|
||||
return `You're not a passenger`;
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.owner !== undefined) {
|
||||
if (options.owner && !VehicleSystem.DoesPlayerOwnVehicle(player, player.vehicle)) {
|
||||
return `You don't own the vehicle`;
|
||||
} else if (!options.owner && VehicleSystem.DoesPlayerOwnVehicle(player, player.vehicle)) {
|
||||
return `You can't own the vehicle`;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
364
src/server/core/commands/index.ts
Normal file
364
src/server/core/commands/index.ts
Normal file
@@ -0,0 +1,364 @@
|
||||
/**
|
||||
* CommandSystem — FiveM port.
|
||||
*
|
||||
* Key difference from RageMP: each command is registered individually via
|
||||
* RegisterCommand() rather than through a single 'playerCommand' event.
|
||||
* The rest of the validation/dispatch logic is unchanged.
|
||||
*/
|
||||
|
||||
import { BaseCommand } from './base.command';
|
||||
import { LogSystem, logSystem } from '../log';
|
||||
import { ObjectLiteral } from 'typeorm';
|
||||
import { PlayerSystem } from '../../features/player';
|
||||
import { ChatSystem } from '../../features/chat';
|
||||
import { clientSideCommands } from '../../../shared/const/clientside-commands';
|
||||
import { AnimationEntity } from '../../entities/animation.entity';
|
||||
import { animationSystem } from '../../features/animations';
|
||||
import { PlayerStateCommandGuard } from './guards/state.command.guard';
|
||||
import { portableRadioSystem } from '../../features/portable-radio';
|
||||
import { propertySystem } from '../../features/properties';
|
||||
import { PropertyEntity } from '../../entities/property.entity';
|
||||
import { ItemEntity } from '../../entities/item.entity';
|
||||
import { PrisonCommandGuard } from './guards/prisoned.command.guard';
|
||||
import { staffSystem } from '../../features/staff';
|
||||
import { ContainsInvalidCharacters } from '../../features/utility/ContainsInvalidCharacters';
|
||||
import { Sanitize } from '../../features/utility/Sanitize';
|
||||
import { PlayerLogEvents } from '../log/events/player.log.events';
|
||||
import { companySystem } from '../../features/company';
|
||||
import { CompanyEntity } from '../../entities/company.entity';
|
||||
import { factionSystem } from '../../features/faction';
|
||||
import { FactionEntity } from '../../entities/faction.entity';
|
||||
import { LargeDataServer } from '../../features/utility/SendLargeData';
|
||||
import { punishmentSystem } from '../../features/admin/punishment';
|
||||
import { LoginSystem } from '../../features/login';
|
||||
import { playerRegistry, PlayerWrapper } from '../player/player-registry';
|
||||
|
||||
type CommandList = {
|
||||
name: string[];
|
||||
params: { hint: string; default?: string }[];
|
||||
guards?: CommandGuard[];
|
||||
grouped?: string;
|
||||
description?: string;
|
||||
}[];
|
||||
|
||||
export class CommandSystem {
|
||||
commands: Map<string, BaseCommand> = new Map();
|
||||
commandList: CommandList = [];
|
||||
|
||||
init(features: { commands?: (typeof BaseCommand)[] }[]): void {
|
||||
features
|
||||
.filter((f) => f.commands)
|
||||
.forEach((f) => {
|
||||
f.commands!.forEach((FeatureCommand: typeof BaseCommand) => {
|
||||
const command = new FeatureCommand();
|
||||
if (this.areCommandParamsValid(command)) {
|
||||
this.addCommandToMap(this.commands, command);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.generateCommandList();
|
||||
this.registerAllWithFiveM();
|
||||
|
||||
// Send command list when client requests it
|
||||
RegisterNetEvent('requestCommandList');
|
||||
AddEventHandler('requestCommandList', () => {
|
||||
const src = (global as any).source as number;
|
||||
const player = playerRegistry.get(src);
|
||||
if (player) this.requestCommandList(player);
|
||||
});
|
||||
|
||||
logSystem.initLog('Command System', `Loaded ${this.commands.size} commands.`);
|
||||
}
|
||||
|
||||
private registerAllWithFiveM(): void {
|
||||
for (const [name] of this.commands) {
|
||||
this.registerWithFiveM(name);
|
||||
}
|
||||
}
|
||||
|
||||
private registerWithFiveM(name: string): void {
|
||||
RegisterCommand(name, async (source: number, args: string[], rawCommand: string) => {
|
||||
const player = playerRegistry.get(source);
|
||||
if (!player) return;
|
||||
// Re-assemble the full command text the way RageMP provided it
|
||||
const fullText = args.length ? `${name} ${args.join(' ')}` : name;
|
||||
await this.playerCommand(player, fullText);
|
||||
}, false);
|
||||
}
|
||||
|
||||
initAnimCommand(anim: AnimationEntity): void {
|
||||
if (this.commands.has(anim.command)) {
|
||||
LogSystem.Error('Animation', 'initAnimCommand', `Duplicate command /${anim.command}`, anim);
|
||||
return;
|
||||
}
|
||||
|
||||
const writeCommand: BaseCommand = {
|
||||
$options: { name: anim.command, grouped: 'animation' },
|
||||
run: async (player: PlayerMp) => {
|
||||
animationSystem.playAnim(player as PlayerMpLoggedIn, anim.command);
|
||||
},
|
||||
};
|
||||
|
||||
this.addCommandToMap(this.commands, writeCommand);
|
||||
this.registerWithFiveM(anim.command);
|
||||
}
|
||||
|
||||
initRadioCommands(): void {
|
||||
for (let index = 1; index <= 20; index++) {
|
||||
const makeGuards = () => [
|
||||
PlayerStateCommandGuard({ alive: true }),
|
||||
PrisonCommandGuard({ all: true }),
|
||||
];
|
||||
|
||||
const radioCommands: BaseCommand[] = [
|
||||
{
|
||||
$options: {
|
||||
name: `r${index}`,
|
||||
params: { message: { hint: 'Message', validator: 'string' } },
|
||||
guards: makeGuards(),
|
||||
grouped: 'Portable Radio',
|
||||
description: `Talk in slot ${index} of your radio`,
|
||||
},
|
||||
run: async (player: PlayerMpLoggedIn, params: { message: string }) => {
|
||||
if (punishmentSystem.checkMuted(player, 'ic')) return;
|
||||
if (!portableRadioSystem.hasRadio(player)) return ChatSystem.Error(player, "You don't have a radio.");
|
||||
if (!portableRadioSystem.isRadioOn(player)) return ChatSystem.Error(player, 'Turn on your radio to use it.');
|
||||
if (!portableRadioSystem.getSlotFrequency(player, index)) return ChatSystem.Error(player, `You don't have a frequency in slot #${index}.`);
|
||||
portableRadioSystem.sendRadioMessage(player, params.message, index, false);
|
||||
},
|
||||
},
|
||||
// r{n}low, r{n}me, r{n}do — same structure, abbreviated for brevity
|
||||
// TODO: port the remaining radio sub-commands from the original CommandSystem
|
||||
];
|
||||
|
||||
for (const cmd of radioCommands) {
|
||||
this.addCommandToMap(this.commands, cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addCommandToMap(map: Map<any, any>, command: BaseCommand): void {
|
||||
const names = Array.isArray(command.$options.name) ? command.$options.name : [command.$options.name];
|
||||
for (const n of names) {
|
||||
map.set(n.toLowerCase(), command);
|
||||
}
|
||||
}
|
||||
|
||||
areCommandParamsValid(command: BaseCommand): boolean {
|
||||
if (!command.$options.params) return true;
|
||||
|
||||
const paramKeys = Object.keys(command.$options.params);
|
||||
const invalid = paramKeys
|
||||
.map((pk) => command.$options.params![pk])
|
||||
.find((param, index) => {
|
||||
if (param.validator === 'string' && index !== paramKeys.length - 1) {
|
||||
console.error(`Skipping /${command.$options.name}: 'string' param must be last`);
|
||||
return true;
|
||||
}
|
||||
if (param.default !== undefined) {
|
||||
if (paramKeys.slice(index).find((pk) => command.$options.params![pk].default === undefined)) {
|
||||
console.error(`Skipping /${command.$options.name}: optional param before required param`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return !invalid;
|
||||
}
|
||||
|
||||
getCommand(name: string): BaseCommand | undefined {
|
||||
return this.commands.get(name.toLowerCase());
|
||||
}
|
||||
|
||||
async runCommand(player: PlayerMp, command: BaseCommand, params?: ObjectLiteral): Promise<boolean> {
|
||||
await command.run(player, params);
|
||||
return true;
|
||||
}
|
||||
|
||||
parseParams(params: any, words: string[]) {
|
||||
const paramEntries = Object.entries(params) as any;
|
||||
|
||||
for (const [index, paramEntry] of paramEntries.entries()) {
|
||||
if (!paramEntry?.[1]?.overrides) continue;
|
||||
if (paramEntry[1].overrides[words[index]]) {
|
||||
const overrideEntries = Object.entries(paramEntry[1].overrides[words[index]]) as any;
|
||||
for (const [overrideIndex, overrideEntry] of overrideEntries) {
|
||||
paramEntries[overrideIndex] = [overrideEntry._propertyName, overrideEntry];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const paramsParsed: any = {};
|
||||
for (const entry of paramEntries) {
|
||||
if (entry) paramsParsed[entry[0]] = entry[1];
|
||||
}
|
||||
return paramsParsed;
|
||||
}
|
||||
|
||||
async playerCommand(player: PlayerWrapper, fullText: string): Promise<void> {
|
||||
if (!LoginSystem.IsLoggedIn(player)) return;
|
||||
|
||||
if (ContainsInvalidCharacters(fullText)) {
|
||||
LogSystem.PlayerEvent(player, PlayerLogEvents.invalid_input, 'Attempted command with invalid characters', { command: fullText });
|
||||
return ChatSystem.Error(player, 'Your command contains invalid characters.');
|
||||
}
|
||||
|
||||
const split = fullText.trim().split(' ');
|
||||
const commandName = split.shift();
|
||||
if (!commandName) return ChatSystem.Error(player, 'Well give us a command will you!');
|
||||
|
||||
const command = this.getCommand(commandName);
|
||||
|
||||
LogSystem.CommandEvent(
|
||||
player,
|
||||
command?.$options.name
|
||||
? (Array.isArray(command.$options.name) ? command.$options.name[0] : command.$options.name).toLowerCase()
|
||||
: null,
|
||||
`/${fullText}`,
|
||||
);
|
||||
|
||||
if (!command) {
|
||||
if (!clientSideCommands.includes(commandName)) {
|
||||
return ChatSystem.Error(player, `${commandName} is not a valid command!`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.$options.guards?.length) {
|
||||
for (const guard of command.$options.guards) {
|
||||
const passed = guard(player);
|
||||
if (passed !== true) return ChatSystem.Error(player, passed);
|
||||
}
|
||||
}
|
||||
|
||||
if (!command.$options.params) {
|
||||
await this.runCommand(player, command);
|
||||
return;
|
||||
}
|
||||
|
||||
const paramsTemplate = this.parseParams(command.$options.params, split);
|
||||
const params: Record<string, any> = {};
|
||||
const paramNames = Object.keys(paramsTemplate);
|
||||
const mandatoryCount = paramNames.filter((pk) => paramsTemplate[pk].default === undefined).length;
|
||||
|
||||
if (split.length < mandatoryCount) {
|
||||
const missing = paramNames
|
||||
.slice(split.length)
|
||||
.map((p) => `${paramsTemplate[p].hint}${paramsTemplate[p].default !== undefined ? ` (default: ${paramsTemplate[p].default})` : ''}`);
|
||||
return ChatSystem.Error(player, `Missing parameter${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`);
|
||||
}
|
||||
|
||||
const errors: [string, string][] = [];
|
||||
|
||||
paramNames.forEach((paramName, index) => {
|
||||
const meta = paramsTemplate[paramName];
|
||||
const { validator, hint } = meta;
|
||||
const value = split[index];
|
||||
|
||||
if (value === undefined && meta.default !== undefined) {
|
||||
params[paramName] = meta.default;
|
||||
return;
|
||||
}
|
||||
|
||||
if (validator === 'number') {
|
||||
if (!/^[+-]?([0-9]*[.])?[0-9]+$/.test(value)) return void errors.push([hint, 'has to be a number']);
|
||||
params[paramName] = Number(value);
|
||||
} else if (validator === 'integer') {
|
||||
if (!/^[+-]?[0-9]+$/.test(value)) return void errors.push([hint, 'has to be a whole number']);
|
||||
params[paramName] = Number(value);
|
||||
} else if (validator === 'string') {
|
||||
const input = split.slice(index).join(' ');
|
||||
if (meta.length && input.length > meta.length) return void errors.push([hint, `longer than ${meta.length} symbols`]);
|
||||
params[paramName] = input;
|
||||
} else if (validator === 'vehicle') {
|
||||
const vehicle = mp.vehicles.at(Number(value));
|
||||
if (!vehicle) return void errors.push([hint, 'invalid ID']);
|
||||
params[paramName] = vehicle;
|
||||
} else if (validator === 'character') {
|
||||
params[paramName] = PlayerSystem.GetPlayerFromString(value, true, false, false);
|
||||
if (!params[paramName]) return void errors.push([hint, 'invalid character name']);
|
||||
} else if (validator === 'account') {
|
||||
params[paramName] = PlayerSystem.GetPlayerFromString(value, false, true, false);
|
||||
if (!params[paramName]) return void errors.push([hint, 'invalid account name']);
|
||||
} else if (validator === 'player') {
|
||||
params[paramName] = PlayerSystem.GetPlayerFromString(value, true, true, true, false, staffSystem.isOnAdminDuty(player));
|
||||
if (!params[paramName]) return void errors.push([hint, 'invalid character/account name']);
|
||||
} else if (validator === 'company') {
|
||||
if (!/^[0-9]+$/.test(value)) return void errors.push([hint, 'invalid company ID']);
|
||||
params[paramName] = companySystem.getCompanyById(Number(value)) ?? null;
|
||||
if (!params[paramName]) return void errors.push([hint, 'invalid company ID']);
|
||||
} else if (validator === 'faction') {
|
||||
if (!/^[0-9]+$/.test(value)) return void errors.push([hint, 'invalid faction ID']);
|
||||
params[paramName] = factionSystem.getFactionFromId(Number(value)) ?? null;
|
||||
if (!params[paramName]) return void errors.push([hint, 'invalid faction ID']);
|
||||
} else if (validator === 'property') {
|
||||
const prop = propertySystem.getPropertyByNetId(Number(value));
|
||||
if (!prop) return void errors.push([hint, 'invalid property ID']);
|
||||
params[paramName] = prop;
|
||||
} else if (validator === 'word') {
|
||||
if (!value) return void errors.push([hint, 'required']);
|
||||
params[paramName] = value;
|
||||
} else if (Array.isArray(validator)) {
|
||||
if (!validator.includes(value)) return void errors.push([hint, `must be one of: ${validator.join(', ')}`]);
|
||||
params[paramName] = value;
|
||||
} else if (validator instanceof RegExp) {
|
||||
if (!validator.test(value)) return void errors.push([hint, 'invalid format']);
|
||||
params[paramName] = value;
|
||||
} else {
|
||||
params[paramName] = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
return ChatSystem.Error(player, `Invalid: ${errors.map(([h, e]) => `${h} ${e}`).join(', ')}`);
|
||||
}
|
||||
|
||||
await this.runCommand(player, command, params);
|
||||
}
|
||||
|
||||
requestCommandList(player: PlayerWrapper): void {
|
||||
if (!LoginSystem.IsLoggedIn(player)) return;
|
||||
|
||||
let list = this.commandList.filter((cmd) => {
|
||||
if (!cmd.guards) return true;
|
||||
for (const guard of cmd.guards) {
|
||||
if (guard.name !== 'permission' && guard.name !== 'staff') continue;
|
||||
if (guard(player) !== true) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
list = list.map((cmd) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(cmd).filter(
|
||||
([_, v]) => (Array.isArray(v) && v.length > 0 && !(v.length === 1 && v[0] === null)) || (!Array.isArray(v) && v != null),
|
||||
),
|
||||
),
|
||||
) as CommandList;
|
||||
|
||||
LargeDataServer.Send(player, 'commandList', JSON.stringify(list));
|
||||
}
|
||||
|
||||
generateCommandList(): CommandList {
|
||||
this.commandList = Array.from(this.commands.values()).map((cmd) => {
|
||||
const params = cmd.$options.params ?? {};
|
||||
return {
|
||||
name: Array.isArray(cmd.$options.name) ? cmd.$options.name : [cmd.$options.name],
|
||||
params: Object.keys(params).map((pk) => ({
|
||||
hint: params[pk].hint,
|
||||
...(params[pk].default !== undefined ? { default: String(params[pk].default) } : {}),
|
||||
...(params[pk].overrides ? { overrides: params[pk].overrides } : {}),
|
||||
})),
|
||||
guards: cmd.$options.guards,
|
||||
grouped: cmd.$options.grouped,
|
||||
description: cmd.$options.description,
|
||||
};
|
||||
});
|
||||
|
||||
return this.commandList;
|
||||
}
|
||||
}
|
||||
|
||||
export const commandSystem = new CommandSystem();
|
||||
15
src/server/core/commands/options.ts
Normal file
15
src/server/core/commands/options.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { BaseCommand } from './base.command';
|
||||
|
||||
export type CommandOptions<T> = {
|
||||
name: string | string[];
|
||||
params?: CommandParamMetaData<T>;
|
||||
guards?: CommandGuard[];
|
||||
grouped?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export function Command<T>(options: CommandOptions<T>) {
|
||||
return function (constructor: typeof BaseCommand): void {
|
||||
constructor.prototype.$options = options;
|
||||
};
|
||||
}
|
||||
47
src/server/core/database.ts
Normal file
47
src/server/core/database.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Connection, createConnection, ConnectionOptions } from 'typeorm';
|
||||
import { ALL_ENTITIES } from '../entities/index';
|
||||
import { logSystem } from './log';
|
||||
|
||||
function buildConfig(): ConnectionOptions {
|
||||
return {
|
||||
type: 'mysql',
|
||||
host: process.env.DB_HOST || 'play.southwest-roleplay.dev',
|
||||
port: Number(process.env.DB_PORT) || 3306,
|
||||
username: process.env.DB_USER || 'username',
|
||||
password: process.env.DB_PASS || 'password',
|
||||
database: process.env.DB_NAME || 'game',
|
||||
logging: ['error', 'log'],
|
||||
entities: ALL_ENTITIES as unknown as any[],
|
||||
synchronize: false,
|
||||
};
|
||||
}
|
||||
|
||||
export class DatabaseSystem {
|
||||
connection?: Connection | void;
|
||||
attempts = 0;
|
||||
|
||||
async init(): Promise<void> {
|
||||
const config = buildConfig();
|
||||
do {
|
||||
try {
|
||||
if (this.connection) {
|
||||
await this.connection.connect();
|
||||
} else {
|
||||
this.connection = await createConnection(config);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.attempts < 5) {
|
||||
console.error(`[Bootstrap] Database connection unsuccessful, retrying ...`, error);
|
||||
this.attempts++;
|
||||
} else {
|
||||
console.error(`[Bootstrap] Database connection unsuccessful`, error);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
} while (!this.connection);
|
||||
|
||||
logSystem.initLog('Database', 'Connection successful');
|
||||
}
|
||||
}
|
||||
|
||||
export const databaseSystem = new DatabaseSystem();
|
||||
14
src/server/core/feature-system.ts
Normal file
14
src/server/core/feature-system.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { BaseCommand } from "./commands/base.command";
|
||||
import { logSystem } from "./log";
|
||||
|
||||
export abstract class FeatureSystem {
|
||||
public commands: typeof BaseCommand[];
|
||||
|
||||
constructor() { }
|
||||
|
||||
postInit?(): Promise<void>;
|
||||
|
||||
public async init(message?: string): Promise<void> {
|
||||
logSystem.initLog(this.constructor.name, message ?? 'Initialized.');
|
||||
}
|
||||
}
|
||||
85
src/server/core/log/events/admin.log.events.ts
Normal file
85
src/server/core/log/events/admin.log.events.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
export const AdminLogEvents = {
|
||||
spectating: 'spectating',
|
||||
stop_spectating: 'stop_spectating',
|
||||
kick: 'kick',
|
||||
ajail: 'ajail',
|
||||
ban: 'ban',
|
||||
unjail: 'unjail',
|
||||
teleport: 'teleport',
|
||||
watch_pm: 'watch_pm',
|
||||
watch_radio: 'watch_radio',
|
||||
mute: 'mute',
|
||||
get_health: 'get_health',
|
||||
set_health: 'set_health',
|
||||
set_armor: 'set_armor',
|
||||
froze: 'froze',
|
||||
slap: 'slap',
|
||||
respawn_car: 'respawn_car',
|
||||
spawn_car: 'spawn_car',
|
||||
park_car: 'park_car',
|
||||
respawn: 'respawn',
|
||||
setstat: 'setstat',
|
||||
anti_cheat: 'anti_cheat',
|
||||
removed_ad: 'removed_ad',
|
||||
viewed_items: 'viewed_items',
|
||||
decrypt_mask: 'decrypt_mask',
|
||||
give_money: 'give_money',
|
||||
vehicle_livery: 'vehicle_livery',
|
||||
noclip: 'noclip',
|
||||
throw_off: 'throw_off',
|
||||
anote_issued: 'anote_issued',
|
||||
anote_deleted: 'anote_deleted',
|
||||
anote_edited: 'anote_edited',
|
||||
anote_expired: 'anote_expired',
|
||||
report: 'report',
|
||||
set_ptp: 'set_ptp',
|
||||
watch_faction: 'watch_faction',
|
||||
create_company: 'create_company',
|
||||
set_company: 'set_company',
|
||||
revived: 'revived',
|
||||
create_faction: 'create_faction',
|
||||
set_faction: 'set_faction',
|
||||
spawn_item: 'spawn_item',
|
||||
set_license: 'set_license',
|
||||
atm: 'atm',
|
||||
payday: 'payday',
|
||||
portable_radio: 'portable_radio',
|
||||
prison: 'prison',
|
||||
set_property: 'set_property',
|
||||
create_property: 'create_property',
|
||||
create_car: 'create_car',
|
||||
set_car: 'set_car',
|
||||
delete_car: 'delete_car',
|
||||
vehicle: 'vehicle',
|
||||
impound_lot: 'impound_lot',
|
||||
weather: 'weather',
|
||||
set_time: 'set_time',
|
||||
set_weather: 'set_weather',
|
||||
override_weather: 'override_weather',
|
||||
trucking: 'trucking',
|
||||
item_type_groups: 'item_type_groups',
|
||||
property_stock: 'property_stock',
|
||||
mission: 'mission',
|
||||
managejob: 'managejob',
|
||||
soberup: 'soberup',
|
||||
deletescene: 'deletescene',
|
||||
gotoscene: 'gotoscene',
|
||||
expirescene: 'expirescene',
|
||||
trip: 'trip',
|
||||
canceltow: 'cancel_tow',
|
||||
graffiti: 'graffiti',
|
||||
pet: 'pet',
|
||||
loan_defaulted: 'loan_defaulted',
|
||||
set_pet: 'set_pet',
|
||||
goto_pet: 'goto_pet',
|
||||
get_pet: 'get_pet',
|
||||
force_tutorial: 'force_tutorial',
|
||||
updated_tutorial: 'updated_tutorial',
|
||||
vehicle_mods: 'vehicle_mods',
|
||||
ipl: 'ipl',
|
||||
roadblock: 'roadblock',
|
||||
find_prop_inventories: 'find_prop_inventories',
|
||||
radio_station: 'radio_station',
|
||||
} as const;
|
||||
|
||||
export type AdminLogEvent = keyof typeof AdminLogEvents;
|
||||
7
src/server/core/log/events/blackjack.log.events.ts
Normal file
7
src/server/core/log/events/blackjack.log.events.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const BlackjackLogEvents = {
|
||||
game: 'game',
|
||||
tillOnline: 'tillOnline',
|
||||
till: 'till',
|
||||
} as const;
|
||||
|
||||
export type BlackjackLogEvent = keyof typeof BlackjackLogEvents;
|
||||
12
src/server/core/log/events/chat.log.events.ts
Normal file
12
src/server/core/log/events/chat.log.events.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const ChatLogEvents = {
|
||||
ic: 'ic',
|
||||
ic_quiet: 'ic_low',
|
||||
ic_loud: 'ic_loud',
|
||||
ic_whisper: 'ic_whisper',
|
||||
ooc: 'ooc',
|
||||
ooc_aooc: 'ooc_aooc',
|
||||
pm: 'pm',
|
||||
vicinity: 'vicinity',
|
||||
} as const;
|
||||
|
||||
export type ChatLogEvent = keyof typeof ChatLogEvents;
|
||||
19
src/server/core/log/events/company.log.events.ts
Normal file
19
src/server/core/log/events/company.log.events.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export const CompanyLogEvents = {
|
||||
created: 'created',
|
||||
primary_property_change: 'primary_property_change',
|
||||
paycheck_reward: 'paycheck_reward',
|
||||
position_created: 'position_created',
|
||||
position_modified: 'position_modified',
|
||||
position_deleted: 'position_deleted',
|
||||
pos_payment: 'pos_payment',
|
||||
type_changed: 'type_changed',
|
||||
protect_changed: 'protect_changed',
|
||||
employee_added: 'employee_added',
|
||||
employee_fired: 'employee_fired',
|
||||
respawn_car: 'respawn_car',
|
||||
tow_cars: 'tow_cars',
|
||||
set_company: 'set_company',
|
||||
vehicle: 'vehicle',
|
||||
} as const;
|
||||
|
||||
export type CompanyLogEvent = keyof typeof CompanyLogEvents;
|
||||
16
src/server/core/log/events/corpse.log.event.ts
Normal file
16
src/server/core/log/events/corpse.log.event.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const CorpseLogEvents = {
|
||||
created: 'created',
|
||||
spawned: 'spawned',
|
||||
despawned: 'despawned',
|
||||
moved_veh: 'moved_veh',
|
||||
moved_morgue: 'moved_morgue',
|
||||
drag: 'drag',
|
||||
stop_drag: 'stop_drag',
|
||||
request_id: 'request_id',
|
||||
identification: 'identification',
|
||||
buried: 'buried',
|
||||
exhumed: 'exhumed',
|
||||
|
||||
} as const;
|
||||
|
||||
export type CorpseLogEvent = keyof typeof CorpseLogEvents;
|
||||
7
src/server/core/log/events/darknet.log.events.ts
Normal file
7
src/server/core/log/events/darknet.log.events.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const DarkNetLogEvents = {
|
||||
order_placed: 'order_placed',
|
||||
delivery_failed: 'delivery_failed',
|
||||
delivered: 'delivered',
|
||||
} as const;
|
||||
|
||||
export type DarkNetLogEvent = keyof typeof DarkNetLogEvents;
|
||||
8
src/server/core/log/events/dmv.log.events.ts
Normal file
8
src/server/core/log/events/dmv.log.events.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const DMVLogEvent = {
|
||||
testing: 'testing',
|
||||
passed: 'passed',
|
||||
failed: 'failed',
|
||||
renewed: 'renewed',
|
||||
} as const;
|
||||
|
||||
export type DMVLogEvent = keyof typeof DMVLogEvent;
|
||||
5
src/server/core/log/events/door.log.events.ts
Normal file
5
src/server/core/log/events/door.log.events.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const DoorLogEvents = {
|
||||
lock_status: 'lock_status',
|
||||
} as const;
|
||||
|
||||
export type DoorLogEvent = keyof typeof DoorLogEvents;
|
||||
27
src/server/core/log/events/faction.log.events.ts
Normal file
27
src/server/core/log/events/faction.log.events.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export const FactionLogEvents = {
|
||||
created: 'created',
|
||||
name_change: 'name_change',
|
||||
abbrev_change: 'abbrev_change',
|
||||
type_change: 'type_change',
|
||||
faction_property: 'faction_property',
|
||||
chat_color_change: 'chat_color_change',
|
||||
primary_property_change: 'primary_property_change',
|
||||
paycheck_reward: 'paycheck_reward',
|
||||
permit_revoke: 'permit_revoke',
|
||||
permit_issue: 'permit_issue',
|
||||
rank_created: 'rank_created',
|
||||
rank_modified: 'rank_modified',
|
||||
rank_deleted: 'rank_deleted',
|
||||
mdc: 'mdc',
|
||||
faction_invite: 'faction_invite',
|
||||
set_faction: 'set_faction',
|
||||
player_ranks: 'player_ranks',
|
||||
faction_uninvite: 'faction_uninvite',
|
||||
prison: 'prison',
|
||||
respawn_car: 'respawn_car',
|
||||
tow_cars: 'tow_cars',
|
||||
pet: 'pet',
|
||||
cut_power: 'cut_power',
|
||||
} as const;
|
||||
|
||||
export type FactionLogEvent = keyof typeof FactionLogEvents;
|
||||
16
src/server/core/log/events/furniture.log.events.ts
Normal file
16
src/server/core/log/events/furniture.log.events.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const FurnitureLogEvents = {
|
||||
created: 'created',
|
||||
modified: 'modified',
|
||||
duplicate: 'duplicate',
|
||||
sold: 'sold',
|
||||
created_inventory: 'created_inventory',
|
||||
tagged: 'tagged',
|
||||
entered_object_editor: 'entered_object_editor',
|
||||
named: 'named',
|
||||
purged_furniture: 'purged_furniture',
|
||||
replaced: 'replaced',
|
||||
lock_status: 'lock_status',
|
||||
created_tv: 'created_tv',
|
||||
} as const;
|
||||
|
||||
export type FurnitureLogEvent = keyof typeof FurnitureLogEvents;
|
||||
8
src/server/core/log/events/graffiti.log.events.ts
Normal file
8
src/server/core/log/events/graffiti.log.events.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const GraffitiLogEvents = {
|
||||
destroyed: 'destroyed',
|
||||
washed: 'washed',
|
||||
sprayed: 'sprayed',
|
||||
created: 'created',
|
||||
} as const;
|
||||
|
||||
export type GraffitiLogEvent = keyof typeof GraffitiLogEvents;
|
||||
18
src/server/core/log/events/item.log.events.ts
Normal file
18
src/server/core/log/events/item.log.events.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const ItemLogEvents = {
|
||||
created: 'created',
|
||||
transaction: 'transaction',
|
||||
dropped: 'dropped',
|
||||
world_place: 'world_place',
|
||||
world_pickup: 'world_pickup',
|
||||
world_moved: 'world_moved',
|
||||
vehicle_world_place: 'vehicle_world_place',
|
||||
vehicle_world_pickup: 'vehicle_world_pickup',
|
||||
vehicle_world_moved: 'vehicle_world_moved',
|
||||
data: 'data',
|
||||
deleted: 'deleted',
|
||||
confiscated: 'confiscate',
|
||||
rename: 'rename',
|
||||
merged: 'merged',
|
||||
} as const;
|
||||
|
||||
export type ItemLogEvent = keyof typeof ItemLogEvents;
|
||||
8
src/server/core/log/events/law.log.events.ts
Normal file
8
src/server/core/log/events/law.log.events.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export const LawLogEvents = {
|
||||
jail: 'jail',
|
||||
arsenal: 'arsenal',
|
||||
loadgun: 'loadgun',
|
||||
takegun: 'takegun',
|
||||
};
|
||||
|
||||
export type LawLogEvent = keyof typeof LawLogEvents;
|
||||
7
src/server/core/log/events/marker.log.events.ts
Normal file
7
src/server/core/log/events/marker.log.events.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const MarkerLogEvents = {
|
||||
expired: 'expired',
|
||||
created: 'created',
|
||||
permanent: 'permanent',
|
||||
} as const;
|
||||
|
||||
export type MarkerLogEvent = keyof typeof MarkerLogEvents;
|
||||
18
src/server/core/log/events/mdc.log.events.ts
Normal file
18
src/server/core/log/events/mdc.log.events.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export const MDCLogEvents = {
|
||||
opened: 'opened',
|
||||
searched: 'searched',
|
||||
trace: 'trace',
|
||||
executable: 'executable',
|
||||
prescription: 'prescription',
|
||||
fine: 'fine',
|
||||
mugshot: 'mugshot',
|
||||
notice: 'notice',
|
||||
medical: 'medical',
|
||||
driver: 'driver',
|
||||
weapon: 'weapon',
|
||||
permit_issue: 'permit_issue',
|
||||
permit_revoke: 'permit_revoke',
|
||||
corpse_id: 'corpse_id',
|
||||
} as const;
|
||||
|
||||
export type MDCLogEvent = keyof typeof MDCLogEvents;
|
||||
13
src/server/core/log/events/pet.log.events.ts
Normal file
13
src/server/core/log/events/pet.log.events.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const PetLogEvents = {
|
||||
created: 'created',
|
||||
removed: 'removed',
|
||||
spawned: 'spawned',
|
||||
despawned: 'despawned',
|
||||
ordered: 'order',
|
||||
name_change: 'name_change',
|
||||
type_change: 'type_change',
|
||||
faction_change: 'faction_change',
|
||||
character_change: 'character_change',
|
||||
};
|
||||
|
||||
export type PetLogEvent = keyof typeof PetLogEvents;
|
||||
98
src/server/core/log/events/player.log.events.ts
Normal file
98
src/server/core/log/events/player.log.events.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
export const PlayerLogEvents = {
|
||||
join: 'join',
|
||||
login: 'login',
|
||||
disconnect: 'disconnect',
|
||||
login_fail: 'login_fail',
|
||||
login_groups: 'login_groups',
|
||||
character_loaded: 'character_loaded',
|
||||
phone_created: 'phone_created',
|
||||
character_create: 'character_create',
|
||||
jailed: 'jailed',
|
||||
kick: 'kick',
|
||||
ajail: 'ajail',
|
||||
muted: 'muted',
|
||||
banned: 'banned',
|
||||
towcars: 'towcars',
|
||||
anti_cheat: 'anti_cheat',
|
||||
transaction_flag: 'transaction_flagged',
|
||||
showing_atp_alerts: 'showing_atp_alerts',
|
||||
read_atp_alerts: 'read_atp_alerts',
|
||||
entered_vehicle: 'entered_vehicle',
|
||||
exited_vehicle: 'exited_vehicle',
|
||||
vehicle_purchase: 'vehicle_purchase',
|
||||
vehicle_modshop: 'vehicle_modshop',
|
||||
canceled_vehicle_purchase: 'canceled_vehicle_purchase',
|
||||
freeze: 'freeze',
|
||||
thawed: 'thawed',
|
||||
unjailed: 'unjailed',
|
||||
report: 'report',
|
||||
anotes_expired: 'anotes_expired',
|
||||
afk: 'afk',
|
||||
animation: 'animation',
|
||||
barber_shop: 'barber_shop',
|
||||
ads: 'ads',
|
||||
employment: 'employment',
|
||||
damage: 'damage',
|
||||
carrying: 'carrying',
|
||||
helpup: 'helpup',
|
||||
weapons: 'weapons',
|
||||
darknet: 'darknet',
|
||||
dmv: 'dmv',
|
||||
door: 'door',
|
||||
duty: 'duty',
|
||||
menu: 'menu',
|
||||
property: 'property',
|
||||
company: 'company',
|
||||
faction: 'faction',
|
||||
vehicle: 'vehicle',
|
||||
mdc: 'mdc',
|
||||
medic: 'medic',
|
||||
restraints: 'restraints',
|
||||
fishing: 'fishing',
|
||||
gambling: 'gambling',
|
||||
object: 'object',
|
||||
inventory: 'inventory',
|
||||
money: 'money',
|
||||
offer: 'offer',
|
||||
permissions: 'permissions',
|
||||
phone: 'phone',
|
||||
level: 'level',
|
||||
spawned: 'spawned',
|
||||
frisk: 'frisk',
|
||||
mask: 'mask',
|
||||
portable_radio: 'portable_radio',
|
||||
prison: 'prison',
|
||||
xm_radio: 'xm_radio',
|
||||
store: 'store',
|
||||
fine: 'fine',
|
||||
left_company: 'left_company',
|
||||
mission: 'mission',
|
||||
job: 'job',
|
||||
invalid_input: 'invalid_input',
|
||||
confiscate: 'confiscate',
|
||||
reset_position: 'reset_position',
|
||||
rent: 'rent',
|
||||
manually_unfroze: 'manually_unfroze',
|
||||
phone_alert: 'phone_alert',
|
||||
graffiti: 'graffiti',
|
||||
camera: 'camera',
|
||||
outfit: 'outfit',
|
||||
clean_cash: 'clean_cash',
|
||||
pawn: 'pawn',
|
||||
pet: 'pet',
|
||||
loan: 'loan',
|
||||
transfer_property: 'transfer_property',
|
||||
expression: 'expression',
|
||||
cinematic: 'cinematic',
|
||||
coroner: 'coroner',
|
||||
scratch_weapon: 'scratch_weapon',
|
||||
convert_appearance: 'convert_appearance',
|
||||
force_tutorial: 'force_tutorial',
|
||||
crawling: 'crawling',
|
||||
corpse: 'corpse',
|
||||
uncaughtException: 'uncaughtException',
|
||||
share: 'share',
|
||||
surgery: 'surgery',
|
||||
} as const;
|
||||
|
||||
export type PlayerLogEvent = keyof typeof PlayerLogEvents;
|
||||
57
src/server/core/log/events/property.log.events.ts
Normal file
57
src/server/core/log/events/property.log.events.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
export const PropertyLogEvents = {
|
||||
created: 'created',
|
||||
renamed: 'renamed',
|
||||
exterior_change: 'exterior_change',
|
||||
interior_change: 'interior_change',
|
||||
type_change: 'type_change',
|
||||
parent_change: 'parent_change',
|
||||
faction_change: 'faction_change',
|
||||
company_change: 'company_change',
|
||||
character_change: 'character_change',
|
||||
furniture_slots: 'furniture_slots',
|
||||
furniture_loading: 'furniture_loading',
|
||||
furniture_unloading: 'furniture_unloading',
|
||||
security_loading: 'security_loading',
|
||||
security_unloading: 'security_unloading',
|
||||
settings_change: 'settings_change',
|
||||
permit_revoke: 'permit_revoke',
|
||||
permit_issue: 'permit_issue',
|
||||
access_key: 'access_key',
|
||||
lease: 'lease',
|
||||
player_entered: 'player_entered',
|
||||
player_exited: 'plaeyr_exited',
|
||||
locked: 'locked',
|
||||
rent: 'rent',
|
||||
interior_props: 'interior_props',
|
||||
interior_ipls: 'interior_ipls',
|
||||
areas: 'areas',
|
||||
address: 'address',
|
||||
master_change: 'master_change',
|
||||
name_change: 'name_change',
|
||||
xm_radio: 'xm_radio',
|
||||
local_time: 'local_time',
|
||||
local_weather: 'local_weather',
|
||||
blip: 'blip',
|
||||
reload: 'reload',
|
||||
stock: 'stock',
|
||||
shop: 'shop',
|
||||
freeze: 'freeze',
|
||||
enter_reward: 'enter_reward',
|
||||
visitor_cap: 'visitor_cap',
|
||||
collection: 'collection',
|
||||
eviction: 'eviction',
|
||||
hospital_price: 'hospital_price',
|
||||
eviction_inactive: 'eviction_inactive',
|
||||
clean_cash: 'clean_cash',
|
||||
pawn: 'pawn',
|
||||
purchased: 'purchased',
|
||||
transfer: 'transfer',
|
||||
lockpick: 'lockpick',
|
||||
security_subscribe: 'security_subscribe',
|
||||
mission: 'mission',
|
||||
power: 'power',
|
||||
metal_detector: 'metal_detector',
|
||||
elevator: 'elevator',
|
||||
};
|
||||
|
||||
export type PropertyLogEvent = keyof typeof PropertyLogEvents;
|
||||
9
src/server/core/log/events/ptp.log.events.ts
Normal file
9
src/server/core/log/events/ptp.log.events.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export const PointToPointLogEvents = {
|
||||
created: 'created',
|
||||
entered: 'entered',
|
||||
exited: 'exited',
|
||||
set_ptp: 'set_ptp',
|
||||
lock: 'lock',
|
||||
} as const;
|
||||
|
||||
export type PointToPointLogEvent = keyof typeof PointToPointLogEvents;
|
||||
7
src/server/core/log/events/roulette.log.event.ts
Normal file
7
src/server/core/log/events/roulette.log.event.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const RouletteLogEvents = {
|
||||
game: 'game',
|
||||
tillOnline: 'tillOnline',
|
||||
till: 'till',
|
||||
} as const;
|
||||
|
||||
export type RouletteLogEvent = keyof typeof RouletteLogEvents;
|
||||
65
src/server/core/log/events/vehicle.log.events.ts
Normal file
65
src/server/core/log/events/vehicle.log.events.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export const VehicleLogEvents = {
|
||||
created: 'created',
|
||||
deleted: 'deleted',
|
||||
spawned: 'spawned',
|
||||
respawned: 'respawned',
|
||||
engine: 'engine',
|
||||
parked: 'parked',
|
||||
rent: 'rent',
|
||||
rent_extend: 'rent_extend',
|
||||
rent_stop: 'rent_stop',
|
||||
plate: 'plate',
|
||||
registration: 'registration',
|
||||
player_entered: 'player_entered',
|
||||
player_exited: 'player_exited',
|
||||
canceled_dealership: 'canceled_dealership',
|
||||
successful_dealership: 'successful_dealership',
|
||||
livery: 'livery',
|
||||
slap: 'slap',
|
||||
ejected: 'ejected',
|
||||
weapons: 'weapons',
|
||||
access_key: 'access_key',
|
||||
helo: 'helo',
|
||||
mechanic: 'mechanic',
|
||||
xm_radio: 'xm_radio',
|
||||
fine: 'fine',
|
||||
extras: 'extras',
|
||||
fuel: 'fuel',
|
||||
flipped: 'flipped',
|
||||
keyless: 'keyless',
|
||||
repair: 'repair',
|
||||
color_change: 'color_change',
|
||||
character_change: 'character_change',
|
||||
faction_change: 'faction_change',
|
||||
company_change: 'company_change',
|
||||
spawn_change: 'spawn_change',
|
||||
wheel_change: 'wheel_change',
|
||||
tint_change: 'tint_change',
|
||||
suspension_change: 'suspension_change',
|
||||
dirt_change: 'dirt_change',
|
||||
manual_change: 'manual_change',
|
||||
protected_change: 'protected_change',
|
||||
engine_change: 'engine_change',
|
||||
model_change: 'model_change',
|
||||
upgrades: 'upgrades',
|
||||
raw: 'raw',
|
||||
transfer_owner: 'transfer_owner',
|
||||
trunk: 'trunk',
|
||||
hood: 'hood',
|
||||
wash: 'wash',
|
||||
impound: 'impound',
|
||||
tow: 'tow',
|
||||
personaltow: 'personaltow',
|
||||
lock: 'lock',
|
||||
alarm: 'alarm',
|
||||
stolen: 'stolen',
|
||||
modshop: 'modshop',
|
||||
pet_entered: 'pet_entered',
|
||||
pet_exited: 'pet_exited',
|
||||
pet_searched: 'pet_searched',
|
||||
insurance: 'insurance',
|
||||
mods_change: 'mods_change',
|
||||
mods_copy: 'mods_copy',
|
||||
} as const;
|
||||
|
||||
export type VehicleLogEvent = keyof typeof VehicleLogEvents;
|
||||
7
src/server/core/log/events/whitelist.log.events.ts
Normal file
7
src/server/core/log/events/whitelist.log.events.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const WhitelistLogEvents = {
|
||||
rejected: 'rejected',
|
||||
removed: 'removed',
|
||||
added: 'added',
|
||||
};
|
||||
|
||||
export type WhitelistLogEvent = keyof typeof WhitelistLogEvents;
|
||||
744
src/server/core/log/index.ts
Normal file
744
src/server/core/log/index.ts
Normal file
@@ -0,0 +1,744 @@
|
||||
import * as colors from 'colors';
|
||||
import { mongoSystem } from '../mongo';
|
||||
import { PlayerLogEvent, PlayerLogEvents } from './events/player.log.events';
|
||||
import { ChatLogEvent, ChatLogEvents } from './events/chat.log.events';
|
||||
import { PropertyEntity } from '../../entities/property.entity';
|
||||
import { PropertyLogEvent, PropertyLogEvents } from './events/property.log.events';
|
||||
import { FactionEntity } from '../../entities/faction.entity';
|
||||
import { FactionLogEvent, FactionLogEvents } from './events/faction.log.events';
|
||||
import { WhitelistLogEvent } from './events/whitelist.log.events';
|
||||
import { AdminLogEvent } from './events/admin.log.events';
|
||||
import { ItemEntity } from '../../entities/item.entity';
|
||||
import { ItemLogEvent } from './events/item.log.events';
|
||||
import { JSONStringifyRecursive } from '../../features/utility/JSONStringifyNoRecursive';
|
||||
import { LawLogEvent } from './events/law.log.events';
|
||||
import { VehicleLogEvent } from './events/vehicle.log.events';
|
||||
import { VehicleSystem } from '../../features/vehicle';
|
||||
import dayjs from 'dayjs';
|
||||
import { CompanyEntity } from '../../entities/company.entity';
|
||||
import { CompanyLogEvent } from './events/company.log.events';
|
||||
import { VehicleEntity } from '../../entities/vehicle.entity';
|
||||
import { ObjectLiteral } from 'typeorm';
|
||||
import { DarkNetLogEvent } from './events/darknet.log.events';
|
||||
import { DoorEntity } from '../../entities/door.entity';
|
||||
import { DoorLogEvent } from './events/door.log.events';
|
||||
import { PointToPointEntity } from '../../entities/point-to-point.entity';
|
||||
import { PointToPointLogEvent } from './events/ptp.log.events';
|
||||
import { FurnitureLogEvent } from './events/furniture.log.events';
|
||||
import { FurnitureItemEntity } from '../../entities/furniture-item.entity';
|
||||
import { mdcSystem } from '../../features/mdc';
|
||||
import { MDCLogEvent } from './events/mdc.log.events';
|
||||
import { DarknetOrderEntity } from '../../entities/darknet-order.entity';
|
||||
import { GraffitiLogEvent } from './events/graffiti.log.events';
|
||||
import { GraffitiEntity } from '../../entities/graffiti.entity';
|
||||
import { PetLogEvent } from './events/pet.log.events';
|
||||
import { PetEntity } from '../../entities/pet.entity';
|
||||
import { BlackjackLogEvent } from './events/blackjack.log.events';
|
||||
import { CorpseLogEvent } from './events/corpse.log.event';
|
||||
import { MarkerLogEvent } from './events/marker.log.events';
|
||||
import { InfoMarkerEntity } from '../../entities/info-markers.entity';
|
||||
import { RouletteLogEvent } from './events/roulette.log.event';
|
||||
|
||||
colors.enable();
|
||||
|
||||
interface LogCollection {
|
||||
name: string;
|
||||
permission: string;
|
||||
hasEvents?: boolean;
|
||||
fields: {
|
||||
name: string;
|
||||
type: string;
|
||||
trace?: string;
|
||||
default?: boolean;
|
||||
required?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export type DarknetOrderData = {
|
||||
id: number;
|
||||
name: string;
|
||||
faction: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type DarknetItems = {
|
||||
type: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
price: number;
|
||||
oocPrice: number;
|
||||
amount: number;
|
||||
unit: string;
|
||||
}[];
|
||||
|
||||
export class LogSystem {
|
||||
lastInitTime: number;
|
||||
firstInitTime: number;
|
||||
|
||||
static Collections: LogCollection[] = [
|
||||
{
|
||||
name: 'players',
|
||||
permission: 'LOG_VIEW_PLAYERS',
|
||||
hasEvents: true,
|
||||
fields: [
|
||||
{ name: 'player', type: 'player', default: true, required: true },
|
||||
{ name: 'ip', trace: 'player.ip', type: 'string' },
|
||||
{ name: 'log', type: 'string', default: true },
|
||||
{ name: 'target', type: 'player' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
init(): void {
|
||||
this.lastInitTime = +new Date();
|
||||
this.firstInitTime = +new Date();
|
||||
}
|
||||
|
||||
initLog(feature: string, content: string): void {
|
||||
const now = +new Date();
|
||||
const time = ((now - this.lastInitTime) / 1000).toFixed(1).padStart(4, ' ') + 's';
|
||||
const totalTime = ((now - this.firstInitTime) / 1000).toFixed(1).padEnd(4, ' ') + 's';
|
||||
LogSystem.Collect(`${(totalTime + ' | ' + time).grey} [init:${feature}]`.green + `: ${content.yellow}`);
|
||||
this.lastInitTime = now;
|
||||
}
|
||||
|
||||
static Log(feature: string, content: string): void {
|
||||
LogSystem.Collect(`[${feature}]`.green + `: ${content.reset}`);
|
||||
// LogSystem.Collect(`LogSystem.Log is for debugging only, all relevant data needs to be stored using event functions`.red);
|
||||
}
|
||||
|
||||
static ComposePlayerObject(player?: PlayerMp, options?: { state?: boolean }): Record<string, any> | undefined {
|
||||
if (!player) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const playerObject: {
|
||||
gameId: number;
|
||||
ip: string;
|
||||
sessionId?: number;
|
||||
accountId?: number;
|
||||
accountName?: string;
|
||||
characterId?: number;
|
||||
characterName?: string;
|
||||
state?: PlayerState;
|
||||
} = {
|
||||
gameId: player.id,
|
||||
ip: player.ip,
|
||||
};
|
||||
|
||||
if (player.session) {
|
||||
playerObject.sessionId = player.session.id;
|
||||
}
|
||||
|
||||
if (player.account) {
|
||||
playerObject.accountId = player.account.id;
|
||||
playerObject.accountName = player.account.name;
|
||||
}
|
||||
|
||||
if (player.character) {
|
||||
playerObject.characterId = player.character.id;
|
||||
playerObject.characterName = player.character.name;
|
||||
}
|
||||
|
||||
if (options?.state) {
|
||||
playerObject.state = player.state;
|
||||
}
|
||||
|
||||
return JSON.parse(JSONStringifyRecursive(playerObject));
|
||||
}
|
||||
|
||||
static ComposeGraffitiEntityObject(graffiti: GraffitiEntity): Record<string, any> | undefined {
|
||||
const graffitiObject: {
|
||||
id: number;
|
||||
content: string;
|
||||
font: string;
|
||||
colour: string;
|
||||
opacity: number;
|
||||
destroyed: Date;
|
||||
creator_id: number;
|
||||
} = {
|
||||
id: graffiti.id,
|
||||
content: graffiti.content,
|
||||
font: graffiti.font,
|
||||
colour: graffiti.colour,
|
||||
opacity: graffiti.opacity,
|
||||
destroyed: graffiti.destroyed,
|
||||
creator_id: graffiti.creator_id,
|
||||
};
|
||||
|
||||
return JSON.parse(JSONStringifyRecursive(graffitiObject));
|
||||
}
|
||||
|
||||
static ComposeDoorEntityObject(door: DoorEntity): Record<string, any> | undefined {
|
||||
const doorObject: {
|
||||
id: number;
|
||||
name: string;
|
||||
permission: string;
|
||||
lockType: string;
|
||||
property: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
} = {
|
||||
id: door.id,
|
||||
name: door.name,
|
||||
permission: door.permission_name || 'null',
|
||||
lockType: door.lock_type,
|
||||
property: door.property
|
||||
? {
|
||||
id: door.property.id,
|
||||
name: door.property.getName(),
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
return JSON.parse(JSONStringifyRecursive(doorObject));
|
||||
}
|
||||
|
||||
static ComposePTPEntityObject(ptp: PointToPointEntity): Record<string, any> | undefined {
|
||||
const ptpObject: {
|
||||
id: number;
|
||||
name: string;
|
||||
permission: string | null;
|
||||
lockStatus: boolean;
|
||||
} = {
|
||||
id: ptp.id,
|
||||
name: ptp.name,
|
||||
permission: ptp.permission,
|
||||
lockStatus: ptp.locked,
|
||||
};
|
||||
|
||||
return JSON.parse(JSONStringifyRecursive(ptpObject));
|
||||
}
|
||||
|
||||
static ComposePlayerPosition(player: PlayerMp): { x: number; y: number; z: number; dimension: number } {
|
||||
return {
|
||||
x: Math.round(player.position.x * 1000) / 1000,
|
||||
y: Math.round(player.position.y * 1000) / 1000,
|
||||
z: Math.round(player.position.z * 1000) / 1000,
|
||||
dimension: player.dimension,
|
||||
};
|
||||
}
|
||||
|
||||
static ComposePlayerName(player: PlayerMp): string {
|
||||
if (!mp.players.exists(player)) {
|
||||
return 'Unknown Player';
|
||||
}
|
||||
let name = player.name;
|
||||
if (player.account) {
|
||||
name = player.account.name;
|
||||
if (player.character) {
|
||||
name = `${name} - ${player.character.name}`;
|
||||
}
|
||||
}
|
||||
name = `${name} [${player.id}]`;
|
||||
return name;
|
||||
}
|
||||
|
||||
static ComposeFurnitureItemObject(item: FurnitureItemEntity): Record<string, any> {
|
||||
const itemObject = {
|
||||
id: item.id,
|
||||
created: mdcSystem.formatDate(item.created),
|
||||
creator: item.created_by_character,
|
||||
price: item.price,
|
||||
name: item.name,
|
||||
tag: item.tag,
|
||||
inventory:
|
||||
item.has_inventory && item.inventory
|
||||
? {
|
||||
inventory_id: item.inventory.id,
|
||||
items: item.inventory.items.map((item) => {
|
||||
return {
|
||||
id: item.id,
|
||||
type: item.type.name,
|
||||
};
|
||||
}),
|
||||
}
|
||||
: false,
|
||||
prison_cell: item.prison_cell_id,
|
||||
property_id: item.property_id,
|
||||
position: {
|
||||
x: item.x,
|
||||
y: item.y,
|
||||
z: item.z,
|
||||
rx: item.rx,
|
||||
ry: item.ry,
|
||||
rz: item.rz,
|
||||
},
|
||||
};
|
||||
|
||||
return JSON.parse(JSONStringifyRecursive(itemObject));
|
||||
}
|
||||
|
||||
static ComposeVehicleObject(vehicle?: VehicleMp): Record<string, any> {
|
||||
if (!vehicle) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const speed = Math.floor(VehicleSystem.GetVehicleMph(vehicle));
|
||||
const vehicleObject: VehicleLogComposition = {
|
||||
gameId: vehicle.id,
|
||||
model: VehicleSystem.GetVehicleNameFromHash(vehicle.model) || String(vehicle.model),
|
||||
plate: vehicle.numberPlate,
|
||||
bodyHealth: vehicle.bodyHealth,
|
||||
engineHealth: vehicle.engineHealth,
|
||||
occupants: LogSystem.ComposeVehicleOccupants(vehicle),
|
||||
speedInMph: speed,
|
||||
};
|
||||
|
||||
if (vehicle.vehicleEntity) {
|
||||
vehicleObject.id = vehicle.vehicleEntity.id;
|
||||
if (vehicle.vehicleEntity.owner) {
|
||||
vehicleObject.owner = vehicle.vehicleEntity.owner;
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.parse(JSONStringifyRecursive(vehicleObject));
|
||||
}
|
||||
|
||||
static ComposeVehiclePosition(vehicle: VehicleMp): { x: number; y: number; z: number; heading: number; dimension: number } {
|
||||
return {
|
||||
x: Math.round(vehicle.position.x * 1000) / 1000,
|
||||
y: Math.round(vehicle.position.z * 1000) / 1000,
|
||||
z: Math.round(vehicle.position.z * 1000) / 1000,
|
||||
heading: Math.round(vehicle.heading),
|
||||
dimension: vehicle.dimension,
|
||||
};
|
||||
}
|
||||
|
||||
static ComposeVehicleOccupants(vehicle: VehicleMp): any {
|
||||
const occupants: {
|
||||
seatId: number;
|
||||
player: any;
|
||||
}[] = [];
|
||||
|
||||
vehicle.getVehicleOccupants().forEach((player) => {
|
||||
const composed = LogSystem.ComposePlayerObject(player);
|
||||
occupants.push({
|
||||
seatId: player.seat,
|
||||
player: composed,
|
||||
});
|
||||
});
|
||||
|
||||
return JSON.parse(JSONStringifyRecursive(occupants));
|
||||
}
|
||||
|
||||
static ComposeVehicleName(vehicle: VehicleMp | VehicleEntity): string {
|
||||
let name = VehicleSystem.GetVehicleName(vehicle);
|
||||
if (!(vehicle instanceof VehicleEntity) && vehicle.vehicleEntity) {
|
||||
name += ` (#${vehicle.vehicleEntity.id}`;
|
||||
if (vehicle.vehicleEntity.owner) {
|
||||
name += `, ${vehicle.vehicleEntity.owner})`;
|
||||
} else {
|
||||
name += ')';
|
||||
}
|
||||
|
||||
name += ` [${vehicle.id}]`;
|
||||
} else if (vehicle instanceof VehicleEntity) {
|
||||
name += ` (Entity #${vehicle.id})`;
|
||||
} else {
|
||||
name += ` [${vehicle.id}]`;
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
static async PlayerEvent(player: PlayerMp, event: PlayerLogEvent, log: string, data?: ObjectLiteral): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'players', {
|
||||
date: new Date(),
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
event,
|
||||
log,
|
||||
position: LogSystem.ComposePlayerPosition(player),
|
||||
data: data !== undefined ? JSON.parse(JSONStringifyRecursive(data)) : undefined,
|
||||
});
|
||||
|
||||
LogSystem.Collect(`[Player:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`);
|
||||
}
|
||||
|
||||
static async GraffitiEvent(graffiti: GraffitiEntity, event: GraffitiLogEvent, log: string, data?: ObjectLiteral): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'graffiti', {
|
||||
date: new Date(),
|
||||
graffiti: graffiti ? LogSystem.ComposeGraffitiEntityObject(graffiti) : undefined,
|
||||
event,
|
||||
log,
|
||||
data: data !== undefined ? JSON.parse(JSONStringifyRecursive(data)) : undefined,
|
||||
});
|
||||
|
||||
LogSystem.Collect(`[Graffiti:${event}]`.green + ` ${log.reset}`);
|
||||
}
|
||||
|
||||
static async VehicleEvent(vehicle: VehicleMp | VehicleEntity, event: VehicleLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'vehicles', {
|
||||
date: new Date(),
|
||||
vehicle: vehicle instanceof VehicleEntity ? { id: vehicle.id } : LogSystem.ComposeVehicleObject(vehicle),
|
||||
event,
|
||||
log,
|
||||
position: vehicle instanceof VehicleEntity ? null : LogSystem.ComposeVehiclePosition(vehicle),
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
});
|
||||
|
||||
LogSystem.Collect(`[Vehicle:${event}]`.green + `: ${LogSystem.ComposeVehicleName(vehicle).yellow} ${log.reset}`);
|
||||
}
|
||||
|
||||
static async ChatEvent(player: PlayerMp, event: ChatLogEvent, log: string, target?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'chat', {
|
||||
date: new Date(),
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
event,
|
||||
log,
|
||||
position: LogSystem.ComposePlayerPosition(player),
|
||||
target: LogSystem.ComposePlayerObject(target),
|
||||
});
|
||||
|
||||
LogSystem.Collect(`[Chat:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`);
|
||||
}
|
||||
|
||||
static async BlackjackEvent(event: BlackjackLogEvent, tableId: number, roundId: string, log: string, player?: PlayerMp) {
|
||||
await mongoSystem.insertOne('logs', 'blackjack', {
|
||||
date: new Date(),
|
||||
tableId,
|
||||
roundId,
|
||||
log,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
|
||||
});
|
||||
|
||||
LogSystem.Collect(`[Blackjack:${event}]`.green + `: ${tableId}/${roundId} ${log.reset}`);
|
||||
}
|
||||
|
||||
static async RouletteEvent(event: RouletteLogEvent, tableId: number, gameId: string, log: string, player?: PlayerMp) {
|
||||
await mongoSystem.insertOne('logs', 'roulette', {
|
||||
date: new Date(),
|
||||
tableId,
|
||||
gameId,
|
||||
log,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
|
||||
});
|
||||
|
||||
LogSystem.Collect(`[Roulette:${event}]`.green + `: ${tableId}/${gameId} ${log.reset}`);
|
||||
}
|
||||
|
||||
static async CorpseEvent(event: CorpseLogEvent, corpseId: number, log: string, player?: PlayerMp) {
|
||||
await mongoSystem.insertOne('logs', 'corpses', {
|
||||
date: new Date(),
|
||||
corpseId,
|
||||
log,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
|
||||
});
|
||||
|
||||
LogSystem.Collect(`[Corpse:${event}]`.green + `: ${corpseId} ${log.reset}`);
|
||||
}
|
||||
|
||||
static async MarkerEvent(event: MarkerLogEvent, marker: InfoMarkerEntity, log: string, player?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'marker', {
|
||||
date: new Date(),
|
||||
marker,
|
||||
log,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
|
||||
});
|
||||
|
||||
LogSystem.Collect(`[Marker:${event}]`.green + `: ${marker.id} ${log.reset}`);
|
||||
}
|
||||
|
||||
static async ItemEvent(item: ItemEntity, event: ItemLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'items', {
|
||||
date: new Date(),
|
||||
item,
|
||||
event,
|
||||
log,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
position: player ? LogSystem.ComposePlayerPosition(player) : undefined,
|
||||
});
|
||||
|
||||
LogSystem.Collect(`[Item:${event}]`.green + `: ${item.inventory_id}/${item.id} ${log.reset}`);
|
||||
}
|
||||
|
||||
static async CommandEvent(player: PlayerMp, command: string | null, log: string): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'commands', {
|
||||
date: new Date(),
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
command,
|
||||
log,
|
||||
position: LogSystem.ComposePlayerPosition(player),
|
||||
});
|
||||
|
||||
LogSystem.Collect(`[Command:${command}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`);
|
||||
}
|
||||
|
||||
static async PropertyEvent(property: PropertyEntity, event: PropertyLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'properties', {
|
||||
date: new Date(),
|
||||
property: {
|
||||
id: property.id,
|
||||
},
|
||||
event,
|
||||
log,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
});
|
||||
|
||||
let eventLog = `[Property(${property.id}):${event}]`.green + `: ${log}`;
|
||||
if (player) {
|
||||
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||
}
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static async PetEvent(pet: PetEntity, event: PetLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'pets', {
|
||||
date: new Date(),
|
||||
pet: {
|
||||
id: pet.id,
|
||||
},
|
||||
event,
|
||||
log,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
});
|
||||
|
||||
let eventLog = `[Pet(${pet.id}):${event}]`.green + `: ${log}`;
|
||||
if (player) {
|
||||
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||
}
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static async MDCEvent(player: PlayerMp, event: MDCLogEvent, log: string, data?: any): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'mdc', {
|
||||
date: new Date(),
|
||||
event,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
faction: player.character?.faction
|
||||
? {
|
||||
id: player.character.faction.id,
|
||||
}
|
||||
: undefined,
|
||||
log,
|
||||
data: !data ? undefined : JSON.parse(JSONStringifyRecursive(data)),
|
||||
});
|
||||
|
||||
const eventLog = `[MDC:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`;
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static async FurnitureEvent(
|
||||
player: PlayerMp,
|
||||
event: FurnitureLogEvent,
|
||||
dimension: number,
|
||||
log: string,
|
||||
furnitureItem?: FurnitureItemEntity,
|
||||
property?: PropertyEntity,
|
||||
): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'furniture', {
|
||||
date: new Date(),
|
||||
event,
|
||||
log,
|
||||
dimension,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
item: furnitureItem ? LogSystem.ComposeFurnitureItemObject(furnitureItem) : undefined,
|
||||
property: property
|
||||
? {
|
||||
id: property.id,
|
||||
currentFurnitureCount: property.furnitureItems ? property.furnitureItems.length : 0,
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const eventLog = `[Furniture:${event}]`.green + `: ${LogSystem.ComposePlayerName(player).yellow} ${log.reset}`;
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static async FactionEvent(faction: FactionEntity, event: FactionLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'factions', {
|
||||
date: new Date(),
|
||||
faction: {
|
||||
id: faction.id,
|
||||
},
|
||||
event,
|
||||
log,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
});
|
||||
|
||||
let eventLog = `[Faction(${faction.id}):${event}]`.green + `: ${log}`;
|
||||
if (player) {
|
||||
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||
}
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static async CompanyEvent(company: CompanyEntity, event: CompanyLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'companies', {
|
||||
date: new Date(),
|
||||
company: {
|
||||
id: company.id,
|
||||
},
|
||||
event,
|
||||
log,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
});
|
||||
|
||||
let eventLog = `[Company(${company.id}):${event}]`.green + `: ${log}`;
|
||||
if (player) {
|
||||
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||
}
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static async Error(system: string, event: string, log: string, trace: any, player?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'errors', {
|
||||
date: new Date(),
|
||||
system,
|
||||
event,
|
||||
log,
|
||||
trace: JSONStringifyRecursive(trace),
|
||||
player: LogSystem.ComposePlayerObject(player, { state: true }),
|
||||
});
|
||||
LogSystem.Collect(`[Error:${system}/${event}]`.red + ` ${log.red}`);
|
||||
console.error(trace);
|
||||
}
|
||||
|
||||
static async WhitelistEvent(event: WhitelistLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'whitelist', {
|
||||
date: new Date(),
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
event,
|
||||
log,
|
||||
});
|
||||
|
||||
let eventLog = `[Whitelist:${event}]`.green + `: ${log}`;
|
||||
if (player) {
|
||||
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||
}
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static async AdminLog(event: AdminLogEvent, log: string, player?: PlayerMp, target?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'admin', {
|
||||
date: new Date(),
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
event,
|
||||
log,
|
||||
target: LogSystem.ComposePlayerObject(target),
|
||||
});
|
||||
|
||||
let eventLog = `[Admin:${event}]`.green + `: ${log}`;
|
||||
if (player) {
|
||||
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||
}
|
||||
if (target) {
|
||||
eventLog += ` (${LogSystem.ComposePlayerName(target)})`;
|
||||
}
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static async DarkNetLog(event: DarkNetLogEvent, log: string, metadata?: DarknetOrderData, order?: DarknetOrderEntity, item?: DarknetItems): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'darknet', {
|
||||
date: new Date(),
|
||||
event,
|
||||
log,
|
||||
metadata,
|
||||
order,
|
||||
item,
|
||||
});
|
||||
|
||||
const eventLog = `[DarkNet:${event}]`.green + `: ${log}`;
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static async DoorLog(door: DoorEntity, event: DoorLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'door', {
|
||||
date: new Date(),
|
||||
door: LogSystem.ComposeDoorEntityObject(door),
|
||||
event,
|
||||
log,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
});
|
||||
|
||||
let eventLog = `[DoorSystem:${event}]`.green + `: Door #${door.id}`.yellow + ` ${log.reset}`;
|
||||
|
||||
if (player) {
|
||||
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||
}
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static async PTPLog(ptp: PointToPointEntity, event: PointToPointLogEvent, log: string, player?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'point_to_point', {
|
||||
date: new Date(),
|
||||
ptp: LogSystem.ComposePTPEntityObject(ptp),
|
||||
event,
|
||||
log,
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
});
|
||||
|
||||
let eventLog = `[PointToPoint:${event}]`.green + `: PTP #${ptp.id}`.yellow + ` ${log.reset}`;
|
||||
|
||||
if (player) {
|
||||
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||
}
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static async LawLog(event: LawLogEvent, log: string, player?: PlayerMp, target?: PlayerMp): Promise<void> {
|
||||
await mongoSystem.insertOne('logs', 'law', {
|
||||
date: new Date(),
|
||||
player: LogSystem.ComposePlayerObject(player),
|
||||
event,
|
||||
log,
|
||||
target: LogSystem.ComposePlayerObject(target),
|
||||
});
|
||||
|
||||
let eventLog = `[Law:${event}]`.green + `: ${log}`;
|
||||
if (player) {
|
||||
eventLog += ` (${LogSystem.ComposePlayerName(player)})`;
|
||||
}
|
||||
if (target) {
|
||||
eventLog += ` (${LogSystem.ComposePlayerName(target)})`;
|
||||
}
|
||||
|
||||
LogSystem.Collect(eventLog);
|
||||
}
|
||||
|
||||
static GetLogEvents(): {
|
||||
players: string[];
|
||||
properties: string[];
|
||||
chat: string[];
|
||||
factions: string[];
|
||||
} {
|
||||
return {
|
||||
players: Object.keys(PlayerLogEvents),
|
||||
properties: Object.keys(PropertyLogEvents),
|
||||
chat: Object.keys(ChatLogEvents),
|
||||
factions: Object.keys(FactionLogEvents),
|
||||
};
|
||||
}
|
||||
|
||||
static Collect(raw: string): void {
|
||||
const now = dayjs(new Date());
|
||||
const timeFormat = now.format('hh:mm:ss');
|
||||
|
||||
console.log('['.white + `${timeFormat}`.grey + '] '.white + raw);
|
||||
}
|
||||
}
|
||||
|
||||
process.on('uncaughtException', (error: Error, origin: string) => {
|
||||
LogSystem.Error('Node.js', 'uncaughtException', 'Uncaught Exception', { origin, error });
|
||||
});
|
||||
|
||||
export const logSystem = new LogSystem();
|
||||
85
src/server/core/mongo.ts
Normal file
85
src/server/core/mongo.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
import { logSystem } from './log';
|
||||
import { redisSystem } from './redis';
|
||||
|
||||
const createJobId = (): string => {
|
||||
const bytes = randomBytes(16);
|
||||
|
||||
// RFC 4122 UUID v4 variant/version bits
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
||||
|
||||
const hex = bytes.toString('hex');
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||
};
|
||||
|
||||
type MongoJobPayload =
|
||||
| {
|
||||
op: 'insertOne';
|
||||
db: string;
|
||||
collection: string;
|
||||
document: unknown;
|
||||
}
|
||||
| {
|
||||
op: 'updateOne';
|
||||
db: string;
|
||||
collection: string;
|
||||
filter: Record<string, unknown>;
|
||||
update: Record<string, unknown>;
|
||||
options?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type MongoJob = MongoJobPayload & {
|
||||
id: string;
|
||||
ts: number;
|
||||
};
|
||||
|
||||
export class MongoSystem {
|
||||
private queueKey = process.env.LSRAGE_MONGO_QUEUE || 'mongo:jobs';
|
||||
|
||||
async init(): Promise<void> {
|
||||
// No direct Mongo connection in game-server anymore.
|
||||
// Only verifies Redis path is available.
|
||||
const redis: any = (redisSystem as any).client ?? (redisSystem as any).redis ?? redisSystem;
|
||||
|
||||
if (!redis) {
|
||||
throw new Error('Redis client is not available for Mongo queue publishing.');
|
||||
}
|
||||
|
||||
logSystem.initLog('MongoQueue', `Enabled (queue=${this.queueKey})`);
|
||||
}
|
||||
|
||||
async publish(job: MongoJobPayload): Promise<void> {
|
||||
const redis: any = (redisSystem as any).client ?? (redisSystem as any).redis ?? redisSystem;
|
||||
const payload: MongoJob = {
|
||||
id: createJobId(),
|
||||
ts: Date.now(),
|
||||
...job,
|
||||
};
|
||||
|
||||
// Support both node-redis (lPush) and ioredis (lpush)
|
||||
if (typeof redis.lPush === 'function') {
|
||||
await redis.lPush(this.queueKey, JSON.stringify(payload));
|
||||
} else if (typeof redis.lpush === 'function') {
|
||||
await redis.lpush(this.queueKey, JSON.stringify(payload));
|
||||
} else {
|
||||
throw new Error('Redis client does not support LPUSH.');
|
||||
}
|
||||
}
|
||||
|
||||
async insertOne(db: string, collection: string, document: unknown): Promise<void> {
|
||||
await this.publish({ op: 'insertOne', db, collection, document });
|
||||
}
|
||||
|
||||
async updateOne(
|
||||
db: string,
|
||||
collection: string,
|
||||
filter: Record<string, unknown>,
|
||||
update: Record<string, unknown>,
|
||||
options?: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
await this.publish({ op: 'updateOne', db, collection, filter, update, options });
|
||||
}
|
||||
}
|
||||
|
||||
export const mongoSystem = new MongoSystem();
|
||||
297
src/server/core/mp-shim.ts
Normal file
297
src/server/core/mp-shim.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* mp-shim.ts — RageMP compatibility layer for FiveM (server-side).
|
||||
*
|
||||
* Exposes a global `mp` object that mirrors the RageMP server API so that
|
||||
* ported feature files compile and run with minimal changes.
|
||||
*
|
||||
* Migration status markers used in this file:
|
||||
* COMPAT — fully implemented equivalent
|
||||
* PARTIAL — works for most cases, edge cases may differ
|
||||
* STUB — not yet implemented; logs a warning when called
|
||||
*/
|
||||
|
||||
import { playerRegistry, PlayerWrapper } from './player/player-registry';
|
||||
import { vehicleRegistry, VehicleWrapper } from './vehicle/vehicle-wrapper';
|
||||
import { rpc } from './rpc/index';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Well-known RageMP server events → FiveM event names
|
||||
// ---------------------------------------------------------------------------
|
||||
const RAGEMP_TO_FIVEM: Record<string, string> = {
|
||||
playerJoin: 'playerConnecting', // COMPAT (slightly different — no char load yet)
|
||||
playerQuit: 'playerDropped', // COMPAT
|
||||
playerReady: 'playerSpawned', // PARTIAL — FiveM fires after first spawn
|
||||
};
|
||||
|
||||
// Events where the handler's first argument should be a PlayerWrapper
|
||||
const PLAYER_SOURCE_EVENTS = new Set([
|
||||
'playerJoin', 'playerQuit', 'playerReady', 'playerDeath',
|
||||
'playerCommand', 'requestCommandList',
|
||||
// All custom client→server events are also player-sourced
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Event bus
|
||||
// ---------------------------------------------------------------------------
|
||||
const localEventEmitter = new (require('events').EventEmitter)();
|
||||
|
||||
const events = {
|
||||
delayInitialization: false,
|
||||
|
||||
// COMPAT — primary event registration
|
||||
add(eventOrMap: string | Record<string, Function>, handler?: Function): void {
|
||||
if (typeof eventOrMap === 'object') {
|
||||
for (const [name, fn] of Object.entries(eventOrMap)) {
|
||||
events.add(name, fn);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const name = eventOrMap as string;
|
||||
const fn = handler!;
|
||||
|
||||
if (name in RAGEMP_TO_FIVEM) {
|
||||
// Map to FiveM built-in event
|
||||
const fivemName = RAGEMP_TO_FIVEM[name];
|
||||
AddEventHandler(fivemName, function (...args: any[]) {
|
||||
const src = (global as any).source as number;
|
||||
if (src > 0) {
|
||||
const player = playerRegistry.getOrCreate(src);
|
||||
fn(player, ...args);
|
||||
} else {
|
||||
fn(...args);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Custom event: register as net event (player-sourced) AND as local event
|
||||
RegisterNetEvent(name);
|
||||
AddEventHandler(name, function (...args: any[]) {
|
||||
const src = (global as any).source as number;
|
||||
if (src > 0) {
|
||||
const player = playerRegistry.getOrCreate(src);
|
||||
fn(player, ...args);
|
||||
} else {
|
||||
fn(...args);
|
||||
}
|
||||
});
|
||||
// Local event bus (for mp.events.call)
|
||||
localEventEmitter.on(name, fn);
|
||||
}
|
||||
},
|
||||
|
||||
// COMPAT — replaces mp.events.addProc (server-side RPC handler)
|
||||
addProc(name: string, handler: (...args: any[]) => any): void {
|
||||
rpc.register(name, handler);
|
||||
},
|
||||
|
||||
// COMPAT — fire a local server-side event (no player source).
|
||||
// Use only the JS EventEmitter — TriggerEvent would msgpack-serialize
|
||||
// complex objects (e.g. PlayerWrapper) and strip their prototype getters.
|
||||
call(eventName: string, ...args: any[]): void {
|
||||
localEventEmitter.emit(eventName, ...args);
|
||||
},
|
||||
|
||||
// PARTIAL — call remote on all players (no per-player targeting here)
|
||||
callRemote(eventName: string, ...args: any[]): void {
|
||||
TriggerClientEvent(eventName, -1, ...args);
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Player pool
|
||||
// ---------------------------------------------------------------------------
|
||||
const players = {
|
||||
// COMPAT
|
||||
at(id: number): PlayerWrapper | undefined {
|
||||
return playerRegistry.get(id);
|
||||
},
|
||||
|
||||
// COMPAT
|
||||
toArray(): PlayerWrapper[] {
|
||||
return playerRegistry.toArray();
|
||||
},
|
||||
|
||||
toArrayFast(): PlayerWrapper[] {
|
||||
return playerRegistry.toArray();
|
||||
},
|
||||
|
||||
forEach(fn: (player: PlayerWrapper) => void): void {
|
||||
playerRegistry.forEach(fn);
|
||||
},
|
||||
|
||||
forEachFast(fn: (player: PlayerWrapper) => void): void {
|
||||
playerRegistry.forEach(fn);
|
||||
},
|
||||
|
||||
// COMPAT — broadcast chat message to all
|
||||
broadcast(message: string): void {
|
||||
TriggerClientEvent('_mp:chatMessage', -1, message);
|
||||
},
|
||||
|
||||
get length(): number {
|
||||
return playerRegistry.toArray().length;
|
||||
},
|
||||
|
||||
exists(player: PlayerWrapper): boolean {
|
||||
return playerRegistry.get(player.id) !== undefined;
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vehicle pool
|
||||
// ---------------------------------------------------------------------------
|
||||
const vehicles = {
|
||||
// COMPAT — replaces mp.vehicles.new(model, position, options)
|
||||
new(model: string | number, position: Vector3, options?: any): VehicleWrapper {
|
||||
return vehicleRegistry.create(model, position, options);
|
||||
},
|
||||
|
||||
at(handle: number): VehicleWrapper | undefined {
|
||||
return vehicleRegistry.get(handle);
|
||||
},
|
||||
|
||||
toArray(): VehicleWrapper[] {
|
||||
return vehicleRegistry.toArray();
|
||||
},
|
||||
|
||||
forEach(fn: (vehicle: VehicleWrapper) => void): void {
|
||||
vehicleRegistry.forEach(fn);
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config (replaces mp.config — reads FiveM ConVars set in server.cfg)
|
||||
// ---------------------------------------------------------------------------
|
||||
const config = new Proxy({} as Record<string, any>, {
|
||||
get(_target, prop: string) {
|
||||
const val = GetConvar(prop, '__undefined__');
|
||||
if (val === '__undefined__') return undefined;
|
||||
if (val === 'true') return true;
|
||||
if (val === 'false') return false;
|
||||
const num = Number(val);
|
||||
return isNaN(num) ? val : num;
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Objects pool — STUB (RageMP server objects = FiveM server-side peds/objects)
|
||||
// ---------------------------------------------------------------------------
|
||||
const objects = {
|
||||
new(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.objects.new() not yet implemented');
|
||||
return null;
|
||||
},
|
||||
at(id: number): any { return null; },
|
||||
toArray(): any[] { return []; },
|
||||
forEach(_fn: any): void {},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Peds pool — STUB
|
||||
// ---------------------------------------------------------------------------
|
||||
const peds = {
|
||||
new(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.peds.new() not yet implemented');
|
||||
return null;
|
||||
},
|
||||
at(id: number): any { return null; },
|
||||
toArray(): any[] { return []; },
|
||||
forEach(_fn: any): void {},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Markers / blips / labels — client-side constructs in FiveM; server sends
|
||||
// creation events to clients. Stubs here so code compiles.
|
||||
// ---------------------------------------------------------------------------
|
||||
const markers = {
|
||||
new(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.markers.new() — use client-side equivalent');
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const blips = {
|
||||
new(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.blips.new() — use client-side equivalent');
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const labels = {
|
||||
new(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.labels.new() — use client-side equivalent');
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const checkpoints = {
|
||||
new(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.checkpoints.new() — use client-side equivalent');
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
// Colshapes are client-side in FiveM (or use server polygons with player position tracking)
|
||||
const colshapes = {
|
||||
newSphere(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||
return null;
|
||||
},
|
||||
newCuboid(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||
return null;
|
||||
},
|
||||
newCircle(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||
return null;
|
||||
},
|
||||
newRectangle(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||
return null;
|
||||
},
|
||||
newTube(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||
return null;
|
||||
},
|
||||
newPolygon(...args: any[]): any {
|
||||
console.warn('[mp-shim] mp.colshapes — use polygon system equivalent');
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Vector3 constructor (replaces new mp.Vector3(x, y, z))
|
||||
class Vector3Impl {
|
||||
constructor(public x: number, public y: number, public z: number) {}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Assemble the global mp object
|
||||
// ---------------------------------------------------------------------------
|
||||
(global as any).mp = {
|
||||
events,
|
||||
players,
|
||||
vehicles,
|
||||
objects,
|
||||
peds,
|
||||
markers,
|
||||
blips,
|
||||
labels,
|
||||
checkpoints,
|
||||
colshapes,
|
||||
config,
|
||||
Vector3: Vector3Impl,
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Player lifecycle — build/tear down PlayerWrapper on connect/disconnect
|
||||
// ---------------------------------------------------------------------------
|
||||
AddEventHandler('playerConnecting', (name: string, _setKickReason: Function, deferrals: any) => {
|
||||
const netId = (global as any).source as number;
|
||||
playerRegistry.add(netId);
|
||||
});
|
||||
|
||||
AddEventHandler('playerDropped', (reason: string) => {
|
||||
const netId = (global as any).source as number;
|
||||
playerRegistry.remove(netId);
|
||||
});
|
||||
69
src/server/core/notify.ts
Normal file
69
src/server/core/notify.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export type NotifyType = 'info' | 'success' | 'warning' | 'error';
|
||||
|
||||
/**
|
||||
* A button shown on an action toast. When clicked, `event` is fired as a
|
||||
* remote event on the server with the supplied `args`.
|
||||
*/
|
||||
export interface NotifyAction {
|
||||
label: string;
|
||||
event: string;
|
||||
args?: (string | number | boolean)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side helper for sending toast notifications to players via the
|
||||
* custom overlay notify component. Replaces raw player.notify() calls.
|
||||
*
|
||||
* Standard toasts:
|
||||
* NotifySystem.Info(player, 'Heading to depot...')
|
||||
* NotifySystem.Success(player, 'Package delivered!')
|
||||
* NotifySystem.Warning(player, 'Van not at depot yet.')
|
||||
* NotifySystem.Error(player, 'You must be in a company vehicle.')
|
||||
*
|
||||
* Action toasts (persistent, with buttons):
|
||||
* NotifySystem.Action(player, 'ride_1', 'Taxi request 1.2km away', 'info', [
|
||||
* { label: 'Accept', event: 'taxiDuty:accept', args: [1] },
|
||||
* { label: 'Decline', event: 'taxiDuty:decline', args: [1] },
|
||||
* ]);
|
||||
*
|
||||
* Dismiss a specific action toast by key:
|
||||
* NotifySystem.Dismiss(player, 'ride_1');
|
||||
*/
|
||||
export class NotifySystem {
|
||||
static Show(player: PlayerMp, message: string, type: NotifyType = 'info', duration = 5000): void {
|
||||
player.call('notify:show', [message, type, duration]);
|
||||
}
|
||||
|
||||
static Info(player: PlayerMp, message: string, duration?: number): void {
|
||||
this.Show(player, message, 'info', duration);
|
||||
}
|
||||
|
||||
static Success(player: PlayerMp, message: string, duration?: number): void {
|
||||
this.Show(player, message, 'success', duration);
|
||||
}
|
||||
|
||||
static Warning(player: PlayerMp, message: string, duration?: number): void {
|
||||
this.Show(player, message, 'warning', duration);
|
||||
}
|
||||
|
||||
static Error(player: PlayerMp, message: string, duration?: number): void {
|
||||
this.Show(player, message, 'error', duration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a persistent toast with action buttons. The toast stays on screen
|
||||
* until the player clicks a button, closes it manually, or the server
|
||||
* calls Dismiss() with the same key.
|
||||
*/
|
||||
static Action(player: PlayerMp, key: string, message: string, type: NotifyType = 'info', actions: NotifyAction[]): void {
|
||||
player.call('notify:action', [key, message, type, JSON.stringify(actions)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific action toast from a player's screen by key.
|
||||
* Safe to call even if the toast has already been dismissed.
|
||||
*/
|
||||
static Dismiss(player: PlayerMp, key: string): void {
|
||||
player.call('notify:dismiss', [key]);
|
||||
}
|
||||
}
|
||||
218
src/server/core/player/player-registry.ts
Normal file
218
src/server/core/player/player-registry.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { CharacterEntity } from '../../entities/character.entity';
|
||||
import { SessionEntity } from '../../entities/session.entity';
|
||||
import { AccountDto } from '../../features/sw-rp-api/index';
|
||||
|
||||
export class PlayerWrapper {
|
||||
readonly id: number; // FiveM net ID (= RageMP player.id)
|
||||
|
||||
state: PlayerState;
|
||||
character?: CharacterEntity;
|
||||
account?: AccountDto;
|
||||
session?: SessionEntity;
|
||||
groups: number[] = [];
|
||||
permissions: EntityPermission[] = [];
|
||||
|
||||
constructor(netId: number) {
|
||||
this.id = netId;
|
||||
this.state = createDefaultPlayerState();
|
||||
}
|
||||
|
||||
// --- Identity ---
|
||||
get name(): string {
|
||||
return GetPlayerName(String(this.id)) ?? `Player${this.id}`;
|
||||
}
|
||||
|
||||
get ip(): string {
|
||||
const endpoint = GetPlayerEndpoint(String(this.id)) ?? '';
|
||||
const colonIdx = endpoint.lastIndexOf(':');
|
||||
return colonIdx > 0 ? endpoint.slice(0, colonIdx) : endpoint;
|
||||
}
|
||||
|
||||
get socialClub(): string {
|
||||
return GetPlayerIdentifierByType(String(this.id), 'steam') ?? '';
|
||||
}
|
||||
|
||||
// --- Position (server-side via ped) ---
|
||||
get position(): Vector3 {
|
||||
const ped = GetPlayerPed(String(this.id));
|
||||
const [x, y, z] = GetEntityCoords(ped);
|
||||
return { x, y, z } as Vector3;
|
||||
}
|
||||
|
||||
get heading(): number {
|
||||
return GetEntityHeading(GetPlayerPed(String(this.id)));
|
||||
}
|
||||
|
||||
// --- Health ---
|
||||
get health(): number {
|
||||
return GetEntityHealth(GetPlayerPed(String(this.id)));
|
||||
}
|
||||
|
||||
set health(value: number) {
|
||||
SetEntityHealth(GetPlayerPed(String(this.id)), value);
|
||||
}
|
||||
|
||||
// --- Dimension (stored server-side, synced via state bag) ---
|
||||
get dimension(): number {
|
||||
return (Player(this.id).state.dimension as number) ?? 0;
|
||||
}
|
||||
|
||||
setDimension(dimension: number): void {
|
||||
Player(this.id).state.set('dimension', dimension, true);
|
||||
}
|
||||
|
||||
// --- Server → Client event (replaces player.call) ---
|
||||
// RageMP: player.call('eventName', [arg1, arg2])
|
||||
// FiveM: TriggerClientEvent('eventName', netId, arg1, arg2)
|
||||
call(eventName: string, args: any[] = []): void {
|
||||
TriggerClientEvent(eventName, this.id, ...args);
|
||||
}
|
||||
|
||||
// --- Notifications (RageMP player.notify) ---
|
||||
notify(message: string): void {
|
||||
TriggerClientEvent('_mp:notify', this.id, message);
|
||||
}
|
||||
|
||||
outputChatBox(message: string): void {
|
||||
TriggerClientEvent('_mp:chatMessage', this.id, message);
|
||||
}
|
||||
|
||||
// --- Synced variables (replaces player.setVariable / getVariable) ---
|
||||
// Non-serialisable values (timers, promises, entity refs) must stay in .state;
|
||||
// only primitives & plain objects should go into state bags.
|
||||
setVariable(key: string, value: any): void {
|
||||
try {
|
||||
Player(this.id).state.set(key, value, true);
|
||||
} catch {
|
||||
// State bags can only hold serialisable values; silently ignore others.
|
||||
}
|
||||
}
|
||||
|
||||
getVariable<T = any>(key: string): T {
|
||||
return Player(this.id).state[key] as T;
|
||||
}
|
||||
|
||||
setOwnVariable(key: string, value: any): void {
|
||||
Player(this.id).state.set(key, value, false); // not broadcast to other clients
|
||||
}
|
||||
|
||||
// --- Player management ---
|
||||
kick(reason: string): void {
|
||||
DropPlayer(String(this.id), reason);
|
||||
}
|
||||
|
||||
kickSilent(): void {
|
||||
DropPlayer(String(this.id), '');
|
||||
}
|
||||
|
||||
ban(reason: string): void {
|
||||
// Record the ban then kick — ban storage handled by admin system
|
||||
this.kick(`[Banned] ${reason}`);
|
||||
}
|
||||
|
||||
// --- Appearance helper (sends to client for ped apply) ---
|
||||
spawn(position: Vector3): void {
|
||||
TriggerClientEvent('_mp:spawn', this.id, position);
|
||||
}
|
||||
|
||||
// --- Vehicle (replaces RageMP player.vehicle / player.seat) ---
|
||||
get vehicle(): import('../vehicle/vehicle-wrapper').VehicleWrapper | null {
|
||||
const { vehicleRegistry } = require('../vehicle/vehicle-wrapper');
|
||||
const ped = GetPlayerPed(String(this.id));
|
||||
const handle = GetVehiclePedIsIn(ped, false);
|
||||
if (handle === 0) return null;
|
||||
return vehicleRegistry.get(handle) ?? null;
|
||||
}
|
||||
|
||||
// Seat number: -1 = driver, 0+ = passenger (matches RageMP/GTA convention)
|
||||
get seat(): number {
|
||||
const ped = GetPlayerPed(String(this.id));
|
||||
const handle = GetVehiclePedIsIn(ped, false);
|
||||
if (handle === 0) return -2;
|
||||
if (GetPedInVehicleSeat(handle, -1) === ped) return -1;
|
||||
for (let i = 0; i < 8; i++) {
|
||||
if (GetPedInVehicleSeat(handle, i) === ped) return i;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
// --- Distance ---
|
||||
dist(other: Vector3): number {
|
||||
const p = this.position;
|
||||
return Math.sqrt((p.x - other.x) ** 2 + (p.y - other.y) ** 2 + (p.z - other.z) ** 2);
|
||||
}
|
||||
|
||||
distSquared(other: Vector3): number {
|
||||
const p = this.position;
|
||||
return (p.x - other.x) ** 2 + (p.y - other.y) ** 2 + (p.z - other.z) ** 2;
|
||||
}
|
||||
|
||||
// --- Type guard helpers (mirrors RageMP pattern) ---
|
||||
get isLoggedIn(): boolean {
|
||||
return !!(this.character && this.account && this.session);
|
||||
}
|
||||
}
|
||||
|
||||
function createDefaultPlayerState(): PlayerState {
|
||||
return {
|
||||
reconnecting: false,
|
||||
phoneAnim: { currentStatus: 'out' },
|
||||
currentHairSlot: 0,
|
||||
togLevel: false,
|
||||
loginAttempts: 0,
|
||||
damageHistory: [],
|
||||
deathState: 'alive' as DeathState,
|
||||
falling: false,
|
||||
pmWatchers: [],
|
||||
pmWatched: [],
|
||||
freqWatched: [],
|
||||
factionWatched: [],
|
||||
companyWatched: [],
|
||||
report: [],
|
||||
props: [],
|
||||
trackedDutyIds: [],
|
||||
wikiBookmarks: [],
|
||||
paymentMutex: null as any, // initialised by MoneySystem
|
||||
outfitOverrides: [],
|
||||
serverWeapons: {},
|
||||
actionLock: 0,
|
||||
maxHealth: 200,
|
||||
dutyHandcuffs: 0,
|
||||
} as any;
|
||||
}
|
||||
|
||||
export class PlayerRegistry {
|
||||
private players: Map<number, PlayerWrapper> = new Map();
|
||||
|
||||
add(netId: number): PlayerWrapper {
|
||||
const wrapper = new PlayerWrapper(netId);
|
||||
this.players.set(netId, wrapper);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
remove(netId: number): void {
|
||||
this.players.delete(netId);
|
||||
}
|
||||
|
||||
get(netId: number): PlayerWrapper | undefined {
|
||||
return this.players.get(netId);
|
||||
}
|
||||
|
||||
getOrCreate(netId: number): PlayerWrapper {
|
||||
return this.players.get(netId) ?? this.add(netId);
|
||||
}
|
||||
|
||||
toArray(): PlayerWrapper[] {
|
||||
return [...this.players.values()];
|
||||
}
|
||||
|
||||
toArrayFast(): PlayerWrapper[] {
|
||||
return this.toArray();
|
||||
}
|
||||
|
||||
forEach(fn: (player: PlayerWrapper) => void): void {
|
||||
this.players.forEach(fn);
|
||||
}
|
||||
}
|
||||
|
||||
export const playerRegistry = new PlayerRegistry();
|
||||
28
src/server/core/preload.ts
Normal file
28
src/server/core/preload.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { getRepository } from 'typeorm';
|
||||
import { GameEntityTypeEntity } from '../entities/game-entity-type.entity';
|
||||
import { logSystem } from './log';
|
||||
|
||||
export class PreloadSystem {
|
||||
private gameEntityTypesMap: {
|
||||
[key: string]: number;
|
||||
} = {};
|
||||
|
||||
async init(): Promise<void> {
|
||||
const gameEntities = await getRepository(GameEntityTypeEntity).find();
|
||||
for (const gameEntity of gameEntities) {
|
||||
this.gameEntityTypesMap[gameEntity.name] = gameEntity.id;
|
||||
}
|
||||
|
||||
logSystem.initLog('Preload System', `Loaded ${gameEntities.length} game entity types.`);
|
||||
}
|
||||
|
||||
getGameEntityTypeId(name: string): number {
|
||||
return this.gameEntityTypesMap[name];
|
||||
}
|
||||
|
||||
getGameEntityTypeById(id: number): string {
|
||||
return Object.entries(this.gameEntityTypesMap).find((type) => type[1] === id)?.[0] ?? 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
export const preloadSystem = new PreloadSystem();
|
||||
87
src/server/core/redis.ts
Normal file
87
src/server/core/redis.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as redis from 'redis';
|
||||
import { LogSystem, logSystem } from './log';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const REDIS_HOST = process.env.REDIS_HOST || 'swrp-redis';
|
||||
const REDIS_PORT = Number(process.env.REDIS_PORT || 6379);
|
||||
const REDIS_URL = process.env.REDIS_URL;
|
||||
|
||||
export class RedisSystem {
|
||||
client: redis.RedisClient = REDIS_URL ? redis.createClient(REDIS_URL) : redis.createClient(REDIS_PORT, REDIS_HOST);
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.client.on('error', this.error.bind(this));
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.client.flushall((error: any) => {
|
||||
if (!error) {
|
||||
logSystem.initLog('Redis', 'Connection Ready');
|
||||
resolve();
|
||||
} else {
|
||||
LogSystem.Error('Redis', 'error', 'Failed to flush', error);
|
||||
process.exit();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async get(key: string): Promise<string | null> {
|
||||
return promisify(this.client.get).call(this.client, key);
|
||||
}
|
||||
|
||||
async hget(key: string, field: string): Promise<string> {
|
||||
return promisify(this.client.hget).call(this.client, key, field);
|
||||
}
|
||||
|
||||
async hdel(key: string, field: string): Promise<number> {
|
||||
// @ts-ignore
|
||||
return promisify(this.client.hdel).call(this.client, key, field);
|
||||
}
|
||||
|
||||
async hgetall(key: string): Promise<any> {
|
||||
return promisify(this.client.hgetall).call(this.client, key);
|
||||
}
|
||||
|
||||
async hset(key: string, field: string, value: string): Promise<number> {
|
||||
// @ts-ignore
|
||||
return promisify(this.client.hset).call(this.client, key, field, value);
|
||||
}
|
||||
|
||||
async smembers(key: string): Promise<string[]> {
|
||||
return promisify(this.client.smembers).call(this.client, key);
|
||||
}
|
||||
|
||||
async sinter(keys: string[]): Promise<string[]> {
|
||||
// @ts-ignore
|
||||
return promisify(this.client.sinter).call(this.client, keys);
|
||||
}
|
||||
|
||||
async sismemebr(key: string, member: string): Promise<number> {
|
||||
return promisify(this.client.sismember).call(this.client, key, member);
|
||||
}
|
||||
|
||||
async scard(key: string): Promise<number> {
|
||||
return promisify(this.client.scard).call(this.client, key);
|
||||
}
|
||||
|
||||
async del(key: string): Promise<number> {
|
||||
// @ts-ignore
|
||||
return promisify(this.client.del).call(this.client, key);
|
||||
}
|
||||
|
||||
async sadd(key: string, member: string | string[]): Promise<number> {
|
||||
// @ts-ignore
|
||||
return promisify(this.client.sadd).call(this.client, key, member);
|
||||
}
|
||||
|
||||
async srem(key: string, member: string): Promise<number> {
|
||||
// @ts-ignore
|
||||
return promisify(this.client.srem).call(this.client, key, member);
|
||||
}
|
||||
|
||||
error(error: string): void {
|
||||
LogSystem.Error('Redis', 'error', error, error);
|
||||
}
|
||||
}
|
||||
|
||||
export const redisSystem = new RedisSystem();
|
||||
73
src/server/core/rpc/index.ts
Normal file
73
src/server/core/rpc/index.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Server-side RPC — replaces RageMP's mp.events.addProc / mp.events.callRemoteProc.
|
||||
*
|
||||
* Server registers a proc:
|
||||
* rpc.register('getClosestTrunk', async (player, ...args) => { return result; });
|
||||
*
|
||||
* Client calls it via the client RPC module (see src/client/core/rpc/index.ts):
|
||||
* const result = await rpc.call('getClosestTrunk', ...args);
|
||||
*
|
||||
* The wire protocol uses two net events:
|
||||
* _rpc:call player → server { id, name, args }
|
||||
* _rpc:result server → player { id, result, error }
|
||||
*/
|
||||
|
||||
import { playerRegistry } from '../player/player-registry';
|
||||
|
||||
type ProcHandler = (player: import('../player/player-registry').PlayerWrapper, ...args: any[]) => any;
|
||||
|
||||
const handlers = new Map<string, ProcHandler>();
|
||||
|
||||
let clientCallCounter = 0;
|
||||
const clientPending = new Map<string, { resolve: (v: any) => void; reject: (e: any) => void }>();
|
||||
|
||||
RegisterNetEvent('_rpc:clientResult');
|
||||
AddEventHandler('_rpc:clientResult', (callId: string, result: any, error: string | null) => {
|
||||
const cb = clientPending.get(callId);
|
||||
if (!cb) return;
|
||||
clientPending.delete(callId);
|
||||
if (error) cb.reject(new Error(error));
|
||||
else cb.resolve(result);
|
||||
});
|
||||
|
||||
export const rpc = {
|
||||
register(name: string, handler: ProcHandler): void {
|
||||
handlers.set(name, handler);
|
||||
},
|
||||
|
||||
// Server → client proc call (replaces mp.events.callRemoteProc / player.callProc)
|
||||
callClient<T = any>(player: import('../player/player-registry').PlayerWrapper, name: string, ...args: any[]): Promise<T> {
|
||||
const id = `${Date.now()}_${clientCallCounter++}`;
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
clientPending.set(id, { resolve, reject });
|
||||
TriggerClientEvent('_rpc:clientCall', player.id, id, name, args);
|
||||
setTimeout(() => {
|
||||
if (clientPending.has(id)) {
|
||||
clientPending.delete(id);
|
||||
reject(new Error(`Client RPC timeout: ${name}`));
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
RegisterNetEvent('_rpc:call');
|
||||
AddEventHandler('_rpc:call', async (callId: string, name: string, args: any[]) => {
|
||||
const netId = (global as any).source as number;
|
||||
const player = playerRegistry.get(netId);
|
||||
|
||||
if (!player) return;
|
||||
|
||||
const handler = handlers.get(name);
|
||||
if (!handler) {
|
||||
TriggerClientEvent('_rpc:result', netId, callId, null, `No proc registered: ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await handler(player, ...(args ?? []));
|
||||
TriggerClientEvent('_rpc:result', netId, callId, result, null);
|
||||
} catch (err: any) {
|
||||
TriggerClientEvent('_rpc:result', netId, callId, null, err?.message ?? String(err));
|
||||
}
|
||||
});
|
||||
41
src/server/core/session-runners.ts
Normal file
41
src/server/core/session-runners.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { QueryRunner } from 'typeorm';
|
||||
|
||||
export class SessionRunnerSystem {
|
||||
pendingRunners: {
|
||||
player: PlayerMp;
|
||||
runner: QueryRunner;
|
||||
}[] = [];
|
||||
|
||||
constructor() {
|
||||
mp.events.add('playerQuit', this.playerQuit.bind(this));
|
||||
}
|
||||
|
||||
register(player: PlayerMp, runner: QueryRunner): void {
|
||||
this.pendingRunners.push({ player, runner });
|
||||
console.log(`Registered runner for ${player.name}`);
|
||||
}
|
||||
|
||||
unregister(player: PlayerMp, runner: QueryRunner): void {
|
||||
this.pendingRunners = this.pendingRunners.filter((pr) => !(pr.player === player && pr.runner === runner));
|
||||
console.log('unregistering runner');
|
||||
}
|
||||
|
||||
playerQuit(player: PlayerMp): void {
|
||||
const playerRunners = this.pendingRunners.filter((entry) => entry.player === player);
|
||||
this.pendingRunners = this.pendingRunners.filter((entry) => entry.player !== player);
|
||||
|
||||
playerRunners.forEach((pendingRunner) => {
|
||||
if (pendingRunner.runner.isTransactionActive) {
|
||||
pendingRunner.runner.rollbackTransaction();
|
||||
}
|
||||
|
||||
if (!pendingRunner.runner.isReleased) {
|
||||
pendingRunner.runner.release();
|
||||
}
|
||||
|
||||
console.log('removing runner on quit');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const sessionRunnerSystem = new SessionRunnerSystem();
|
||||
423
src/server/core/types.d.ts
vendored
Normal file
423
src/server/core/types.d.ts
vendored
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* Global type augmentations for the FiveM port.
|
||||
*
|
||||
* PlayerMp / VehicleMp etc. are aliased to our wrapper classes so that
|
||||
* existing feature code that references these types compiles without changes.
|
||||
*/
|
||||
|
||||
import { PlayerWrapper } from './player/player-registry';
|
||||
import { VehicleWrapper } from './vehicle/vehicle-wrapper';
|
||||
import { CharacterEntity } from '../entities/character.entity';
|
||||
import { AccountDto } from '../features/sw-rp-api/index';
|
||||
import { SessionEntity } from '../entities/session.entity';
|
||||
|
||||
// Re-export wrappers under the RageMP type names so existing code just works
|
||||
declare global {
|
||||
type PlayerMp = PlayerWrapper;
|
||||
type VehicleMp = VehicleWrapper;
|
||||
|
||||
// Logged-in subtype (mirrors original)
|
||||
interface PlayerMpLoggedIn extends PlayerWrapper {
|
||||
character: CharacterEntity;
|
||||
account: AccountDto;
|
||||
session: SessionEntity;
|
||||
}
|
||||
|
||||
// ObjectMp — FiveM server-side objects (peds, props); stub for now
|
||||
interface ObjectMp {
|
||||
id: number;
|
||||
handle: number;
|
||||
setVariable(key: string, value: any): void;
|
||||
getVariable<T = any>(key: string): T;
|
||||
furnitureOfProperty?: import('../entities/property.entity').PropertyEntity;
|
||||
inventory?: import('../entities/inventory.entity').InventoryEntity;
|
||||
itemLock: boolean;
|
||||
bluetoothConnectedTo?: PlayerWrapper;
|
||||
notifyStreaming?: boolean;
|
||||
createdBy?: string;
|
||||
createdOn?: string;
|
||||
doorLocked?: boolean;
|
||||
doorAjar?: boolean;
|
||||
}
|
||||
|
||||
// Vector3 — matches RageMP's Vector3 shape
|
||||
interface Vector3 {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
type Array4d = [number, number, number, number];
|
||||
|
||||
// ColshapeMp stub
|
||||
interface ColshapeMp {
|
||||
vehicleDealership?: import('../entities/vehicle-dealership.entity').VehicleDealershipEntity;
|
||||
property?: import('../entities/property.entity').PropertyEntity;
|
||||
forProperty?: import('../entities/property.entity').PropertyEntity;
|
||||
}
|
||||
|
||||
// PedMp stub
|
||||
interface PedMp {
|
||||
entity?: import('../entities/corpse.entity').CorpseEntity;
|
||||
handle: number;
|
||||
}
|
||||
|
||||
interface EntityPermission {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
repositoryId: number;
|
||||
repositoryType: string;
|
||||
}
|
||||
|
||||
// VehicleMpDatabase — vehicle with a guaranteed DB entity
|
||||
interface VehicleMpDatabase extends VehicleWrapper {
|
||||
vehicleEntity: import('../entities/vehicle.entity').VehicleEntity;
|
||||
}
|
||||
|
||||
// ---- PlayerState -------------------------------------------------------
|
||||
// Copied verbatim from the original types.d.ts — nothing in here references
|
||||
// RageMP APIs directly so it's portable as-is.
|
||||
interface PlayerState {
|
||||
diedToFall?: boolean;
|
||||
reconnecting: boolean;
|
||||
lastPlayerDeath?: number;
|
||||
isOnPhone?: boolean;
|
||||
phoneAnim: {
|
||||
currentStatus: 'out' | 'call' | 'text';
|
||||
lastDict?: string;
|
||||
lastAnim?: string;
|
||||
};
|
||||
companyChat?: import('../entities/company.entity').CompanyEntity;
|
||||
interviewing?: PlayerWrapper;
|
||||
interviewedBy?: PlayerWrapper;
|
||||
shift?: { companyId: number; startTime: Date; paycheckBuffer: number; paydays: number };
|
||||
currentHairSlot: number;
|
||||
metroDuty?: string;
|
||||
sentenceId?: number | null;
|
||||
prisoned?: Date | null;
|
||||
jailed?: Date | null;
|
||||
jailedFor?: number | null;
|
||||
jailCellId?: number | null;
|
||||
paroled?: Date | null;
|
||||
segregated?: Date | null;
|
||||
segregatedFor?: number | null;
|
||||
segregatedUntil?: Date | null;
|
||||
sentencedUntil?: Date | null;
|
||||
remanded?: Date | null;
|
||||
paroleEligible?: number | null;
|
||||
noticeServed?: { release: boolean; parole: boolean };
|
||||
togLevel: boolean;
|
||||
togXChat?: boolean;
|
||||
phoneCall?: {
|
||||
id: number; number: string; party?: PlayerMpLoggedIn; objectParty?: ObjectMp;
|
||||
automatedParty?: string; outgoing: boolean; status?: string; start?: number;
|
||||
active: boolean; dialTimeout?: NodeJS.Timeout; timeoutTimeout?: NodeJS.Timeout;
|
||||
clearingTimeout?: NodeJS.Timeout; loudSpeaker?: boolean; muted?: boolean;
|
||||
};
|
||||
posResetUnlock: number;
|
||||
propertyInteractionUnlock: number;
|
||||
vehicleInteraction: boolean;
|
||||
destinationMarker?: Vector3;
|
||||
emergencyDispatch?: any;
|
||||
nonEmergencyDispatch?: any;
|
||||
menuCompany?: import('../entities/company.entity').CompanyEntity;
|
||||
phoneCaller?: PlayerMpLoggedIn;
|
||||
phoneDialTimeout?: NodeJS.Timeout;
|
||||
clothing: import('../features/clothing/index').StatefulClothingItem[];
|
||||
characterLoadedAt?: number;
|
||||
inProperty?: import('../entities/property.entity').PropertyEntity;
|
||||
inPropertyArea?: any;
|
||||
mechanicOperation?: VehicleMechanicOperation;
|
||||
atPropertyCol?: import('../entities/property.entity').PropertyEntity;
|
||||
atBackdoorCol?: import('../entities/property.entity').PropertyEntity;
|
||||
jailTimer?: NodeJS.Timeout;
|
||||
insideJail?: import('../entities/jail-cell.entity').JailCellEntity;
|
||||
jailReleaseTime?: number;
|
||||
paymentMutex: import('async-mutex').MutexInterface;
|
||||
mission?: import('../features/mission/mission.model').Mission;
|
||||
navDestCallback?: (...args: any[]) => void;
|
||||
adjustingWeapon?: number;
|
||||
objectEditor?: any;
|
||||
objectSelector?: any;
|
||||
vehicleTask?: any;
|
||||
aimControls?: any;
|
||||
furnishing?: any;
|
||||
furnitureEditor?: any;
|
||||
dmvTest?: any;
|
||||
dmvTmpKnowledge?: boolean;
|
||||
dmvTesting?: boolean;
|
||||
dmvWaivedPayments?: number;
|
||||
report: any[];
|
||||
wakeUpTimer?: NodeJS.Timeout;
|
||||
spectating?: any;
|
||||
itemOffer?: any;
|
||||
bluetoothConnectedTo?: ObjectMp;
|
||||
localStation?: any;
|
||||
localStationVolume?: number;
|
||||
killEvent?: any;
|
||||
killTime?: number;
|
||||
damageHistory: any[];
|
||||
deathState: DeathState;
|
||||
falling: boolean;
|
||||
lastMissclick?: number;
|
||||
drugVisualEffects?: any[];
|
||||
drugCameraEffect?: any;
|
||||
smoking?: any;
|
||||
drinking?: any;
|
||||
offer?: PlayerOffer;
|
||||
mdcInitialized?: boolean;
|
||||
loginTimeOut?: NodeJS.Timeout;
|
||||
loginAttempts: number;
|
||||
afkTimeout?: NodeJS.Timeout;
|
||||
trackedDutyIds: number[];
|
||||
afkStartTime?: number;
|
||||
afkDutyStartTime?: number;
|
||||
afkDutyEndTime?: number;
|
||||
tabStartTime?: number;
|
||||
tabEndTime?: number;
|
||||
adminDutyStartTime?: number;
|
||||
testerDutyStartTime?: number;
|
||||
quietModeStartTime?: number;
|
||||
pmWatchers: PlayerMpLoggedIn[];
|
||||
pmWatched: PlayerMpLoggedIn[];
|
||||
freqWatched: number[];
|
||||
factionWatched: any[];
|
||||
companyWatched: any[];
|
||||
muted?: any;
|
||||
lastKillAlert?: number;
|
||||
adminJail?: any;
|
||||
characterCreatorCharacterId?: number;
|
||||
helpmeRequest?: any;
|
||||
wikiBookmarks: any[];
|
||||
weapons?: any[];
|
||||
atm?: any;
|
||||
atmAccountUsed?: any;
|
||||
healthTracker?: NodeJS.Timer;
|
||||
actionLock: number;
|
||||
polygonEditor?: any;
|
||||
duty?: DutyState;
|
||||
serverWeapons: Record<string, number>;
|
||||
dutyTimeOverall?: number;
|
||||
specialDutyTimeOverall?: number;
|
||||
specialDuty?: number;
|
||||
lastWound?: number;
|
||||
maxHealth: number;
|
||||
regenTick?: number;
|
||||
deptFrequency?: any;
|
||||
friskRequests?: PlayerMpLoggedIn[];
|
||||
frisksApproved?: PlayerMpLoggedIn[];
|
||||
fishing?: any;
|
||||
barberShop?: any;
|
||||
adCooldown?: number;
|
||||
lastLessLethalHit?: number;
|
||||
lastPaintballHit?: number;
|
||||
lastPaintbillRuleHit?: number;
|
||||
paintballRuleHits?: number;
|
||||
attachingPlate?: any;
|
||||
removingPlate?: any;
|
||||
blockPm?: any;
|
||||
prisonCell?: any;
|
||||
propertyRadioColshapes?: any;
|
||||
vehicleLockBreakin?: VehicleWrapper;
|
||||
vehicleHotwire?: VehicleWrapper;
|
||||
bleedOut?: any;
|
||||
propertyBreakin?: number;
|
||||
dutyHandcuffs: number;
|
||||
applyingMakeup?: any;
|
||||
staticWeather?: string;
|
||||
staticTime?: number;
|
||||
pendingVehicle?: boolean;
|
||||
oocChatDisabled?: boolean;
|
||||
staffMessagesDisabled?: boolean;
|
||||
processingMissionKey?: boolean;
|
||||
showJoinMessages?: boolean;
|
||||
nametagDamageBlipTimeout?: NodeJS.Timeout;
|
||||
outfitOverrides: any[];
|
||||
lastWeaponShot?: any;
|
||||
autoLow?: any;
|
||||
vehicleModShop?: any;
|
||||
tvAccess?: number;
|
||||
lastPMSender?: PlayerMpLoggedIn;
|
||||
handlingCorpse?: boolean;
|
||||
tutorialCooldown?: number;
|
||||
mdcExtraData?: any;
|
||||
currentCallMenu?: string;
|
||||
radioFreqOrder?: any;
|
||||
lastEquippedBulletWeapon?: string;
|
||||
lastEquippedMeleeWeapon?: string;
|
||||
holdingObject?: boolean;
|
||||
tackleCooldown?: number;
|
||||
tackled?: boolean;
|
||||
changeCharacterCooldown?: number;
|
||||
corpseDigging?: boolean;
|
||||
chatHighlight?: number[];
|
||||
pushingGurney?: ObjectMp;
|
||||
onGurney?: ObjectMp;
|
||||
processingPhonePower?: boolean;
|
||||
lastTeleport?: any;
|
||||
fakeCall?: boolean;
|
||||
props: [number, number][];
|
||||
}
|
||||
|
||||
// ---- Misc types from original types.d.ts -------------------------------
|
||||
type DeathState = 'ALIVE' | 'WOUNDED' | 'KNOCKED' | 'DEAD';
|
||||
|
||||
interface PlayerOffer {
|
||||
target: PlayerWrapper;
|
||||
autoReject: NodeJS.Timeout;
|
||||
proximity?: number;
|
||||
resolve: (value: boolean) => void;
|
||||
}
|
||||
|
||||
interface VehicleMechanicOperation {
|
||||
name: string;
|
||||
mechanic: PlayerMpLoggedIn;
|
||||
customer: PlayerMpLoggedIn;
|
||||
vehicle: VehicleWrapper;
|
||||
companyId: number;
|
||||
vehicleEntity: import('../entities/vehicle.entity').VehicleEntity;
|
||||
activity: 'paint' | 'wheels' | 'tint' | 'upgrade' | 'repair' | 'suspension' | 'engine' | 'brakes';
|
||||
data: Record<string, any>;
|
||||
timer?: NodeJS.Timeout;
|
||||
timeReady: number;
|
||||
invoiceId: number;
|
||||
mechanicReward: number;
|
||||
}
|
||||
|
||||
interface VehicleAttachment {
|
||||
attachment: any;
|
||||
bone: string;
|
||||
collision: boolean;
|
||||
x: number; y: number; z: number;
|
||||
rx: number; ry: number; rz: number;
|
||||
opacity: number;
|
||||
}
|
||||
|
||||
interface VehicleLabel {
|
||||
text: string; font: number; color: number; size: number;
|
||||
outline: boolean; bone: string; offset: Vector3; opacity: number; distance: number;
|
||||
}
|
||||
|
||||
type CommandParamValidator =
|
||||
| 'player' | 'character' | 'account' | 'number' | 'integer'
|
||||
| 'string' | 'word' | 'vehicle' | 'property' | 'item'
|
||||
| 'company' | 'faction' | 'ped'
|
||||
| readonly any[] | any[] | RegExp;
|
||||
|
||||
type CommandGuard = (player: PlayerWrapper) => true | string;
|
||||
|
||||
type DutyState = { callsign: string; start: number; weapons: any[] };
|
||||
type BoundingBox = [number, number, number, number, number, number];
|
||||
type FineEntities = 'person' | 'vehicle' | 'company' | 'faction' | 'property';
|
||||
type EngineData = [string, string, number, number, number, number];
|
||||
type KeyValueCollection = Record<string, any>;
|
||||
type EventMpThis = any;
|
||||
}
|
||||
|
||||
declare global {
|
||||
// DamageHistory / KillEntry — from features/damage/state.d.ts
|
||||
type DamageHistory = {
|
||||
id: number;
|
||||
damager: { id: number; name: string; position: Vector3; dimension: number; health: number; armor: number };
|
||||
weapon: { gameId: number; weaponName: string; ammoType: string; currentWeaponAmmo: number };
|
||||
boneIndex: number; damage: number; time: number; position: Vector3; dimension: number;
|
||||
armorHit: boolean; distance: number; health: number; armor: number;
|
||||
bleeding: number | null; postMortem?: boolean;
|
||||
};
|
||||
|
||||
interface KillEntry {
|
||||
_id: string; victim_id: number; victim_name: string; victim_damages: DamageHistory[];
|
||||
killer_id: number | null; killer_name: string | null;
|
||||
kill_type: 'WOUNDED' | 'KNOCKED'; weapon: string; time: string;
|
||||
executed_time: string | null; executed_by_id: number | null;
|
||||
executed_by_name: string | null; executed_by_weapon: string | null;
|
||||
revived: string | null; respawned: string | null; accepted_death: string | null;
|
||||
rescued: string | null; rescued_by: string | null;
|
||||
}
|
||||
|
||||
// mp global — set at runtime by mp-shim.ts, declared here for TypeScript
|
||||
const mp: {
|
||||
events: {
|
||||
add(event: string, handler: Function): void;
|
||||
add(events: Record<string, Function>): void;
|
||||
addProc(name: string, handler: Function): void;
|
||||
call(event: string, ...args: any[]): void;
|
||||
callRemote(event: string, ...args: any[]): void;
|
||||
delayInitialization: boolean;
|
||||
};
|
||||
players: {
|
||||
at(id: number): PlayerWrapper | undefined;
|
||||
toArray(): PlayerWrapper[];
|
||||
toArrayFast(): PlayerWrapper[];
|
||||
forEach(fn: (player: PlayerWrapper) => void): void;
|
||||
broadcast(message: string): void;
|
||||
length: number;
|
||||
exists(player: PlayerWrapper): boolean;
|
||||
};
|
||||
Vector3: new (x: number, y: number, z: number) => Vector3;
|
||||
vehicles: {
|
||||
new(model: string | number, position: Vector3, options?: any): import('./vehicle/vehicle-wrapper').VehicleWrapper;
|
||||
at(id: number): import('./vehicle/vehicle-wrapper').VehicleWrapper | undefined;
|
||||
toArray(): import('./vehicle/vehicle-wrapper').VehicleWrapper[];
|
||||
forEach(fn: (vehicle: import('./vehicle/vehicle-wrapper').VehicleWrapper) => void): void;
|
||||
};
|
||||
config: Record<string, any>;
|
||||
objects: { new(...args: any[]): any; at(id: number): any; toArray(): any[]; forEach(fn: any): void };
|
||||
peds: { new(...args: any[]): any; at(id: number): any; toArray(): any[]; forEach(fn: any): void };
|
||||
markers: { new(...args: any[]): any };
|
||||
blips: { new(...args: any[]): any };
|
||||
labels: { new(...args: any[]): any };
|
||||
colshapes: {
|
||||
newSphere(...args: any[]): any;
|
||||
newCuboid(...args: any[]): any;
|
||||
newCircle(...args: any[]): any;
|
||||
newRectangle(...args: any[]): any;
|
||||
newTube(...args: any[]): any;
|
||||
newPolygon(...args: any[]): any;
|
||||
};
|
||||
};
|
||||
|
||||
type CommandParamMetaData<T> = {
|
||||
[K in keyof T]: {
|
||||
hint: string;
|
||||
validator: CommandParamValidator;
|
||||
default?: any;
|
||||
length?: number;
|
||||
overrides?: Record<string, Record<number, CommandParamMetaData<any>>>;
|
||||
};
|
||||
};
|
||||
|
||||
interface VehicleLogComposition {
|
||||
id?: number;
|
||||
gameId: number;
|
||||
owner?: string;
|
||||
model: string;
|
||||
plate: string;
|
||||
occupants: string;
|
||||
bodyHealth: number;
|
||||
engineHealth: number;
|
||||
speedInMph: number;
|
||||
}
|
||||
|
||||
// Client-side map object stubs (server never creates these directly in FiveM)
|
||||
interface BlipMp { destroy(): void; }
|
||||
interface MarkerMp { destroy(): void; }
|
||||
interface LabelMp { destroy(): void; }
|
||||
interface CheckpointMp { destroy(): void; }
|
||||
interface TextLabelMp { destroy(): void; text: string; }
|
||||
|
||||
// PedCustomization — from original types.d.ts
|
||||
interface PedCustomization {
|
||||
headBlend: { shapes: number[]; skins: number[]; shapeMix: number; skinMix: number; thirdMix: number };
|
||||
headOverlay: { overlayId: number; data: Array4d }[];
|
||||
faceFeatures: { featureId: number; data: number }[];
|
||||
hairData: { drawable: number; color1: number; color2: number };
|
||||
outfitData: { component: number; drawable: number[] }[];
|
||||
propData: { propIndex: number; prop: [number, number] }[];
|
||||
vehicle?: { status: boolean; vehID: number; seat: number };
|
||||
weapon?: number;
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
129
src/server/core/vehicle/vehicle-wrapper.ts
Normal file
129
src/server/core/vehicle/vehicle-wrapper.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { VehicleEntity } from '../../entities/vehicle.entity';
|
||||
import { ActiveJob } from '../../features/job/job.interface';
|
||||
|
||||
export class VehicleWrapper {
|
||||
readonly handle: number; // FiveM server entity handle
|
||||
|
||||
vehicleEntity?: VehicleEntity;
|
||||
job?: ActiveJob;
|
||||
dmvTest?: any;
|
||||
extraBitmap: number = 0;
|
||||
fuelLastPosition: Vector3 = { x: 0, y: 0, z: 0 } as Vector3;
|
||||
alarmTimer?: NodeJS.Timeout;
|
||||
keyInIgnition: boolean = false;
|
||||
engine: boolean = false; // tracked via client→server sync event
|
||||
bodyHealth: number = 1000; // synced from client
|
||||
engineHealth: number = 1000; // synced from client
|
||||
dimension: number = 0; // stored server-side
|
||||
mechanicOperation?: VehicleMechanicOperation;
|
||||
attachments?: VehicleAttachment[];
|
||||
labels?: VehicleLabel[];
|
||||
|
||||
constructor(handle: number) {
|
||||
this.handle = handle;
|
||||
}
|
||||
|
||||
// RageMP compat: vehicle.id maps to network ID (what clients use to identify the vehicle)
|
||||
get id(): number {
|
||||
return NetworkGetNetworkIdFromEntity(this.handle);
|
||||
}
|
||||
|
||||
get model(): number {
|
||||
return GetEntityModel(this.handle);
|
||||
}
|
||||
|
||||
get position(): Vector3 {
|
||||
const [x, y, z] = GetEntityCoords(this.handle);
|
||||
return { x, y, z } as Vector3;
|
||||
}
|
||||
|
||||
set position(v: Vector3) {
|
||||
SetEntityCoords(this.handle, v.x, v.y, v.z, false, false, false, false);
|
||||
}
|
||||
|
||||
get heading(): number {
|
||||
return GetEntityHeading(this.handle);
|
||||
}
|
||||
|
||||
set heading(h: number) {
|
||||
SetEntityHeading(this.handle, h);
|
||||
}
|
||||
|
||||
get numberPlate(): string {
|
||||
return GetVehicleNumberPlateText(this.handle);
|
||||
}
|
||||
|
||||
set numberPlate(plate: string) {
|
||||
SetVehicleNumberPlateText(this.handle, plate);
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
vehicleRegistry.remove(this.handle);
|
||||
DeleteEntity(this.handle);
|
||||
}
|
||||
|
||||
getVehicleOccupants(): import('../player/player-registry').PlayerWrapper[] {
|
||||
const { playerRegistry } = require('../player/player-registry');
|
||||
return playerRegistry.toArray().filter((p: any) => {
|
||||
const ped = GetPlayerPed(String(p.id));
|
||||
return GetVehiclePedIsIn(ped, false) === this.handle;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class VehicleRegistry {
|
||||
private vehicles: Map<number, VehicleWrapper> = new Map();
|
||||
|
||||
create(model: string | number, position: Vector3, options?: {
|
||||
heading?: number;
|
||||
numberPlate?: string;
|
||||
dimension?: number;
|
||||
color?: [number, number];
|
||||
}): VehicleWrapper {
|
||||
const modelHash = typeof model === 'string' ? GetHashKey(model) : model;
|
||||
const heading = options?.heading ?? 0;
|
||||
|
||||
const handle = CreateVehicle(
|
||||
modelHash,
|
||||
position.x, position.y, position.z,
|
||||
heading,
|
||||
true, // isNetwork
|
||||
false, // netMissionEntity
|
||||
);
|
||||
|
||||
if (options?.numberPlate) {
|
||||
SetVehicleNumberPlateText(handle, options.numberPlate);
|
||||
}
|
||||
|
||||
if (options?.color) {
|
||||
SetVehicleColours(handle, options.color[0], options.color[1]);
|
||||
}
|
||||
|
||||
const wrapper = new VehicleWrapper(handle);
|
||||
this.vehicles.set(handle, wrapper);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
remove(handle: number): void {
|
||||
this.vehicles.delete(handle);
|
||||
}
|
||||
|
||||
get(handle: number): VehicleWrapper | undefined {
|
||||
return this.vehicles.get(handle);
|
||||
}
|
||||
|
||||
getByNetId(netId: number): VehicleWrapper | undefined {
|
||||
const handle = NetworkGetEntityFromNetworkId(netId);
|
||||
return this.vehicles.get(handle);
|
||||
}
|
||||
|
||||
toArray(): VehicleWrapper[] {
|
||||
return [...this.vehicles.values()];
|
||||
}
|
||||
|
||||
forEach(fn: (vehicle: VehicleWrapper) => void): void {
|
||||
this.vehicles.forEach(fn);
|
||||
}
|
||||
}
|
||||
|
||||
export const vehicleRegistry = new VehicleRegistry();
|
||||
Reference in New Issue
Block a user