Git Product home page Git Product logo

rage-rpc's Introduction

fork of micaww/rage-rpc

Motivation

A very common workflow when developing with any kind of client-server platform is not only sending data between the server and clients, but also receiving data back after performing some kind of action. An example would be a client asking for information from a database in order to display to the user. One technique to achieve this is called remote procedure calls (RPC) which allows one application context to call code in a completely separate context and return the result back to the caller, as if it were local to begin with.

In RAGE Multiplayer, this kind of functionality is not supported natively. In order for a player to ask something of the server, the server must set up an event handler that the player calls remotely, then the server does its processing and calls another event handler that resides on the client. There are many pitfalls to this approach, including but not limited to messy code and false identification (am i sending the response to the right caller instance?). Natively, the server cannot directly communicate with CEF instances at all. You have to route all requests through the client. Suddenly, you have 16 different events to handle one simple data request. It's horrible. And when your codebase starts growing, it becomes a huge hassle to deal with.

This is pretty much what everybody has learned to deal with, until now. rage-rpc simplifies two-way communication between the RAGE Multiplayer server, client, and browser instances by providing a easy-to-use API for calling remote code and retrieving results. Any context can call a function that resides in any other context and immediately get access to its return value without messing with events. This means any CEF instance can call code on the server, the client, or any other CEF instances and easily see the result.


Installation

Option 1

You can install via github

Use github:leonardssh/rage-rpc#build to latest build

# With npm
npm i github:leonardssh/rage-rpc#v0.2.5

# With yarn
yarn add github:leonardssh/rage-rpc#v0.2.5

# With pnpm
pnpm add github:leonardssh/rage-rpc#v0.2.5

From here, you can simply require the package in any RAGE context:

const rpc = require('rage-rpc');

rpc.register('hi', () => 'hello!');

Option 2

In the dist/ folder of build branch is a JS file (rage-rpc.js) that you can download and request in any RAGE context. It works the same as the above option, but you will have to manually reload the file when new versions are released.

const rpc = require('./rage-rpc.js');

rpc.register('hi', () => 'hello!');

Option 3 (Browser Only)

In order to use require in the browser, you'll need either an AMD loader or some kind of bundler like Webpack or Rollup. If those options don't suit your project, you can load the file into browser contexts with just a script tag before the code you use it in. It will expose a global rpc variable that you can use on your page.

<html>
	<head>
		<title>My CEF Page</title>
		<script type="text/javascript" src="./rage-rpc.umd.js"></script>
		<script type="text/javascript">
			rpc.register('hi', () => 'hello from cef!');

			// ...
		</script>
	</head>
</html>

Examples

Server to Client

Situation: The server wants to ask a specific player if they are currently climbing anything.

Client-side
const rpc = require('rage-rpc');

rpc.register('getIsClimbing', () => mp.players.local.isClimbing());
Server-side
const rpc = require('rage-rpc');

const player = mp.players.at(0);

rpc.callClient(player, 'getIsClimbing').then((climbing) => {
	if (climbing) {
		console.log('The player is climbing!');
	} else {
		console.log('The player is not climbing!');
	}
});

// or even just this inside an async function:
const isClimbing = await rpc.callClient(player, 'getIsClimbing');

That's it! No extra code to sort out who is asking for what, or setting up multiple events on each side just to send a single piece of data back to the caller.


CEF to Server

Situation: A CEF instance wants a list of all vehicle license plates directly from the server.

Server-side
const rpc = require('rage-rpc');

rpc.register('getAllLicensePlates', () => mp.vehicles.toArray().map((vehicle) => vehicle.numberPlate));
Client-side
// even if not using RPC on the client, it must be required somewhere before CEF can send any events
require('rage-rpc');
Browser
const rpc = require('rage-rpc');

rpc.callServer('getAllLicensePlates').then((plates) => {
	alert(plates.join(', '));
});

With rage-rpc, CEF can directly communicate with the server and vice-versa, without having to pass everything through the client-side JS.

In vanilla RAGE, you would have to set up multiple events for sending/receiving on the client-side, call them from CEF, then resend the data to the server and back. It's a huge hassle.

Client to Server

Situation: Give the clients/CEF the ability to log to the server's console.

Server-side
const rpc = require('rage-rpc');

rpc.register('log', (message, info) => {
	/*
    the second argument, info, gives information about the request such as
    - the internal ID of the request
    - the environment in which the request was sent (server, client, or cef)
    - the player who sent the request, if any
    */

	console.log(info.player.name + ': ' + message);
});
Client-side OR Browser
const rpc = require('rage-rpc');

function log(message) {
	return rpc.callServer('log', message);
}

// send it and forget it
log('Hello, Server!');

// send it again, but make sure it was successfully received
log('Hello again!')
	.then(() => {
		// the server acknowledged and processed the message
	})
	.catch(() => {
		// the message either timed out or the procedure was never registered
	});

Note: Once any side of the game registers a procedure, any context can immediately start accessing it. You could call rpc.callServer('log', message); from any CEF instance or anywhere in the client without any further setup.

API

This library is universal to RAGE, which means you can load the same package into all 3 contexts: browser, client JS, and server JS.

There are only 7 functions that you can use almost anywhere around your game. However, depending on the current context, the usage of some functions might differ slightly.

Universal

setDebugMode(state)

Set whether the rpc to display debugging messages or not.

Examples
rpc.setDebugMode(true); // disabled by default

rpc.register('hello', () => 'hi!');

Logs RPC (server): Registered procedure "hello".


rpc.setDebugMode(false);

rpc.register('hello', () => 'hi!');

No logs

Parameters
  • state boolean - Set whether the rpc to display debugging messages or not.

register(name, callback)

Registers a procedure in the current context.

The return value of the callback will be sent back to the caller, even if it fails. If a Promise is returned, it will finish before returning its result or error to the caller.

The return value must be JSON-able in order to be sent over the network. This doesn't matter if the procedure call is local.

Parameters
  • name string - The unique identifier, relative to the current context, of the procedure.
  • callback function - The procedure. This function will receive 2 arguments.
    • args - The arguments that were provided by the caller. This parameter's type will be the same that was sent by the caller. undefined if no arguments were sent.
    • info object - Various information about the caller.
      • id string - The internal ID used to keep track of this request.
      • environment string - The caller's environment. Can be cef, client, or server.
      • player Player - The caller. Only exists in the server context if remotely called from cef or client.
Examples
rpc.register('hello', () => 'hi!');

Returns hi! to the caller.


rpc.register('getUser', async (id) => {
	const user = await someLongOperationThatReturnsUserFromId(id);
	return user;
});

Waits for the returned Promise to finish before returning the resolved user to the caller.


rpc.register('echo', (message, info) => {
	console.log(`${info.player.name} via ${info.environment}: ${message}`);
});

Server-side example only. The passed argument will be logged to the console along with the caller's name and the environment which they called from.

unregister(name)

Unregisters a procedure from the current context. It will no longer take requests unless it is re-registered.

  • name string - The unique identifier, relative to the current context, of the procedure.

call(name, args?, options?)

Calls a procedure that has been registered in the current context.

  • name string - The name of the previously registered procedure.
  • args? - Optional arguments to pass to the procedure. Can be of any type, since call does not traverse the network.
  • options? - Optional options to control how the procedure is called.
Example
rpc.register('hi', () => 'hello!');

rpc.call('hi')
	.then((result) => {
		// result = hello!
		console.log(result);
	})
	.catch((err) => {
		console.error(err);
	});
Returns Promise resolving or failing due to the procedure's result. If the procedure called does not exist, PROCEDURE_NOT_FOUND will be thrown.

callServer(name, args?, options?)

Calls a procedure that has been registered on the server.

  • name string - The name of the previously registered procedure.
  • args? - Optional arguments to pass to the procedure. Must be JSON-able if the current context is not the server. Use an array or object to pass multiple arguments.
  • options? - Optional options to control how the procedure is called.
Example

Server-side:

rpc.register('getWeather', () => mp.world.weather);

Client-side OR Browser OR Server:

rpc.callServer('getWeather')
	.then((weather) => {
		mp.gui.chat.push(`The current weather is ${weather}.`);
	})
	.catch((err) => {
		// handle error
	});
Returns Promise resolving or failing due to the procedure's result. If the procedure called does not exist, PROCEDURE_NOT_FOUND will be thrown.

Server-side

callClient(player, name, args?)

Calls a procedure that has been registered on a specific client.

  • player Player - The player to call the procedure on.
  • name string - The name of the registered procedure.
  • args? - Optional arguments to pass to the procedure. Must be JSON-able. Use an array or object to pass multiple arguments.
  • options? - Optional options to control how the procedure is called.
Example

Client-side:

rpc.register('toggleChat', (toggle) => {
	mp.gui.chat.show(toggle);
});

Server-side:

mp.players.forEach((player) => {
	rpc.callClient(player, 'toggleChat', false);
});
Returns Promise resolving or failing due to the procedure's result. If the procedure called does not exist, PROCEDURE_NOT_FOUND will be thrown.

callBrowsers(player, name, args?, options?)

Calls a procedure that has been registered in any CEF instance on a specific client.

Any CEF instance can register the procedure. The client will iterate through each instance and call the procedure on the first instance that it exists on.

  • player Player - The player to call the procedure on.
  • name string - The name of the registered procedure.
  • args? - Optional arguments to pass to the procedure. Must be JSON-able. Use an array or object to pass multiple arguments.
  • options? - Optional options to control how the procedure is called.
Example

Browser:

rpc.register('toggleHUD', (toggle) => {
	// if jQuery is your thing
	$('#hud').toggle(toggle);
});

Server-side:

mp.players.forEach((player) => {
	rpc.callClient(player, 'toggleChat', false);
});
Returns Promise resolving or failing due to the procedure's result. If the procedure called does not exist, PROCEDURE_NOT_FOUND will be thrown.

Client-side

callBrowser(browser, name, args?, options?)

Calls a procedure that has been registered in a specific CEF instance.

  • browser Browser - The browser to call the procedure on.
  • name string - The name of the registered procedure.
  • args? - Optional arguments to pass to the procedure. Must be JSON-able. Use an array or object to pass multiple arguments.
  • options? - Optional options to control how the procedure is called.
Example

Browser:

rpc.register('getInputValue', () => {
	// if jQuery is your thing
	return $('#input').val();
});

Client-side:

const browser = mp.browsers.at(0);

rpc.callBrowser(browser, 'getInputValue')
	.then((value) => {
		mp.gui.chat.push(`The CEF input value is: ${value}`);
	})
	.catch((err) => {
		// handle errors
	});
Returns Promise resolving or failing due to the procedure's result. If the procedure called does not exist, PROCEDURE_NOT_FOUND will be thrown.

CEF or Client-side

callBrowsers(name, args?, options?)

Calls a procedure that has been registered in any CEF instance on a specific client.

Any CEF instance can register the procedure. The client will iterate through each instance and call the procedure on the first instance that it exists on.

  • name string - The name of the registered procedure.
  • args? - Optional arguments to pass to the procedure. Must be JSON-able. Use an array or object to pass multiple arguments.
  • options? - Optional options to control how the procedure is called.
Example

Browser:

rpc.register('toggleHUD', (toggle) => {
	// if jQuery is your thing
	$('#hud').toggle(toggle);
});

Client-side OR Browser:

rpc.callBrowsers('toggleChat', false);
Returns Promise resolving or failing due to the procedure's result. If the procedure called does not exist, PROCEDURE_NOT_FOUND will be thrown.

callClient(name, args?, options?)

Calls a procedure that has been registered on the local client.

  • name string - The name of the registered procedure.
  • args? - Optional arguments to pass to the procedure. Must be JSON-able if the current context is not this client. Use an array or object to pass multiple arguments.
  • options? - Optional options to control how the procedure is called.
Example

Client-side:

rpc.register('toggleChat', (toggle) => {
	mp.gui.chat.show(toggle);
});

Client-side OR Browser:

rpc.callClient('toggleChat', false);
Returns Promise resolving or failing due to the procedure's result. If the procedure called does not exist, PROCEDURE_NOT_FOUND will be thrown.

Options

For remote procedure calling functions, there are optional options you can pass as the last parameter:

  • timeout (number): The amount of time in milliseconds to reject the call automatically
  • noRet (boolean): Prevent the remote context from sending data back. Saves bandwidth, but the promise will never return or reject. Similar to using trigger.

Events

You can now use rage-rpc as a full on replacement for mp.events. API functions that start with "trigger" use the same syntax as the ones that start with "call", except they do not return anything. They call remote events on any context where there can be many handlers or none.

Changelog

Check the releases tab for an up-to-date changelog.

0.2.5

  • FIX: Use Player & Browser instead of Mp version
  • FIX: ProcedureListenerInfo player & browser types

0.2.4

  • IMPROVE: Type-safe & Type Definitions

0.2.3

  • FIX: Player null type assignment

0.2.2

  • ADD: Terser to minify generated bundle

0.2.1

  • ADD: Logs for on & off listeners

0.2.0

  • FIX: ES6, CommonJS and UMD compatibility
  • ADD: Debug Mode
  • ADD: Generics type-safe to call functions

0.1.0

  • ADD: Bundled Typescript definitions
  • IMPROVE: CEF outgoing call returning performance
  • IMRPOVE: callBrowsers performance on all contexts
  • FIX: Some code simplifications

0.0.3

  • ADD: Extra player verification for outgoing server calls
  • FIX: Bug that prevented multiple resources from using RPC at the same time
  • FIX: False alarm for multiple CEF instances receiving the same result

0.0.2

  • FIX: UMD exposing for correct Node.js importing

0.0.1

  • Initial commit

rage-rpc's People

Contributors

leonardssh avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

rage-rpc's Issues

PROCEDURE_NOT_FOUND

Hello, I am using your template in my project and when calling an rpc event in CEF from the server side, a PROCEDURE_NOT_FOUND error occurs.

If I try to call an event from CEF on the server, then the event is called, but the Promise is not returned.

When trying to trigger events and return a Promise from the server to the client and vice versa, everything works fine.

CEF is collected via Vue-CLI

PlayerMp & BrowserMp is not assignable

Current Behavior:

At the moment, properties from ProcedureListenerInfo cannot be used in the rage environment because they are hardcoded and do not have all the required properties and methods.

https://github.com/LeonardSSH/rage-rpc/blob/928742602a99dfbeda577e687f072166cd0d073d/types/index.d.ts#L1-L10

image

Possible solution:

  • replace PlayerMp with a generic type, with default any, and let the developer pass as argument the corresponding interface for type-safe.
export declare type ProcedureListener<T = any, K = any> = (args: any, info: ProcedureListenerInfo<T, K>) => any;

export declare interface ProcedureListenerInfo<T = any, K = any> {
	environment: string;
	id?: string;
	player?: T;
	browser?: K;
}
function myFunction({ player /* :any */ }: rpc.ProcedureListenerInfo) { ... }

// should be

function myFunction({ player /* :PlayerMp */  }: rpc.ProcedureListenerInfo<PlayerMp>) { ... }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.