v0.1.0 #1

Merged
sashagoncharov19 merged 32 commits from dev into unstable 2024-06-15 13:48:41 +00:00
36 changed files with 1536 additions and 50 deletions

View File

@ -0,0 +1,63 @@
name: Bug Report
about: Create a report to help us improve
rules:
- All issues must be reported in English
- Ensure you have searched for existing issues before creating a new one
- Provide as much detail as possible to help us resolve the issue efficiently
body:
- type: dropdown
id: package_version
attributes:
label: Package Version
description: Select the version of the package where the bug was found
options:
- 0.1.0
validations:
required: true
- type: textarea
id: bug_description
attributes:
label: Bug Description
description: Provide a detailed description of the bug
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction (Optional)
description: Steps to reproduce the bug, if applicable
validations:
required: true
- type: dropdown
id: os
attributes:
label: OS Where the Bug Was Found
description: Select the operating system where the bug was found
options:
- Windows
- macOS
- Linux
- Other (please specify at the end of bug description)
validations:
required: true
- type: input
id: cli_version
attributes:
label: CLI Version (Optional)
description: Specify the CLI version if used
validations:
required: false
- type: textarea
id: additional_information
attributes:
label: Additional Information
description: Add any other information that might be useful in diagnosing the issue
validations:
required: false

View File

@ -1,10 +1,11 @@
{ {
"name": "rage-fw-cef", "name": "rage-fw-cef",
"version": "0.0.20-alpha.0", "version": "0.1.0",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
"files": [ "files": [
"dist/**/*" "dist/**/*",
"readme.md"
], ],
"scripts": { "scripts": {
"build": "tsup" "build": "tsup"

2
cef/readme.md Normal file
View File

@ -0,0 +1,2 @@
# RageFW CEF
[Read docs for details](https://git.entityseven.com/entityseven/rage-framework/wiki/Docs)

View File

@ -21,8 +21,10 @@ class Cef {
eventName: EventName, eventName: EventName,
callback: RageFW_CefCallback<EventName>, callback: RageFW_CefCallback<EventName>,
): void { ): void {
if ('mp' in window) {
rpc.register(eventName, callback) rpc.register(eventName, callback)
} }
}
public trigger<EventName extends keyof RageFW_ICustomCefEvent>( public trigger<EventName extends keyof RageFW_ICustomCefEvent>(
eventName: EventName, eventName: EventName,
@ -30,26 +32,44 @@ class Cef {
? [RageFW_CefArguments<EventName>] ? [RageFW_CefArguments<EventName>]
: [] : []
): Promise<RageFW_CefReturn<EventName>> { ): Promise<RageFW_CefReturn<EventName>> {
if ('mp' in window) {
return rpc.call(eventName, args) return rpc.call(eventName, args)
} }
return Promise.reject(
'RageFW was started in window which not contain global variable MP!',
)
}
public triggerServer<EventName extends keyof RageFW_ICustomServerEvent>( public triggerServer<EventName extends keyof RageFW_ICustomServerEvent>(
eventName: EventName, eventName: EventName,
...args: _ServerEventHasArgs<EventName> extends true ...args: _ServerEventHasArgs<EventName> extends true
? [RageFW_ServerArguments<EventName>] ? [RageFW_ServerArguments<EventName>]
: [] : []
): Promise<RageFW_ServerReturn<EventName>> { ): Promise<RageFW_ServerReturn<EventName>> {
if ('mp' in window) {
return rpc.callServer(eventName, args) return rpc.callServer(eventName, args)
} }
return Promise.reject(
'RageFW was started in window which not contain global variable MP!',
)
}
public triggerClient<EventName extends keyof RageFW_ICustomClientEvent>( public triggerClient<EventName extends keyof RageFW_ICustomClientEvent>(
eventName: EventName, eventName: EventName,
...args: _ClientEventHasArgs<EventName> extends true ...args: _ClientEventHasArgs<EventName> extends true
? [RageFW_ClientArguments<EventName>] ? [RageFW_ClientArguments<EventName>]
: [] : []
): Promise<RageFW_ClientReturn<EventName>> { ): Promise<RageFW_ClientReturn<EventName>> {
if ('mp' in window) {
return rpc.callClient(eventName, args) return rpc.callClient(eventName, args)
} }
return Promise.reject(
'RageFW was started in window which not contain global variable MP!',
)
}
} }
export const fw = { export const fw = {

6
cli/.prettierrc.yaml Normal file
View File

@ -0,0 +1,6 @@
tabWidth: 4
printWidth: 80
singleQuote: true
semi: false
arrowParens: avoid
endOfLine: auto

35
cli/package.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "create-rage-fw",
"version": "0.1.0",
"bin": {
"rage-fw": "dist/index.js"
},
"main": "dist/index.js",
"scripts": {
"watch": "tsc -w",
"build": "tsup",
"start": "npx ./dist create"
},
"files": [
"dist/**/*",
"readme.md"
],
"description": "CLI to scaffold a template project for RageFW",
"keywords": [],
"author": "rilaxik",
"license": "ISC",
"dependencies": {
"@inquirer/prompts": "^5.0.5",
"axios": "^1.7.2",
"chalk": "4.1.2",
"git-clone": "^0.2.0",
"yargs": "^17.7.2"
},
"devDependencies": {
"@types/git-clone": "^0.2.4",
"@types/node": "^20.14.2",
"@types/yargs": "^17.0.32",
"prettier": "^3.3.2",
"typescript": "^5.4.5"
}
}

29
cli/readme.md Normal file
View File

@ -0,0 +1,29 @@
# RageFW CLI
To make you life easier while using RageFW we created a basic CLI. At the moment automation we have only works via [pnpm](https://pnpm.io/)
``pnpm create rage-fw@latest``
## TL;DR
- ``Initialize new project`` - create new template project
- ``Install RAGE:MP updater`` - download and update RAGE:MP server files
## Options
For now, you will see a few available options. They are described in detail below
- ``Initialize new project``
- ``Install RAGE:MP updater``
### Initialize new project
Using this options will forward you to common project-creation menu
- ``Enter project name``
This option will specify a name for your project which is used as a folder name too. Defaults to **rage-fw**
- ``Select frontend``
Use this selector menu to choose which frontend framework you want to use. We will do our best to expand this menu after some time.
Defaults to **React + TypeScript (Vite)**
### Install Rage:MP updater
This option will simplify installation process of Rage:MP server files required to start your server

View File

@ -0,0 +1,66 @@
import c from 'chalk'
import { input, select } from '@inquirer/prompts'
import clone from 'git-clone'
import path from 'node:path'
export async function initProject() {
let folder
let framework
if (!folder) {
folder = await input({
message: c.gray('Enter project name:'),
default: 'rage-fw',
})
} else {
console.log(c.gray('Project name:'), folder)
}
if (!framework) {
framework = await select({
message: c.gray('Select frontend:'),
default: 'react',
loop: true,
choices: [
{
name: 'React + TypeScript (Vite)',
value: 'react',
description: 'React + TypeScript (Vite) as a frontend',
},
// {
// name: 'vue',
// value: 'vue',
// description: 'npm is the most popular package manager',
// },
],
})
} else {
console.log(c.gray('Frontend:'), framework)
}
console.log(
c.gray('\nScaffolding template project into'),
folder,
c.gray('with'),
framework,
c.gray('as a frontend..'),
)
clone(
'https://git.entityseven.com/entityseven/rage-framework-example',
path.join(process.cwd(), folder),
{},
err => {
if (err) {
console.log(c.red('Error occured: \n', err))
return
}
console.log(c.gray('Scaffolded project into'), folder)
console.log(
c.gray(
`Project was created ar dir: ${path.join(process.cwd(), folder)}`,
),
)
},
)
}

View File

@ -0,0 +1,37 @@
import axios from 'axios'
import * as fs from 'node:fs'
const latestReleases =
'https://git.entityseven.com/api/v1/repos/entityseven/rage-server-downloader/releases?page=1&limit=1'
type Release = {
id: number
}
type Asset = {
browser_download_url: string
}
export async function downloadUpdater(): Promise<void> {
const id = await getLatestReleaseID()
const latestAssets = `https://git.entityseven.com/api/v1/repos/entityseven/rage-server-downloader/releases/${id}/assets?page=1&limit=1`
axios.get<Asset[]>(latestAssets).then(async ({ data }) => {
const downloadURL = data[0].browser_download_url
const file = await axios.get(data[0].browser_download_url, {
responseType: 'arraybuffer',
})
const fileData = Buffer.from(file.data, 'binary')
const fileSplit = downloadURL.split('/')
const fileName = fileSplit[fileSplit.length - 1]
fs.writeFileSync(`./${fileName}`, fileData)
})
}
async function getLatestReleaseID() {
return axios.get<Release[]>(latestReleases).then(({ data }) => data[0].id)
}

40
cli/src/index.ts Normal file
View File

@ -0,0 +1,40 @@
import c from 'chalk'
import { select } from '@inquirer/prompts'
import { checkForUpdate } from './utils/update'
import { initProject } from './commands/create'
import { downloadUpdater } from './commands/download-updater'
enum Actions {
INIT_PROJECT = 'INIT_PROJECT',
UPDATER = 'UPDATER',
}
;(async () => {
await checkForUpdate()
console.log(
c.blueBright('Rage FW CLI | Powered by Entity Seven Group <3'),
)
const action = await select({
message: c.gray('Select action:'),
choices: [
{
name: 'Initialize new project',
value: Actions.INIT_PROJECT,
description: 'Initialize new project and start develop',
},
{
name: 'Install RAGE:MP updater',
value: Actions.UPDATER,
description:
'Use our custom updater to download and update RAGE:MP server files.',
},
],
loop: true,
})
if (action === Actions.INIT_PROJECT) await initProject()
if (action === Actions.UPDATER) await downloadUpdater()
})()

32
cli/src/utils/update.ts Normal file
View File

@ -0,0 +1,32 @@
import axios from 'axios'
import c from 'chalk'
import yargs from 'yargs'
const latestVersionURL =
'https://git.entityseven.com/api/v1/repos/entityseven/rage-framework/tags?page=1&limit=1'
type Version = {
name: string
message: string
}
export async function checkForUpdate(): Promise<void> {
return new Promise(res => {
yargs.showVersion(version =>
axios
.get<Version[]>(latestVersionURL)
.then(({ data }) => {
const latestVersion = data[0].name
if (latestVersion !== `v${version}`)
notifyUserAboutUpdate(latestVersion)
else console.log(c.yellow(`Version: ${version}`))
})
.then(() => res()),
)
})
}
function notifyUserAboutUpdate(version: string) {
console.log(c.green(`Update available. New version: ${version}`))
}

24
cli/tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"lib": ["DOM", "ES6"],
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "bin",
"esModuleInterop": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noImplicitAny": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}

10
cli/tsup.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
outDir: './dist',
bundle: true,
splitting: false,
sourcemap: false,
clean: true,
})

View File

@ -1,10 +1,11 @@
{ {
"name": "rage-fw-client", "name": "rage-fw-client",
"version": "0.0.20-alpha.0", "version": "0.1.0",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
"files": [ "files": [
"dist/**/*" "dist/**/*",
"readme.md"
], ],
"scripts": { "scripts": {
"build": "tsup" "build": "tsup"

2
client/readme.md Normal file
View File

@ -0,0 +1,2 @@
# RageFW Client
[Read docs for details](https://git.entityseven.com/entityseven/rage-framework/wiki/Docs)

View File

@ -4,16 +4,22 @@ import type { RageFW_ICustomClientEvent } from 'rage-fw-shared-types'
/** /**
* Union of all available client event names * Union of all available client event names
* These only include custom events * These include custom and system events
*/ */
export type RageFW_ClientEvent = keyof RageFW_ICustomClientEvent export type RageFW_ClientEvent =
| keyof RageFW_ICustomClientEvent
| keyof IClientEvents
/** /**
* Array of arguments for an event, name of which you pass as a generic * Array of arguments for an event, name of which you pass as a generic
* These only include custom events * These include custom and system events
*/ */
export type RageFW_ClientEventArguments<K extends RageFW_ClientEvent> = export type RageFW_ClientEventArguments<K extends RageFW_ClientEvent> =
Parameters<RageFW_ICustomClientEvent[K]> K extends keyof RageFW_ICustomClientEvent
? Parameters<RageFW_ICustomClientEvent[K]>
: K extends keyof IClientEvents
? Parameters<IClientEvents[K]>
: never
/** /**
* Callback (function) for an event, name of which you pass as a generic * Callback (function) for an event, name of which you pass as a generic

View File

@ -1,5 +1,5 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "0.0.20-alpha.0", "version": "0.1.0",
"npmClient": "pnpm" "npmClient": "pnpm"
} }

13
license.md Normal file
View File

@ -0,0 +1,13 @@
# Custom Attribution-NoDerivs Software License
This license allows you to use, copy, and distribute the RageFW (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.

330
pnpm-lock.yaml generated
View File

@ -9,7 +9,7 @@ importers:
dependencies: dependencies:
'@microsoft/api-extractor': '@microsoft/api-extractor':
specifier: ^7.47.0 specifier: ^7.47.0
version: 7.47.0 version: 7.47.0(@types/node@20.14.2)
'@ragempcommunity/types-cef': '@ragempcommunity/types-cef':
specifier: ^2.1.8 specifier: ^2.1.8
version: 2.1.8 version: 2.1.8
@ -39,7 +39,7 @@ importers:
version: 0.4.0 version: 0.4.0
tsup: tsup:
specifier: ^8.1.0 specifier: ^8.1.0
version: 8.1.0(@microsoft/api-extractor@7.47.0)(typescript@5.4.5) version: 8.1.0(@microsoft/api-extractor@7.47.0(@types/node@20.14.2))(typescript@5.4.5)
typescript: typescript:
specifier: ^5.4.5 specifier: ^5.4.5
version: 5.4.5 version: 5.4.5
@ -59,6 +59,40 @@ importers:
specifier: ^0.4.0 specifier: ^0.4.0
version: 0.4.0 version: 0.4.0
cli:
dependencies:
'@inquirer/prompts':
specifier: ^5.0.5
version: 5.0.5
axios:
specifier: ^1.7.2
version: 1.7.2
chalk:
specifier: 4.1.2
version: 4.1.2
git-clone:
specifier: ^0.2.0
version: 0.2.0
yargs:
specifier: ^17.7.2
version: 17.7.2
devDependencies:
'@types/git-clone':
specifier: ^0.2.4
version: 0.2.4
'@types/node':
specifier: ^20.14.2
version: 20.14.2
'@types/yargs':
specifier: ^17.0.32
version: 17.0.32
prettier:
specifier: ^3.3.2
version: 3.3.2
typescript:
specifier: ^5.4.5
version: 5.4.5
client: client:
dependencies: dependencies:
'@ragempcommunity/types-client': '@ragempcommunity/types-client':
@ -386,6 +420,90 @@ packages:
} }
engines: { node: '>=6.9.0' } engines: { node: '>=6.9.0' }
'@inquirer/checkbox@2.3.5':
resolution:
{
integrity: sha512-3V0OSykTkE/38GG1DhxRGLBmqefgzRg2EK5A375zz+XEvIWfAHcac31e+zlBDPypRHxhmXc/Oh6v9eOPbH3nAg==,
}
engines: { node: '>=18' }
'@inquirer/confirm@3.1.9':
resolution:
{
integrity: sha512-UF09aejxCi4Xqm6N/jJAiFXArXfi9al52AFaSD+2uIHnhZGtd1d6lIGTRMPouVSJxbGEi+HkOWSYaiEY/+szUw==,
}
engines: { node: '>=18' }
'@inquirer/core@8.2.2':
resolution:
{
integrity: sha512-K8SuNX45jEFlX3EBJpu9B+S2TISzMPGXZIuJ9ME924SqbdW6Pt6fIkKvXg7mOEOKJ4WxpQsxj0UTfcL/A434Ww==,
}
engines: { node: '>=18' }
'@inquirer/editor@2.1.9':
resolution:
{
integrity: sha512-5xCD7CoCh993YqXcsZPt45qkE3gl+03Yfv9vmAkptRi4nrzaUDmyhgBzndKdRG8SrKbQLBmOtztnRLGxvG/ahg==,
}
engines: { node: '>=18' }
'@inquirer/expand@2.1.9':
resolution:
{
integrity: sha512-ymnR8qu2ie/3JpOeyZ3QSGJ+ai8qqtjBwopxLjzIZm7mZVKT6SV1sURzijkOLRgGUHwPemOfYX5biqXuqhpoBg==,
}
engines: { node: '>=18' }
'@inquirer/figures@1.0.3':
resolution:
{
integrity: sha512-ErXXzENMH5pJt5/ssXV0DfWUZqly8nGzf0UcBV9xTnP+KyffE2mqyxIMBrZ8ijQck2nU0TQm40EQB53YreyWHw==,
}
engines: { node: '>=18' }
'@inquirer/input@2.1.9':
resolution:
{
integrity: sha512-1xTCHmIe48x9CG1+8glAHrVVdH+QfYhzgBUbgyoVpp5NovnXgRcjSn/SNulepxf9Ol8HDq3gzw3ZCAUr+h1Eyg==,
}
engines: { node: '>=18' }
'@inquirer/password@2.1.9':
resolution:
{
integrity: sha512-QPtVcT12Fkn0TyuZJelR7QOtc5l1d/6pB5EfkHOivTzC6QTFxRCHl+Gx7Q3E2U/kgJeCCmDov6itDFggk9nkgA==,
}
engines: { node: '>=18' }
'@inquirer/prompts@5.0.5':
resolution:
{
integrity: sha512-LV2XZzc8ls4zhUzYNSpsXcnA8djOptY4G01lFzp3Bey6E1oiZMzIU25N9cb5AOwNz6pqDXpjLwRFQmLQ8h6PaQ==,
}
engines: { node: '>=18' }
'@inquirer/rawlist@2.1.9':
resolution:
{
integrity: sha512-GuMmfa/v1ZJqEWSkUx1hMxzs5/0DCUP0S8IicV/wu8QrbjfBOh+7mIQgtsvh8IJ3sRkRcQ+9wh9CE9jiYqyMgw==,
}
engines: { node: '>=18' }
'@inquirer/select@2.3.5':
resolution:
{
integrity: sha512-IyBj8oEtmdF2Gx4FJTPtEya37MD6s0KATKsHqgmls0lK7EQbhYSq9GQlcFq6cBsYe/cgQ0Fg2cCqYYPi/d/fxQ==,
}
engines: { node: '>=18' }
'@inquirer/type@1.3.3':
resolution:
{
integrity: sha512-xTUt0NulylX27/zMx04ZYar/kr1raaiFTVvQ5feljQsiAgdm0WPj4S73/ye0fbslh+15QrIuDvfCXTek7pMY5A==,
}
engines: { node: '>=18' }
'@isaacs/cliui@8.0.2': '@isaacs/cliui@8.0.2':
resolution: resolution:
{ {
@ -1069,6 +1187,12 @@ packages:
integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==, integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==,
} }
'@types/git-clone@0.2.4':
resolution:
{
integrity: sha512-1ybApDpKU12dychtOp2zBe93ZwAsxVSjOqKUqH7NCDm4GXuPnjmcz2P9K2S1z+BCX2AnLmFFuB6pI6CMZ3j9sQ==,
}
'@types/minimatch@3.0.5': '@types/minimatch@3.0.5':
resolution: resolution:
{ {
@ -1081,6 +1205,18 @@ packages:
integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==, integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==,
} }
'@types/mute-stream@0.0.4':
resolution:
{
integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==,
}
'@types/node@20.14.2':
resolution:
{
integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==,
}
'@types/normalize-package-data@2.4.4': '@types/normalize-package-data@2.4.4':
resolution: resolution:
{ {
@ -1093,6 +1229,24 @@ packages:
integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==, integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==,
} }
'@types/wrap-ansi@3.0.0':
resolution:
{
integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==,
}
'@types/yargs-parser@21.0.3':
resolution:
{
integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==,
}
'@types/yargs@17.0.32':
resolution:
{
integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==,
}
'@typescript-eslint/eslint-plugin@7.13.0': '@typescript-eslint/eslint-plugin@7.13.0':
resolution: resolution:
{ {
@ -1667,6 +1821,13 @@ packages:
} }
engines: { node: '>= 10' } engines: { node: '>= 10' }
cli-width@4.1.0:
resolution:
{
integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==,
}
engines: { node: '>= 12' }
cliui@7.0.4: cliui@7.0.4:
resolution: resolution:
{ {
@ -2340,10 +2501,10 @@ packages:
debug: debug:
optional: true optional: true
foreground-child@3.1.1: foreground-child@3.2.0:
resolution: resolution:
{ {
integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==, integrity: sha512-CrWQNaEl1/6WeZoarcM9LHupTo3RpZO2Pdk1vktwzPiQTsJnAKJmm3TACKeG5UZbWDfaH2AbvYxzP96y0MT7fA==,
} }
engines: { node: '>=14' } engines: { node: '>=14' }
@ -2458,6 +2619,12 @@ packages:
} }
engines: { node: '>=10' } engines: { node: '>=10' }
git-clone@0.2.0:
resolution:
{
integrity: sha512-1UAkEPIFbyjHaddljUKvPhhLRnrKaImT71T7rdvSvWLXw95nLdhdi6Qmlx0KOWoV1qqvHGLq5lMLJEZM0JXk8A==,
}
git-raw-commits@3.0.0: git-raw-commits@3.0.0:
resolution: resolution:
{ {
@ -5086,6 +5253,12 @@ packages:
engines: { node: '>=0.8.0' } engines: { node: '>=0.8.0' }
hasBin: true hasBin: true
undici-types@5.26.5:
resolution:
{
integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==,
}
unique-filename@3.0.0: unique-filename@3.0.0:
resolution: resolution:
{ {
@ -5490,6 +5663,87 @@ snapshots:
'@hutson/parse-repository-url@3.0.2': {} '@hutson/parse-repository-url@3.0.2': {}
'@inquirer/checkbox@2.3.5':
dependencies:
'@inquirer/core': 8.2.2
'@inquirer/figures': 1.0.3
'@inquirer/type': 1.3.3
ansi-escapes: 4.3.2
chalk: 4.1.2
'@inquirer/confirm@3.1.9':
dependencies:
'@inquirer/core': 8.2.2
'@inquirer/type': 1.3.3
'@inquirer/core@8.2.2':
dependencies:
'@inquirer/figures': 1.0.3
'@inquirer/type': 1.3.3
'@types/mute-stream': 0.0.4
'@types/node': 20.14.2
'@types/wrap-ansi': 3.0.0
ansi-escapes: 4.3.2
chalk: 4.1.2
cli-spinners: 2.9.2
cli-width: 4.1.0
mute-stream: 1.0.0
signal-exit: 4.1.0
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
'@inquirer/editor@2.1.9':
dependencies:
'@inquirer/core': 8.2.2
'@inquirer/type': 1.3.3
external-editor: 3.1.0
'@inquirer/expand@2.1.9':
dependencies:
'@inquirer/core': 8.2.2
'@inquirer/type': 1.3.3
chalk: 4.1.2
'@inquirer/figures@1.0.3': {}
'@inquirer/input@2.1.9':
dependencies:
'@inquirer/core': 8.2.2
'@inquirer/type': 1.3.3
'@inquirer/password@2.1.9':
dependencies:
'@inquirer/core': 8.2.2
'@inquirer/type': 1.3.3
ansi-escapes: 4.3.2
'@inquirer/prompts@5.0.5':
dependencies:
'@inquirer/checkbox': 2.3.5
'@inquirer/confirm': 3.1.9
'@inquirer/editor': 2.1.9
'@inquirer/expand': 2.1.9
'@inquirer/input': 2.1.9
'@inquirer/password': 2.1.9
'@inquirer/rawlist': 2.1.9
'@inquirer/select': 2.3.5
'@inquirer/rawlist@2.1.9':
dependencies:
'@inquirer/core': 8.2.2
'@inquirer/type': 1.3.3
chalk: 4.1.2
'@inquirer/select@2.3.5':
dependencies:
'@inquirer/core': 8.2.2
'@inquirer/figures': 1.0.3
'@inquirer/type': 1.3.3
ansi-escapes: 4.3.2
chalk: 4.1.2
'@inquirer/type@1.3.3': {}
'@isaacs/cliui@8.0.2': '@isaacs/cliui@8.0.2':
dependencies: dependencies:
string-width: 5.1.2 string-width: 5.1.2
@ -5595,23 +5849,23 @@ snapshots:
- supports-color - supports-color
- typescript - typescript
'@microsoft/api-extractor-model@7.29.2': '@microsoft/api-extractor-model@7.29.2(@types/node@20.14.2)':
dependencies: dependencies:
'@microsoft/tsdoc': 0.15.0 '@microsoft/tsdoc': 0.15.0
'@microsoft/tsdoc-config': 0.17.0 '@microsoft/tsdoc-config': 0.17.0
'@rushstack/node-core-library': 5.4.1 '@rushstack/node-core-library': 5.4.1(@types/node@20.14.2)
transitivePeerDependencies: transitivePeerDependencies:
- '@types/node' - '@types/node'
'@microsoft/api-extractor@7.47.0': '@microsoft/api-extractor@7.47.0(@types/node@20.14.2)':
dependencies: dependencies:
'@microsoft/api-extractor-model': 7.29.2 '@microsoft/api-extractor-model': 7.29.2(@types/node@20.14.2)
'@microsoft/tsdoc': 0.15.0 '@microsoft/tsdoc': 0.15.0
'@microsoft/tsdoc-config': 0.17.0 '@microsoft/tsdoc-config': 0.17.0
'@rushstack/node-core-library': 5.4.1 '@rushstack/node-core-library': 5.4.1(@types/node@20.14.2)
'@rushstack/rig-package': 0.5.2 '@rushstack/rig-package': 0.5.2
'@rushstack/terminal': 0.13.0 '@rushstack/terminal': 0.13.0(@types/node@20.14.2)
'@rushstack/ts-command-line': 4.22.0 '@rushstack/ts-command-line': 4.22.0(@types/node@20.14.2)
lodash: 4.17.21 lodash: 4.17.21
minimatch: 3.0.8 minimatch: 3.0.8
resolve: 1.22.8 resolve: 1.22.8
@ -5890,7 +6144,7 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.18.0': '@rollup/rollup-win32-x64-msvc@4.18.0':
optional: true optional: true
'@rushstack/node-core-library@5.4.1': '@rushstack/node-core-library@5.4.1(@types/node@20.14.2)':
dependencies: dependencies:
ajv: 8.13.0 ajv: 8.13.0
ajv-draft-04: 1.0.0(ajv@8.13.0) ajv-draft-04: 1.0.0(ajv@8.13.0)
@ -5900,20 +6154,24 @@ snapshots:
jju: 1.4.0 jju: 1.4.0
resolve: 1.22.8 resolve: 1.22.8
semver: 7.5.4 semver: 7.5.4
optionalDependencies:
'@types/node': 20.14.2
'@rushstack/rig-package@0.5.2': '@rushstack/rig-package@0.5.2':
dependencies: dependencies:
resolve: 1.22.8 resolve: 1.22.8
strip-json-comments: 3.1.1 strip-json-comments: 3.1.1
'@rushstack/terminal@0.13.0': '@rushstack/terminal@0.13.0(@types/node@20.14.2)':
dependencies: dependencies:
'@rushstack/node-core-library': 5.4.1 '@rushstack/node-core-library': 5.4.1(@types/node@20.14.2)
supports-color: 8.1.1 supports-color: 8.1.1
optionalDependencies:
'@types/node': 20.14.2
'@rushstack/ts-command-line@4.22.0': '@rushstack/ts-command-line@4.22.0(@types/node@20.14.2)':
dependencies: dependencies:
'@rushstack/terminal': 0.13.0 '@rushstack/terminal': 0.13.0(@types/node@20.14.2)
'@types/argparse': 1.0.38 '@types/argparse': 1.0.38
argparse: 1.0.10 argparse: 1.0.10
string-argv: 0.3.2 string-argv: 0.3.2
@ -5995,14 +6253,32 @@ snapshots:
'@types/estree@1.0.5': {} '@types/estree@1.0.5': {}
'@types/git-clone@0.2.4': {}
'@types/minimatch@3.0.5': {} '@types/minimatch@3.0.5': {}
'@types/minimist@1.2.5': {} '@types/minimist@1.2.5': {}
'@types/mute-stream@0.0.4':
dependencies:
'@types/node': 20.14.2
'@types/node@20.14.2':
dependencies:
undici-types: 5.26.5
'@types/normalize-package-data@2.4.4': {} '@types/normalize-package-data@2.4.4': {}
'@types/triple-beam@1.3.5': {} '@types/triple-beam@1.3.5': {}
'@types/wrap-ansi@3.0.0': {}
'@types/yargs-parser@21.0.3': {}
'@types/yargs@17.0.32':
dependencies:
'@types/yargs-parser': 21.0.3
'@typescript-eslint/eslint-plugin@7.13.0(@typescript-eslint/parser@7.13.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': '@typescript-eslint/eslint-plugin@7.13.0(@typescript-eslint/parser@7.13.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)':
dependencies: dependencies:
'@eslint-community/regexpp': 4.10.1 '@eslint-community/regexpp': 4.10.1
@ -6361,6 +6637,8 @@ snapshots:
cli-width@3.0.0: {} cli-width@3.0.0: {}
cli-width@4.1.0: {}
cliui@7.0.4: cliui@7.0.4:
dependencies: dependencies:
string-width: 4.2.3 string-width: 4.2.3
@ -6796,7 +7074,7 @@ snapshots:
follow-redirects@1.15.6: {} follow-redirects@1.15.6: {}
foreground-child@3.1.1: foreground-child@3.2.0:
dependencies: dependencies:
cross-spawn: 7.0.3 cross-spawn: 7.0.3
signal-exit: 4.1.0 signal-exit: 4.1.0
@ -6866,6 +7144,8 @@ snapshots:
get-stream@6.0.1: {} get-stream@6.0.1: {}
git-clone@0.2.0: {}
git-raw-commits@3.0.0: git-raw-commits@3.0.0:
dependencies: dependencies:
dargs: 7.0.0 dargs: 7.0.0
@ -6905,7 +7185,7 @@ snapshots:
glob@10.4.1: glob@10.4.1:
dependencies: dependencies:
foreground-child: 3.1.1 foreground-child: 3.2.0
jackspeak: 3.4.0 jackspeak: 3.4.0
minimatch: 9.0.4 minimatch: 9.0.4
minipass: 7.1.2 minipass: 7.1.2
@ -7186,13 +7466,13 @@ snapshots:
jake@10.9.1: jake@10.9.1:
dependencies: dependencies:
async: 3.2.5 async: 3.2.5
chalk: 4.1.0 chalk: 4.1.2
filelist: 1.0.4 filelist: 1.0.4
minimatch: 3.1.2 minimatch: 3.1.2
jest-diff@29.7.0: jest-diff@29.7.0:
dependencies: dependencies:
chalk: 4.1.0 chalk: 4.1.2
diff-sequences: 29.6.3 diff-sequences: 29.6.3
jest-get-type: 29.6.3 jest-get-type: 29.6.3
pretty-format: 29.7.0 pretty-format: 29.7.0
@ -7783,7 +8063,7 @@ snapshots:
'@yarnpkg/parsers': 3.0.0-rc.46 '@yarnpkg/parsers': 3.0.0-rc.46
'@zkochan/js-yaml': 0.0.7 '@zkochan/js-yaml': 0.0.7
axios: 1.7.2 axios: 1.7.2
chalk: 4.1.0 chalk: 4.1.2
cli-cursor: 3.1.0 cli-cursor: 3.1.0
cli-spinners: 2.6.1 cli-spinners: 2.6.1
cliui: 8.0.1 cliui: 8.0.1
@ -7858,7 +8138,7 @@ snapshots:
ora@5.3.0: ora@5.3.0:
dependencies: dependencies:
bl: 4.1.0 bl: 4.1.0
chalk: 4.1.0 chalk: 4.1.2
cli-cursor: 3.1.0 cli-cursor: 3.1.0
cli-spinners: 2.6.1 cli-spinners: 2.6.1
is-interactive: 1.0.0 is-interactive: 1.0.0
@ -8487,7 +8767,7 @@ snapshots:
tslib@2.6.3: {} tslib@2.6.3: {}
tsup@8.1.0(@microsoft/api-extractor@7.47.0)(typescript@5.4.5): tsup@8.1.0(@microsoft/api-extractor@7.47.0(@types/node@20.14.2))(typescript@5.4.5):
dependencies: dependencies:
bundle-require: 4.2.1(esbuild@0.21.5) bundle-require: 4.2.1(esbuild@0.21.5)
cac: 6.7.14 cac: 6.7.14
@ -8504,7 +8784,7 @@ snapshots:
sucrase: 3.35.0 sucrase: 3.35.0
tree-kill: 1.2.2 tree-kill: 1.2.2
optionalDependencies: optionalDependencies:
'@microsoft/api-extractor': 7.47.0 '@microsoft/api-extractor': 7.47.0(@types/node@20.14.2)
typescript: 5.4.5 typescript: 5.4.5
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@ -8551,6 +8831,8 @@ snapshots:
uglify-js@3.18.0: uglify-js@3.18.0:
optional: true optional: true
undici-types@5.26.5: {}
unique-filename@3.0.0: unique-filename@3.0.0:
dependencies: dependencies:
unique-slug: 4.0.0 unique-slug: 4.0.0

View File

@ -2,4 +2,5 @@ packages:
- "server" - "server"
- "client" - "client"
- "cef" - "cef"
- "cli"
- "shared-types" - "shared-types"

1
rage-rpc/README.md Normal file
View File

@ -0,0 +1 @@
Currently not maintained.

24
rage-rpc/package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "rage-fw-rpc",
"version": "0.0.23-alpha.0",
"main": "dist/index.js",
"types": "dist/src/index.d.ts",
"files": [
"dist/**/*"
],
"scripts": {
"build": "tsup"
},
"dependencies": {
"rage-rpc": "^0.4.0"
},
"peerDependencies": {
"@ragempcommunity/types-client": "^2.1.8",
"rage-fw-shared-types": "workspace:^"
},
"keywords": [],
"author": "SashaGoncharov19",
"license": "MIT",
"description": "Client side of rage-fw",
"gitHead": "053e4fd12aa120d53e11e0d2009c0df78c1a2ad0"
}

42
rage-rpc/src/defs.d.ts vendored Normal file
View File

@ -0,0 +1,42 @@
declare var mp: any;
declare var global: any;
declare var window: any;
declare type ProcedureListener = (args: any, info: ProcedureListenerInfo) => any;
declare interface Player {
call: (eventName: string, args?: any[]) => void;
[property: string]: any;
}
declare interface Browser {
url: string;
execute: (code: string) => void;
[property: string]: any;
}
declare interface ProcedureListenerInfo {
environment: string;
id?: string;
player?: Player;
browser?: Browser;
}
declare interface CallOptions {
timeout?: number;
noRet?: boolean;
}
declare interface Event {
req?: number;
ret?: number;
b?: string;
id: string;
name?: string;
args?: any;
env: string;
fenv?: string;
res?: any;
err?: any;
noRet?: number;
}

568
rage-rpc/src/index.ts Normal file
View File

@ -0,0 +1,568 @@
import * as util from './util';
const environment = util.getEnvironment();
if(!environment) throw 'Unknown RAGE environment';
const ERR_NOT_FOUND = 'PROCEDURE_NOT_FOUND';
const IDENTIFIER = '__rpc:id';
const PROCESS_EVENT = '__rpc:process';
const BROWSER_REGISTER = '__rpc:browserRegister';
const BROWSER_UNREGISTER = '__rpc:browserUnregister';
const TRIGGER_EVENT = '__rpc:triggerEvent';
const TRIGGER_EVENT_BROWSERS = '__rpc:triggerEventBrowsers';
const glob = environment === 'cef' ? window : global;
if(!glob[PROCESS_EVENT]){
glob.__rpcListeners = {};
glob.__rpcPending = {};
glob.__rpcEvListeners = {};
glob[PROCESS_EVENT] = (player: Player | string, rawData?: string) => {
if(environment !== "server") rawData = player as string;
const data: Event = util.parseData(rawData);
if(data.req){ // someone is trying to remotely call a procedure
const info: ProcedureListenerInfo = {
id: data.id,
environment: data.fenv || data.env
};
if(environment === "server") info.player = player as Player;
const part = {
ret: 1,
id: data.id,
env: environment
};
let ret: (ev: Event) => void;
switch(environment){
case "server":
ret = ev => info.player.call(PROCESS_EVENT, [util.stringifyData(ev)]);
break;
case "client": {
if(data.env === "server"){
ret = ev => mp.events.callRemote(PROCESS_EVENT, util.stringifyData(ev));
}else if(data.env === "cef"){
const browser = data.b && glob.__rpcBrowsers[data.b];
info.browser = browser;
ret = ev => browser && util.isBrowserValid(browser) && passEventToBrowser(browser, ev, true);
}
break;
}
case "cef": {
ret = ev => mp.trigger(PROCESS_EVENT, util.stringifyData(ev));
}
}
if(ret){
const promise = callProcedure(data.name, data.args, info);
if(!data.noRet) promise.then(res => ret({ ...part, res })).catch(err => ret({ ...part, err: err ? err : null }));
}
}else if(data.ret){ // a previously called remote procedure has returned
const info = glob.__rpcPending[data.id];
if(environment === "server" && info.player !== player) return;
if(info){
info.resolve(data.hasOwnProperty('err') ? util.promiseReject(data.err) : util.promiseResolve(data.res));
delete glob.__rpcPending[data.id];
}
}
};
if(environment !== "cef"){
mp.events.add(PROCESS_EVENT, glob[PROCESS_EVENT]);
if(environment === "client"){
// set up internal pass-through events
register('__rpc:callServer', ([name, args, noRet], info) => _callServer(name, args, { fenv: info.environment, noRet }));
register('__rpc:callBrowsers', ([name, args, noRet], info) => _callBrowsers(null, name, args, { fenv: info.environment, noRet }));
// set up browser identifiers
glob.__rpcBrowsers = {};
const initBrowser = (browser: Browser): void => {
const id = util.uid();
Object.keys(glob.__rpcBrowsers).forEach(key => {
const b = glob.__rpcBrowsers[key];
if(!b || !util.isBrowserValid(b) || b === browser) delete glob.__rpcBrowsers[key];
});
glob.__rpcBrowsers[id] = browser;
browser.execute(`
window.name = '${id}';
if(typeof window['${IDENTIFIER}'] === 'undefined'){
window['${IDENTIFIER}'] = Promise.resolve(window.name);
}else{
window['${IDENTIFIER}:resolve'](window.name);
}
`);
};
mp.browsers.forEach(initBrowser);
mp.events.add('browserCreated', initBrowser);
// set up browser registration map
glob.__rpcBrowserProcedures = {};
mp.events.add(BROWSER_REGISTER, (data: string) => {
const [browserId, name] = JSON.parse(data);
glob.__rpcBrowserProcedures[name] = browserId;
});
mp.events.add(BROWSER_UNREGISTER, (data: string) => {
const [browserId, name] = JSON.parse(data);
if(glob.__rpcBrowserProcedures[name] === browserId) delete glob.__rpcBrowserProcedures[name];
});
register(TRIGGER_EVENT_BROWSERS, ([name, args], info) => {
Object.values(glob.__rpcBrowsers).forEach(browser => {
_callBrowser(browser, TRIGGER_EVENT, [name, args], { fenv: info.environment, noRet: 1 });
});
});
}
}else{
if(typeof glob[IDENTIFIER] === 'undefined'){
glob[IDENTIFIER] = new Promise(resolve => {
if (window.name) {
resolve(window.name);
}else{
glob[IDENTIFIER+':resolve'] = resolve;
}
});
}
}
register(TRIGGER_EVENT, ([name, args], info) => callEvent(name, args, info));
}
function passEventToBrowser(browser: Browser, data: Event, ignoreNotFound: boolean): void {
const raw = util.stringifyData(data);
browser.execute(`var process = window["${PROCESS_EVENT}"]; if(process){ process(${JSON.stringify(raw)}); }else{ ${ignoreNotFound ? '' : `mp.trigger("${PROCESS_EVENT}", '{"ret":1,"id":"${data.id}","err":"${ERR_NOT_FOUND}","env":"cef"}');`} }`);
}
function callProcedure(name: string, args: any, info: ProcedureListenerInfo): Promise<any> {
const listener = glob.__rpcListeners[name];
if(!listener) return util.promiseReject(ERR_NOT_FOUND);
return util.promiseResolve(listener(args, info));
}
/**
* Register a procedure.
* @param {string} name - The name of the procedure.
* @param {function} cb - The procedure's callback. The return value will be sent back to the caller.
* @returns {Function} The function, which unregister the event.
*/
export function register(name: string, cb: ProcedureListener): Function {
if(arguments.length !== 2) throw 'register expects 2 arguments: "name" and "cb"';
if(environment === "cef") glob[IDENTIFIER].then((id: string) => mp.trigger(BROWSER_REGISTER, JSON.stringify([id, name])));
glob.__rpcListeners[name] = cb;
return () => unregister(name);
}
/**
* Unregister a procedure.
* @param {string} name - The name of the procedure.
*/
export function unregister(name: string): void {
if(arguments.length !== 1) throw 'unregister expects 1 argument: "name"';
if(environment === "cef") glob[IDENTIFIER].then((id: string) => mp.trigger(BROWSER_UNREGISTER, JSON.stringify([id, name])));
glob.__rpcListeners[name] = undefined;
}
/**
* Calls a local procedure. Only procedures registered in the same context will be resolved.
*
* Can be called from any environment.
*
* @param name - The name of the locally registered procedure.
* @param args - Any parameters for the procedure.
* @param options - Any options.
* @returns The result from the procedure.
*/
export function call(name: string, args?: any, options: CallOptions = {}): Promise<any> {
if(arguments.length < 1 || arguments.length > 3) return util.promiseReject('call expects 1 to 3 arguments: "name", optional "args", and optional "options"');
return util.promiseTimeout(callProcedure(name, args, { environment }), options.timeout);
}
function _callServer(name: string, args?: any, extraData: any = {}): Promise<any> {
switch(environment){
case "server": {
return call(name, args);
}
case "client": {
const id = util.uid();
return new Promise(resolve => {
if(!extraData.noRet){
glob.__rpcPending[id] = {
resolve
};
}
const event: Event = {
req: 1,
id,
name,
env: environment,
args,
...extraData
};
mp.events.callRemote(PROCESS_EVENT, util.stringifyData(event));
});
}
case "cef": {
return callClient('__rpc:callServer', [name, args, +extraData.noRet]);
}
}
}
/**
* Calls a remote procedure registered on the server.
*
* Can be called from any environment.
*
* @param name - The name of the registered procedure.
* @param args - Any parameters for the procedure.
* @param options - Any options.
* @returns The result from the procedure.
*/
export function callServer(name: string, args?: any, options: CallOptions = {}): Promise<any> {
if(arguments.length < 1 || arguments.length > 3) return util.promiseReject('callServer expects 1 to 3 arguments: "name", optional "args", and optional "options"');
let extraData: any = {};
if(options.noRet) extraData.noRet = 1;
return util.promiseTimeout(_callServer(name, args, extraData), options.timeout);
}
function _callClient(player: Player, name: string, args?: any, extraData: any = {}): Promise<any> {
switch(environment){
case 'client': {
return call(name, args);
}
case 'server': {
const id = util.uid();
return new Promise(resolve => {
if(!extraData.noRet){
glob.__rpcPending[id] = {
resolve,
player
};
}
const event: Event = {
req: 1,
id,
name,
env: environment,
args,
...extraData
};
player.call(PROCESS_EVENT, [util.stringifyData(event)]);
});
}
case 'cef': {
const id = util.uid();
return glob[IDENTIFIER].then((browserId: string) => {
return new Promise(resolve => {
if(!extraData.noRet){
glob.__rpcPending[id] = {
resolve
};
}
const event: Event = {
b: browserId,
req: 1,
id,
name,
env: environment,
args,
...extraData
};
mp.trigger(PROCESS_EVENT, util.stringifyData(event));
});
});
}
}
}
/**
* Calls a remote procedure registered on the client.
*
* Can be called from any environment.
*
* @param player - The player to call the procedure on.
* @param name - The name of the registered procedure.
* @param args - Any parameters for the procedure.
* @param options - Any options.
* @returns The result from the procedure.
*/
export function callClient(player: Player | string, name?: string | any, args?: any, options: CallOptions = {}): Promise<any> {
switch(environment){
case 'client': {
options = args || {};
args = name;
name = player;
player = null;
if((arguments.length < 1 || arguments.length > 3) || typeof name !== 'string') return util.promiseReject('callClient from the client expects 1 to 3 arguments: "name", optional "args", and optional "options"');
break;
}
case 'server': {
if((arguments.length < 2 || arguments.length > 4) || typeof player !== 'object') return util.promiseReject('callClient from the server expects 2 to 4 arguments: "player", "name", optional "args", and optional "options"');
break;
}
case 'cef': {
options = args || {};
args = name;
name = player;
player = null;
if((arguments.length < 1 || arguments.length > 3) || typeof name !== 'string') return util.promiseReject('callClient from the browser expects 1 to 3 arguments: "name", optional "args", and optional "options"');
break;
}
}
let extraData: any = {};
if(options.noRet) extraData.noRet = 1;
return util.promiseTimeout(_callClient(player as Player, name, args, extraData), options.timeout);
}
function _callBrowser(browser: Browser, name: string, args?: any, extraData: any = {}): Promise<any> {
return new Promise(resolve => {
const id = util.uid();
if(!extraData.noRet){
glob.__rpcPending[id] = {
resolve
};
}
passEventToBrowser(browser, {
req: 1,
id,
name,
env: environment,
args,
...extraData
}, false);
});
}
function _callBrowsers(player: Player, name: string, args?: any, extraData: any = {}): Promise<any> {
switch(environment){
case 'client':
const browserId = glob.__rpcBrowserProcedures[name];
if(!browserId) return util.promiseReject(ERR_NOT_FOUND);
const browser = glob.__rpcBrowsers[browserId];
if(!browser || !util.isBrowserValid(browser)) return util.promiseReject(ERR_NOT_FOUND);
return _callBrowser(browser, name, args, extraData);
case 'server':
return _callClient(player, '__rpc:callBrowsers', [name, args, +extraData.noRet], extraData);
case 'cef':
return _callClient(null, '__rpc:callBrowsers', [name, args, +extraData.noRet], extraData);
}
}
/**
* Calls a remote procedure registered in any browser context.
*
* Can be called from any environment.
*
* @param player - The player to call the procedure on.
* @param name - The name of the registered procedure.
* @param args - Any parameters for the procedure.
* @param options - Any options.
* @returns The result from the procedure.
*/
export function callBrowsers(player: Player | string, name?: string | any, args?: any, options: CallOptions = {}): Promise<any> {
let promise;
let extraData: any = {};
switch(environment){
case 'client':
case 'cef':
options = args || {};
args = name;
name = player;
if(arguments.length < 1 || arguments.length > 3) return util.promiseReject('callBrowsers from the client or browser expects 1 to 3 arguments: "name", optional "args", and optional "options"');
if(options.noRet) extraData.noRet = 1;
promise = _callBrowsers(null, name, args, extraData);
break;
case 'server':
if(arguments.length < 2 || arguments.length > 4) return util.promiseReject('callBrowsers from the server expects 2 to 4 arguments: "player", "name", optional "args", and optional "options"');
if(options.noRet) extraData.noRet = 1;
promise = _callBrowsers(player as Player, name, args, extraData);
break;
}
if(promise){
return util.promiseTimeout(promise, options.timeout);
}
}
/**
* Calls a remote procedure registered in a specific browser instance.
*
* Client-side environment only.
*
* @param browser - The browser instance.
* @param name - The name of the registered procedure.
* @param args - Any parameters for the procedure.
* @param options - Any options.
* @returns The result from the procedure.
*/
export function callBrowser(browser: Browser, name: string, args?: any, options: CallOptions = {}): Promise<any> {
if(environment !== 'client') return util.promiseReject('callBrowser can only be used in the client environment');
if(arguments.length < 2 || arguments.length > 4) return util.promiseReject('callBrowser expects 2 to 4 arguments: "browser", "name", optional "args", and optional "options"');
let extraData: any = {};
if(options.noRet) extraData.noRet = 1;
return util.promiseTimeout(_callBrowser(browser, name, args, extraData), options.timeout);
}
function callEvent(name: string, args: any, info: ProcedureListenerInfo){
const listeners = glob.__rpcEvListeners[name];
if(listeners){
listeners.forEach(listener => listener(args, info));
}
}
/**
* Register an event handler.
* @param {string} name - The name of the event.
* @param cb - The callback for the event.
* @returns {Function} The function, which off the event.
*/
export function on(name: string, cb: ProcedureListener): Function {
if(arguments.length !== 2) throw 'on expects 2 arguments: "name" and "cb"';
const listeners = glob.__rpcEvListeners[name] || new Set();
listeners.add(cb);
glob.__rpcEvListeners[name] = listeners;
return () => off(name, cb);
}
/**
* Unregister an event handler.
* @param {string} name - The name of the event.
* @param cb - The callback for the event.
*/
export function off(name: string, cb: ProcedureListener){
if(arguments.length !== 2) throw 'off expects 2 arguments: "name" and "cb"';
const listeners = glob.__rpcEvListeners[name];
if(listeners){
listeners.delete(cb);
}
}
/**
* Triggers a local event. Only events registered in the same context will be triggered.
*
* Can be called from any environment.
*
* @param name - The name of the locally registered event.
* @param args - Any parameters for the event.
*/
export function trigger(name: string, args?: any){
if(arguments.length < 1 || arguments.length > 2) throw 'trigger expects 1 or 2 arguments: "name", and optional "args"';
callEvent(name, args, { environment });
}
/**
* Triggers an event registered on the client.
*
* Can be called from any environment.
*
* @param player - The player to call the procedure on.
* @param name - The name of the event.
* @param args - Any parameters for the event.
*/
export function triggerClient(player: Player | string, name?: string | any, args?: any){
switch(environment){
case 'client': {
args = name;
name = player;
player = null;
if((arguments.length < 1 || arguments.length > 2) || typeof name !== 'string') throw 'triggerClient from the client expects 1 or 2 arguments: "name", and optional "args"';
break;
}
case 'server': {
if((arguments.length < 2 || arguments.length > 3) || typeof player !== 'object') throw 'triggerClient from the server expects 2 or 3 arguments: "player", "name", and optional "args"';
break;
}
case 'cef': {
args = name;
name = player;
player = null;
if((arguments.length < 1 || arguments.length > 2) || typeof name !== 'string') throw 'triggerClient from the browser expects 1 or 2 arguments: "name", and optional "args"';
break;
}
}
_callClient(player as Player, TRIGGER_EVENT, [name, args], { noRet: 1 });
}
/**
* Triggers an event registered on the server.
*
* Can be called from any environment.
*
* @param name - The name of the event.
* @param args - Any parameters for the event.
*/
export function triggerServer(name: string, args?: any){
if(arguments.length < 1 || arguments.length > 2) throw 'triggerServer expects 1 or 2 arguments: "name", and optional "args"';
_callServer(TRIGGER_EVENT, [name, args], { noRet: 1 });
}
/**
* Triggers an event registered in any browser context.
*
* Can be called from any environment.
*
* @param player - The player to call the procedure on.
* @param name - The name of the event.
* @param args - Any parameters for the event.
*/
export function triggerBrowsers(player: Player | string, name?: string | any, args?: any){
switch(environment){
case 'client':
case 'cef':
args = name;
name = player;
player = null;
if(arguments.length < 1 || arguments.length > 2) throw 'triggerBrowsers from the client or browser expects 1 or 2 arguments: "name", and optional "args"';
break;
case 'server':
if(arguments.length < 2 || arguments.length > 3) throw 'triggerBrowsers from the server expects 2 or 3 arguments: "player", "name", and optional "args"';
break;
}
_callClient(player as Player, TRIGGER_EVENT_BROWSERS, [name, args], { noRet: 1 });
}
/**
* Triggers an event registered in a specific browser instance.
*
* Client-side environment only.
*
* @param browser - The browser instance.
* @param name - The name of the event.
* @param args - Any parameters for the event.
*/
export function triggerBrowser(browser: Browser, name: string, args?: any){
if(environment !== 'client') throw 'callBrowser can only be used in the client environment';
if(arguments.length < 2 || arguments.length > 4) throw 'callBrowser expects 2 or 3 arguments: "browser", "name", and optional "args"';
_callBrowser(browser, TRIGGER_EVENT, [name, args], { noRet: 1});
}
export default {
register,
unregister,
call,
callServer,
callClient,
callBrowsers,
callBrowser,
on,
off,
trigger,
triggerServer,
triggerClient,
triggerBrowsers,
triggerBrowser
};

122
rage-rpc/src/util.ts Normal file
View File

@ -0,0 +1,122 @@
enum MpTypes {
Blip = 'b',
Checkpoint = 'cp',
Colshape = 'c',
Label = 'l',
Marker = 'm',
Object = 'o',
Pickup = 'p',
Player = 'pl',
Vehicle = 'v'
}
function isObjectMpType(obj: any, type: MpTypes){
const client = getEnvironment() === 'client';
if(obj && typeof obj === 'object' && typeof obj.id !== 'undefined'){
const test = (type, collection, mpType) => client ? obj.type === type && collection.at(obj.id) === obj : obj instanceof mpType;
switch(type){
case MpTypes.Blip: return test('blip', mp.blips, mp.Blip);
case MpTypes.Checkpoint: return test('checkpoint', mp.checkpoints, mp.Checkpoint);
case MpTypes.Colshape: return test('colshape', mp.colshapes, mp.Colshape);
case MpTypes.Label: return test('textlabel', mp.labels, mp.TextLabel);
case MpTypes.Marker: return test('marker', mp.markers, mp.Marker);
case MpTypes.Object: return test('object', mp.objects, mp.Object);
case MpTypes.Pickup: return test('pickup', mp.pickups, mp.Pickup);
case MpTypes.Player: return test('player', mp.players, mp.Player);
case MpTypes.Vehicle: return test('vehicle', mp.vehicles, mp.Vehicle);
}
}
return false;
}
export function uid(): string {
const first = (Math.random() * 46656) | 0;
const second = (Math.random() * 46656) | 0;
const firstPart = ('000' + first.toString(36)).slice(-3);
const secondPart = ('000' + second.toString(36)).slice(-3);
return firstPart + secondPart;
}
export function getEnvironment(): string {
if ('mp' in window) return 'cef';
if (mp.joaat) return 'server';
else if (mp.game && mp.game.joaat) return 'client';
}
export function stringifyData(data: any): string {
const env = getEnvironment();
return JSON.stringify(data, (_, value) => {
if(env === 'client' || env === 'server' && value && typeof value === 'object'){
let type;
if(isObjectMpType(value, MpTypes.Blip)) type = MpTypes.Blip;
else if(isObjectMpType(value, MpTypes.Checkpoint)) type = MpTypes.Checkpoint;
else if(isObjectMpType(value, MpTypes.Colshape)) type = MpTypes.Colshape;
else if(isObjectMpType(value, MpTypes.Marker)) type = MpTypes.Marker;
else if(isObjectMpType(value, MpTypes.Object)) type = MpTypes.Object;
else if(isObjectMpType(value, MpTypes.Pickup)) type = MpTypes.Pickup;
else if(isObjectMpType(value, MpTypes.Player)) type = MpTypes.Player;
else if(isObjectMpType(value, MpTypes.Vehicle)) type = MpTypes.Vehicle;
if(type) return {
__t: type,
i: typeof value.remoteId === 'number' ? value.remoteId : value.id
};
}
return value;
});
}
export function parseData(data: string): any {
const env = getEnvironment();
return JSON.parse(data, (_, value) => {
if((env === 'client' || env === 'server') && value && typeof value === 'object' && typeof value['__t'] === 'string' && typeof value.i === 'number' && Object.keys(value).length === 2){
const id = value.i;
const type = value['__t'];
let collection;
switch(type){
case MpTypes.Blip: collection = mp.blips; break;
case MpTypes.Checkpoint: collection = mp.checkpoints; break;
case MpTypes.Colshape: collection = mp.colshapes; break;
case MpTypes.Label: collection = mp.labels; break;
case MpTypes.Marker: collection = mp.markers; break;
case MpTypes.Object: collection = mp.objects; break;
case MpTypes.Pickup: collection = mp.pickups; break;
case MpTypes.Player: collection = mp.players; break;
case MpTypes.Vehicle: collection = mp.vehicles; break;
}
if(collection) return collection[env === 'client' ? 'atRemoteId' : 'at'](id);
}
return value;
});
}
export function promiseResolve(result: any): Promise<any> {
return new Promise(resolve => setTimeout(() => resolve(result), 0));
}
export function promiseReject(error: any): Promise<any> {
return new Promise((_, reject) => setTimeout(() => reject(error), 0));
}
export function promiseTimeout(promise: Promise<any>, timeout?: number){
if(typeof timeout === 'number'){
return Promise.race([
new Promise((_, reject) => {
setTimeout(() => reject('TIMEOUT'), timeout);
}),
promise
]);
}else return promise;
}
export function isBrowserValid(browser: Browser): boolean {
try {
browser.url;
}catch(e){ return false; }
return true;
}

25
rage-rpc/tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Base",
"exclude": [
"node_modules"
],
"compilerOptions": {
"incremental": false,
"composite": false,
"target": "ES2022",
"experimentalDecorators": true,
"moduleDetection": "auto",
"module": "CommonJS",
"resolveJsonModule": true,
"declaration": false,
"declarationMap": false,
"sourceMap": false,
"downlevelIteration": false,
"inlineSourceMap": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}

12
rage-rpc/tsup.config.ts Normal file
View File

@ -0,0 +1,12 @@
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
outDir: './dist',
format: ['cjs'],
noExternal: ['rage-rpc'],
experimentalDts: true,
splitting: false,
sourcemap: false,
clean: true,
})

View File

@ -9,7 +9,16 @@ RageFW is a type-safe framework for developing Rage:MP servers. Designed with de
- **Logging System:** Keep track of server activities and debug like a pro with our built-in, feature-rich logging system. After all, even virtual cops need evidence - **Logging System:** Keep track of server activities and debug like a pro with our built-in, feature-rich logging system. After all, even virtual cops need evidence
## Getting Started ## Getting Started
*soon* You can find out more about our CLI [here](https://git.entityseven.com/entityseven/rage-framework/wiki/CLI)
At the moment automation we have only works via [pnpm](https://pnpm.io/). To scaffold a basic project with minor settings you can use our CLI:
``pnpm create rage-fw``
This will give you a few options, among them, you can find ``Initialize new project``. Use that option to scaffold a new project for yourself using the preferred frontend framework
## Documentation
[Available here](https://git.entityseven.com/entityseven/rage-framework/wiki/Docs+%40+0.0.30-alpha.0.-)
## Contributing ## Contributing
Join our community of developers and contribute to the ongoing development of RageFW. At the moment the only way to contribute is opening issues Join our community of developers and contribute to the ongoing development of RageFW. At the moment the only way to contribute is opening issues
@ -17,4 +26,7 @@ Join our community of developers and contribute to the ongoing development of Ra
## Support ## Support
Need help? Reach out via our community forums or contact us directly through our support channels. We're committed to help you as we can Need help? Reach out via our community forums or contact us directly through our support channels. We're committed to help you as we can
## License
Licensed under **Custom Attribution-NoDerivs Software License**
> *RageFW - because in the world of GTA:RP, nobody has time for type errors* > *RageFW - because in the world of GTA:RP, nobody has time for type errors*

View File

@ -1,10 +1,11 @@
{ {
"name": "rage-fw-server", "name": "rage-fw-server",
"version": "0.0.20-alpha.0", "version": "0.1.0",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
"files": [ "files": [
"dist/**/*" "dist/**/*",
"readme.md"
], ],
"scripts": { "scripts": {
"build": "tsup" "build": "tsup"

2
server/readme.md Normal file
View File

@ -0,0 +1,2 @@
# RageFW Server
[Read docs for details](https://git.entityseven.com/entityseven/rage-framework/wiki/Docs)

View File

@ -34,7 +34,7 @@ class Server {
rpc.register( rpc.register(
eventName, eventName,
async (args: RageFW_ServerEventArguments<EventName>, info) => { async (args: RageFW_ServerEventArguments<EventName>, info) => {
callback(info.player as PlayerMp, args) callback([info.player as PlayerMp, ...args])
}, },
) )
} }
@ -43,7 +43,11 @@ class Server {
eventName: EventName, eventName: EventName,
callback: RageFW_ServerEventCallbackNative<EventName>, callback: RageFW_ServerEventCallbackNative<EventName>,
): void { ): void {
mp.events.add(eventName, callback) mp.events.add(
eventName,
(...args: Parameters<IServerEvents[EventName]>) =>
callback([...args]),
)
} }
public register<EventName extends RageFW_ServerEvent>( public register<EventName extends RageFW_ServerEvent>(

View File

@ -54,8 +54,7 @@ export type RageFW_ServerEventReturn<K extends RageFW_ServerEvent> =
export type RageFW_ServerEventCallbackCustom< export type RageFW_ServerEventCallbackCustom<
K extends keyof RageFW_ICustomServerEvent = keyof RageFW_ICustomServerEvent, K extends keyof RageFW_ICustomServerEvent = keyof RageFW_ICustomServerEvent,
> = ( > = (
player: PlayerMp, payload: [player: PlayerMp, ...args: RageFW_ServerEventArguments<K>],
args: RageFW_ServerEventArguments<K>,
) => RageFW_ServerEventReturn<K> ) => RageFW_ServerEventReturn<K>
/** /**
@ -64,7 +63,7 @@ export type RageFW_ServerEventCallbackCustom<
*/ */
export type RageFW_ServerEventCallbackNative< export type RageFW_ServerEventCallbackNative<
K extends keyof IServerEvents = keyof IServerEvents, K extends keyof IServerEvents = keyof IServerEvents,
> = IServerEvents[K] > = (payload: Parameters<IServerEvents[K]>) => ReturnType<IServerEvents[K]>
export type _ServerEventHasArgs< export type _ServerEventHasArgs<
EventName extends keyof RageFW_ICustomServerEvent, EventName extends keyof RageFW_ICustomServerEvent,

View File

@ -1,6 +1,6 @@
{ {
"name": "rage-fw-shared-types", "name": "rage-fw-shared-types",
"version": "0.0.20-alpha.0", "version": "0.1.0",
"types": "types/types/index.d.ts", "types": "types/types/index.d.ts",
"files": [ "files": [
"types/**/*" "types/**/*"

2
shared-types/readme.md Normal file
View File

@ -0,0 +1,2 @@
# RageFW Shared types
[Read docs for details](https://git.entityseven.com/entityseven/rage-framework/wiki/Docs)

View File

@ -3,7 +3,5 @@ declare module 'rage-fw-shared-types' {
export interface RageFW_ICustomClientEvent {} export interface RageFW_ICustomClientEvent {}
export interface RageFW_ICustomCefEvent { export interface RageFW_ICustomCefEvent {}
test(test: string): void
}
} }

View File

@ -1,7 +1,11 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"lib": ["ESNext","ES2019"], "lib": [
"ESNext",
"ES2019",
"dom"
],
"moduleResolution": "node", "moduleResolution": "node",
"module": "ESNext", "module": "ESNext",
"esModuleInterop": true, "esModuleInterop": true,