# 客户端中继多人游戏

**URL:** https://heroiclabs.com/docs/zh/nakama/concepts/multiplayer/relayed/
**Summary:** 借助实时多人游戏引擎，用户可以方便地设置和加入比赛，他们可以在其中与对手快速交换数据。用户可以通过从客户端发送消息的方式创建、加入和退出比赛；在最后一个参与者退出之前，比赛都在服务器上。

---


# 客户端中继多人游戏

借助实时多人游戏引擎，用户可以方便地设置和加入比赛，他们可以在其中与对手快速交换数据。这种中继多人游戏（也称为客户端权威）模式适用于许多游戏类型，例如简单的 1 对 1 或合作游戏，其中服务器上的权威控制并不重要（例如欺骗无关紧要）。

在中继多人游戏中，无论消息大小或内容如何，Nakama 都能促进数据交换。通过比赛传送的任何数据立即转发给请求比赛对手的客户端。**Nakama 维护的仅有比赛数据**是**比赛 ID 和此比赛中状态的列表**。

发往其他连接客户端的客户端消息由服务器转发，而不进行检查。由于 Nakama 不跟踪中继多人游戏比赛中转发的数据量或内容，因此没有欺骗检测、错误纠正或其他此类功能。此方法依赖每个比赛中的某个客户端（由客户端自己决定）充当[主机](#match-host-rotation)。此主机将协调对等机之间的状态更改，并对从不良客户端发送的不明确或恶意消息执行仲裁。

任何用户都可以参加与其他用户的比赛，但无法对比赛的访问进行密码保护或以其他方式加以限制。对于比赛中的玩家数量没有明确限制，只有游戏设计（消息大小和频率）和可用资源（服务器硬件和网络能力）施加的实际限制。

用户可从客户端发送消息，[创建](#create-a-match)、[加入](#join-a-match)和[退出](#leave-a-match)比赛。 

比赛始终停留在服务器上，直到最后一个参与者离开。它们保存在内存内，无法永远存在。

## 创建比赛

任何用户可调用“比赛创建”操作，创建新的比赛。在创建比赛时，创建者无法提供比赛状态。 

服务器将向新比赛分配唯一 ID。可将此 ID 分享给其他用户，让他们[加入比赛](#join-a-match)。

{{< code type="client" >}}
```javascript
var response = await socket.createMatch();
console.log("Created match with ID:", response.match.match_id);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var match = await socket.CreateMatchAsync();
Console.WriteLine("New match with id '{0}'.", match.Id);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
rtClient->createMatch([](const NMatch& match)
{
    std::cout << "Created Match with ID: " << match.matchId << std::endl;
});
```
{{< / code >}}

{{< code type="client" >}}
```java
Match match = socket.createMatch().get();
System.out.format("Created match with ID %s.", match.getId());
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var created_match : NakamaRTAPI.Match = yield(socket.create_match_async(), "completed")

if created_match.is_exception():
    print("An error occurred: %s" % created_match)
    return

print("New match with id %s.", created_match.match_id)
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
local result = socket.match_create()

if result.error then
  print(result.error.message)
  return
end

print("Created match with ID", result.match.match_id)
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

在创建新比赛时，用户可以选择提供 `name`。此名称用于生成比赛 ID，这意味着两名创建同名比赛的玩家将得到相同的比赛 ID，结果处于同一场比赛中。

{{< code type="client" >}}
```csharp
var matchName = "Heroes";
var match = await socket.CreateMatchAsync(matchName);
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var match_name = "Heroes";
var match : NakamaRTAPI.Match = yield(socket.create_match_async(matchName), "completed");
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
local match_name = "Heroes"
local result = socket.match_create(match_name)
```
{{< / code >}}

{{< missing type="client" lang="cpp" />}}
{{< missing type="client" lang="javascript" />}}
{{< missing type="client" lang="java" />}}
{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

用户可以随时[退出比赛](#leave-a-match)，所有其他用户将得到通知。所有用户都离开后，比赛将不再存在。

## 加入比赛

用户可用特定比赛的 ID 加入其中。无法用密码保护或关闭比赛。如果用户有比赛 ID，则他们能够加入其中。用户可随时加入比赛，直到最后一名参与者退出。

{{< code type="client" >}}
```javascript
var id = "<matchid>";
var match = await socket.joinMatch(id);

var connectedOpponents = match.presences.filter((presence) => {
  // Remove your own user from list.
  return presence.user_id != match.self.user_id;
});

connectedOpponents.forEach((opponent) => {
  console.log("User id %o, username %o.", opponent.user_id, opponent.username);
});
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var matchId = "<matchid>";
var match = await socket.JoinMatchAsync(matchId);

foreach (var presence in match.Presences)
{
    Console.WriteLine("User id '{0}' name '{1}'.", presence.UserId, presence.Username);
}
```
{{< / code >}}

{{< code type="client" >}}
```cpp
string matchId = "<matchid>";
rtClient->joinMatch(matchId, {}, [](const NMatch& match)
{
    std::cout << "Joined Match!" << std::endl;

    for (auto& presence : match.presences)
    {
        if (presence.userId != match.self.userId)
        {
            std::cout << "User id " << presence.userId << " username " << presence.username << std::endl;
        }
    }
});
```
{{< / code >}}

{{< code type="client" >}}
```java
String matchId = "<matchid>";
Match match = socket.joinMatch(matchId).get();

for (UserPresence presence : match.getPresences()) {
    System.out.format("User id %s name %s.", presence.getUserId(), presence.getUsername());
}
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var match_id = "<matchid>"
var joined_match = yield(socket.join_match_async(match_id), "completed")

if joined_match.is_exception():
    print("An error occurred: %s" % joined_match)
    return

for presence in joined_match.presences:
    print("User id %s name %s'." % [presence.user_id, presence.username])
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
local match_id = "<matchid>"
local result = socket.match_join(match_id)

if result.error then
  print(result.error.message)
  return
end

for _,user in  ipairs(result.match.presences) do
  print("User id", user.user_id, "name", user.name)
end
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

在加入比赛时，将在成功回调中返回一个比赛对手列表。记住此表可能并不包含所有用户，只包含_在那个时间点_连接到比赛的用户。

## 列出对手

当用户创建或加入一个新的比赛时，他们会接收到一个已连接对手的初始列表。在这个初始列表之后，服务器将事件推送到发生比赛加入和比赛退出的已连接客户端 – 如果状态列表没有发生变化，则不发送服务器更新。为了提高效率，对事件进行批处理，这意味着任何事件都可以包含多次加入和/退出。

这些事件可用于更新连接对手的列表，以便您的玩家可以准确地看到所有比赛参与者。


{{< code type="client" >}}
```javascript
var connectedOpponents = [];
socket.onmatchpresence = (presences) => {
  // Remove all users who left.
  connectedOpponents = connectedOpponents.filter(function(co) {
    var stillConnectedOpponent = true;

    presences.leaves.forEach((leftOpponent) => {
      if (leftOpponent.user_id == co.user_id) {
        stillConnectedOpponent = false;
      }
    });

    return stillConnectedOpponent;
  });

  // Add all users who joined.
  connectedOpponents = connectedOpponents.concat(presences.joins);
};
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var connectedOpponents = new List<IUserPresence>(2);

socket.ReceivedMatchPresence += presenceEvent =>
{
    foreach (var presence in presenceEvent.Leaves)
    {
        connectedOpponents.Remove(presence);
    }

    connectedOpponents.AddRange(presenceEvent.Joins);

    // Remove yourself from connected opponents.
    connectedOpponents.Remove(self);
    Console.WriteLine("Connected opponents: [{0}]", string.Join(",\n  ", connectedOpponents));
};
```
{{< / code >}}

{{< code type="client" >}}
```cpp
rtListener->setMatchPresenceCallback([](const NMatchPresenceEvent& event)
{
    for (auto& presence : event.joins)
    {
        std::cout << "Joined user: " << presence.username << std::endl;
    }

    for (auto& presence : event.leaves)
    {
        std::cout << "Left user: " << presence.username << std::endl;
    }
});
```
{{< / code >}}

{{< code type="client" >}}
```java
List<UserPresence> connectedOpponents = new ArrayList<UserPresence>();

public void onMatchPresence(final MatchPresenceEvent matchPresence) {
    connectedOpponents.addAll(matchPresence.getJoins());

    for (UserPresence leave : matchPresence.getLeaves()) {
        for (int i = 0; i < connectedOpponents.size(); i++) {
            if (connectedOpponents.get(i).getUserId().equals(leave.getUserId())) {
                connectedOpponents.remove(i);
            }
        }
    };
});
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var connected_opponents = {}

func _ready():
    # First, setup the socket as explained in the authentication section.
    socket.connect("received_match_presence", self, "_on_match_presence")

func _on_match_presence(p_presence : NakamaRTAPI.MatchPresenceEvent):
    for p in p_presence.joins:
        connected_opponents[p.user_id] = p

    for p in p_presence.leaves:
        connected_opponents.erase(p.user_id)

    print("Connected opponents: %s" % [connected_opponents])
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
local connected_opponents = {}
socket.on_matchpresence(function(message)
  for _,p in ipairs(message.match_presence_event.leaves) do
    connected_opponents[p.user_id] = nil
  end

  for _,p in ipairs(message.match_presence_event.joins) do
    connected_opponents[p.user_id] = p
  end
end)
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

记住列出对手的一些最佳做法：

* 在加入或创建比赛_之前_注册客户端状态事件侦听器
* 对于同一状态同时具有加入和退出的批处理的事件，对于列表中已存在的状态，先处理退出，然后再处理加入；对于列表中没有的状态，处理加入，然后再处理退出 

## 发送数据消息

比赛中的用户可以发送数据消息，所有其他对手都会接收到这些消息。这些消息实时流式传输到目标客户端，可以包含任何二进制内容。Nakama **以接收顺序**而不一定是发送顺序广播消息。

每条数据消息中的二进制内容应当**尽量小**，不超出最大传输单位 (MTU) `1500` 字节数。通常使用的是 JSON，最好使用 [Protocol Buffers](https://developers.google.com/protocol-buffers/) 或 [FlatBuffers](https://google.github.io/flatbuffers/) 等紧凑二进制格式。 

在无法进一步减少消息大小和/或频率的情况下，最好优先发送**更少的消息**。例如，1 条每秒 `1000` 字节的消息优于 5 条每秒 `200` 字节消息。

为了将每条消息标识为特定的“命令”，它包含一个[操作码](#op-codes)以及有效负载。

{{< code type="client" >}}
```javascript
var id = "<matchid>";
var opCode = 1;
var data = { "move": {"dir": "left", "steps": 4} };
socket.sendMatchState(id, opCode, data);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
// using Nakama.TinyJson;
var matchId = "<matchid>";
var opCode = 1;
var newState = new Dictionary<string, string> {{"hello", "world"}}.ToJson();
socket.SendMatchStateAsync(matchId, opCode, newState);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
string id = "<matchid>";
int64_t opCode = 1;
NBytes data = "{ \"move\": {\"dir\": \"left\", \"steps\" : 4} }";
rtClient->sendMatchData(id, opCode, data);
```
{{< / code >}}

{{< code type="client" >}}
```java
String id = "<matchid>";
int opCode = 1;
String data = "{\"message\":\"Hello world\"}";
socket.sendMatchData(id, opCode, data);
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var match_id = "<matchid>"
var op_code = 1
var new_state = {"hello": "world"}
socket.send_match_state_async(match_id, op_code, JSON.print(new_state))
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
local match_id = "<matchid>"
local op_code = 1

local data = json.encode({
  move = {
    dir = "left",
    steps = 4
  }
})

local result = socket.match_data_send(match_id, op_code, data)

if result.error then
  print(result.error.message)
  return
end
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

默认情况下消息会广播到所有其他比赛状态，但用户可以选择指定所需的比赛参与者子集（即他们的好友、队友等）以独占方式接收消息。

{{< code type="client" >}}
```javascript
// Only send data to the first presence in the match presences array
socket.sendMatchState(id, opCode, data, [match.presences[0]]);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
// Only send data to the first presence in the match presences array
await socket.SendMatchStateAsync(matchId, opCode, newState, new [] { match.presences.First() });
```
{{< / code >}}

{{< code type="client" >}}
```cpp
// Only send data to the first presence in the match presences array
rtClient->sendMatchData(id, opCode, data, { match.presences[0] });
```
{{< / code >}}

{{< code type="client" >}}
```java
// Only send data to the first presence in the match presences array
socket.sendMatchData(id, opCode, data, match.getPresences().get(0));
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
// Only send data to the first presence in the match presences array
socket.send_match_state_async(match_id, op_code, JSON.print(new_state), [current_match.presences[0]])
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
-- Only send data to the first presence in the match presences array
local result = socket.match_data_send(match_id, op_code, data, { match.presences[0] })
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

### 操作码

操作码是发送的消息类型的数值标识符。操作码可以让用户在对消息解码之前了解消息的目的和内容。 

可以将其用于定义游戏中属于某些用户操作的命令，例如：

* 初始状态同步
* 就绪状态
* Ping / Pong
* 游戏状态更新
* 表情

有关示例实现，请参阅 [Fish Game 教程](../../../tutorials/unity/fishgame/#operation-codes)。

## 接收数据消息

服务器按照处理来自客户端的数据消息的顺序交付数据。客户端可以为传入的比赛数据消息添加回调。这应该在他们创建（或加入）和退出比赛之前完成。

{{< code type="client" >}}
```javascript
socket.onmatchdata = (result) => {
  var content = result.data;

  switch (result.op_code) {
    case 101:
      console.log("A custom opcode.");
      break;
    default:
      console.log("User %o sent %o", result.presence.user_id, content);
  }
};
```
{{< / code >}}

{{< code type="client" >}}
```csharp
// Use whatever decoder for your message contents.
var enc = System.Text.Encoding.UTF8;
socket.ReceivedMatchState += newState =>
{
    var content = enc.GetString(newState.State);

    switch (newState.OpCode)
    {
        case 101:
            Console.WriteLine("A custom opcode.");
            break;
        default:
            Console.WriteLine("User '{0}'' sent '{1}'", newState.UserPresence.Username, content);
    }
};
```
{{< / code >}}

{{< code type="client" >}}
```cpp
rtListener->setMatchDataCallback([](const NMatchData& data)
{
    switch (data.opCode)
    {
        case 101:
            std::cout << "A custom opcode." << std::endl;
            break;
        default:
            std::cout << "User " << data.presence.userId << " sent " << data.data << std::endl;
            break;
    }
});
```
{{< / code >}}

{{< code type="client" >}}
```java
SocketListener listener = new AbstractSocketListener() {
    @Override
    public void onMatchData(final MatchData matchData) {
        System.out.format("Received match data %s with opcode %d", matchData.getData(), matchData.getOpCode());
    }
};
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
func _ready():
    # First, setup the socket as explained in the authentication section.
    socket.connect("received_match_state", self, "_on_match_state")

func _on_match_state(p_state : NakamaRTAPI.MatchData):
    print("Received match state with opcode %s, data %s" % [p_state.op_code, parse_json(p_state.data)])
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
socket.on_matchdata(function(message)
  local data = json.decode(message.match_data.data)
  local op_code = tonumber(message.match_data.op_code)
end)
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

## 退出比赛

用户可随时退出比赛。这可能通过客户端操作（退出比赛）自愿发生，也可能非自愿发生（例如因网络连接的原因），但在这两种情况下，都必须在游戏逻辑中进行适当的考虑和处理。

{{< code type="client" >}}
```javascript
var id = "<matchid>";
socket.leaveMatch(id);
```
{{< / code >}}

{{< code type="client" >}}
```csharp
var matchId = "<matchid>";
await socket.LeaveMatchAsync(matchId);
```
{{< / code >}}

{{< code type="client" >}}
```cpp
string matchId = "<matchid>";
rtClient->leaveMatch(matchId);
```
{{< / code >}}

{{< code type="client" >}}
```java
String matchId = "<matchid>";
socket.leaveMatch(matchId).get();
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var match_id = "<matchid>"
var leave : NakamaAsyncResult = yield(socket.leave_match_async(match_id), "completed")
if leave.is_exception():
    print("An error occurred: %s" % leave)
    return
print("Match left")
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
local match_id = "<matchid>"
local result = socket.match_leave(match_id)
if result.error then
  print(result.error.message)
  return
end
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}

所有用户退出后，游戏结束。此时其 ID 失效，无法再次用于重新加入。

## 示例

### 比赛主机轮换

您必须确定如何从连接的客户端中选择比赛主机。最好的实现方式是不需要在比赛参与者之间进行任何“协商”，同时确保所有客户端都承认同一主机。

您可以通过对比赛状态进行决定性的排序，并根据任何所需的因素选择主机来实现这一点。在下面的示例中，我们对状态列表进行排序，并选择最低的索引会话 ID 作为主机：

{{< code type="client" >}}
```javascript
// Declare a variable to store which presence is the host
var hostPresence;

// Upon receiving a matchmaker matched event, deterministically calculate the host by sorting the session Ids
socket.onmatchmakermatched = (matchmakerMatched) => {
  var hostSessionId = matchmakerMatched.users.map(user => user.presence.session_id).sort();
  hostPresence = matchmakerMatched.users.filter(user => user.presence.session_id == hostSessionId)[0];
};

// When receiving a match presence event, check if the host left and if so recalculate the host presence
socket.onmatchpresence = (matchPresence) => {
  if (matchPresence.leaves.find(presence => presence.user_id === hostPresence.user_id))
  {
    var hostSessionId = match.presences.map(presence => presence.session_id).sort();
    hostPresence = match.presences.filter(presence => presence.session_id === hostSessionId)[0];
  }
};
```
{{< / code >}}

{{< code type="client" >}}
```csharp
// Declare a variable to store which presence is the host
IUserPresence hostPresence;

// Upon receiving a matchmaker matched event, deterministically calculate the host by sorting the session Ids
socket.ReceivedMatchmakerMatched += matched =>
{
  hostPresence = matched.Users.OrderBy(x => x.Presence.SessionId).First().Presence;
};

// When receiving a match presence event, check if the host left and if so recalculate the host presence
socket.ReceivedMatchPresence += matchPresenceEvent =>
{
  if (matchPresenceEvent.Leaves.Any(x => x.UserId == hostPresence.UserId))
  {
    hostPresence = match.Presences.OrderBy(x => x.SessionId).First();
  }
};
```
{{< / code >}}

{{< code type="client" >}}
```cpp
// Declare a variable to store which presence is the host
NUserPresence hostPresence;

// Upon receiving a matchmaker matched event, deterministically calculate the host by sorting the session Ids
listener.setMatchmakerMatchedCallback([&done, &hostPresence](NMatchmakerMatchedPtr matchmakerMatched) {
    std::sort(matchmakerMatched->users.begin(), matchmakerMatched->users.end(), [](const NMatchmakerUser lhs, const NMatchmakerUser rhs) {
        return lhs.presence.sessionId < rhs.presence.sessionId;
    });
    hostPresence = matchmakerMatched->users[0].presence;
});

// When receiving a match presence event, check if the host left and if so recalculate the host presence
listener.setMatchPresenceCallback([&match, &hostPresence](NMatchPresenceEvent matchPresenceEvent) {
    for (int i = 0; i < matchPresenceEvent.leaves.size(); i++) {
        if (matchPresenceEvent.leaves[i].sessionId == hostPresence.sessionId) {
            std::sort(match.presences.begin(), match.presences.end(), [](const NUserPresence lhs, const NUserPresence rhs) {
                return lhs.sessionId < rhs.sessionId;
            });
            hostPresence = match.presences[0];
        }
    }
});
```
{{< / code >}}

{{< code type="client" >}}
```java
// Declare a variable to store which presence is the host (as a final 1 length array so we can access it correctly)
final UserPresence[] hostPresence = new UserPresence[1];

SocketListener socketListener = new SocketListener() {
  // Upon receiving a matchmaker matched event, deterministically calculate the host by sorting the session Ids
  @Override
  public void onMatchmakerMatched(MatchmakerMatched matchmakerMatched) {
    List<MatchmakerUser> users = matchmakerMatched.getUsers();
    users.sort((a, b) -> String.CASE_INSENSITIVE_ORDER.compare(a.getPresence().getSessionId(), b.getPresence().getSessionId()));
    hostPresence[0] = users.get(0).getPresence();
  }

  // When receiving a match presence event, check if the host left and if so recalculate the host presence
  @Override
  public void onMatchPresence(MatchPresenceEvent e) {
    if (e.getLeaves() != null) {
      e.getLeaves().forEach(presence -> {
        if (presence.getSessionId() == hostPresence[0].getSessionId()) {
          List<UserPresence> matchPresences = match.getPresences();
          matchPresences.sort((a, b) -> String.CASE_INSENSITIVE_ORDER.compare(a.getSessionId(), b.getSessionId()));
          hostPresence[0] = matchPresences.get(0);
        }
      });
    }
  }
}
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
# Declare a variable to store which presence is the host
var host_presence : NakamaRTAPI.UserPresence

# Define comparer functions
func _presence_comparer(a : NakamaRTAPI.UserPresence, b : NakamaRTAPI.UserPresence):
	return a.session_id < b.session_id

func _user_comparer(a : NakamaRTAPI.MatchmakerUser, b : NakamaRTAPI.MatchmakerUser):
	return a.presence.session_id < b.presence.session_id

# Upon receiving a matchmaker matched event, deterministically calculate the host by sorting the session Ids
func _on_matchmaker_matched(matchmaker_matched : NakamaRTAPI.MatchmakerMatched):
	matchmaker_matched.users.sort_custom(self, "_user_comparer")
	host_presence = matchmaker_matched.users[0].presence
	current_match = yield(socket.join_match_async(matchmaker_matched.match_id), "completed")

# When receiving a match presence event, check if the host left and if so recalculate the host presence
func _on_match_presence(match_presence_event : NakamaRTAPI.MatchPresenceEvent):
	for presence in match_presence_event.leaves:
		if presence.session_id == host_presence.session_id:
			current_match.presences.sort_custom(self, "_presence_comparer")
			if len(current_match.presences) < 1:
				host_presence = current_match.self_user
			else:
				host_presence = current_match.presences[0]
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua

local host = nil

-- Upon receiving a matchmaker matched event, deterministically calculate the host by sorting the session Ids
socket.on_matchmaker_matched(function(matched)
    table.sort(matched.users, function(a, b) return a.presence.session_id < b.presence.session_id end)
    host = matched.users[1]
end)

-- When receiving a match presence event, check if the host left and if so recalculate the host presence
socket.on_match_presence_event(function(presence)
    for i,presence in ipairs(presence.leaves) do
        if presence.session_id == host.session_id then
            table.sort(match.presences, function(a, b) return a.session_id < b.session_id end)
            host = match.presences[1]
        end
    end
end)
```
{{< / code >}}

{{< missing type="client" lang="bash" />}}
{{< missing type="client" lang="shell" />}}
