diff --git a/.gitea/ISSUE_TEMPLATE.yaml b/.gitea/ISSUE_TEMPLATE.yaml new file mode 100644 index 0000000..3f7ce9a --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE.yaml @@ -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 diff --git a/cef/package.json b/cef/package.json index a34eff2..10dc1a6 100644 --- a/cef/package.json +++ b/cef/package.json @@ -1,10 +1,11 @@ { "name": "rage-fw-cef", - "version": "0.0.20-alpha.0", + "version": "0.1.0", "main": "dist/index.js", "types": "dist/src/index.d.ts", "files": [ - "dist/**/*" + "dist/**/*", + "readme.md" ], "scripts": { "build": "tsup" diff --git a/cef/readme.md b/cef/readme.md new file mode 100644 index 0000000..6b33d7e --- /dev/null +++ b/cef/readme.md @@ -0,0 +1,2 @@ +# RageFW CEF +[Read docs for details](https://git.entityseven.com/entityseven/rage-framework/wiki/Docs) \ No newline at end of file diff --git a/cef/src/index.ts b/cef/src/index.ts index 10b2ef5..9e56774 100644 --- a/cef/src/index.ts +++ b/cef/src/index.ts @@ -21,7 +21,9 @@ class Cef { eventName: EventName, callback: RageFW_CefCallback, ): void { - rpc.register(eventName, callback) + if ('mp' in window) { + rpc.register(eventName, callback) + } } public trigger( @@ -30,7 +32,13 @@ class Cef { ? [RageFW_CefArguments] : [] ): Promise> { - return rpc.call(eventName, args) + if ('mp' in window) { + return rpc.call(eventName, args) + } + + return Promise.reject( + 'RageFW was started in window which not contain global variable MP!', + ) } public triggerServer( @@ -39,7 +47,13 @@ class Cef { ? [RageFW_ServerArguments] : [] ): Promise> { - return rpc.callServer(eventName, args) + if ('mp' in window) { + return rpc.callServer(eventName, args) + } + + return Promise.reject( + 'RageFW was started in window which not contain global variable MP!', + ) } public triggerClient( @@ -48,7 +62,13 @@ class Cef { ? [RageFW_ClientArguments] : [] ): Promise> { - return rpc.callClient(eventName, args) + if ('mp' in window) { + return rpc.callClient(eventName, args) + } + + return Promise.reject( + 'RageFW was started in window which not contain global variable MP!', + ) } } diff --git a/cli/.prettierrc.yaml b/cli/.prettierrc.yaml new file mode 100644 index 0000000..aa7ff98 --- /dev/null +++ b/cli/.prettierrc.yaml @@ -0,0 +1,6 @@ +tabWidth: 4 +printWidth: 80 +singleQuote: true +semi: false +arrowParens: avoid +endOfLine: auto diff --git a/cli/package.json b/cli/package.json new file mode 100644 index 0000000..95b6ef9 --- /dev/null +++ b/cli/package.json @@ -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" + } +} diff --git a/cli/readme.md b/cli/readme.md new file mode 100644 index 0000000..f8c9983 --- /dev/null +++ b/cli/readme.md @@ -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 \ No newline at end of file diff --git a/cli/src/commands/create.ts b/cli/src/commands/create.ts new file mode 100644 index 0000000..f6429e6 --- /dev/null +++ b/cli/src/commands/create.ts @@ -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)}`, + ), + ) + }, + ) +} diff --git a/cli/src/commands/download-updater.ts b/cli/src/commands/download-updater.ts new file mode 100644 index 0000000..4ce885f --- /dev/null +++ b/cli/src/commands/download-updater.ts @@ -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 { + 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(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(latestReleases).then(({ data }) => data[0].id) +} diff --git a/cli/src/index.ts b/cli/src/index.ts new file mode 100644 index 0000000..912bc83 --- /dev/null +++ b/cli/src/index.ts @@ -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() +})() diff --git a/cli/src/utils/update.ts b/cli/src/utils/update.ts new file mode 100644 index 0000000..cb8a44d --- /dev/null +++ b/cli/src/utils/update.ts @@ -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 { + return new Promise(res => { + yargs.showVersion(version => + axios + .get(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}`)) +} diff --git a/cli/tsconfig.json b/cli/tsconfig.json new file mode 100644 index 0000000..9543d5d --- /dev/null +++ b/cli/tsconfig.json @@ -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" + ] +} \ No newline at end of file diff --git a/cli/tsup.config.ts b/cli/tsup.config.ts new file mode 100644 index 0000000..d0c0ce1 --- /dev/null +++ b/cli/tsup.config.ts @@ -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, +}) diff --git a/client/package.json b/client/package.json index 76c39de..9c18bea 100644 --- a/client/package.json +++ b/client/package.json @@ -1,10 +1,11 @@ { "name": "rage-fw-client", - "version": "0.0.20-alpha.0", + "version": "0.1.0", "main": "dist/index.js", "types": "dist/src/index.d.ts", "files": [ - "dist/**/*" + "dist/**/*", + "readme.md" ], "scripts": { "build": "tsup" diff --git a/client/readme.md b/client/readme.md new file mode 100644 index 0000000..0b76af4 --- /dev/null +++ b/client/readme.md @@ -0,0 +1,2 @@ +# RageFW Client +[Read docs for details](https://git.entityseven.com/entityseven/rage-framework/wiki/Docs) \ No newline at end of file diff --git a/client/src/types/client.ts b/client/src/types/client.ts index 6cc633b..bbc5ce5 100644 --- a/client/src/types/client.ts +++ b/client/src/types/client.ts @@ -4,16 +4,22 @@ import type { RageFW_ICustomClientEvent } from 'rage-fw-shared-types' /** * 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 - * These only include custom events + * These include custom and system events */ export type RageFW_ClientEventArguments = - Parameters + K extends keyof RageFW_ICustomClientEvent + ? Parameters + : K extends keyof IClientEvents + ? Parameters + : never /** * Callback (function) for an event, name of which you pass as a generic diff --git a/lerna.json b/lerna.json index 2dc86c4..c44c203 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.0.20-alpha.0", + "version": "0.1.0", "npmClient": "pnpm" } diff --git a/license.md b/license.md new file mode 100644 index 0000000..8aca84b --- /dev/null +++ b/license.md @@ -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. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be0022b..07cb918 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ importers: dependencies: '@microsoft/api-extractor': specifier: ^7.47.0 - version: 7.47.0 + version: 7.47.0(@types/node@20.14.2) '@ragempcommunity/types-cef': specifier: ^2.1.8 version: 2.1.8 @@ -39,7 +39,7 @@ importers: version: 0.4.0 tsup: 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: specifier: ^5.4.5 version: 5.4.5 @@ -59,6 +59,40 @@ importers: specifier: ^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: dependencies: '@ragempcommunity/types-client': @@ -386,6 +420,90 @@ packages: } 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': resolution: { @@ -1069,6 +1187,12 @@ packages: integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==, } + '@types/git-clone@0.2.4': + resolution: + { + integrity: sha512-1ybApDpKU12dychtOp2zBe93ZwAsxVSjOqKUqH7NCDm4GXuPnjmcz2P9K2S1z+BCX2AnLmFFuB6pI6CMZ3j9sQ==, + } + '@types/minimatch@3.0.5': resolution: { @@ -1081,6 +1205,18 @@ packages: 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': resolution: { @@ -1093,6 +1229,24 @@ packages: 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': resolution: { @@ -1667,6 +1821,13 @@ packages: } engines: { node: '>= 10' } + cli-width@4.1.0: + resolution: + { + integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==, + } + engines: { node: '>= 12' } + cliui@7.0.4: resolution: { @@ -2340,10 +2501,10 @@ packages: debug: optional: true - foreground-child@3.1.1: + foreground-child@3.2.0: resolution: { - integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==, + integrity: sha512-CrWQNaEl1/6WeZoarcM9LHupTo3RpZO2Pdk1vktwzPiQTsJnAKJmm3TACKeG5UZbWDfaH2AbvYxzP96y0MT7fA==, } engines: { node: '>=14' } @@ -2458,6 +2619,12 @@ packages: } engines: { node: '>=10' } + git-clone@0.2.0: + resolution: + { + integrity: sha512-1UAkEPIFbyjHaddljUKvPhhLRnrKaImT71T7rdvSvWLXw95nLdhdi6Qmlx0KOWoV1qqvHGLq5lMLJEZM0JXk8A==, + } + git-raw-commits@3.0.0: resolution: { @@ -5086,6 +5253,12 @@ packages: engines: { node: '>=0.8.0' } hasBin: true + undici-types@5.26.5: + resolution: + { + integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==, + } + unique-filename@3.0.0: resolution: { @@ -5490,6 +5663,87 @@ snapshots: '@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': dependencies: string-width: 5.1.2 @@ -5595,23 +5849,23 @@ snapshots: - supports-color - typescript - '@microsoft/api-extractor-model@7.29.2': + '@microsoft/api-extractor-model@7.29.2(@types/node@20.14.2)': dependencies: '@microsoft/tsdoc': 0.15.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: - '@types/node' - '@microsoft/api-extractor@7.47.0': + '@microsoft/api-extractor@7.47.0(@types/node@20.14.2)': 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-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/terminal': 0.13.0 - '@rushstack/ts-command-line': 4.22.0 + '@rushstack/terminal': 0.13.0(@types/node@20.14.2) + '@rushstack/ts-command-line': 4.22.0(@types/node@20.14.2) lodash: 4.17.21 minimatch: 3.0.8 resolve: 1.22.8 @@ -5890,7 +6144,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.18.0': optional: true - '@rushstack/node-core-library@5.4.1': + '@rushstack/node-core-library@5.4.1(@types/node@20.14.2)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -5900,20 +6154,24 @@ snapshots: jju: 1.4.0 resolve: 1.22.8 semver: 7.5.4 + optionalDependencies: + '@types/node': 20.14.2 '@rushstack/rig-package@0.5.2': dependencies: resolve: 1.22.8 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.13.0': + '@rushstack/terminal@0.13.0(@types/node@20.14.2)': 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 + 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: - '@rushstack/terminal': 0.13.0 + '@rushstack/terminal': 0.13.0(@types/node@20.14.2) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -5995,14 +6253,32 @@ snapshots: '@types/estree@1.0.5': {} + '@types/git-clone@0.2.4': {} + '@types/minimatch@3.0.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/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)': dependencies: '@eslint-community/regexpp': 4.10.1 @@ -6361,6 +6637,8 @@ snapshots: cli-width@3.0.0: {} + cli-width@4.1.0: {} + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -6796,7 +7074,7 @@ snapshots: follow-redirects@1.15.6: {} - foreground-child@3.1.1: + foreground-child@3.2.0: dependencies: cross-spawn: 7.0.3 signal-exit: 4.1.0 @@ -6866,6 +7144,8 @@ snapshots: get-stream@6.0.1: {} + git-clone@0.2.0: {} + git-raw-commits@3.0.0: dependencies: dargs: 7.0.0 @@ -6905,7 +7185,7 @@ snapshots: glob@10.4.1: dependencies: - foreground-child: 3.1.1 + foreground-child: 3.2.0 jackspeak: 3.4.0 minimatch: 9.0.4 minipass: 7.1.2 @@ -7186,13 +7466,13 @@ snapshots: jake@10.9.1: dependencies: async: 3.2.5 - chalk: 4.1.0 + chalk: 4.1.2 filelist: 1.0.4 minimatch: 3.1.2 jest-diff@29.7.0: dependencies: - chalk: 4.1.0 + chalk: 4.1.2 diff-sequences: 29.6.3 jest-get-type: 29.6.3 pretty-format: 29.7.0 @@ -7783,7 +8063,7 @@ snapshots: '@yarnpkg/parsers': 3.0.0-rc.46 '@zkochan/js-yaml': 0.0.7 axios: 1.7.2 - chalk: 4.1.0 + chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 cliui: 8.0.1 @@ -7858,7 +8138,7 @@ snapshots: ora@5.3.0: dependencies: bl: 4.1.0 - chalk: 4.1.0 + chalk: 4.1.2 cli-cursor: 3.1.0 cli-spinners: 2.6.1 is-interactive: 1.0.0 @@ -8487,7 +8767,7 @@ snapshots: 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: bundle-require: 4.2.1(esbuild@0.21.5) cac: 6.7.14 @@ -8504,7 +8784,7 @@ snapshots: sucrase: 3.35.0 tree-kill: 1.2.2 optionalDependencies: - '@microsoft/api-extractor': 7.47.0 + '@microsoft/api-extractor': 7.47.0(@types/node@20.14.2) typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -8551,6 +8831,8 @@ snapshots: uglify-js@3.18.0: optional: true + undici-types@5.26.5: {} + unique-filename@3.0.0: dependencies: unique-slug: 4.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4dda521..401e708 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,4 +2,5 @@ packages: - "server" - "client" - "cef" + - "cli" - "shared-types" \ No newline at end of file diff --git a/rage-rpc/README.md b/rage-rpc/README.md new file mode 100644 index 0000000..2af1ed4 --- /dev/null +++ b/rage-rpc/README.md @@ -0,0 +1 @@ +Currently not maintained. \ No newline at end of file diff --git a/rage-rpc/package.json b/rage-rpc/package.json new file mode 100644 index 0000000..a8074f6 --- /dev/null +++ b/rage-rpc/package.json @@ -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" +} diff --git a/rage-rpc/src/defs.d.ts b/rage-rpc/src/defs.d.ts new file mode 100644 index 0000000..00cf5bf --- /dev/null +++ b/rage-rpc/src/defs.d.ts @@ -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; +} \ No newline at end of file diff --git a/rage-rpc/src/index.ts b/rage-rpc/src/index.ts new file mode 100644 index 0000000..06b7cd2 --- /dev/null +++ b/rage-rpc/src/index.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 +}; \ No newline at end of file diff --git a/rage-rpc/src/util.ts b/rage-rpc/src/util.ts new file mode 100644 index 0000000..10a9c05 --- /dev/null +++ b/rage-rpc/src/util.ts @@ -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 { + return new Promise(resolve => setTimeout(() => resolve(result), 0)); +} + +export function promiseReject(error: any): Promise { + return new Promise((_, reject) => setTimeout(() => reject(error), 0)); +} + +export function promiseTimeout(promise: Promise, 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; +} \ No newline at end of file diff --git a/rage-rpc/tsconfig.json b/rage-rpc/tsconfig.json new file mode 100644 index 0000000..00ca838 --- /dev/null +++ b/rage-rpc/tsconfig.json @@ -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 + } +} diff --git a/rage-rpc/tsup.config.ts b/rage-rpc/tsup.config.ts new file mode 100644 index 0000000..af724fb --- /dev/null +++ b/rage-rpc/tsup.config.ts @@ -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, +}) diff --git a/readme.md b/readme.md index e6e3171..484be8a 100644 --- a/readme.md +++ b/readme.md @@ -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 ## 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 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 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* \ No newline at end of file diff --git a/server/package.json b/server/package.json index f73bc95..9ccae0e 100644 --- a/server/package.json +++ b/server/package.json @@ -1,10 +1,11 @@ { "name": "rage-fw-server", - "version": "0.0.20-alpha.0", + "version": "0.1.0", "main": "dist/index.js", "types": "dist/src/index.d.ts", "files": [ - "dist/**/*" + "dist/**/*", + "readme.md" ], "scripts": { "build": "tsup" diff --git a/server/readme.md b/server/readme.md new file mode 100644 index 0000000..66f12cb --- /dev/null +++ b/server/readme.md @@ -0,0 +1,2 @@ +# RageFW Server +[Read docs for details](https://git.entityseven.com/entityseven/rage-framework/wiki/Docs) \ No newline at end of file diff --git a/server/src/index.ts b/server/src/index.ts index 20c6e4b..7595f40 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -34,7 +34,7 @@ class Server { rpc.register( eventName, async (args: RageFW_ServerEventArguments, info) => { - callback(info.player as PlayerMp, args) + callback([info.player as PlayerMp, ...args]) }, ) } @@ -43,7 +43,11 @@ class Server { eventName: EventName, callback: RageFW_ServerEventCallbackNative, ): void { - mp.events.add(eventName, callback) + mp.events.add( + eventName, + (...args: Parameters) => + callback([...args]), + ) } public register( diff --git a/server/src/types/server.ts b/server/src/types/server.ts index 5f64db6..e7a487b 100644 --- a/server/src/types/server.ts +++ b/server/src/types/server.ts @@ -54,8 +54,7 @@ export type RageFW_ServerEventReturn = export type RageFW_ServerEventCallbackCustom< K extends keyof RageFW_ICustomServerEvent = keyof RageFW_ICustomServerEvent, > = ( - player: PlayerMp, - args: RageFW_ServerEventArguments, + payload: [player: PlayerMp, ...args: RageFW_ServerEventArguments], ) => RageFW_ServerEventReturn /** @@ -64,7 +63,7 @@ export type RageFW_ServerEventCallbackCustom< */ export type RageFW_ServerEventCallbackNative< K extends keyof IServerEvents = keyof IServerEvents, -> = IServerEvents[K] +> = (payload: Parameters) => ReturnType export type _ServerEventHasArgs< EventName extends keyof RageFW_ICustomServerEvent, diff --git a/shared-types/package.json b/shared-types/package.json index ed048d3..418368f 100644 --- a/shared-types/package.json +++ b/shared-types/package.json @@ -1,6 +1,6 @@ { "name": "rage-fw-shared-types", - "version": "0.0.20-alpha.0", + "version": "0.1.0", "types": "types/types/index.d.ts", "files": [ "types/**/*" diff --git a/shared-types/readme.md b/shared-types/readme.md new file mode 100644 index 0000000..44c7ed5 --- /dev/null +++ b/shared-types/readme.md @@ -0,0 +1,2 @@ +# RageFW Shared types +[Read docs for details](https://git.entityseven.com/entityseven/rage-framework/wiki/Docs) \ No newline at end of file diff --git a/shared-types/types/types/index.d.ts b/shared-types/types/types/index.d.ts index 3914b9e..a65c140 100644 --- a/shared-types/types/types/index.d.ts +++ b/shared-types/types/types/index.d.ts @@ -3,7 +3,5 @@ declare module 'rage-fw-shared-types' { export interface RageFW_ICustomClientEvent {} - export interface RageFW_ICustomCefEvent { - test(test: string): void - } + export interface RageFW_ICustomCefEvent {} } diff --git a/tsconfig.json b/tsconfig.json index 3c0fb42..f01dbdf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ESNext", - "lib": ["ESNext","ES2019"], + "lib": [ + "ESNext", + "ES2019", + "dom" + ], "moduleResolution": "node", "module": "ESNext", "esModuleInterop": true,