Repclient

Documentation Objective

This document provides an overview of how clients can authenticate to, send and receive data from a simulation.

Overview

Repclient (replication client) is a module responsible for authentication and communication with the simulation. The repclient code is provided as a part of the SDK and shared by all simulation clients.

architecture

Prerequisites

To start working with the repclient you need to have a client project set up, with aether-sdk directory imported into the project. The aether-sdk contains the repclient header repclient.hh. You also need a running simulation project to connect to. This guide assumes you have the Aether\Example\PhysxExample\PhysicsDemo\Simulation running and you have the Aether\Example\PhysxExample\PhysicsDemo\Client code available (see Physics Tutorial ).


First run

To see repclient in action, run the Aether\Example\PhysxExample\PhysicsDemo\Simulation simulation and Aether\Example\PhysxExample\PhysicsDemo\Client client.

A high level overview of the usage of repclient in the Physx demo client:

// Replication client header
#include <aether/repclient.hh>
void main() {
// 1. Create repclient
aether::replication::client repstate("aether-sdk.mshome.net", "8881");
// 2. Initialize connection
const uint64_t player_id = 0;
repstate.authenticate_player_id(player_id);
bool done = false;
size_t msg_size;
// game loop
while (!done) {
// 3. poll received messages, until all messages since last update are read
for (void *msg; (msg = repstate.tick(&msg_size)); ) {
// handle any received messages
}
// 4. send messages to the simulation
std::vector<unsigned char> buf;
repstate.send(&buf.front(), buf.size());
}
}

Repclient characteristics

  • In the case above, the object is used from a single thread, but the repclient is thread-safe so it’s possible to have separate threads handle sending and receiving of messages.

  • The repclient will buffer received messages until they’re consumed using the repstate.tick(size_t*) method. The size of the message in bytes is written to the size_t valued pointed to by the parameter. The returned pointer is valid until the next call to this function.

  • Currently there’s no API to query for errors. In case of a connection failure repclient will not attempt to reconnect.

  • The repclient communicates with the muxer using TCP/IP.

  • The exact binary protocol used for communication between simulation and repclient is simulation and netcode-specific. Aether Engine SDK provides utilities for implementing a protocol tailored to your simulation.


Configuration

Specifying the muxer hostname and port

The hostname and port of the muxer used by the client is specified in the constructor. The connection parameters for connecting to the muxer hosted in the SDK virtual machine are as follows:

aether::replication::client repstate("aether-sdk.mshome.net", "8881")

Authentication

If the muxer used by the simulation requires authentication, you need to configure the authentication on the client side too. To do this, call the authenticate_player_id_with_token method which takes an authentication token:

// We assume the existance of get_authentication_token() which fetches
// an authentication token which has been stored elsewhere.
const std::array<unsigned char, 32> token = get_authentication_token();
repstate.authenticate_player_id_with_token(player_id, token);

Recording Messages

You can configure repclient to record the messages received from the simulation, or to play the recording back, by using different constructors

Recording data received from a simulation to path/to/file.dump:

aether::replication::client repstate("aether-sdk.mshome.net", "8881",
"path/to/file.dump");

Playing back a recording from path/to/file.dump:

aether::replication::client repstate("path/to/file.dump");

In playback mode the repclient doesn’t connect to a simulation, instead playing back the original recording. The delays between messages are preserved so that the recording plays back at the original speed.


Walkthrough for the Physx demo client

1. Initialization

// in physx_client.cpp
physx_client::physx_client(const char *host, const char *port,
std::optional<std::array<unsigned char, 32>> token) :
repstate(host, port), token(std::move(token))
// configures host, port and token
{
authenticate();
...
}

2. Authentication - initialise connection to the muxer

// in physx_client.cpp
void physx_client::authenticate() {
// use the security token if provided
if(token.has_value()) {
repstate.authenticate_player_id_with_token(current_player, *token);
} else {
repstate.authenticate_player_id(current_player);
}
}

3. Send and receive updates in the client update loop

// in physx_client.cpp
// client update loop
bool physx_client::tick() {
/*...*/
size_t msg_size;
// process all the messages that arrived since last update
for (void *msg; (msg = repstate.tick(&msg_size)); ) {
process_packet(msg, msg_size);
}
// Sends a message to the simulation every 100 frames.
// In this example we send a size prefixed string.
if (current_frame % 100 == 0) {
char str[256];
const int n = snprintf(str, 256,
"Interaction test in frame %zu from OpenGL client",
current_frame);
std::vector<unsigned char> buf(1+1+strlen(str));
buf[0] = DEBUG_MSG;
buf[1] = strlen(str);
memcpy(&buf[1+1], str, strlen(str));
repstate.send(&buf[0], buf.size());
}
/* ... */
}
void physx_client::process_packet(uint64_t id, void *message_data, size_t count) {
auto demarshaller = aether::trivial_marshalling<trivial_marshalling_traits>()
.create_demarshaller();
// use the marshaling utilities from aether to decode the network packet
const bool success = demarshaller.decode(message_data, count);
assert(success && "Failed to decode packet from simulation");
const auto headers = demarshaller.get_worker_data();
for(const auto &[id, header] : headers) {
// process cell headers, each cell is a single worker
}
const auto message_entities = demarshaller.get_entities();
for(size_t entity_id = 0; entity_id < message_entities.size(); ++entity_id) {
const auto &entity = message_entities[entity_id];
const vec3f position =
protocol::base::net_decode_position_3f(entity.net_encoded_position);
ui_point point;
point.p = { position.x, position.y, position.z };
// process entity updates received from the server
// in this case we update the cache in the physx client
}
}