Links

Muxer Client Plugin

Overview

The Muxer Replication Client Plugin (repclient) is a module responsible for authentication and communication with the simulation designed to be embedded in the client application.
The plugin code is provided as a part of the SDK and shared by all simulation clients.
Prerequisites
To start working with the plugin you need to have a client project set up, with aether-sdk directory imported into the project. The aether-sdk contains the client 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 plugin in action, run the Aether\Example\PhysxExample\PhysicsDemo\Simulation simulation and Aether\Example\PhysxExample\PhysicsDemo\Client client.
The following is a high level overview of the usage of the Muxer Client Plugin 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;
}

Muxer Client Plugin characteristics

  • In the case above, the object is used from a single thread, but the plugin is thread-safe so it’s possible to have separate threads handle sending and receiving of messages.
  • The plugin 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 plugin will not attempt to reconnect on its own.
  • The plugin communicates with Connect using TCP/IP.
  • The exact binary protocol used for communication between simulation and plugin is simulation and netcode-specific. Aether Engine SDK provides utilities for implementing a protocol tailored to your simulation.

Configuration

Specifying the Hadean Connect hostname and port

The hostname and port of Connect used by the client is specified in the constructor. The connection parameters for connecting to Connect hosted on your SDK WSL container are as follows:
auto repstate = std::make_unique<aether::replication::client>();
{
const auto ret = repstate->connect("127.0.0.1", "8881");
assert(ret == 0 && "Failed to connect to simulation");
}

Authentication

If Connect 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 plugin 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 plugin doesn’t connect to a simulation via Connect, 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 Connect
// 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
}
}