Skip to main content

SDK Integration

Authentication

Unlike game clients which use the Matchmaking interface, the game server does not need to perform specific authentication in order to use the Match Life Cycle interface. Instead, environment variables are used to provide tokens on outgoing HTTP request headers, or to verify tokens on incoming requests.

Ensure that you have the correct tokens set for the following environment variables:

  • FACEIT_SERVER_PLUGIN_INCOMING_TOKEN for incoming requests.
  • FACEIT_FACEBOT_WORKER_TOKEN and FACEIT_GSR_API_TOKEN if using Facebot.
  • FACEIT_GAMES_API_TOKEN if not using Facebot.

Beginning a Session

To activate the Match Life Cycle interface, call Start() and supply some session settings, as demonstrated in the example below.

#include "FACEITGameServerSDK/MatchLifeCycle/Session.h"
#include "FACEITGameServerSDK/Utility/StringHelpers.h"

void MyGameServerInstance::ActivateMatchLifeCycle()
{
FGSSDK_MLC_BeginSessionSettings sessionSettings {};

// The session's user data can be whatever is convenient for the use case.
// User data is passed into any callbacks that are called by the
// Anti-Cheat implementation. For a persistent game instance class,
// passing "this" allows you to easily call functions on the instance
// when responding to a callback.
settings.userData = this;

// Set up some callbacks for responding to MLC events.
// These callbacks are explained in detail later.

// Called once the MLC interface is fully initialised.
// Calling Start() begins this asynchronous process.
settings.beginSessionCallback = &Callback_MLC_BeginSession;

// Called when a match configuration is received from FACEIT.
settings.matchConfigurationReceivedCallback = &Callback_MLC_MatchConfigurationReceived;

// Called when a match cancellation is requested from FACEIT.
settings.matchCancellationReceivedCallback = &Callback_MLC_MatchCancellationReceived;

// Called when a match reaches an ending state (finished, aborted, or cancelled).
settings.matchStatusCompletedCallback = &Callback_MLC_MatchStatusCompleted;

// Called when a snapshot of server metrics should be submitted.
settings.metricsDataRequestedCallback = &Callback_MLC_MetricsDataRequested;

// Handlers for responses to messages that have been sent to the FACEIT backend.
// These are usually used to verify that the message was received successfully.
// These callbacks are all optional, and may be omitted if the server does not
// care about their response, or if it never plans to send any messages of the
// corresponding type.
settings.matchReadyResponseCallback = &Callback_MLC_MatchReadyResponse;
settings.matchAbortedResponseCallback = &Callback_MLC_MatchAbortedResponse;
settings.matchStartedResponseCallback = &Callback_MLC_MatchStartedResponse;
settings.matchUpdatedResponseCallback = &Callback_MLC_MatchUpdatedResponse;
settings.matchFinishedResponseCallback = &Callback_MLC_MatchFinishedResponse;
settings.matchStatsResponseCallback = &Callback_MLC_MatchStatsResponse;
settings.matchDemoReadyResponseCallback = &Callback_MLC_MatchDemoReadyResponse;
settings.endOfRoundResponseCallback = &Callback_MLC_EndOfRoundResponse;
settings.playerReleaseResponseCallback = &Callback_MLC_PlayerReleaseResponse;

// Attempt to begin the session.
const FGSSDK_MLC_ResultCode result = FGSSDK_MLC_BeginSession(&settings);

// If the result is not OK, the session could not be started.
if ( result != FGSSDK_MLC_OK )
{
// Handle the error in an appropriate way
// (out of scope of this documentation).

Log("Match Life Cycle session failed to start: %s",
FGSSDK_ToString(&FGSSDK_MLC_ResultCode_GetDescription, result).c_str());

return;
}
}

The OnBeginSessionCallback callback is called once the Match Life Cycle session has started. If the call to Start() returns ResultCode.OK, this callback is guaranteed to be called at some point in future. The result of the process is provided in the callback data.

// Static callback function:
void Callback_MLC_BeginSession(const FGSSDK_MLC_BeginSessionResultInfo* info)
{
// Call into game instance - see below.
static_cast<MyGameServerInstance*>(info->userData)->MLC_BeginSession(info);
}

void MyGameServerInstance::MLC_BeginSession(const FGSSDK_MLC_BeginSessionResultInfo* info)
{
if ( info->result == FGSSDK_MLC_OK )
{
// The session started correctly.
// Perform any post-init tasks here.
}
else
{
// The session failed to start.
Log("Match Life Cycle session failed to start: %s",
FGSSDK_ToString(&FGCSDK_MLC_ResultCode_GetDescription, info->result).c_str());
}
}

Ending a Session

Later, when ending a session, call FGSSDK_MLC_EndSession(). It is recommended to continue to call FGSSDK_Poll() even after the session has ended, as this will allow any shutdown notifications sent to the FACEIT backend to complete properly.


void MyGameServerInstance::EndMLCSession()
{
// End the MLC session.
FGSSDK_MLC_EndSession();
}

Receiving Match Configurations

The OnMatchConfigurationReceivedCallback callback is called when FACEIT sends a match configuration to the server. The game server should send a response indicating whether or not it accepts the match.

An example match configuration might look something like the following:

Expand match configuration
{
"match_id": "<guid>",
"faceit_match_id": "<guid>",
"game": "my-game-id",
"region": "EU",
"map": "Example Map",
"location": "UK",
"token": "<guid>",
"worker_name": "<url>",
"game_custom_data": {
"matchReadyWaitingTime": 60,
"maxMatchDuration": 30,
"minimumRequiredTeamPlayers": 1,
"scorePerDeath": 0,
"scorePerKill": 1,
"location": {
"class_name": "UK",
"game_location_id": "UK",
"guid": "UK",
"image_lg": "<url>",
"image_sm": "<url>",
"name": "UK"
},
"map": {
"class_name": "Example Map",
"game_map_id": "Example Map",
"guid": "<guid>",
"image_lg": "<url>",
"image_sm": "<url>",
"name": "Example Map"
}
},
"factions": {
"faction1": {
"leader": "<game id>",
"name": "Team 1",
"players": [
{
"nickname": "Player 1",
"id": "<guid>",
"game_id": "<game id>",
"game_name": "Player 1",
"anticheat_required": false
}
],
"id": "<guid>"
},
"faction2": {
"leader": "<game id>",
"name": "Team 2",
"players": [
{
"nickname": "Player 2",
"id": "<guid>",
"game_id": "<game id>",
"game_name": "Player 2",
"anticheat_required": false
}
],
"id": "<guid>"
}
},
"timestamp": 1635779323841,
"type": "DEFAULT",
"allow_ongoing_join": false
}

The configuration is passed into the match configuration handler as a JSON string, as the request argument. The game server may parse this JSON message and decide whether the configuration is supported (eg. whether the specified map actually exists on the server).

// Static callback function:
FGSSDK_MLC_RequestAckCode Callback_MLC_MatchConfigurationReceived(
const FGSSDK_MLC_RequestReceivedInfo* info)
{
// Call into game instance - see below.
return static_cast<MyGameServerInstance*>(info->userData)
->MLC_MatchConfigurationReceived(info);
}

FGSSDK_MLC_RequestAckCode MyGameServerInstance::MLC_MatchConfigurationReceived(
const FGSSDK_MLC_RequestReceivedInfo* info)
{
// Perform any checks here to verify that the match is supported by the server.
// The request is passed as a JSON string, and is guaranteed to be parseable.
JSONDocument requestJSON = ParseFromJSON(info.requestPayload);

// For example, if some aspect of the configuration is not supported
// by the server, a rejection can be returned as follows:
if ( !ConfigurationIsSupported(requestJSON) )
{
return FGSSDK_MLC_REQACK_REFUSED;
}

// To accept the configuration:
ScheduleMatchSetup(requestJSON);
return FGSSDK_MLC_REQACK_ACCEPTED;
}

Receiving Match Cancellations

The OnMatchCancellationReceivedCallback callback is called when FACEIT requests that an ongoing match be cancelled. This callback will only be called if a match is currently active.

Ideally, match cleanup actions should not occur during the match cancellation request handler. Specifically, calls to SetServerAvailable() will fail within this handler, as until it has finished executing, the match state will still be considered active. Perform cleanup in the OnMatchStatusCompletedCallback callback, which will be called once the Match Life Cycle interface has fully processed the cancellation.

FGSSDK_MLC_RequestAckCode Callback_MLC_MatchCancellationReceived(
const FGSSDK_MLC_RequestReceivedInfo* info)
{
// Call into game instance - see below.
return static_cast<MyGameServerInstance*>(info->userData)
->MLC_MatchCancellationReceived(info);
}

FGSSDK_MLC_RequestAckCode MyGameServerInstance::MLC_MatchCancellationReceived(
const FGSSDK_MLC_RequestReceivedInfo* info)
{
// A match cancellation can be rejected, but this is expected to only occur
// under exceptional circumstances. Otherwise, simply accept the cancellation.
ScheduleMatchShutdown();
return FGSSDK_MLC_REQACK_ACCEPTED;
}

Sending Standard Match Messages

Certain messages must be sent from the server to the FACEIT platform in order for the active match to progress through its life cycle:

  • A Match Ready message must be sent once the server is ready for players to join. This is usually after the map has been loaded, and all resources have been initialised.
  • A Match Started message must be sent once all players have connected and the match has begun. A Match Aborted message should be sent instead if not all players join in the required time.
  • If the match starts successfully, a Match Finished message must be sent when the match has finished. This should contain the final scores for the match.

Match Ready

To send a Match Ready message, call SendMatchReadyMessage(). The data supplied should be a serialised JSON object, containing information that will eventually be passed back to each client who is waiting to connect. The format of this data is up to the game developer themselves, but should provide enough information for a client to connect to the server.

#include "FACEITGameServerSDK/MatchLifeCycle/OutboundMessages.h"

// A function called when the match has been fully set up and the server
// is ready for players to join.
void MyGameServerInstance::OnMatchReadyForPlayers()
{
// An example data payload. This would be built appropriately in real-world circumstances.
const std::string payload = "{ \"ip\": \"101.102.103.104\", \"port\": 1234 }";

FGSSDK_MLC_MatchReadyMessageData message {};

// The data payload is set as the user data object on this message,
// since the format is custom and game-specific. The only restriction
// on the format is that it must represent a valid JSON object.
message.userDataObject = payload.c_str();

FGSSDK_MLC_SendMatchReadyMessage(&message);
}

Match Started

To send a Match Started message, call SendMatchStartedMessage(). No extra data is required on this request.

#include "FACEITGameServerSDK/MatchLifeCycle/OutboundMessages.h"

// A function called when all players have joined and the match is ready to begin.
void MyGameServerInstance::OnMatchBeginPlay()
{
FGSSDK_MLC_MatchStartedMessageArgs message {};
FGSSDK_MLC_SendMatchStartedMessage(&message);
}

Match Aborted

To send a Match Aborted message, call SendMatchAbortedMessage(). This message expects an array of player game IDs representing the players who did not join the match in time.

#include "FACEITGameServerSDK/MatchLifeCycle/OutboundMessages.h"

// A function called when the connection time allowed for all players
// has expired, and not all players have connected.
void MyGameServerInstance::OnJoinMatchTimeoutExpired()
{
// Get a list of game IDs for all players who failed to connect.
// This function is defined below.
std::vector<std::string> afkIDList = GetExpectedPlayersWhoWereNotPresent();

// The message expects an array of const char* pointers.
// This indirection vector acts as that array.
std::vector<const char*> afkStringIndirectionList;
afkStringIndirectionList.reserve(afkIDList.size());

// Keep a pointer to each game ID string.
for ( const std::string& gameID : afkIDList )
{
afkStringIndirectionList.emplace_back(gameID.c_str());
}

FGSSDK_MLC_MatchAbortedMessageData message {};

// Pass the array by providing its base pointer and its length.
message.afkGameIDsArray = afkStringIndirectionList.data();
message.afkGameIDsCount = afkStringIndirectionList.size();

// Send the message.
FGSSDK_MLC_SendMatchAbortedMessage(&message);
}

std::vector<std::string> MyGameServerInstance::GetExpectedPlayersWhoWereNotPresent()
{
// Prepare a list of game IDs for AFK players.
std::vector<std::string> gameIDList;

const FGSSDK_Size factionCount = FGSSDK_MLC_GetFactionCount();

// Check each faction in the match.
for ( FGSSDK_Size factionIndex = 0; factionIndex < factionCount; ++factionIndex )
{
const FGSSDK_Size factionPlayerCount = FGSSDK_MLC_GetFactionPlayerCount(factionIndex);

// Check each player in the faction.
for ( FGSSDK_Size playerIndex = 0; playerIndex < factionPlayerCount; ++playerIndex )
{
// Get the game ID of this player from the match config.
const std::string gameID = FGSSDK_ToString(
&FGSSDK_MLC_GetPlayerGameID,
factionIndex,
playerIndex);

// Here, we assume that IsPlayerConnected() is a function present in
// the game, that reports back whether the player with the provided
// game ID is still connected to the server.
if ( !IsPlayerConnected(gameID) )
{
// Record that this player is not connected.
gameIDList.emplace_back(gameID);
}
}
}

return gameIDList;
}

Match Finished

To send a Match Finished message, call SendMatchFinishedMessage(). This message expects a number of parameters:

NameTypeDescription
Faction scoresFactionScoreRecordA list of scores representing the final score for each faction in the match.
LeaversstringA list of player game IDs representing any players who left while the match was in progress.
Higher score is betterboolWhether a higher score is better (eg. where the score represents points), or a lower score is better (eg. where the score represents the faction's position relative to other factions).
Round numberintThe round number, if applicable for a multi-round match on FACEIT. 0 indicates that rounds are not applicable.
StatsstringAny game-specific match stats, as a JSON object string.
#include "FACEITGameServerSDK/MatchLifeCycle/OutboundMessages.h"
#include "FACEITGameServerSDK/MatchLifeCycle/Match.h"
#include "FACEITGameServerSDK/Utility/StringHelpers.h"

// A function called when the match finishes.
void MyGameServerInstance::OnMatchEndPlay()
{
std::vector<std::string> factionIDs;
std::vector<std::string> leaversStringList;
std::vector<const char*> leaversRawStringList;
std::vector<FGSSDK_MLC_FactionScore> factionScoreList;

const FGSSDK_Size factionCount = FGSSDK_MLC_GetFactionCount();

// We know that we will need one entry for every faction in the match,
// so size the arrays appropriately.
factionIDs.reserve(factionCount);
factionScoreList.reserve(factionCount);

// Check each faction that was involved in the match, and record its score.
for ( FGSSDK_Size factionIndex = 0; factionIndex < factionCount; ++factionIndex )
{
FGSSDK_MLC_FactionScore scoreItem {};

// Store the faction's ID string so that the pointer remains valid.
factionIDs.emplace_back(
FGSSDK_ToString(&FGSSDK_MLC_GetFactionConfigurationID, factionIndex));

// Add the faction ID to this score entry.
scoreItem.factionConfigID = factionIDs.back().c_str();

// Here, we assume that GetTeamCurrentScore() is a function present
// in the game, that returns the overall score for this faction.
scoreItem.score = GetTeamCurrentScore(factionIDs.back());

// Store the score entry.
factionScoreList.emplace_back(scoreItem);
}

// Get a list of players who are listed as participating in the match,
// but who have disconnected from the server. These players are "leavers",
// which FACEIT records in order to hand out appropriate punishments.
// The GetExpectedPlayersWhoWereNotPresent() function is defined in
// the Match Aborted code example.
leaversStringList = GetExpectedPlayersWhoWereNotPresent();

// Record the const char* pointers for the leaver strings,
// so that they may be passed on the message.
leaversRawStringList.reserve(leaversStringList.size());

for ( const std::string& gameID : leaversStringList )
{
leaversRawStringList.emplace_back(gameID.c_str());
}

// Construct the message to send.
FGSSDK_MLC_MatchFinishedMessageData message {};

message.factionScoresArray = factionScoreList.data();
message.factionScoresCount = factionScoreList.size();
message.leaverGameIDsArray = leaversRawStringList.data();
message.leaverGameIDsCount = leaversRawStringList.size();
message.higherScoreIsBetter = true;
message.round = 0;

// Send the message.
FGSSDK_MLC_SendMatchFinishedMessage(&message);
}

Sending Optional Match Messages

Other types of match message are optional, and may be sent from the game server to FACEIT if they are relevant:

  • A Match Updated message may be sent if an event happens during a match that FACEIT should know about, eg. a team scores a point.
  • A Match Stats message may be sent after the match has finished, if there are stats to compute asynchronously that cannot be sent on the Match Finished message itself.

Match Updated

To send a Match Updated message, call SendMatchUpdatedMessage(). Though Match Updated messages are optional, since scores can be sent when the match finishes, FACEIT recommends sending Match Update messages so that the current scores of an ongoing match can be displayed accurately.

A Match Updated message expects a number of parameters:

NameTypeDescription
Faction scoresFactionScoreRecordA list of scores representing the current score for each faction in the match.
Higher score is betterboolWhether a higher score is better (eg. where the score represents points), or a lower score is better (eg. where the score represents the faction's position relative to other factions).
Round numberintThe round number, if applicable for a multi-round match on FACEIT. 0 indicates that rounds are not applicable.
// A function called when the match finishes.
void MyGameServerInstance::OnTeamScoredPoint()
{
std::vector<std::string> factionIDs;
std::vector<FGSSDK_MLC_FactionScore> factionScoreList;

// We know that we will need one entry for every faction in the match,
// so size the arrays appropriately.
const FGSSDK_Size factionCount = FGSSDK_MLC_GetFactionCount();
factionIDs.reserve(factionCount);
factionScoreList.reserve(factionCount);

// Check the score for each faction.
for ( FGSSDK_Size factionIndex = 0; factionIndex < factionCount; ++factionIndex )
{
FGSSDK_MLC_FactionScore scoreItem {};

// Store the faction's ID string so that the pointer remains valid.
factionIDs.emplace_back(
FGSSDK_ToString(&FGSSDK_MLC_GetFactionConfigurationID, factionIndex));

// Add the faction ID to this score entry.
scoreItem.factionConfigID = factionIDs.back().c_str();

// Here, we assume that GetTeamCurrentScore() is a function present
// in the game, that returns the overall score for this faction.
scoreItem.score = GetTeamCurrentScore(factionIDs.back());

// Store the score entry.
factionScoreList.emplace_back(scoreItem);
}

// Construct the message to send.
FGSSDK_MLC_MatchUpdatedMessageData message {};

message.factionScoresArray = factionScoreList.data();
message.factionScoresCount = factionScoreList.size();
message.higherScoreIsBetter = true;
message.round = 0;

// Send the message.
FGSSDK_MLC_SendMatchFinishedMessage(&message);
}

Match Stats

To send a Match Stats message, call SendMatchStatsMessage(). The format of the stats object is completely dependent on the data the game wishes to provide, but must be encapsulated as a valid JSON object.

The structure of the JSON object should correspond to the stats keys defined for the game in the FACEIT Game Studio.

void MyGameServerInstance::SendMatchStats()
{
// Compute match stats and serialise to a string.
// How this happens in a real-world scenarion is up to the game itself.
JSONDocument statsJSONObject = ComputeMatchStats();
std::string statsObjectString = SerialiseJSON(statsJSONObject);

// Construct the message.
FGSSDK_MLC_MatchStatsMessageData message {};

message.statsObject statsObjectString.c_str();

// Send the message.
FGSSDK_MLC_SendMatchStatsMessage(&message);
}

Ending A Match

When a match completes for any reason (it finishes, is aborted, or is cancelled), the OnMatchStatusCompletedCallback callback is called. The callback is called after any other message callbacks have finished executing.

The game server should use this callback to perform any cleanup tasks required once a match has completed. Once ready, the game server must set the server's status back to "available", by calling SetServerAvailable(). If the server is not set back to being available, FACEIT will still consider the server busy and will not send any new match configurations.

If custom match stats must be computed, this process may be started from the OnMatchStatusCompletedCallback callback. Any Match Stats message must be sent no earlier than when the OnMatchStatusCompletedCallback callback is called, and before the server is set back to an available state.

// The function assigned to OnMatchStatusCompletedCallback when initialising:
void MyGameServerInstance::MLC_MatchStatusCompleted()
{
// Compute whatever status are required for the match.
// See the earlier example for the Match Stats message.
SendMatchStats();

// Set the server back to being available.
// After this call, all information about the match configuration
// is cleared from the SDK.
FGSSDK_MLC_SetServerAvailable();
}

Manually Controlling Server Availability

Most game servers will only need to call SetServerAvailable() once a match has completed. However, if a game server wishes to avoid being considered for new matches for a period of time, but wishes to remain online and operational during this time, it may call SetServerBusy() manually while no match is active. This will notify FACEIT that the server is busy, and FACEIT will not send any new match configurations to the server until SetServerAvailable() is called later.

Reporting Server Metrics

If metrics collection is enabled in the Match Life Cycle server settings, the SDK will periodically request metrics data from the game server when a heartbeat message is being prepared. Metrics collection only occurs between when a match has started and when it completes.

When metrics settings are required, the OnMetricsRequestedCallback callback is called. In this callback, the game server should provide metrics data about the overall performance of the server, and about the network connection of each player in the current match.

If the game server chooses to provide metrics, all metrics values must be supplied, and true should be returned from the callback. If the game server cannot provide metrics at the time the callback is called, false should be returned from the callback.

The different types of metrics data required by the Match Life Cycle interface are described below.

Server Metrics

These metrics values apply to the server as a whole.

Metrics ValueFunction NameDescription
Tick RateSetServerTickRate()The target number of ticks per second that the server is running at. For example, competitive first person shooter game servers might run at a tick rate of 120 ticks per second.
FPS (Frames Per Second)SetServerFPS()The actual number of frames per second that the server is processing. This may differ from the target tick rate, eg. if the server is under heavy load.

Client Metrics

These metrics values apply to individual clients connected to the server. All values are expected to be supplied for each client who is part of the currently active matc configuration.

When supplying metrics for a player, identify the player by their unique in-game ID string. For example, in a game which uses Steam as an authentication platform, a player's game ID string would be their Steam ID.

Metrics ValueFunction NameDescription
IP AddressSetPlayerIP()The IP address of the player, as identified by their connection to the server.
LatencySetPlayerLatency()The round-trip network latency for the player, in milliseconds.
JitterSetPlayerJitter()The variance in latency of the player, in milliseconds.
Packet LossSetPlayerLoss()The amount of network packet loss experienced on the player's connection, as a percentage.

Sending Metrics

// The function assigned to OnMetricsRequestedCallback when initialising:
bool MyGameServerInstance::MLC_MetricsRequested()
{
// The functions used to obtain metrics values in this example
// are assumed to exist within the game. In a real world scenario,
// these functions would be implemented by the game developer.

// Provide the target tick rate that the server is attempting to achieve.
FGSSDK_MLC_MetricsSetServerTargetTickRate(GetServerTickRate());

// Provide the actual FPS rate that the server is running at.
FGSSDK_MLC_MetricsSetServerActualFramesPerSecond(GetServerFPS());

const FGSSDK_Size factionCount = FGSSDK_MLC_GetFactionCount();

// Iterate over each faction.
for ( FGSSDK_Size factionIndex = 0; factionIndex < factionCount; ++factionIndex )
{
const FGSSDK_Size factionPlayerCount = FGSSDK_MLC_GetFactionPlayerCount(factionIndex);

// Iterate over each player in the faction.
for ( FGSSDK_Size playerIndex = 0; playerIndex < factionPlayerCount; ++playerIndex )
{
// Fetch the player's game ID.
const std::string gameID = FGSSDK_ToString(
&FGSSDK_MLC_GetPlayerGameID,
factionIndex,
playerIndex);

// Provide metrics for this player.
// The player's game ID is used as the key.
FGSSDK_MLC_MetricsSetPlayerLatency(gameID.c_str(), GetPlayerLatency(gameID));
FGSSDK_MLC_MetricsSetPlayerJitter(gameID.c_str(), GetPlayerJitter(gameID));
FGSSDK_MLC_MetricsSetPlayerLoss(gameID.c_str(), GetPlayerPacketLoss(gameID));

const std::string ipAddress = GetPlayerIPAddress(gameID);
FGSSDK_MLC_MetricsSetPlayerIP(gameID.c_str(), ipAddress.c_str());
}
}

// Return that we were able to fetch metrics.
return true;
}