From 821cd589d9714840b2f291bf21b11151e5b3a071 Mon Sep 17 00:00:00 2001 From: Danya H Date: Tue, 8 Jul 2025 22:27:10 +0100 Subject: [PATCH] init --- .gitignore | 5 + biome.json | 40 +++ license.md | 15 + package.json | 37 ++ readme.md | 0 rpc/license.md | 15 + rpc/package.json | 37 ++ rpc/readme.md | 0 rpc/src/core/client.ts | 402 +++++++++++++++++++++ rpc/src/core/server.ts | 379 ++++++++++++++++++++ rpc/src/core/webview.ts | 289 +++++++++++++++ rpc/src/core/wrapper.ts | 51 +++ rpc/src/index.ts | 74 ++++ rpc/src/utils/emitter.ts | 54 +++ rpc/src/utils/funcs.ts | 50 +++ rpc/src/utils/native.ts | 321 +++++++++++++++++ rpc/src/utils/types.ts | 535 ++++++++++++++++++++++++++++ rpc/tsconfig.json | 19 + rpc/tsup.config.ts | 14 + shared-types/license.md | 15 + shared-types/package.json | 30 ++ shared-types/readme.md | 0 shared-types/types/types/index.d.ts | 52 +++ 23 files changed, 2434 insertions(+) create mode 100644 .gitignore create mode 100644 biome.json create mode 100644 license.md create mode 100644 package.json create mode 100644 readme.md create mode 100644 rpc/license.md create mode 100644 rpc/package.json create mode 100644 rpc/readme.md create mode 100644 rpc/src/core/client.ts create mode 100644 rpc/src/core/server.ts create mode 100644 rpc/src/core/webview.ts create mode 100644 rpc/src/core/wrapper.ts create mode 100644 rpc/src/index.ts create mode 100644 rpc/src/utils/emitter.ts create mode 100644 rpc/src/utils/funcs.ts create mode 100644 rpc/src/utils/native.ts create mode 100644 rpc/src/utils/types.ts create mode 100644 rpc/tsconfig.json create mode 100644 rpc/tsup.config.ts create mode 100644 shared-types/license.md create mode 100644 shared-types/package.json create mode 100644 shared-types/readme.md create mode 100644 shared-types/types/types/index.d.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..341de79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea +.vscode +dist +node_modules +bun.lock \ No newline at end of file diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..3ef3e3a --- /dev/null +++ b/biome.json @@ -0,0 +1,40 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false + }, + "formatter": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "javascript": { + "formatter": { + "arrowParentheses": "asNeeded", + "bracketSpacing": true, + "indentWidth": 2, + "lineEnding": "crlf", + "lineWidth": 80, + "quoteStyle": "single", + "semicolons": "asNeeded", + "trailingCommas": "all" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + } +} diff --git a/license.md b/license.md new file mode 100644 index 0000000..23ae589 --- /dev/null +++ b/license.md @@ -0,0 +1,15 @@ +Custom Attribution-NoDerivs Software License + +Copyright (c) 2025 Entity Seven Group + +This license allows you to use, copy, and distribute these packages (the "Software"), including for commercial purposes, provided that the following conditions are met: + +1. **Attribution:** You must give appropriate credit to the original author of the Software, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. + +2. **No Derivative Works:** You may not modify, transform, or build upon the Software. + +3. **Usage and Commercial Use:** You are allowed to use, sell, and gain income from projects that utilize the Software, as long as you comply with the terms of this license. + +4. **No Additional Restrictions:** You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/package.json b/package.json new file mode 100644 index 0000000..a88323f --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "workspaces": [ + "rpc", + "shared-types" + ], + "devDependencies": { + "@biomejs/biome": "^2.1.1", + "@types/bun": "^1.2.18" + }, + "peerDependencies": { + "typescript": "^5" + }, + "scripts": { + "check": "bunx biome check", + "format": "bunx biome format --write" + }, + "private": true, + "type": "module", + "license": "CC0-1.0", + "author": "Entity Seven Group", + "contributors": [ + { + "name": "Danya H", + "email": "dev.rilaxik@gmail.com", + "url": "https://github.com/rilaxik/" + }, + { + "name": "Oleksandr Honcharov", + "email": "0976053529@ukr.net", + "url": "https://github.com/SashaGoncharov19/" + } + ], + "repository": { + "type": "git", + "url": "https://github.com/rilaxik/fivem-rpc.git" + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/rpc/license.md b/rpc/license.md new file mode 100644 index 0000000..23ae589 --- /dev/null +++ b/rpc/license.md @@ -0,0 +1,15 @@ +Custom Attribution-NoDerivs Software License + +Copyright (c) 2025 Entity Seven Group + +This license allows you to use, copy, and distribute these packages (the "Software"), including for commercial purposes, provided that the following conditions are met: + +1. **Attribution:** You must give appropriate credit to the original author of the Software, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. + +2. **No Derivative Works:** You may not modify, transform, or build upon the Software. + +3. **Usage and Commercial Use:** You are allowed to use, sell, and gain income from projects that utilize the Software, as long as you comply with the terms of this license. + +4. **No Additional Restrictions:** You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/rpc/package.json b/rpc/package.json new file mode 100644 index 0000000..17af1eb --- /dev/null +++ b/rpc/package.json @@ -0,0 +1,37 @@ +{ + "name": "@entityseven/fivem-rpc", + "description": "FiveM RPC is an abstraction for events in GTA V FiveM servers in JS/TS", + "version": "0.1.0", + "main": "", + "types": "", + "files": [ + "types/**/*", + "readme.md", + "license.md" + ], + "keywords": [ + "fivem-rpc", + "fivem-rpc-shared-types", + "fivem", + "gta" + ], + "type": "module", + "author": "Entity Seven Group", + "contributors": [ + { + "name": "Danya H", + "email": "dev.rilaxik@gmail.com", + "url": "https://github.com/rilaxik/" + } + ], + "license": "Custom-Attribution-NoDerivs", + "devDependencies": { + "@microsoft/api-extractor": "^7.47.9", + "@citizenfx/client": "^2.0.15015-1", + "@citizenfx/server": "^2.0.14862-1", + "tsup": "^8.3.0" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/rpc/readme.md b/rpc/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/rpc/src/core/client.ts b/rpc/src/core/client.ts new file mode 100644 index 0000000..bc97ee7 --- /dev/null +++ b/rpc/src/core/client.ts @@ -0,0 +1,402 @@ +/// + +import type * as s from '@entityseven/fivem-rpc-shared-types' +import { Emitter } from '../utils/emitter' +import { generateUUID, parse, stringify, stringifyWeb } from '../utils/funcs' +import { + NATIVE_CLIENT_EVENTS, + NATIVE_CLIENT_NETWORK_EVENTS, +} from '../utils/native' +import { + type RPCConfig, + RPCErrors, + RPCEvents, + type RPCNativeClientEvents, + type RPCNativeClientNetworksEvents, + type RPCState, + type RPCStateRaw, + type RPCStateWeb, +} from '../utils/types' +import { Wrapper } from './wrapper' + +export class RPCInstanceClient extends Wrapper { + private readonly _emitterServer: Emitter + private readonly _pendingServer: Emitter + private readonly _emitterWeb: Emitter + private readonly _pendingWeb: Emitter + private readonly _pendingWebToServer: Emitter + + constructor(props: RPCConfig<'client'>) { + super(props) + + this._emitterServer = new Emitter() + this._pendingServer = new Emitter() + this._emitterWeb = new Emitter() + this._pendingWeb = new Emitter() + this._pendingWebToServer = new Emitter() + + this.console.log('[RPC] Initialized Client') + + onNet(RPCEvents.LISTENER_SERVER, this._handleServer.bind(this)) + RegisterNuiCallbackType(RPCEvents.LISTENER_WEB) + on( + `__cfx_nui:${RPCEvents.LISTENER_WEB}`, + async (data: RPCState, callback: (res: unknown) => void) => { + const res = await this._handleWeb(data) + callback(res) + }, + ) + } + + // ===== HANDLERS ===== + + private async _handleServer(payloadRaw: RPCStateRaw) { + try { + parse(payloadRaw) + } catch (e) { + throw new Error(RPCErrors.INVALID_DATA) + } + const payload = parse(payloadRaw) + + if (this.debug) { + this.console.log( + `[RPC]:client:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, + ) + } + + if (payload.type === 'event') { + if (payload.calledTo === 'client') { + this.verifyEvent(this._emitterServer, payload) + + const responseData = await this._emitterServer.emit( + payload.event, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + + const response: RPCState = { + event: payload.event, + uuid: payload.uuid, + calledFrom: 'client', + calledTo: 'server', + error: null, + data: [responseData], + player: payload.player, + type: 'response', + } + + emitNet(RPCEvents.LISTENER_CLIENT, stringify(response)) + } + if (payload.calledTo === 'webview') { + this._sendWebMessage({ + origin: RPCEvents.LISTENER_SERVER, + data: payload, + }) + } + } + if (payload.type === 'response') { + if (payload.calledTo === 'client') { + await this._pendingServer.emit( + payload.uuid, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + } + if (payload.calledTo === 'webview') { + await this._pendingWebToServer.emit( + payload.uuid, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + } + } + } + + private async _handleWeb(payload: RPCState): Promise { + if (this.debug) { + this.console.log( + `[RPC]:client:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, + ) + } + + if (payload.type === 'event') { + if (payload.calledTo === 'client') { + return await this._emitterWeb.emit( + payload.event, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + } + if (payload.calledTo === 'server') { + payload.player = GetPlayerServerId(PlayerId()) + emitNet(RPCEvents.LISTENER_WEB, stringify(payload)) + + return new Promise(res => { + this._pendingWebToServer.once(payload.uuid, res) + }) + } + } + + if (payload.type === 'response') { + if (payload.calledTo === 'client') { + await this._pendingWeb.emit( + payload.uuid, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + + return { status: 'ok' } + } + if (payload.calledTo === 'server') { + payload.player = GetPlayerServerId(PlayerId()) + emitNet(RPCEvents.LISTENER_WEB, stringify(payload)) + + return { status: 'ok' } + } + } + return { status: 'unknown' } + } + + // ===== SERVER ===== + + public onServer< + EventName extends keyof s.RPCEvents_ServerClient, + CallbackArguments extends Parameters, + CallbackReturn extends ReturnType, + >( + eventName: EventName, + cb: ( + ...args: CallbackArguments + ) => Awaited | Promise>, + ): this { + if (this.debug) { + this.console.log(`[RPC]:onServer ${eventName}`) + } + + this._emitterServer.on(eventName, cb) + + return this + } + + public offServer( + eventName: EventName, + ): this { + if (this.debug) { + this.console.log(`[RPC]:offServer ${eventName}`) + } + + this._emitterServer.off(eventName) + + return this + } + + public async emitServer< + EventName extends keyof s.RPCEvents_ClientServer, + Arguments extends Parameters, + Response extends ReturnType, + >(eventName: EventName, ...args: Arguments): Promise> { + const payload: RPCState = { + event: eventName, + uuid: generateUUID(), + calledFrom: 'client', + calledTo: 'server', + error: null, + data: args.length ? args : null, + player: GetPlayerServerId(PlayerId()), + type: 'event', + } + + emitNet(RPCEvents.LISTENER_CLIENT, stringify(payload)) + + return new Promise>(res => { + this._pendingServer.once(payload.uuid, res) + }) + } + + // ===== WEBVIEW ===== + + public onWebview< + EventName extends keyof s.RPCEvents_WebviewClient, + CallbackArguments extends Parameters, + CallbackReturn extends ReturnType, + >( + eventName: EventName, + cb: ( + ...args: CallbackArguments + ) => Awaited | Promise>, + ): this { + if (this.debug) { + this.console.log(`[RPC]:onWebview ${eventName}`) + } + + this._emitterWeb.on(eventName, cb) + + return this + } + + public offWebview( + eventName: EventName, + ): this { + if (this.debug) { + this.console.log(`[RPC]:offWebview ${eventName}`) + } + + this._emitterWeb.off(eventName) + + return this + } + + public async emitWebview< + EventName extends keyof s.RPCEvents_ClientWebview, + Arguments extends Parameters, + Response extends ReturnType, + >(eventName: EventName, ...args: Arguments): Promise> { + const payload: RPCState = { + event: eventName, + uuid: generateUUID(), + calledFrom: 'client', + calledTo: 'webview', + error: null, + data: args.length ? args : null, + player: PlayerId(), + type: 'event', + } + + this._sendWebMessage({ + origin: RPCEvents.LISTENER_CLIENT, + data: payload, + }) + + return new Promise>(res => { + this._pendingWeb.once(payload.uuid, res) + }) + } + + // ===== SELF ===== + + public onSelf< + EventName extends keyof s.RPCEvents_Client, + CallbackArguments extends Parameters, + CallbackReturn extends ReturnType, + >( + eventName: EventName, + cb: ( + ...args: CallbackArguments + ) => Awaited | Promise>, + ): this { + if (this.debug) { + this.console.log(`[RPC]:onSelf ${eventName}`) + } + + this._emitterLocal.on(eventName, cb) + + return this + } + + public offSelf( + eventName: EventName, + ): this { + if (this.debug) { + this.console.log(`[RPC]:offSelf ${eventName}`) + } + + this._emitterLocal.off(eventName) + + return this + } + + public async emitSelf< + EventName extends keyof s.RPCEvents_Client, + Arguments extends Parameters, + Response extends ReturnType, + >(eventName: EventName, ...args: Arguments): Promise> { + const payload: RPCState = { + event: eventName, + uuid: generateUUID(), + calledFrom: 'client', + calledTo: 'client', + error: null, + data: args.length ? args : null, + player: null, + type: 'event', + } + + if (this.debug) { + this.console.log( + `[RPC]:accepted ${payload.event} from ${payload.calledFrom}`, + ) + } + + this.verifyEvent(this._emitterLocal, payload) + + return await this._emitterLocal.emit>( + payload.event, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + } + + // ===== OTHER ===== + + public onCommand< + CommandName extends s.RPCCommands_Client, + CallbackArguments extends unknown[], + >( + command: CommandName, + cb: (player: number, args: CallbackArguments, commandRaw: string) => void, + ): this { + if (this.debug) { + this.console.log(`[RPC]:onCommand ${command}`) + } + + RegisterCommand(command, cb, false) + + return this + } + + public onNativeEvent< + EventName extends keyof RPCNativeClientEvents, + CallbackArguments extends Parameters, + >(eventName: EventName, cb: (...args: CallbackArguments) => void): this { + if (!NATIVE_CLIENT_EVENTS.includes(eventName)) { + throw new Error(RPCErrors.UNKNOWN_NATIVE) + } + + if (this.debug) { + this.console.log(`[RPC]:onNativeEvent ${eventName}`) + } + + on(eventName, cb) + + return this + } + + public onNativeNetworkEvent< + EventName extends keyof RPCNativeClientNetworksEvents, + CallbackArguments extends Parameters< + RPCNativeClientNetworksEvents[EventName] + >, + >(eventName: EventName, cb: (...args: CallbackArguments) => void): this { + if (!NATIVE_CLIENT_NETWORK_EVENTS.includes(eventName)) { + throw new Error(RPCErrors.UNKNOWN_NATIVE) + } + + if (this.debug) { + this.console.log(`[RPC]:onNativeNetworkEvent ${eventName}`) + } + + on(eventName, cb) + + return this + } + + public setWebviewFocus(hasFocus: boolean, hasCursor: boolean): this { + if (this.debug) { + this.console.log(`[RPC]:setWebviewFocus ${hasFocus} ${hasCursor}`) + } + + SetNuiFocus(hasFocus, hasCursor) + + return this + } + + // ===== UTILS ===== + + private _sendWebMessage(payload: RPCStateWeb): void { + SendNuiMessage(stringifyWeb(payload)) + } +} diff --git a/rpc/src/core/server.ts b/rpc/src/core/server.ts new file mode 100644 index 0000000..868b898 --- /dev/null +++ b/rpc/src/core/server.ts @@ -0,0 +1,379 @@ +/// + +import type * as s from '@entityseven/fivem-rpc-shared-types' +import { Emitter } from '../utils/emitter' +import { generateUUID, parse, stringify } from '../utils/funcs' +import { NATIVE_SERVER_EVENTS } from '../utils/native' +import { + type RPCConfig, + RPCErrors, + RPCEvents, + type RPCNativeServerEvents, + type RPCState, + type RPCStateRaw, +} from '../utils/types' +import { Wrapper } from './wrapper' + +export class RPCInstanceServer extends Wrapper { + private readonly _emitterClient: Emitter + private readonly _pendingClient: Emitter + private readonly _emitterWeb: Emitter + private readonly _pendingWeb: Emitter + + constructor(props: RPCConfig<'server'>) { + super(props) + + this._emitterClient = new Emitter() + this._pendingClient = new Emitter() + this._emitterWeb = new Emitter() + this._pendingWeb = new Emitter() + + this.console.log('[RPC] Initialized Server') + + onNet(RPCEvents.LISTENER_CLIENT, this._handleClient.bind(this)) + onNet(RPCEvents.LISTENER_WEB, this._handleWeb.bind(this)) + } + + // ===== HANDLERS ===== + + private async _handleClient(payloadRaw: RPCStateRaw) { + try { + parse(payloadRaw) + } catch (e) { + throw new Error(RPCErrors.INVALID_DATA) + } + const payload = parse(payloadRaw) + + if (this.debug) { + this.console.log( + `[RPC]:server:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, + ) + } + + if (payload.calledFrom === 'client') { + if (payload.type === 'event') { + this.verifyEvent(this._emitterClient, payload) + if (payload.player === null || payload.player === -1) { + payload.error = RPCErrors.NO_PLAYER + this.triggerError(payload) + return + } + + const responseData = await this._emitterClient.emit( + payload.event, + payload.player, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + + const response: RPCState = { + event: payload.event, + uuid: payload.uuid, + calledFrom: 'server', + calledTo: 'client', + error: null, + data: [responseData], + player: payload.player, + type: 'response', + } + + emitNet(RPCEvents.LISTENER_SERVER, response.player, stringify(response)) + } + if (payload.type === 'response') { + await this._pendingClient.emit( + payload.uuid, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + } + } + } + + private async _handleWeb(payloadRaw: RPCStateRaw) { + try { + parse(payloadRaw) + } catch (e) { + throw new Error(RPCErrors.INVALID_DATA) + } + const payload = parse(payloadRaw) + + if (this.debug) { + this.console.log( + `[RPC]:server:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, + ) + } + + if (payload.calledFrom === 'webview') { + if (payload.type === 'event') { + this.verifyEvent(this._emitterWeb, payload) + if (payload.player === null || payload.player === -1) { + payload.error = RPCErrors.NO_PLAYER + this.triggerError(payload) + return + } + + const responseData = await this._emitterWeb.emit( + payload.event, + payload.player, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + + const response: RPCState = { + event: payload.event, + uuid: payload.uuid, + calledFrom: 'server', + calledTo: 'webview', + error: null, + data: [responseData], + player: payload.player, + type: 'response', + } + + emitNet(RPCEvents.LISTENER_SERVER, response.player, stringify(response)) + } + if (payload.type === 'response') { + await this._pendingWeb.emit( + payload.uuid, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + } + } + } + + // ===== CLIENT ===== + + public onClient< + EventName extends keyof s.RPCEvents_ClientServer, + CallbackArguments extends Parameters, + CallbackReturn extends ReturnType, + >( + eventName: EventName, + cb: ( + player: number, + ...args: CallbackArguments + ) => Awaited | Promise>, + ): this { + if (this.debug) { + this.console.log(`[RPC]:onClient ${eventName}`) + } + + this._emitterClient.on(eventName, cb) + + return this + } + + public offClient( + eventName: EventName, + ): this { + if (this.debug) { + this.console.log(`[RPC]:offClient ${eventName}`) + } + + this._emitterClient.off(eventName) + + return this + } + + public async emitClient< + EventName extends keyof s.RPCEvents_ServerClient, + Arguments extends Parameters, + Response extends ReturnType, + >( + player: number, + eventName: EventName, + ...args: Arguments + ): Promise> { + const payload: RPCState = { + event: eventName, + uuid: generateUUID(), + calledFrom: 'server', + calledTo: 'client', + error: null, + data: args.length ? args : null, + player: player, + type: 'event', + } + + emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload)) + + return new Promise>(res => { + this._pendingClient.once(payload.uuid, res) + }) + } + + public async emitClientEveryone< + EventName extends keyof s.RPCEvents_ServerClient, + Arguments extends Parameters, + >(eventName: EventName, ...args: Arguments): Promise { + const payload: RPCState = { + event: eventName, + uuid: generateUUID(), + calledFrom: 'server', + calledTo: 'client', + error: null, + data: args.length ? args : null, + player: -1, + type: 'event', + } + + emitNet(RPCEvents.LISTENER_SERVER, -1, stringify(payload)) + } + + // ===== WEBVIEW ===== + + public onWebview< + EventName extends keyof s.RPCEvents_WebviewServer, + CallbackArguments extends Parameters, + CallbackReturn extends ReturnType, + >( + eventName: EventName, + cb: ( + player: number, + ...args: CallbackArguments + ) => Awaited | Promise>, + ): this { + if (this.debug) { + this.console.log(`[RPC]:onWebview ${eventName}`) + } + + this._emitterWeb.on(eventName, cb) + + return this + } + + public offWebview( + eventName: EventName, + ): this { + if (this.debug) { + this.console.log(`[RPC]:offWebview ${eventName}`) + } + + this._emitterWeb.off(eventName) + + return this + } + + public async emitWebview< + EventName extends keyof s.RPCEvents_ServerWebview, + Arguments extends Parameters, + Response extends ReturnType, + >( + player: number, + eventName: EventName, + ...args: Arguments + ): Promise> { + const payload: RPCState = { + event: eventName, + uuid: generateUUID(), + calledFrom: 'server', + calledTo: 'webview', + error: null, + data: args.length ? args : null, + player: player, + type: 'event', + } + + emitNet(RPCEvents.LISTENER_SERVER, player, stringify(payload)) + + return new Promise>(res => { + this._pendingWeb.once(payload.uuid, res) + }) + } + + // ===== SELF ===== + + public onSelf< + EventName extends keyof s.RPCEvents_Server, + CallbackArguments extends Parameters, + CallbackReturn extends ReturnType, + >( + eventName: EventName, + cb: ( + ...args: CallbackArguments + ) => Awaited | Promise>, + ): this { + if (this.debug) { + this.console.log(`[RPC]:onSelf ${eventName}`) + } + + this._emitterLocal.on(eventName, cb) + + return this + } + + public offSelf( + eventName: EventName, + ): this { + if (this.debug) { + this.console.log(`[RPC]:offSelf ${eventName}`) + } + + this._emitterLocal.off(eventName) + + return this + } + + public async emitSelf< + EventName extends keyof s.RPCEvents_Server, + Arguments extends Parameters, + Response extends ReturnType, + >(eventName: EventName, ...args: Arguments): Promise> { + const payload: RPCState = { + event: eventName, + uuid: generateUUID(), + calledFrom: 'server', + calledTo: 'server', + error: null, + data: args.length ? args : null, + player: null, + type: 'event', + } + + if (this.debug) { + this.console.log( + `[RPC]:accepted ${payload.event} from ${payload.calledFrom}`, + ) + } + + this.verifyEvent(this._emitterLocal, payload) + + return await this._emitterLocal.emit>( + payload.event, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + } + + // ===== OTHER ===== + + public onCommand< + CommandName extends s.RPCCommands_Server, + CallbackArguments extends unknown[], + >( + command: CommandName, + cb: (player: number, args: CallbackArguments, commandRaw: string) => void, + restricted = false, + ): this { + if (this.debug) { + this.console.log(`[RPC]:onCommand ${command}`) + } + + RegisterCommand(command, cb, restricted) + + return this + } + + public onNativeEvent< + EventName extends keyof RPCNativeServerEvents, + CallbackArguments extends Parameters, + >(eventName: EventName, cb: (...args: CallbackArguments) => void): this { + if (!NATIVE_SERVER_EVENTS.includes(eventName)) { + throw new Error(RPCErrors.UNKNOWN_NATIVE) + } + + if (this.debug) { + this.console.log(`[RPC]:onNativeEvent ${eventName}`) + } + + on(eventName, cb) + + return this + } +} diff --git a/rpc/src/core/webview.ts b/rpc/src/core/webview.ts new file mode 100644 index 0000000..a92cb52 --- /dev/null +++ b/rpc/src/core/webview.ts @@ -0,0 +1,289 @@ +import type * as s from '@entityseven/fivem-rpc-shared-types' +import { Emitter } from '../utils/emitter' +import { generateUUID, stringify } from '../utils/funcs' +import { + RPCEvents, + type RPCConfig, + type RPCState, + type RPCStateRaw, + type RPCStateWeb, +} from '../utils/types' +import { Wrapper } from './wrapper' + +declare global { + interface Window { + GetParentResourceName?: () => string + } +} + +export class RPCInstanceWebview extends Wrapper { + private readonly _emitterClient: Emitter + private readonly _emitterServer: Emitter + + constructor(props: RPCConfig<'webview'>) { + super(props) + + this._emitterClient = new Emitter() + this._emitterServer = new Emitter() + + this.console.log('[RPC] Initialized Webview') + + window.addEventListener('message', (e: MessageEvent) => { + if (e.data.origin === RPCEvents.LISTENER_CLIENT) { + this._handleClient(e.data.data) + } + if (e.data.origin === RPCEvents.LISTENER_SERVER) { + this._handleServer(e.data.data) + } + }) + } + + // ===== HANDLERS ===== + + private async _handleClient(payload: RPCState) { + if (this.debug) { + this.console.log( + `[RPC]:webview:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, + ) + } + + if (payload.calledFrom === 'client' && payload.type === 'event') { + this.verifyEvent(this._emitterClient, payload) + + const responseData = await this._emitterClient.emit( + payload.event, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + + const response: RPCState = { + event: payload.event, + uuid: payload.uuid, + calledFrom: 'webview', + calledTo: 'client', + error: null, + data: [responseData], + player: payload.player, + type: 'response', + } + + await this._createHttpClientRequest(response).then() + } + } + + private async _handleServer(payload: RPCState) { + if (this.debug) { + this.console.log( + `[RPC]:webview:accepted ${payload.type} ${payload.event} from ${payload.calledFrom}`, + ) + } + + if (payload.calledFrom === 'server' && payload.type === 'event') { + this.verifyEvent(this._emitterServer, payload) + + const responseData = await this._emitterServer.emit( + payload.event, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + + const response: RPCState = { + event: payload.event, + uuid: payload.uuid, + calledFrom: 'webview', + calledTo: 'server', + error: null, + data: [responseData], + player: payload.player, + type: 'response', + } + + await this._createHttpClientRequest(response) + } + } + + // ===== CLIENT ===== + + public onClient< + EventName extends keyof s.RPCEvents_ClientWebview, + CallbackArguments extends Parameters, + CallbackReturn extends ReturnType, + >( + eventName: EventName, + cb: ( + ...args: CallbackArguments + ) => Awaited | Promise>, + ): this { + if (this.debug) { + this.console.log(`[RPC]:onClient ${eventName}`) + } + + this._emitterClient.on(eventName, cb) + + return this + } + + public offClient( + eventName: EventName, + ): this { + if (this.debug) { + this.console.log(`[RPC]:offClient ${eventName}`) + } + + this._emitterClient.off(eventName) + + return this + } + + public async emitClient< + EventName extends keyof s.RPCEvents_WebviewClient, + Arguments extends Parameters, + Response extends ReturnType, + >(eventName: EventName, ...args: Arguments): Promise> { + const payload: RPCState = { + event: eventName, + uuid: generateUUID(), + calledFrom: 'webview', + calledTo: 'client', + error: null, + data: args.length ? args : null, + player: null, + type: 'event', + } + + return await this._createHttpClientRequest>(payload) + } + + // ===== SERVER ===== + + public onServer< + EventName extends keyof s.RPCEvents_ServerWebview, + CallbackArguments extends Parameters, + CallbackReturn extends ReturnType, + >( + eventName: EventName, + cb: ( + ...args: CallbackArguments + ) => Awaited | Promise>, + ): this { + if (this.debug) { + this.console.log(`[RPC]:onServer ${eventName}`) + } + + this._emitterServer.on(eventName, cb) + + return this + } + + public offServer( + eventName: EventName, + ): RPCInstanceWebview { + if (this.debug) { + this.console.log(`[RPC]:offServer ${eventName}`) + } + + this._emitterServer.off(eventName) + + return this + } + + public async emitServer< + EventName extends keyof s.RPCEvents_WebviewServer, + Arguments extends Parameters, + Response extends ReturnType, + >(eventName: EventName, ...args: Arguments): Promise> { + const payload: RPCState = { + event: eventName, + uuid: generateUUID(), + calledFrom: 'webview', + calledTo: 'server', + error: null, + data: args.length ? args : null, + player: null, + type: 'event', + } + + return await this._createHttpClientRequest>(payload) + } + + // ===== SELF ===== + + public onSelf< + EventName extends keyof s.RPCEvents_Webview, + CallbackArguments extends Parameters, + CallbackReturn extends ReturnType, + >( + eventName: EventName, + cb: ( + ...args: CallbackArguments + ) => Awaited | Promise>, + ): this { + if (this.debug) { + this.console.log(`[RPC]:onSelf ${eventName}`) + } + + this._emitterLocal.on(eventName, cb) + + return this + } + + public offSelf( + eventName: EventName, + ): this { + if (this.debug) { + this.console.log(`[RPC]:offSelf ${eventName}`) + } + + this._emitterLocal.off(eventName) + + return this + } + + public async emitSelf< + EventName extends keyof s.RPCEvents_Webview, + Arguments extends Parameters, + Response extends ReturnType, + >(eventName: EventName, ...args: Arguments): Promise> { + const payload: RPCState = { + event: eventName, + uuid: generateUUID(), + calledFrom: 'webview', + calledTo: 'webview', + error: null, + data: args.length ? args : null, + player: null, + type: 'event', + } + + if (this.debug) { + this.console.log( + `[RPC]:accepted ${payload.event} from ${payload.calledFrom}`, + ) + } + + this.verifyEvent(this._emitterLocal, payload) + + return await this._emitterLocal.emit>( + payload.event, + ...(payload.data && payload.data.length > 0 ? payload.data : []), + ) + } + + // ===== UTILS ===== + + private async _createHttpClientRequest( + data: RPCStateRaw | RPCState, + ): Promise { + const dataRaw = typeof data === 'string' ? data : stringify(data) + const options = { + method: 'post', + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: dataRaw, + } + const resourceName = window?.GetParentResourceName?.() ?? 'nui-frame-app' + return fetch( + `https://${resourceName}/${RPCEvents.LISTENER_WEB}`, + options, + ).then(res => res.json()) + } +} diff --git a/rpc/src/core/wrapper.ts b/rpc/src/core/wrapper.ts new file mode 100644 index 0000000..86f3061 --- /dev/null +++ b/rpc/src/core/wrapper.ts @@ -0,0 +1,51 @@ +import { Emitter } from '../utils/emitter' +import { parse } from '../utils/funcs' +import { + type RPCConfig, + type RPCEnvironment, + RPCErrors, + type RPCState, + type RPCStateRaw, +} from '../utils/types' + +export class Wrapper { + protected env: RPCEnvironment + protected _emitterLocal: Emitter + protected debug: boolean + protected console: Console + + constructor(cfg: RPCConfig) { + this.env = cfg.env + this._emitterLocal = new Emitter() + this.debug = cfg.debug ?? false + this.console = console + } + + protected verifyEvent(state: Emitter, data: RPCStateRaw | RPCState) { + const rpcData = typeof data === 'string' ? parse(data) : data + + if (!state.has(rpcData.event)) { + rpcData.error = RPCErrors.EVENT_NOT_REGISTERED + this.triggerError(rpcData) + } + } + + protected triggerError(rpcData: RPCState, error?: string): Error { + const errorMessage = [ + `${rpcData.error}`, + `Event: ${rpcData.event}`, + `Uuid: ${rpcData.uuid}`, + `From: ${rpcData.calledFrom}`, + `To: ${rpcData.calledTo}`, + `Player: ${rpcData.player}`, + `Type: ${rpcData.type}`, + `Data: ${rpcData.data}`, + ] + + if (error) { + errorMessage.push(`Info: ${error}`) + } + + throw new Error(errorMessage.join('\n | ')) + } +} diff --git a/rpc/src/index.ts b/rpc/src/index.ts new file mode 100644 index 0000000..9cd2671 --- /dev/null +++ b/rpc/src/index.ts @@ -0,0 +1,74 @@ +import { RPCInstanceClient } from './core/client' +import { RPCInstanceServer } from './core/server' +import { RPCInstanceWebview } from './core/webview' +import { Wrapper } from './core/wrapper' +import { + type RPCConfig, + type RPCEnvironment, + type RPCEnvironmentResolved, + RPCErrors, +} from './utils/types' + +/** + * RPC Factory + * + * @example + * // returns RPCInstanceServer + * const rpc = new RPCFactory({ env: "server" }).get() + * + * @example + * // returns RPCInstanceClient + * const rpc = new RPCFactory({ env: "client" }).get() + * + * @example + * // returns RPCInstanceWebview + * const rpc = new RPCFactory({ env: "webview" }).get() + * + * @class + */ +class RPCFactory extends Wrapper { + private readonly operator: + | RPCInstanceServer + | RPCInstanceClient + | RPCInstanceWebview + + /** + * Instance options + * @param {object} opts - Options + * @param {string} opts.env - Instance environment + * @param {boolean} opts.debug - Show additional logs + */ + constructor(opts: RPCConfig) { + super(opts) + + this.console.log('[RPC] Initializing...') + + switch (opts.env) { + case 'server': + this.operator = new RPCInstanceServer(opts as RPCConfig<'server'>) + break + case 'client': + this.operator = new RPCInstanceClient(opts as RPCConfig<'client'>) + break + case 'webview': + this.operator = new RPCInstanceWebview(opts as RPCConfig<'webview'>) + break + default: + throw new Error(RPCErrors.UNKNOWN_ENVIRONMENT) + } + } + + public get(): RPCEnvironmentResolved { + return this.operator as RPCEnvironmentResolved + } +} + +export { RPCFactory } +// export const rpcClient = new RPCFactory({ env: "client" }).get(); +// export const rpcServer = new RPCFactory({ env: "server" }).get(); +// export const rpcWebview = new RPCFactory({ env: "webview" }).get(); +export * from './utils/types' +export * from './utils/native' +export type * from './core/server' +export type * from './core/client' +export type * from './core/webview' diff --git a/rpc/src/utils/emitter.ts b/rpc/src/utils/emitter.ts new file mode 100644 index 0000000..9adca71 --- /dev/null +++ b/rpc/src/utils/emitter.ts @@ -0,0 +1,54 @@ +import { RPCErrors } from './types' + +export class Emitter { + /** Map */ + private _storage: Map any, boolean]> + + constructor() { + this._storage = new Map() + } + + get _raw_storage() { + return this._storage + } + + public on(event: string, cb: (...args: any[]) => any): this { + this._storage.set(event, [cb, false]) + return this + } + + public once(event: string, cb: (...args: any[]) => any): this { + this._storage.set(event, [cb, true]) + return this + } + + public off(event: string): this { + this._storage.delete(event) + return this + } + + public has(event: string): boolean { + return this._storage.has(event) + } + + public async emit(event: string, ...args: any[]): Promise { + return new Promise((res, rej) => { + if (!this._storage.has(event)) { + rej(RPCErrors.EVENT_NOT_REGISTERED) + } + + const [cb, once] = this._storage.get(event) as [ + (...args: any[]) => any, + boolean, + ] + + if (once) { + this._storage.delete(event) + } + + Promise.resolve(cb(...args)) + .then(res) + .catch(rej) + }) + } +} diff --git a/rpc/src/utils/funcs.ts b/rpc/src/utils/funcs.ts new file mode 100644 index 0000000..56c81c9 --- /dev/null +++ b/rpc/src/utils/funcs.ts @@ -0,0 +1,50 @@ +import type { + RPCState, + RPCStateRaw, + RPCStateWeb, + RPCStateWebRaw, +} from './types' + +/** + * **Internal** + * + * Typed data parser + */ +export function parse(data: RPCStateRaw): RPCState { + return JSON.parse(data) +} + +/** + * **Internal** + * + * Typed data serializer + */ +export function stringify(data: RPCState): RPCStateRaw { + return JSON.stringify(data) as RPCStateRaw +} + +// automatically parsed by FiveM +// export function parseWeb(data: RPCStateWebRaw): RPCStateWeb { +// return JSON.parse(data) +// } + +/** + * **Internal** + * + * Typed data serializer + */ +export function stringifyWeb(data: RPCStateWeb): RPCStateWebRaw { + return JSON.stringify(data) as RPCStateWebRaw +} + +/** **Internal** */ +export function generateUUID(): string { + let uuid = '' + let random = 0 + for (let i = 0; i < 32; i++) { + random = (Math.random() * 16) | 0 + if (i === 8 || i === 12 || i === 16 || i === 20) uuid += '-' + uuid += (i === 12 ? 4 : i === 16 ? (random & 3) | 8 : random).toString(16) + } + return uuid +} diff --git a/rpc/src/utils/native.ts b/rpc/src/utils/native.ts new file mode 100644 index 0000000..ab739c9 --- /dev/null +++ b/rpc/src/utils/native.ts @@ -0,0 +1,321 @@ +import type { + RPCNativeClientEvents, + RPCNativeClientNetworkEventsNames, + RPCNativeServerEvents, +} from './types' + +/** + * https://docs.fivem.net/docs/scripting-reference/events/server-events/ + * @readonly + */ +export const NATIVE_SERVER_EVENTS: readonly (keyof RPCNativeServerEvents)[] = [ + 'entityCreated', + 'entityCreating', + 'entityRemoved', + 'onResourceListRefresh', + 'onResourceStart', + 'onResourceStarting', + 'onResourceStop', + 'onServerResourceStart', + 'onServerResourceStop', + 'playerConnecting', + 'playerEnteredScope', + 'playerJoining', + 'playerLeftScope', + 'ptFxEvent', + 'removeAllWeaponsEvent', + 'startProjectileEvent', + 'weaponDamageEvent', +] as const + +/** + * https://docs.fivem.net/docs/scripting-reference/events/client-events/ + * @readonly + */ +export const NATIVE_CLIENT_EVENTS: readonly (keyof RPCNativeClientEvents)[] = [ + 'entityDamaged', + 'gameEventTriggered', + 'mumbleConnected', + 'mumbleDisconnected', + 'onClientResourceStart', + 'onClientResourceStop', + 'onResourceStart', + 'onResourceStarting', + 'onResourceStop', + 'populationPedCreating', +] as const + +/** + * https://docs.fivem.net/docs/game-references/game-events/ + * @readonly + */ +export const NATIVE_CLIENT_NETWORK_EVENTS: readonly RPCNativeClientNetworkEventsNames[] = + [ + 'CEventAcquaintancePed', + 'CEventAcquaintancePedDead', + 'CEventAcquaintancePedDislike', + 'CEventAcquaintancePedHate', + 'CEventAcquaintancePedLike', + 'CEventAcquaintancePedWanted', + 'CEventAgitated', + 'CEventAgitatedAction', + 'CEventCallForCover', + 'CEventCarUndriveable', + 'CEventClimbLadderOnRoute', + 'CEventClimbNavMeshOnRoute', + 'CEventCombatTaunt', + 'CEventCommunicateEvent', + 'CEventCopCarBeingStolen', + 'CEventCrimeCryForHelp', + 'CEventCrimeReported', + 'CEventDamage', + 'CEventDataDecisionMaker', + 'CEventDataFileMounter', + 'CEventDataResponseAggressiveRubberneck', + 'CEventDataResponseDeferToScenarioPointFlags', + 'CEventDataResponseFriendlyAimedAt', + 'CEventDataResponseFriendlyNearMiss', + 'CEventDataResponsePlayerDeath', + 'CEventDataResponsePoliceTaskWanted', + 'CEventDataResponseSwatTaskWanted', + 'CEventDataResponseTask', + 'CEventDataResponseTaskAgitated', + 'CEventDataResponseTaskCombat', + 'CEventDataResponseTaskCower', + 'CEventDataResponseTaskCrouch', + 'CEventDataResponseTaskDuckAndCover', + 'CEventDataResponseTaskEscapeBlast', + 'CEventDataResponseTaskEvasiveStep', + 'CEventDataResponseTaskExhaustedFlee', + 'CEventDataResponseTaskExplosion', + 'CEventDataResponseTaskFlee', + 'CEventDataResponseTaskFlyAway', + 'CEventDataResponseTaskGrowlAndFlee', + 'CEventDataResponseTaskGunAimedAt', + 'CEventDataResponseTaskHandsUp', + 'CEventDataResponseTaskHeadTrack', + 'CEventDataResponseTaskLeaveCarAndFlee', + 'CEventDataResponseTaskScenarioFlee', + 'CEventDataResponseTaskSharkAttack', + 'CEventDataResponseTaskShockingEventBackAway', + 'CEventDataResponseTaskShockingEventGoto', + 'CEventDataResponseTaskShockingEventHurryAway', + 'CEventDataResponseTaskShockingEventReact', + 'CEventDataResponseTaskShockingEventReactToAircraft', + 'CEventDataResponseTaskShockingEventStopAndStare', + 'CEventDataResponseTaskShockingEventThreatResponse', + 'CEventDataResponseTaskShockingEventWatch', + 'CEventDataResponseTaskShockingNiceCar', + 'CEventDataResponseTaskShockingPoliceInvestigate', + 'CEventDataResponseTaskThreat', + 'CEventDataResponseTaskTurnToFace', + 'CEventDataResponseTaskWalkAway', + 'CEventDataResponseTaskWalkRoundEntity', + 'CEventDataResponseTaskWalkRoundFire', + 'CEventDeadPedFound', + 'CEventDeath', + 'CEventDecisionMakerResponse', + 'CEventDisturbance', + 'CEventDraggedOutCar', + 'CEventEditableResponse', + 'CEventEncroachingPed', + 'CEventEntityDamaged', + 'CEventEntityDestroyed', + 'CEventExplosion', + 'CEventExplosionHeard', + 'CEventFireNearby', + 'CEventFootStepHeard', + 'CEventFriendlyAimedAt', + 'CEventFriendlyFireNearMiss', + 'CEventGetOutOfWater', + 'CEventGivePedTask', + 'CEventGroupScriptAI', + 'CEventGroupScriptNetwork', + 'CEventGunAimedAt', + 'CEventGunShot', + 'CEventGunShotBulletImpact', + 'CEventGunShotWhizzedBy', + 'CEventHelpAmbientFriend', + 'CEventHurtTransition', + 'CEventInAir', + 'CEventInfo', + 'CEventInfoBase', + 'CEventInjuredCryForHelp', + 'CEventLeaderEnteredCarAsDriver', + 'CEventLeaderExitedCarAsDriver', + 'CEventLeaderHolsteredWeapon', + 'CEventLeaderLeftCover', + 'CEventLeaderUnholsteredWeapon', + 'CEventMeleeAction', + 'CEventMustLeaveBoat', + 'CEventNetworkAdminInvited', + 'CEventNetworkAttemptHostMigration', + 'CEventNetworkBail', + 'CEventNetworkCashTransactionLog', + 'CEventNetworkCheatTriggered', + 'CEventNetworkClanInviteReceived', + 'CEventNetworkClanJoined', + 'CEventNetworkClanKicked', + 'CEventNetworkClanLeft', + 'CEventNetworkClanRankChanged', + 'CEventNetworkCloudEvent', + 'CEventNetworkCloudFileResponse', + 'CEventNetworkEmailReceivedEvent', + 'CEventNetworkEndMatch', + 'CEventNetworkEndSession', + 'CEventNetworkEntityDamage', + 'CEventNetworkFindSession', + 'CEventNetworkFollowInviteReceived', + 'CEventNetworkHostMigration', + 'CEventNetworkHostSession', + 'CEventNetworkIncrementStat', + 'CEventNetworkInviteAccepted', + 'CEventNetworkInviteConfirmed', + 'CEventNetworkInviteRejected', + 'CEventNetworkJoinSession', + 'CEventNetworkJoinSessionResponse', + 'CEventNetworkOnlinePermissionsUpdated', + 'CEventNetworkPedLeftBehind', + 'CEventNetworkPickupRespawned', + 'CEventNetworkPlayerArrest', + 'CEventNetworkPlayerCollectedAmbientPickup', + 'CEventNetworkPlayerCollectedPickup', + 'CEventNetworkPlayerCollectedPortablePickup', + 'CEventNetworkPlayerDroppedPortablePickup', + 'CEventNetworkPlayerEnteredVehicle', + 'CEventNetworkPlayerJoinScript', + 'CEventNetworkPlayerLeftScript', + 'CEventNetworkPlayerScript', + 'CEventNetworkPlayerSession', + 'CEventNetworkPlayerSpawn', + 'CEventNetworkPresenceInvite', + 'CEventNetworkPresenceInviteRemoved', + 'CEventNetworkPresenceInviteReply', + 'CEventNetworkPresenceTriggerEvent', + 'CEventNetworkPresence_StatUpdate', + 'CEventNetworkPrimaryClanChanged', + 'CEventNetworkRequestDelay', + 'CEventNetworkRosChanged', + 'CEventNetworkScAdminPlayerUpdated', + 'CEventNetworkScAdminReceivedCash', + 'CEventNetworkScriptEvent', + 'CEventNetworkSessionEvent', + 'CEventNetworkShopTransaction', + 'CEventNetworkSignInStateChanged', + 'CEventNetworkSocialClubAccountLinked', + 'CEventNetworkSpectateLocal', + 'CEventNetworkStartMatch', + 'CEventNetworkStartSession', + 'CEventNetworkStorePlayerLeft', + 'CEventNetworkSummon', + 'CEventNetworkSystemServiceEvent', + 'CEventNetworkTextMessageReceived', + 'CEventNetworkTimedExplosion', + 'CEventNetworkTransitionEvent', + 'CEventNetworkTransitionGamerInstruction', + 'CEventNetworkTransitionMemberJoined', + 'CEventNetworkTransitionMemberLeft', + 'CEventNetworkTransitionParameterChanged', + 'CEventNetworkTransitionStarted', + 'CEventNetworkTransitionStringChanged', + 'CEventNetworkVehicleUndrivable', + 'CEventNetworkVoiceConnectionRequested', + 'CEventNetworkVoiceConnectionResponse', + 'CEventNetworkVoiceConnectionTerminated', + 'CEventNetworkVoiceSessionEnded', + 'CEventNetworkVoiceSessionStarted', + 'CEventNetworkWithData', + 'CEventNetwork_InboxMsgReceived', + 'CEventNewTask', + 'CEventObjectCollision', + 'CEventOnFire', + 'CEventOpenDoor', + 'CEventPedCollisionWithPed', + 'CEventPedCollisionWithPlayer', + 'CEventPedEnteredMyVehicle', + 'CEventPedJackingMyVehicle', + 'CEventPedOnCarRoof', + 'CEventPedSeenDeadPed', + 'CEventPlayerCollisionWithPed', + 'CEventPlayerDeath', + 'CEventPlayerUnableToEnterVehicle', + 'CEventPotentialBeWalkedInto', + 'CEventPotentialBlast', + 'CEventPotentialGetRunOver', + 'CEventPotentialWalkIntoVehicle', + 'CEventProvidingCover', + 'CEventRanOverPed', + 'CEventReactionEnemyPed', + 'CEventReactionInvestigateDeadPed', + 'CEventReactionInvestigateThreat', + 'CEventRequestHelp', + 'CEventRequestHelpWithConfrontation', + 'CEventRespondedToThreat', + 'CEventScanner', + 'CEventScenarioForceAction', + 'CEventScriptCommand', + 'CEventScriptWithData', + 'CEventShocking', + 'CEventShockingBicycleCrash', + 'CEventShockingBicycleOnPavement', + 'CEventShockingCarAlarm', + 'CEventShockingCarChase', + 'CEventShockingCarCrash', + 'CEventShockingCarOnCar', + 'CEventShockingCarPileUp', + 'CEventShockingDangerousAnimal', + 'CEventShockingDeadBody', + 'CEventShockingDrivingOnPavement', + 'CEventShockingEngineRevved', + 'CEventShockingExplosion', + 'CEventShockingFire', + 'CEventShockingGunFight', + 'CEventShockingGunshotFired', + 'CEventShockingHelicopterOverhead', + 'CEventShockingHornSounded', + 'CEventShockingInDangerousVehicle', + 'CEventShockingInjuredPed', + 'CEventShockingMadDriver', + 'CEventShockingMadDriverBicycle', + 'CEventShockingMadDriverExtreme', + 'CEventShockingMugging', + 'CEventShockingNonViolentWeaponAimedAt', + 'CEventShockingParachuterOverhead', + 'CEventShockingPedKnockedIntoByPlayer', + 'CEventShockingPedRunOver', + 'CEventShockingPedShot', + 'CEventShockingPlaneFlyby', + 'CEventShockingPotentialBlast', + 'CEventShockingPropertyDamage', + 'CEventShockingRunningPed', + 'CEventShockingRunningStampede', + 'CEventShockingSeenCarStolen', + 'CEventShockingSeenConfrontation', + 'CEventShockingSeenGangFight', + 'CEventShockingSeenInsult', + 'CEventShockingSeenMeleeAction', + 'CEventShockingSeenNiceCar', + 'CEventShockingSeenPedKilled', + 'CEventShockingSiren', + 'CEventShockingStudioBomb', + 'CEventShockingVehicleTowed', + 'CEventShockingVisibleWeapon', + 'CEventShockingWeaponThreat', + 'CEventShockingWeirdPed', + 'CEventShockingWeirdPedApproaching', + 'CEventShoutBlockingLos', + 'CEventShoutTargetPosition', + 'CEventShovePed', + 'CEventSoundBase', + 'CEventStatChangedValue', + 'CEventStaticCountReachedMax', + 'CEventStuckInAir', + 'CEventSuspiciousActivity', + 'CEventSwitch2NM', + 'CEventUnidentifiedPed', + 'CEventVehicleCollision', + 'CEventVehicleDamage', + 'CEventVehicleDamageWeapon', + 'CEventVehicleOnFire', + 'CEventWrithe', + ] as const diff --git a/rpc/src/utils/types.ts b/rpc/src/utils/types.ts new file mode 100644 index 0000000..e08ad43 --- /dev/null +++ b/rpc/src/utils/types.ts @@ -0,0 +1,535 @@ +import type { RPCInstanceClient } from '../core/client' +import type { RPCInstanceServer } from '../core/server' +import type { RPCInstanceWebview } from '../core/webview' + +/** + * Possible environment states for `RPCConfig` + */ +export type RPCEnvironment = 'server' | 'client' | 'webview' + +export type RPCEnvironmentResolved = + T extends 'server' + ? RPCInstanceServer + : T extends 'client' + ? RPCInstanceClient + : T extends 'webview' + ? RPCInstanceWebview + : never + +/** + * `RPCFactory` config. + * + * If environment does not match will throw `RPCErrors.UNKNOWN_ENVIRONMENT` + */ +export type RPCConfig = { + env: T + debug?: boolean +} + +/** **Internal** */ +export type RPCEventType = 'event' | 'response' + +/** + * **Internal** + * + * Similar to what Errors look like + */ +export type RPCState = { + event: string + uuid: string + calledFrom: RPCEnvironment + calledTo: RPCEnvironment + error: string | null + data: unknown[] | null + player: number | null + type: RPCEventType +} + +/** + * **Internal** + * + * `JSON.stringify` version of `RPCState`. Makes TS think this is not type `string` for better dx + */ +export type RPCStateRaw = string & { __brand: 'RPCStateRaw' } + +/** Internal */ +export type RPCStateWeb = { + origin: RPCEvents + data: RPCState +} + +/** + * **Internal** + * + * `JSON.stringify` version of `RPCStateWeb`. Makes TS think this is not type `string` for better dx + */ +export type RPCStateWebRaw = string & { __brand: 'RPCWebStateRaw' } + +/** + * **Internal** + * + * Do not create same listeners to avoid unexpected behaviour + */ +export enum RPCEvents { + LISTENER_SERVER = '__rpc:listenerServer', + LISTENER_CLIENT = '__rpc:listenerClient', + LISTENER_WEB = '__rpc:listenerWeb', +} + +/** + * Errors to check against + */ +export enum RPCErrors { + EVENT_NOT_REGISTERED = 'Event not registered', + INVALID_DATA = 'Invalid data (possibly broken JSON)', + NO_PLAYER = 'No player (failed to resolve from local index)', + UNKNOWN_NATIVE = 'Unknown native event (if you are sure this exists - use native handler)', + UNKNOWN_ENVIRONMENT = 'Unknown environment (must be either "server", "client" or "webview")', +} + +/** + * https://docs.fivem.net/docs/scripting-reference/events/server-events/ + */ +export type RPCNativeServerEvents = { + entityCreated(handle: number): void + entityCreating(handle: number): void + entityRemoved(entity: number): void + onResourceListRefresh(): void + onResourceStart(resource: string): void + onResourceStarting(resource: string): void + onResourceStop(resource: string): void + onServerResourceStart(resource: string): void + onServerResourceStop(resource: string): void + playerConnecting( + playerName: string, + setKickReason: (reason: string) => void, + deferrals: { + defer: () => void + done: (failureReason?: string) => void + handover: (data: Record) => void + presentCard: ( + card: string | object, + cb?: (data: unknown, rawData: string) => void, + ) => void + update: (message: string) => void + }, + source: number, + ): void + playerEnteredScope(data: { for: string; player: string }): void + playerJoining(source: string, oldID: string): void + playerLeftScope(data: { for: string; player: string }): void + ptFxEvent( + sender: number, + data: { + assetHash: number + axisBitset: number + effectHash: number + entityNetId: number + f100: number + f105: number + f106: number + f107: number + f109: boolean + f110: boolean + f111: boolean + f92: number + isOnEntity: boolean + offsetX: number + offsetY: number + offsetZ: number + posX: number + posY: number + posZ: number + rotX: number + rotY: number + rotZ: number + scale: number + }, + ): void + removeAllWeaponsEvent(sender: number, data: { pedId: number }): void + startProjectileEvent( + sender: number, + data: { + commandFireSingleBullet: boolean + effectGroup: number + firePositionX: number + firePositionY: number + firePositionZ: number + initialPositionX: number + initialPositionY: number + initialPositionZ: number + ownerId: number + projectileHash: number + targetEntity: number + throwTaskSequence: number + unk10: number + unk11: number + unk12: number + unk13: number + unk14: number + unk15: number + unk16: number + unk3: number + unk4: number + unk5: number + unk6: number + unk7: number + unk9: number + unkX8: number + unkY8: number + unkZ8: number + weaponHash: number + }, + ): void + weaponDamageEvent( + sender: number, + data: { + actionResultId: number + actionResultName: number + damageFlags: number + damageTime: number + damageType: number + f104: number + f112: boolean + f112_1: number + f120: number + f133: boolean + hasActionResult: boolean + hasImpactDir: boolean + hasVehicleData: boolean + hitComponent: number + hitEntityWeapon: boolean + hitGlobalId: number + hitGlobalIds: number[] + hitWeaponAmmoAttachment: boolean + impactDirX: number + impactDirY: number + impactDirZ: number + isNetTargetPos: boolean + localPosX: number + localPosY: number + localPosZ: number + overrideDefaultDamage: boolean + parentGlobalId: number + silenced: boolean + suspensionIndex: number + tyreIndex: number + weaponDamage: number + weaponType: number + willKill: boolean + }, + ): void +} + +/** + * https://docs.fivem.net/docs/scripting-reference/events/client-events/ + */ +export type RPCNativeClientEvents = { + entityDamaged( + victim: number, + culprit: number, + weapon: number, + baseDamage: number, + ): void + gameEventTriggered( + name: RPCNativeClientNetworksEvents | string, + data: number[], + ): void + mumbleConnected(address: string, reconnecting: boolean): void + mumbleDisconnected(address: string): void + onClientResourceStart(resource: string): void + onClientResourceStop(resource: string): void + onResourceStart(resource: string): void + onResourceStarting(resource: string): void + onResourceStop(resource: string): void + populationPedCreating( + x: number, + y: number, + z: number, + model: number, + overrideCalls: { + setModel: (model: string | number) => void + setPosition: (x: number, y: number, z: number) => void + }, + ): void +} + +export type RPCNativeClientNetworksEvents = { + [name in RPCNativeClientNetworkEventsNames]: ( + entities: number[], + eventEntity: number, + data: unknown[], + ) => void +} + +/** + * https://docs.fivem.net/docs/game-references/game-events/ + */ +export type RPCNativeClientNetworkEventsNames = + | 'CEventAcquaintancePed' + | 'CEventAcquaintancePedDead' + | 'CEventAcquaintancePedDislike' + | 'CEventAcquaintancePedHate' + | 'CEventAcquaintancePedLike' + | 'CEventAcquaintancePedWanted' + | 'CEventAgitated' + | 'CEventAgitatedAction' + | 'CEventCallForCover' + | 'CEventCarUndriveable' + | 'CEventClimbLadderOnRoute' + | 'CEventClimbNavMeshOnRoute' + | 'CEventCombatTaunt' + | 'CEventCommunicateEvent' + | 'CEventCopCarBeingStolen' + | 'CEventCrimeCryForHelp' + | 'CEventCrimeReported' + | 'CEventDamage' + | 'CEventDataDecisionMaker' + | 'CEventDataFileMounter' + | 'CEventDataResponseAggressiveRubberneck' + | 'CEventDataResponseDeferToScenarioPointFlags' + | 'CEventDataResponseFriendlyAimedAt' + | 'CEventDataResponseFriendlyNearMiss' + | 'CEventDataResponsePlayerDeath' + | 'CEventDataResponsePoliceTaskWanted' + | 'CEventDataResponseSwatTaskWanted' + | 'CEventDataResponseTask' + | 'CEventDataResponseTaskAgitated' + | 'CEventDataResponseTaskCombat' + | 'CEventDataResponseTaskCower' + | 'CEventDataResponseTaskCrouch' + | 'CEventDataResponseTaskDuckAndCover' + | 'CEventDataResponseTaskEscapeBlast' + | 'CEventDataResponseTaskEvasiveStep' + | 'CEventDataResponseTaskExhaustedFlee' + | 'CEventDataResponseTaskExplosion' + | 'CEventDataResponseTaskFlee' + | 'CEventDataResponseTaskFlyAway' + | 'CEventDataResponseTaskGrowlAndFlee' + | 'CEventDataResponseTaskGunAimedAt' + | 'CEventDataResponseTaskHandsUp' + | 'CEventDataResponseTaskHeadTrack' + | 'CEventDataResponseTaskLeaveCarAndFlee' + | 'CEventDataResponseTaskScenarioFlee' + | 'CEventDataResponseTaskSharkAttack' + | 'CEventDataResponseTaskShockingEventBackAway' + | 'CEventDataResponseTaskShockingEventGoto' + | 'CEventDataResponseTaskShockingEventHurryAway' + | 'CEventDataResponseTaskShockingEventReact' + | 'CEventDataResponseTaskShockingEventReactToAircraft' + | 'CEventDataResponseTaskShockingEventStopAndStare' + | 'CEventDataResponseTaskShockingEventThreatResponse' + | 'CEventDataResponseTaskShockingEventWatch' + | 'CEventDataResponseTaskShockingNiceCar' + | 'CEventDataResponseTaskShockingPoliceInvestigate' + | 'CEventDataResponseTaskThreat' + | 'CEventDataResponseTaskTurnToFace' + | 'CEventDataResponseTaskWalkAway' + | 'CEventDataResponseTaskWalkRoundEntity' + | 'CEventDataResponseTaskWalkRoundFire' + | 'CEventDeadPedFound' + | 'CEventDeath' + | 'CEventDecisionMakerResponse' + | 'CEventDisturbance' + | 'CEventDraggedOutCar' + | 'CEventEditableResponse' + | 'CEventEncroachingPed' + | 'CEventEntityDamaged' + | 'CEventEntityDestroyed' + | 'CEventExplosion' + | 'CEventExplosionHeard' + | 'CEventFireNearby' + | 'CEventFootStepHeard' + | 'CEventFriendlyAimedAt' + | 'CEventFriendlyFireNearMiss' + | 'CEventGetOutOfWater' + | 'CEventGivePedTask' + | 'CEventGroupScriptAI' + | 'CEventGroupScriptNetwork' + | 'CEventGunAimedAt' + | 'CEventGunShot' + | 'CEventGunShotBulletImpact' + | 'CEventGunShotWhizzedBy' + | 'CEventHelpAmbientFriend' + | 'CEventHurtTransition' + | 'CEventInAir' + | 'CEventInfo' + | 'CEventInfoBase' + | 'CEventInjuredCryForHelp' + | 'CEventLeaderEnteredCarAsDriver' + | 'CEventLeaderExitedCarAsDriver' + | 'CEventLeaderHolsteredWeapon' + | 'CEventLeaderLeftCover' + | 'CEventLeaderUnholsteredWeapon' + | 'CEventMeleeAction' + | 'CEventMustLeaveBoat' + | 'CEventNetworkAdminInvited' + | 'CEventNetworkAttemptHostMigration' + | 'CEventNetworkBail' + | 'CEventNetworkCashTransactionLog' + | 'CEventNetworkCheatTriggered' + | 'CEventNetworkClanInviteReceived' + | 'CEventNetworkClanJoined' + | 'CEventNetworkClanKicked' + | 'CEventNetworkClanLeft' + | 'CEventNetworkClanRankChanged' + | 'CEventNetworkCloudEvent' + | 'CEventNetworkCloudFileResponse' + | 'CEventNetworkEmailReceivedEvent' + | 'CEventNetworkEndMatch' + | 'CEventNetworkEndSession' + | 'CEventNetworkEntityDamage' + | 'CEventNetworkFindSession' + | 'CEventNetworkFollowInviteReceived' + | 'CEventNetworkHostMigration' + | 'CEventNetworkHostSession' + | 'CEventNetworkIncrementStat' + | 'CEventNetworkInviteAccepted' + | 'CEventNetworkInviteConfirmed' + | 'CEventNetworkInviteRejected' + | 'CEventNetworkJoinSession' + | 'CEventNetworkJoinSessionResponse' + | 'CEventNetworkOnlinePermissionsUpdated' + | 'CEventNetworkPedLeftBehind' + | 'CEventNetworkPickupRespawned' + | 'CEventNetworkPlayerArrest' + | 'CEventNetworkPlayerCollectedAmbientPickup' + | 'CEventNetworkPlayerCollectedPickup' + | 'CEventNetworkPlayerCollectedPortablePickup' + | 'CEventNetworkPlayerDroppedPortablePickup' + | 'CEventNetworkPlayerEnteredVehicle' + | 'CEventNetworkPlayerJoinScript' + | 'CEventNetworkPlayerLeftScript' + | 'CEventNetworkPlayerScript' + | 'CEventNetworkPlayerSession' + | 'CEventNetworkPlayerSpawn' + | 'CEventNetworkPresenceInvite' + | 'CEventNetworkPresenceInviteRemoved' + | 'CEventNetworkPresenceInviteReply' + | 'CEventNetworkPresenceTriggerEvent' + | 'CEventNetworkPresence_StatUpdate' + | 'CEventNetworkPrimaryClanChanged' + | 'CEventNetworkRequestDelay' + | 'CEventNetworkRosChanged' + | 'CEventNetworkScAdminPlayerUpdated' + | 'CEventNetworkScAdminReceivedCash' + | 'CEventNetworkScriptEvent' + | 'CEventNetworkSessionEvent' + | 'CEventNetworkShopTransaction' + | 'CEventNetworkSignInStateChanged' + | 'CEventNetworkSocialClubAccountLinked' + | 'CEventNetworkSpectateLocal' + | 'CEventNetworkStartMatch' + | 'CEventNetworkStartSession' + | 'CEventNetworkStorePlayerLeft' + | 'CEventNetworkSummon' + | 'CEventNetworkSystemServiceEvent' + | 'CEventNetworkTextMessageReceived' + | 'CEventNetworkTimedExplosion' + | 'CEventNetworkTransitionEvent' + | 'CEventNetworkTransitionGamerInstruction' + | 'CEventNetworkTransitionMemberJoined' + | 'CEventNetworkTransitionMemberLeft' + | 'CEventNetworkTransitionParameterChanged' + | 'CEventNetworkTransitionStarted' + | 'CEventNetworkTransitionStringChanged' + | 'CEventNetworkVehicleUndrivable' + | 'CEventNetworkVoiceConnectionRequested' + | 'CEventNetworkVoiceConnectionResponse' + | 'CEventNetworkVoiceConnectionTerminated' + | 'CEventNetworkVoiceSessionEnded' + | 'CEventNetworkVoiceSessionStarted' + | 'CEventNetworkWithData' + | 'CEventNetwork_InboxMsgReceived' + | 'CEventNewTask' + | 'CEventObjectCollision' + | 'CEventOnFire' + | 'CEventOpenDoor' + | 'CEventPedCollisionWithPed' + | 'CEventPedCollisionWithPlayer' + | 'CEventPedEnteredMyVehicle' + | 'CEventPedJackingMyVehicle' + | 'CEventPedOnCarRoof' + | 'CEventPedSeenDeadPed' + | 'CEventPlayerCollisionWithPed' + | 'CEventPlayerDeath' + | 'CEventPlayerUnableToEnterVehicle' + | 'CEventPotentialBeWalkedInto' + | 'CEventPotentialBlast' + | 'CEventPotentialGetRunOver' + | 'CEventPotentialWalkIntoVehicle' + | 'CEventProvidingCover' + | 'CEventRanOverPed' + | 'CEventReactionEnemyPed' + | 'CEventReactionInvestigateDeadPed' + | 'CEventReactionInvestigateThreat' + | 'CEventRequestHelp' + | 'CEventRequestHelpWithConfrontation' + | 'CEventRespondedToThreat' + | 'CEventScanner' + | 'CEventScenarioForceAction' + | 'CEventScriptCommand' + | 'CEventScriptWithData' + | 'CEventShocking' + | 'CEventShockingBicycleCrash' + | 'CEventShockingBicycleOnPavement' + | 'CEventShockingCarAlarm' + | 'CEventShockingCarChase' + | 'CEventShockingCarCrash' + | 'CEventShockingCarOnCar' + | 'CEventShockingCarPileUp' + | 'CEventShockingDangerousAnimal' + | 'CEventShockingDeadBody' + | 'CEventShockingDrivingOnPavement' + | 'CEventShockingEngineRevved' + | 'CEventShockingExplosion' + | 'CEventShockingFire' + | 'CEventShockingGunFight' + | 'CEventShockingGunshotFired' + | 'CEventShockingHelicopterOverhead' + | 'CEventShockingHornSounded' + | 'CEventShockingInDangerousVehicle' + | 'CEventShockingInjuredPed' + | 'CEventShockingMadDriver' + | 'CEventShockingMadDriverBicycle' + | 'CEventShockingMadDriverExtreme' + | 'CEventShockingMugging' + | 'CEventShockingNonViolentWeaponAimedAt' + | 'CEventShockingParachuterOverhead' + | 'CEventShockingPedKnockedIntoByPlayer' + | 'CEventShockingPedRunOver' + | 'CEventShockingPedShot' + | 'CEventShockingPlaneFlyby' + | 'CEventShockingPotentialBlast' + | 'CEventShockingPropertyDamage' + | 'CEventShockingRunningPed' + | 'CEventShockingRunningStampede' + | 'CEventShockingSeenCarStolen' + | 'CEventShockingSeenConfrontation' + | 'CEventShockingSeenGangFight' + | 'CEventShockingSeenInsult' + | 'CEventShockingSeenMeleeAction' + | 'CEventShockingSeenNiceCar' + | 'CEventShockingSeenPedKilled' + | 'CEventShockingSiren' + | 'CEventShockingStudioBomb' + | 'CEventShockingVehicleTowed' + | 'CEventShockingVisibleWeapon' + | 'CEventShockingWeaponThreat' + | 'CEventShockingWeirdPed' + | 'CEventShockingWeirdPedApproaching' + | 'CEventShoutBlockingLos' + | 'CEventShoutTargetPosition' + | 'CEventShovePed' + | 'CEventSoundBase' + | 'CEventStatChangedValue' + | 'CEventStaticCountReachedMax' + | 'CEventStuckInAir' + | 'CEventSuspiciousActivity' + | 'CEventSwitch2NM' + | 'CEventUnidentifiedPed' + | 'CEventVehicleCollision' + | 'CEventVehicleDamage' + | 'CEventVehicleDamageWeapon' + | 'CEventVehicleOnFire' + | 'CEventWrithe' diff --git a/rpc/tsconfig.json b/rpc/tsconfig.json new file mode 100644 index 0000000..eb2d78a --- /dev/null +++ b/rpc/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "moduleResolution": "node", + "lib": ["ES6", "dom"], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + + "outDir": "dist", + "esModuleInterop": true, + + "strict": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true + }, + "include": ["src/**/*"] +} diff --git a/rpc/tsup.config.ts b/rpc/tsup.config.ts new file mode 100644 index 0000000..c91be33 --- /dev/null +++ b/rpc/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + outDir: './dist', + target: 'node16', + platform: 'node', + format: ['cjs'], + splitting: false, + sourcemap: false, + clean: false, + experimentalDts: true, + noExternal: [/.*/], +}) diff --git a/shared-types/license.md b/shared-types/license.md new file mode 100644 index 0000000..23ae589 --- /dev/null +++ b/shared-types/license.md @@ -0,0 +1,15 @@ +Custom Attribution-NoDerivs Software License + +Copyright (c) 2025 Entity Seven Group + +This license allows you to use, copy, and distribute these packages (the "Software"), including for commercial purposes, provided that the following conditions are met: + +1. **Attribution:** You must give appropriate credit to the original author of the Software, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. + +2. **No Derivative Works:** You may not modify, transform, or build upon the Software. + +3. **Usage and Commercial Use:** You are allowed to use, sell, and gain income from projects that utilize the Software, as long as you comply with the terms of this license. + +4. **No Additional Restrictions:** You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/shared-types/package.json b/shared-types/package.json new file mode 100644 index 0000000..7cd1f00 --- /dev/null +++ b/shared-types/package.json @@ -0,0 +1,30 @@ +{ + "name": "@entityseven/fivem-rpc-shared-types", + "description": "Shared types for @entityseven/fivem-rpc. Highly recommended to install together", + "version": "0.1.0", + "types": "types/types/index.d.ts", + "files": [ + "types/**/*", + "readme.md", + "license.md" + ], + "keywords": [ + "fivem-rpc-shared-types", + "fivem-rpc", + "fivem", + "gta" + ], + "type": "module", + "author": "Entity Seven Group", + "contributors": [ + { + "name": "Danya H", + "email": "dev.rilaxik@gmail.com", + "url": "https://github.com/rilaxik/" + } + ], + "license": "Custom-Attribution-NoDerivs", + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/shared-types/readme.md b/shared-types/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/shared-types/types/types/index.d.ts b/shared-types/types/types/index.d.ts new file mode 100644 index 0000000..4c2634d --- /dev/null +++ b/shared-types/types/types/index.d.ts @@ -0,0 +1,52 @@ +declare module '@entityseven/fivem-rpc-shared-types' { + // Client commands names + export type RPCCommands_Client = '' + + // Server commands names + export type RPCCommands_Server = '' + + // Client -> Client events + export interface RPCEvents_Client { + _(): void + } + + // Client -> Server events + export interface RPCEvents_ClientServer { + _(): void + } + + // Client -> Webview events + export interface RPCEvents_ClientWebview { + _(): void + } + + // Server -> Server events + export interface RPCEvents_Server { + _(): void + } + + // Server -> Client events + export interface RPCEvents_ServerClient { + _(): void + } + + // Server -> Server events + export interface RPCEvents_ServerWebview { + _(): void + } + + // Webview -> Webview events + export interface RPCEvents_Webview { + _(): void + } + + // Webview -> Client events + export interface RPCEvents_WebviewClient { + _(): void + } + + // Webview -> Server events + export interface RPCEvents_WebviewServer { + _(): void + } +}