Repclient

Documentation Objective

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

Overview

The 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 Demo).


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 Physics demo client:

// Replication client header
#include <aether/repclient.hh>
int main() {
// 1. Create repclient
aether::replication::client repstate;
// 2. Connect to simulation
if (repstate.connect("aether-sdk.mshome.net", "8881") != 0) {
fprintf(stderr, "Failed to connect to simulation\n");
exit(EXIT_FAILURE);
}
// 3. Authenticate
const uint64_t player_id = 0;
const std::array<unsigned char, 32> authentication_token{};
repstate.authenticate_player_id_with_token(player_id, authentication_token);
// game loop
while (repstate.is_connected()) {
// 4. poll available updates
size_t msg_size;
while(void *msg = repstate.tick(&msg_size)) {
// handle any received messages
}
// 5. send messages to the simulation
std::vector<unsigned char> buf;
repstate.send(&buf.front(), buf.size());
}
return EXIT_SUCCESS;
}

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 any non-const repclient function.

  • In the case of a connection failure the repclient will not attempt to reconnect on its own.

  • 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:

auto repstate = std::make_unique<aether::replication::client>();
{
const auto ret = repstate->connect("aether-sdk.mshome.net", "8881");
assert(ret == 0 && "Failed to connect to simulation");
}

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:

auto repstate = std::make_unique<aether::replication::client>();
{
const auto ret = repstate->connect_record("aether-sdk.mshome.net", "8881",
"path/to/file.dump");
assert(ret == 0 && "Failed to connect to simulation");
}

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

auto repstate = std::make_unique<aether::replication::client>();
{
const auto ret = repstate->connect_replay("path/to/file.dump");
assert(ret == 0 && "Failed to connect to simulation");
}

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 arguments &args) : token(args.token) {
if (args.mode == arguments::client_mode::connect) {
// Connect to host and port from parsed arguments
const auto ret = repstate.connect(args.host.c_str(), args.port.c_str());
if (ret != 0) {
fprintf(stderr, "Couldn't connect to host: %s port: %s\n",
args.host.c_str(), args.host.c_str());
exit(EXIT_FAILURE);
}
}
...
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() {
if (!repstate.is_connected()) {
fprintf(stderr, "Disconnected from server so terminating client\n");
exit(EXIT_SUCCESS);
}
/*...*/
size_t msg_size;
// process all the messages that arrived since last update
while (void *msg = repstate.tick(&msg_size)) {
process_packet(msg, msg_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 auto ret = demarshaller.decode(message_data, count);
assert(ret == 0 && "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(const auto &entity : message_entities) {
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
}
}