Muxer Client Plugin

Overview

The Muxer Client Plugin 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.
Possible Architecture for

Prerequisites

To start working with the plugin you need:
  • A client project set up, with the aether directory imported into the project, this can be found in the muxer-sdk/common/include/aether directory of your Connect download. You will need to add this to your build files include
  • Either a project publishing data with the Muxer Server Plugin or a running Hadean Simulate simulation project.

Lifecycle for Connect Communication

Example code is provided from the Connect Hello World Demo that can be found here. The code is fully commented and it can be useful to follow along by looking at the code with its full context.
  1. 1.
    Connecting to Connect
  2. 2.
    Authenticating
  3. 3.
    Receiving When Data received from upstream
    1. 1.
      Understanding when an entity is no longer in range
    2. 2.
      Understanding when an entity no longer exists
  4. 4.
    Reacting when client is able to receive more data
  5. 5.
    Sending data back from client to upstream servers

1. Connecting to Connect

In order to connect to a Connect instance in the Connect cluster you will need to pass in a URI and port. This can be done using the repclient->connect (hostname, port) method as shown below.
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");
}
In a typical deployment multiple Connect instances will exist within a cluster. It does not matter which one a client connects to, as a load balancer can be placed in front to even out load. When running across multiple data centres it can be an advantage to determine latency in each region and then connect to a Connect instance in a region with the lowest latency.
The plugin communicates with the Connect using TCP/IP, in the event of a connection failure the plugin will not attempt to reconnect. This logic should be implemented in the clients user code

2. Authenticating

To connect as a player the repclient->authenticate_player_id_with_token(player_id, token) is used. The player id is an unsigned 64bit integer which needs to be unique per player. The token also needs to be unique and can be used to authenticate the player with Connect if you chose to implement this logic.
// 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);

3. Receiving Data from an Upstream Source

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.
while (repstate.is_connected()) {
// 4. poll available updates
size_t msg_size;
while(void *msg = repstate.tick(&msg_size)) {
// handle any received messages
}
Data received should be processed by your client as required. For example, to update the status of Unreal Actors, however the exact binary protocol used for communication between simulation and plugin is simulation and netcode-specific.
Connect's SDK provides utilities that can help with implementing a protocol tailored to your use case.

4. Sending Data from Client back to an Upstream Source

std::vector<unsigned char> buf;
repstate.send(&buf.front(), buf.size());
The plugin is thread-safe so it’s possible to have separate threads handle sending and receiving of messages.

Deserialising Data

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.

A simple client example

This example is based on the client.cc file that is distributed with our Connect - Demo example.
#include <iostream>
#include <aether/repclient.hh>
#include <aether/common/container/optional.hh>
int main(int argc, char** argv) {
std::string ip_addr = "127.0.0.1";
std::string muxer_port = "8881";
if (argc > 4) {
std::cerr << "usage: " << argv[0] << " hostname port [token]" << std::endl;
exit(EXIT_FAILURE);
}
if (argc < 2) {
std::cerr << "Connecting to muxer in localhost. A different muxer IP can be provided as CLI argument"
<< std::endl;
}
if (argc >= 2) {
ip_addr = argv[1];
}
if (argc >= 3) {
muxer_port = argv[2];
}
std::array<unsigned char, 32> token{};
// initializing token with default value as 0.
token.fill(0);
if (argc == 4) {
if (std::strlen(argv[3]) != 32) {
std::cerr << "Token must be 32 characters long!" << std::endl;
exit(EXIT_FAILURE);
}
token = std::array<unsigned char, 32>();
std::copy(argv[3], argv[3] + 32, token.data());
}
// 1. Create repclient
aether::replication::client repstate;
// 2. Connect to simulation
while (repstate.connect(ip_addr.c_str(), muxer_port.c_str()) != 0) {
std::cout << "Failed to connect to simulation\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// 3. Authenticate
const uint64_t player_id = 0;
repstate.authenticate_player_id_with_token(player_id, token);
// game loop
while (repstate.is_connected()) {
// 4. poll available updates
size_t data_len;
while (void* data = repstate.tick(&data_len)) {
// here we use `sizeof(int)`, but this is just an example
// in a real-world scenario you would need to check if
// data_len <= sizeof(your_struct)
if (data == nullptr || data_len <= sizeof(int)) {
continue;
}
// use `data`
}
}
return EXIT_SUCCESS;
}