Page MenuHomePhabricator

Quicksilver
Updated 2,339 Days AgoPublic

Discussion: {Z46}

Purpose

Quicksilver is the system that allows Ratscript to call functions in the various components of the RATS Game Engine. This applies to Trailcrest [Project], Stormsound [Project], Anari [Project], and Lightrift Game Studio [Project].

Registration

Before any library C++ function can be called, it must be registered with Ratscript. Ratscript needs to have a function register() that accepts the name of the function, the op code, the required argument count, the max argument count, and the argument type list.

The argument list is an array[6] of integers, but only two of those are required. The maximum argument count is the effective size of the array - everything beyond that is ignored. Each number corresponds with a different data type (i.e. 0 = int, 1 = bool, etc.). It may be useful to use typedefs here.

register("Utils.rand", 0x02AB, 2, 6, [0,0,0,0,0,0])
register("Utils.foo", 0x02AC, 0 [0,0,0,0,0,0,0])
NOTE: Ratscript really does not care about your class structure. "Utils.rand" would be treated as the function name, even if Utils is the class, and rand is the member function. It just makes life easier.

The data received by register() is stored by Ratscript in an "external function" dictionary, with the name as the key, as the other data in a struct.

Interface

Each system that uses Quicksilver is required to have a quicksilver_receive() function with a single one-byte argument, and a libsigc++ signal called quicksilver_send() with two one-byte arguments. This allows each system to use Quicksilver without having any knowledge of the other classes or systems involved.

All communication will occur via the Quicksilver Postmaster.

Mirroring

Ratscript receives the function call, and parses the name and array of arguments. It would then validate the arguments using the data in the external function dictionary. The actual call will be a bytestream.

The following is an example transmission, using the op code 0x02AB, two integer arguments of 3 and 6 respectively. Ratscript calls are in bold, and Trailcrest (for this example) in italics.

ENQ
ACK
SOH 02AB STX
ACK

A NAK at this point would indicate an invalid opcode. We should actually check if the function exists in Trailcrest at this point.

2 FS
ACK
3 FS
ACK
6 FS
ACK
ETX
ACK
EOT

There is no response at this point, as the previous ACK indicates that the entire thing was valid and received. If we had received an error, we might retry the block instead of sending EOT. After EOT, Ratscript is listening for Trailcrest's response.

From this point, Trailcrest would convert and send the data from the bytestream to the appropriate class' mirror. Note that, in general, the first part of the OP code should indicate the class, to make this easier to do.

Mirror would validate the argument count and types for the function being called. Arguments would be converted to ArgList. One version of each function exposed to Mercury should accept a single ArgList, instead of its usual arguments. This version of the function would perform the final conversions and call the usual version of the function.

The response would be sent backwards through Mirror to the manager class of Trailcrest, and that would be sent to Ratscript via bytecode. Following the earlier example, we'll return an integer (in this case, 5).

ENQ
ACK
SOH 5
ACK
EOT

Ratscript would then pass the 5 back into itself and do whatever it needed to.

If there had been an error in the function, this conversation would instead say something like the following, where 105A is the error code.

ENQ
ACK
SOH NAK 105A
ACK
EOT

Quicksilver Postmaster

In order to transfer messages, we need a Quicksilver Postmaster class that is aware of all of the other systems and classes. The postmaster class will contain callbacks to each system's quicksilver_send() signal, and then will use the first one-byte argument to determine which system/class the second argument should be sent to using that system's quicksilver_receive().

Thus, a simple implementation of a Quicksilver postmaster might look like this...

Quicksilver Postmaster
#include <bitset>

/* Basic Quicksilver Postmaster implementation.
 * Let's assume we have instances of ratscript,
 * anari, stormsound, and trailcrest within the
 * scope of this code.
 */

enum SystemAddresses
{
	RATSCRIPT = 0x00,
	ANARI = 0x01,
	TRAILCREST = 0x02,
	STORMSOUND = 0x03
};

// Save typing by defining our byte as a bitset of 8.
typedef bitset<8> byte;

class QuicksilverPostmaster
{
	private:
		void incoming(byte dest, byte packet)
		{
			/* Send the packet to the destination system via its
			 * quicksilver_receive() function.
			 */
			switch(dest.to_ulong())
			{
				case RATSCRIPT:
					ratscript.quicksilver_receive(packet);
				break;
				case ANARI:
					anari.quicksilver_receive(packet);
				break;
				case STORMSOUND:
					stormsound.quicksilver_receive(packet);
				break;
				case TRAILCREST:
					trailcrest.quicksilver_receive(packet);
				break;
			}
		}
	public:
		QuicksilverPostmaster()
		{
			/* Within the constructor, we register all our systems'
			 * quicksilver_send signals with the `incoming()`
			 * callback function.
			 */
			anari.quicksilver_send.connect(sigc::ptr_fun(incoming));
			ratscript.quicksilver_send.connect(sigc::ptr_fun(incoming));
			stormsound.quicksilver_send.connect(sigc::ptr_fun(incoming));
			trailcrest.quicksilver_send.connect(sigc::ptr_fun(incoming));
		}
		~QuicksilverPostmaster(){}
};
Last Author
jcmcdonald
Last Edited
Mar 14 2016, 6:15 PM