> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qwacks.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Connecting Players

> Connect a session's players over Netcode for GameObjects or Unreal's own networking, through Flock's relay when needed.

Once players share a [session](/guides/sessions.md), the SDK connects them over your game's
networking: Netcode for GameObjects on Unity, the engine's own networking on Unreal. The host's
game is the server. Players on the host's network connect to it directly, and everyone else goes
through **Flock's relay**, so nobody forwards a port. The host lets in only the session's players,
and when the host changes, every game starts its networking again for its new part.

> [!NOTE]
>
> **Scope:** the relay is switched on or off per **game**, under **Multiplayer settings**.

> [!NOTE]
>
> Flock's relay runs on Cloudflare's network and is on for every game until it is switched off under **Multiplayer
> settings**. With it switched off, or while a studio's relay is paused until its bill is paid, players connect on the
> host's network or through a port forwarded to the host, and a player on another network gets `RelayFailed` (Unreal:
> `relay_failed`) with `relay_not_offered` or `relay_paused`.

## Starting the netcode

**Unity**

One call does the whole hand-shake, for the host and for each player. On the host it starts
Netcode for GameObjects as host and says where to reach it. On a player it waits for that (up to
60 seconds), connects, and proves the player's seat.

```csharp theme={null}
FlockNetcodeStartResult started = await session.StartNetcodeAsync(NetworkManager.Singleton);
switch (started.Outcome)
{
    case FlockNetcodeStartOutcome.Restarted:
        return;   // a newer start on this NetworkManager took over
    case FlockNetcodeStartOutcome.StartedAsHost:
        ShowMessage("Hosting. Code: " + session.JoinCode);
        break;
    case FlockNetcodeStartOutcome.Connected:
        ShowMessage(started.ThroughRelay ? "Connected through Flock's relay." : "Connected.");
        break;
    case FlockNetcodeStartOutcome.Refused:
        ShowMessage("The host refused you: " + started.RefusedReason);
        break;
    case FlockNetcodeStartOutcome.RelayFailed:
        ShowMessage("The relay could not be used: " + started.RelayFailureReason);
        break;
    default:
        ShowMessage("Could not connect: " + started.Outcome);
        break;
}
```

A start that neither hosts nor connects keeps the player's seat; call `session.LeaveAsync()`
if the player cannot play.

**Unreal**

One call brings each game into the session's game, the host's and each player's:
`TravelToSession`. On the host it opens the host's level listening for players (the engine's
`?listen`) and says where to reach it. On a player it waits for that (up to 60 seconds), asks
Flock for a join token, and travels to the host, which checks the token before letting the
player in. The engine's own net driver carries every player, through Flock's relay too: the SDK
stands in for the other side on the device's own loopback, so your networking code stays as it
is.

Call it from your game instance: it completes once the game is in its new level, and the level
the call was made from is gone by then. Here `TravelOptions` is an `FFlockTravelToSessionOptions`
kept on the game instance, so a host change can pass the same one again.

```cpp theme={null}
#include "FlockSubsystem.h"
#include "Multiplayer/FlockMultiplayerSession.h"

// Every player's game, the host's included, once it holds the session.
TravelOptions.HostLevel = TEXT("/Game/Maps/Arena");   // the level the host opens; players travel to whatever the host has open
const TWeakObjectPtr<UMyGameInstance> Game = this;
Session->TravelToSession(this, TravelOptions, [Game](TFlockResult<FFlockTravelToSessionResult> Result)
{
    if (!Game.IsValid())
    {
        return;
    }
    if (!Result.IsSuccess())
    {
        UE_LOG(LogTemp, Warning, TEXT("%s"), *Result.Error.ToDisplayText());   // a mistake, a refused publish or a cancel, with its fix
        return;
    }
    const FFlockTravelToSessionResult& Travel = Result.Value;
    if (Travel.Outcome == FlockTravelToSessionOutcomes::Restarted)
    {
        return;   // a newer Travel To Session took over
    }
    if (Travel.Outcome == FlockTravelToSessionOutcomes::StartedAsHost)
    {
        Game->ShowMessage(TEXT("Hosting. Code: ") + Game->Session->GetJoinCode());
    }
    else if (Travel.Outcome == FlockTravelToSessionOutcomes::Connected)
    {
        Game->ShowMessage(Travel.bThroughRelay ? TEXT("Connected through Flock's relay.") : TEXT("Connected."));
    }
    else if (Travel.Outcome == FlockTravelToSessionOutcomes::Refused)
    {
        Game->ShowMessage(TEXT("The host refused you: ") + Travel.RefusedReason);
    }
    else if (Travel.Outcome == FlockTravelToSessionOutcomes::RelayFailed)
    {
        Game->ShowMessage(TEXT("The relay could not be used: ") + Travel.RelayFailureReason);
    }
    else
    {
        Game->ShowMessage(TEXT("Could not connect: ") + Travel.Outcome);
    }
});
```

The first argument is anything in the game's world, such as the game instance. A travel that
neither hosts nor connects keeps the player's seat; call `Session->Leave` if the player cannot
play.

**The host's game mode checks each joining player.** Base it on `AFlockGameModeBase` or
`AFlockGameMode`, or call Flock's check from its own `PreLoginAsync`; see
[Who is joining](#who-is-joining). A host whose game mode does neither lets nobody in
(`join_check_missing`).

**Travel To Session needs the engine's IP networking.** A game whose net driver uses Steam's
sockets (the Steam online subsystem's `bUseSteamNetworking`, on unless set to false) cannot
listen on an IP address, and ends `could_not_start`. Set `bUseSteamNetworking=False` under
`[OnlineSubsystemSteam]` in `Config/DefaultEngine.ini`, or publish a Steam connection of your
own (see [Other networking](#other-networking)).

In Blueprint, call **Flock Travel To Session** from the Game Instance Blueprint, where the
engine's own **NetworkError** and **TravelError** events are; called from inside a level, it
warns in the log. It fires **On Joined** (started as host, or connected), **On Not Joined** (the
result's Outcome and Refused Reason say why) or **On Failure** (a mistake, such as an ended
session), once the level has changed. Its **Async Action** pin hands out the node, whose
**Cancel** stops the travel. Compare the Outcome with the **Flock Travel Outcome** nodes, and
keep a default.

How a start ended is its outcome. On Unity it is a `FlockNetcodeStartOutcome`; on Unreal it is a
string, compared with the constants of `FlockTravelToSessionOutcomes`, which carry the same names
(`FlockTravelToSessionOutcomes::StartedAsHost` is `started_as_host`). More can come, so keep a
default.

| Unity | Unreal | Meaning |
| - | - | - |
| `StartedAsHost` | `started_as_host` | This device hosts, and players can reach it. |
| `Connected` | `connected` | This device joined the host. `ThroughRelay` (Unreal: `bThroughRelay`) says whether through the relay. |
| `Refused` | `refused` | The host refused this player; `RefusedReason` says why (see [Who is joining](#who-is-joining)). On Unreal, the engine has taken the game back to its default level. |
| `RelayFailed` | `relay_failed` | The relay was needed and could not be used; `RelayFailureReason` says why and what to do (see below). |
| `HostNeverPublished` | `host_never_published` | The host did not say where to reach it within 60 seconds (`FlockNetcodeOptions.WaitForHost`, or `WaitForHostSeconds` on Unreal). Make sure the host's game started too, then start again. |
| `CouldNotConnect` | `could_not_connect` | The host did not answer. On Unreal, the engine gives up after its own timeout (about 20 seconds) and loads the default level. Outside the host's network with the relay off, forward a port to the host. |
| `CouldNotStart` | `could_not_start` | The host could not start. On Unity, Netcode for GameObjects refused to start, such as when the port is in use: free the port or pick another. On Unreal, the host's level did not open listening on an IP address, such as with Steam's sockets: turn `bUseSteamNetworking` off. |
| `SessionEnded` | `session_ended` | The session ended while waiting. Host or join another session. |
| `NotDirect` | `not_direct` | The host published a connection of its own kind, such as Steam; see [Other networking](#other-networking). |
| `Restarted` | `restarted` | The game started again before this start ended, and the newer start took over. Nothing to do: the newer one's answer follows. |

## Through the relay, or directly

`FlockNetcodeOptions.Relay` on Unity, and `FFlockTravelToSessionOptions::Relay` on Unreal, choose
who goes through the relay. Set it the same on every player's game.

| `FlockRelayUse` / `EFlockRelayUse` | Who goes through the relay | Pick it for |
| - | - | - |
| `WhenNeeded` (default) | Players on the host's network try its local address for a few seconds, then the relay if it does not answer. Everyone else goes through the relay, then moves to a direct path when they and the host can reach each other. | Almost every game. |
| `Always` | Every player, on the host's network too, and they stay on the relay. Players never learn the host's IP address, nor the host theirs. On Unreal, the host turns away a player that reaches it another way (`relay_only`). | Streamers and competitive play, or networks that keep devices apart. |
| `Never` | Nobody. Players reach the host on its network, or on a port forwarded to it. | LAN games, or a game that does not want relay traffic. |

```csharp Unity theme={null}
await session.StartNetcodeAsync(NetworkManager.Singleton, new FlockNetcodeOptions { Relay = FlockRelayUse.Always });
```

```cpp Unreal theme={null}
TravelOptions.Relay = EFlockRelayUse::Always;
Session->TravelToSession(this, TravelOptions, [](TFlockResult<FFlockTravelToSessionResult> Travel) { /* ... */ });
```

### Direct paths

With `WhenNeeded`, once the host has let a relayed player in, the two try to reach each other directly: each asks the relay,
or a STUN server Flock lists, where it sees it, they swap those addresses through the relay, and each sends a few small packets to the other. When they get
through both ways, the game's packets go directly, usually a shorter path than the relay's, and the relay has nothing left to
carry. The relay stays ready: a direct path that goes quiet for 4 seconds hands the packets back to it. The game does nothing in
either case. Most home routers allow this; when both sides are behind routers that give each destination a port of its own (as
some mobile carriers do), the packets stay on the relay. From Unity SDK 1.83.0 and Unreal SDK 1.38.0.

On Unreal, the engine's connection carries on through each move: the host's net driver hears the player from the same port
throughout. Each move is said once in the log, at Info. A game on an Unreal SDK before 1.38.0 at either end keeps the pair on
the relay, since the two sides first say whether they understand each other's direct-path messages.

The trade-off: with `WhenNeeded`, the host and a relayed player learn each other's IP address once the player is let in. On
Unreal, the host publishes its own addresses beside its relay address, so players can read the host's. Use `Always` to keep
them hidden.

When the relay cannot be used, `RelayFailureReason` is one of these, the same on both engines unless the row says otherwise:

| Reason | When | What to do |
| - | - | - |
| `relay_not_offered` | **Hand out relay credentials** is off for the game, or Flock lists no relay. | Turn it on under **Multiplayer settings**, or connect players directly. |
| `relay_paused` | The studio's relay is paused until its bill is paid. | Settle the bill in the dashboard; meanwhile players connect directly. |
| `relay_unreachable` | The network blocks UDP to the relay, as some school, office and hotel networks do. | Check the device is online and a firewall lets the game send UDP, or try another network. |
| `relay_not_on_this_platform` | Unity only: a web player. A browser cannot send UDP. | Play from a desktop or phone build to use the relay. |
| `relay_not_offered_by_host` | The host takes nobody through the relay: its `Relay` is `Never`, or its relay could not be opened. | Set `Relay` the same on every player's game; the host's log says why its relay could not be opened. |
| `relay_too_many_logins` | More than 12 relay logins in a minute, or 10 for one player in one session. | Do not start the netcode (on Unreal, Travel To Session) in a loop; another session gives a seat of its own. |
| `relay_server_not_listed` | Flock does not list, for this player, the relay server the host is on. | Have the host start again, so both use a relay server Flock lists for each. |
| `relay_not_in_session` | Flock says the player holds no seat in the session. | Join the session again before connecting. |
| `flock_unreachable`, `relay_logins_refused` | Flock did not answer the request for relay logins, or refused it. | Check the device is online, then start again; the log names what Flock refused. |
| `relay_wrong_login`, `relay_full`, `relay_open_refused`, `relay_refused` | The relay server refused the login, had no room, would not let the host's packets in, or refused for another reason. | Start again, so Flock gives fresh logins; if it keeps refusing, connect players directly. |
| `relay_lost`, `relay_closed` | The relay let this device's address go or has answered nothing for 45 s (this device's packets no longer reach it), or the session's end closed it. | After `relay_lost`, start again; after `relay_closed`, host or join another session. |
| `relay_no_thread` | Unreal only: the game runs without threads (`-nothreading`), and the relay needs one of its own. | Launch the game without `-nothreading`. |

The server can add reasons, so keep a default.

## Who is joining

The host lets in only players whose seat Flock confirms, one connection each. Name players by
their connection, never by what a player's game sends. The host's check of each joining player is
billed as one API call.

**Unity**

The host started by `StartNetcodeAsync` checks each joining player itself. Find who a
connection is:

```csharp theme={null}
string playerId = session.PlayerForConnection(clientId);   // on the host
```

Add your own check after Flock's, such as a ban list or a team limit:

```csharp theme={null}
FlockNetcodeOptions options = new FlockNetcodeOptions
{
    ApproveJoiningPlayer = (player, response) =>
    {
        if (bannedPlayers.Contains(player.PlayerId))
        {
            response.Approved = false;
            response.Reason = "banned";
        }
    },
};
```

A refused player's `RefusedReason` is `no_join_token`, `join_token_invalid`, `already_connected`,
`flock_unreachable`, `session_ended`, `not_host`, `refused_by_the_game` or `game_check_failed`.

> [!NOTE]
>
> Both ends of a game must run Flock SDK 1.77.0 or later, and the same major version of Netcode for
> GameObjects: a host refuses players on an older Flock SDK, or on another major version of Netcode.

**Unreal**

The engine asks the host's game mode about each joining player in `PreLoginAsync`, so the host's
game mode calls Flock's check there, in place of the engine's own. The simplest way is to base
it on `AFlockGameModeBase` (on `AGameModeBase`) or `AFlockGameMode` (on `AGameMode`, for match
states): in C++, or as a Blueprint based on **Flock Game Mode Base** or **Flock Game Mode**, set
in **Project Settings → Maps & Modes**. Or call the check from a game mode of your own:

```cpp theme={null}
// MyGameMode.h
virtual void PreLoginAsync(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId,
    const FOnPreLoginCompleteDelegate& OnComplete) override;

// MyGameMode.cpp
#include "Multiplayer/FlockJoinChecks.h"

void AMyGameMode::PreLoginAsync(const FString& Options, const FString& Address, const FUniqueNetIdRepl& UniqueId,
    const FOnPreLoginCompleteDelegate& OnComplete)
{
    // Flock's check, then the engine's own (PreLogin, such as a full game), then your own below.
    const TWeakObjectPtr<AMyGameMode> Mode = this;
    FlockJoinChecks::CheckJoiningPlayer(this, Options, Address, UniqueId, OnComplete,
        [Mode](const FFlockJoiningPlayer& Player, FString& OutRefusedReason)
        {
            if (Mode.IsValid() && Mode->IsBanned(Player.PlayerId))
            {
                OutRefusedReason = TEXT("banned");   // the joining player's RefusedReason
                return false;
            }
            return true;
        });
}
```

Your own check is optional, and runs only for a player Flock has let in. It gets the `PlayerId`
Flock verified, the travel's `Options` without Flock's token, and the `Address` (127.0.0.1 for a
player that came through Flock's relay). On the Flock game modes, override **Approve Joining
Player** (`ApproveJoiningPlayer`) instead; it lets everyone in until overridden.

Find who a connection is, on the host, such as in `PostLogin`:

```cpp theme={null}
FString PlayerId = Session->PlayerForConnection(PlayerController);   // empty for a connection Flock's check did not let in
```

A refused player's `RefusedReason` is one of `FlockTravelToSessionRefusedReasons`, your own
reason, or the engine's (such as `Server full.`), so keep a default. The joining player's log
names the fix too.

| Reason | When | What to do |
| - | - | - |
| `no_join_token` | The player's travel carried no token: it did not come through `TravelToSession`. | Connect with `TravelToSession`. |
| `join_token_invalid` | Flock said the token is not good for this session: made up, another session's, expired (tokens last 60 seconds), or its player gave up the seat. | Join the session again, then travel. |
| `already_connected` | The same player is already in through another connection the host still hears from. | Close the player's other game, then travel again. |
| `flock_unreachable` | Flock did not answer the host within 8 seconds, or the host was over its checks a minute. Nobody is let in unchecked. | Check the host's device is online, then travel again. |
| `session_ended` | The session has ended. | Host or join another session. |
| `not_host` | The device reached no longer hosts the session. | Travel again: it finds the new host. |
| `refused_by_the_game` | Your own check turned the player away without a reason of its own. | Give the player a reason of your own. |
| `join_check_missing` | The host's game mode does not call Flock's check, so nobody is let in. | Base the game mode on a Flock game mode, or call `CheckJoiningPlayer` from its `PreLoginAsync`. |
| `relay_only` | The host's `Relay` is `Always`, and the player reached it another way. | Set `Relay` the same on every player's game. |

In Blueprint, override **Approve Joining Player** on a game mode based on **Flock Game Mode
Base**; its Refused Reason reaches the player's Travel To Session (empty reads as Refused By
The Game). Name players by **Player For Connection** on the session, such as in
**OnPostLogin**.

> [!NOTE]
>
> Run the same Flock Unreal SDK on every player's game. A player on 1.30.0 or earlier sends no
> join token (`no_join_token`), and one on 1.33.0 or earlier cannot come through the relay.

## When the host changes

When the host leaves or its game stops, Flock makes the player seated longest the host and raises
the session's host-changed event on every game (`HostChanged` on Unity, `OnHostChanged` on
Unreal). Neither Netcode for GameObjects nor Unreal's networking can move a running game to a new
host, so each game starts its networking again: the new host hosts, and the others join it. Do the
same when this device's relay stops working (`RelayLost` on Unity, `OnRelayLost` on Unreal).

**Unity**

Shut the netcode down and start it again:

```csharp theme={null}
session.HostChanged += StartAgain;
session.RelayLost += reason => StartAgain();

async void StartAgain()
{
    if (session.HasEnded)
        return;
    NetworkManager.Singleton.Shutdown();
    await session.StartNetcodeAsync(NetworkManager.Singleton);   // waits for the shutdown to finish
}
```

Players are back about 20 seconds after a host leaves, and about a minute after a host's game
crashes (the **Host timeout**). Everything the old host held is gone: its spawned objects, network
variables and scene state. Keep what a new host needs on every player, such as a snapshot of the
game the host sends every few seconds, and rebuild from it. While waiting for a crashed host to be
replaced, players lose the netcode connection first; keep the session and wait for `HostChanged`.

**Unreal**

Call `TravelToSession` again with the same options, from your game instance. A newer travel
ends the one under way as `restarted`, and does this device's part from wherever the game is.

```cpp theme={null}
// Once the session is held.
Session->OnHostChanged.AddDynamic(this, &UMyGameInstance::PlayAgain);
Session->OnRelayLost.AddDynamic(this, &UMyGameInstance::PlayAgainAfterRelayLost);

void UMyGameInstance::PlayAgain()
{
    if (!Session->HasEnded())
    {
        Session->TravelToSession(this, TravelOptions, [](TFlockResult<FFlockTravelToSessionResult> Result)
        {
            // restarted: a newer call took over, and its own answer follows.
        });
    }
}

void UMyGameInstance::PlayAgainAfterRelayLost(const FString& Reason)
{
    PlayAgain();
}
```

| This game was | Now | Travel To Session |
| - | - | - |
| the host | still the host | Keeps its level and its players, opens its relay address again when it was lost, and publishes again. A refused publish, a cancel or a newer call leaves the level listening. |
| the host | a player | Stops listening (its players' connections close), waits for the new host, and joins it. |
| a player | the host | Opens the level it played with the old host listening, unless `HostLevel` names another, and publishes. |
| a player | a player | Waits for the new host in the level it is in, then connects; the engine closes the old connection. |
| a player in the host's game | a player, the same host | Answers `connected` at once and keeps its connection, so a level's Begin Play that calls it can run on every player. |

On a player, **On Relay Lost** is raised too when its host publishes another relay address (the
host's own relay was lost), since the player's relay no longer reaches it. A new host's address is
**On Host Changed**'s alone.

Games hear On Host Changed within about 20 seconds of a host leaving and are back in the new
host's game about 10 seconds later (about 30 seconds in all); a host's game that stops without
leaving is replaced a little over a minute on (the **Host timeout**, then Flock's sweep), its
players back about 80 seconds on. Everything the old
host held is gone: its actors, its game state and its level's state. Keep what a new host needs
on every player, and rebuild from it.

A player's connection to a host that went without a word times out in the engine (a minute by
default), which raises **NetworkError** and takes the game back to its default level. Calling
Travel To Session again from the game instance's NetworkError event, while the session lasts,
takes the player to the session's host, whoever that is by then. The host lets the player's newer
connection in once it has heard nothing from the earlier one for 5 seconds, with the new join token
Travel To Session brings.

In Blueprint, bind the session's **On Host Changed** and **On Relay Lost** in the Game Instance
Blueprint, each to **Flock Travel To Session** with the same Options. The travel it replaces
fires **On Not Joined** with the Outcome **Restarted**, which needs nothing done. The plugin's
multiplayer sample does exactly this:

![Set Session, then Bind Event to On Host Changed, On Relay Lost and On Ended; the bound HostChanged and RelayLost events each call Travel Again](https://mintcdn.com/qwacks/sKCVX05gMuqzHxSa/images/guides/unreal-multiplayer-sample-play.png?fit=max\&auto=format\&n=sKCVX05gMuqzHxSa\&q=85\&s=792db93d3303064888af1fe309d5da21)

The sample's Game Instance holds the session and binds On Host Changed and On Relay Lost to its Travel Again event.

![Two Branches, Flock Travel To Session, On Joined setting the status, On Not Joined comparing the Outcome with Restarted, On Failure leaving the session](https://mintcdn.com/qwacks/sKCVX05gMuqzHxSa/images/guides/unreal-multiplayer-sample-travel-again.png?fit=max\&auto=format\&n=sKCVX05gMuqzHxSa\&q=85\&s=d4b8c6f1750cf95f0eeae1cf5066f9fe)

Travel Again: Flock Travel To Session for the session held. Restarted needs nothing; any other way of not getting in gives the seat up.

## Other networking

Games on Steam, Epic Online Services or their own networking still use Flock's parties,
matchmaking, sessions and join check. They publish and read the connection themselves.

**Unity**

```csharp theme={null}
// The host says how to reach it: a kind of its own and the values it needs, under 4 KB.
await session.PublishConnectionAsync("steam", new Dictionary<string, object> { ["lobby"] = lobbyId.ToString() });

// A player waits for it; null when the time passes or the session ends.
FlockMultiplayerSessionConnection connection = await session.WaitForConnectionAsync(TimeSpan.FromSeconds(60));

// The player proves its seat with a token, good for 60 seconds, sent over your own networking.
FlockMultiplayerSessionJoinToken token = await session.RequestJoinTokenAsync();

// The host checks it and learns who the player is; a bad token throws MultiplayerInvalidJoinToken.
string playerId = await session.VerifyJoinTokenAsync(tokenFromPlayer);
```

`PublishDirectConnectionAsync(port)` publishes this device's own address, and a player's
`connection.FindDirectAddressAsync()` picks the one to use: the host's local address on a shared
network, its public address otherwise. Outside a local network, players reach that address only
through a port forwarded to the host.

**Unreal**

```cpp theme={null}
// The host says how to reach it: a mode of its own (32 characters at most) and the values it needs, under 4 KB.
Session->PublishConnection(TEXT("steam"), FFlockCommandData().Set(TEXT("lobby"), LobbyId), nullptr);

// A player waits for it: the session comes back with HasConnection(), HasEnded(), or neither when the time passes.
Session->WaitForConnection(60.f, [this](TFlockResult<UFlockMultiplayerSession*> Waited)
{
    FString Lobby;
    if (Waited.IsSuccess() && Waited.Value->HasConnection()
        && Waited.Value->GetConnection().Mode.Equals(TEXT("steam"), ESearchCase::CaseSensitive)
        && Waited.Value->GetConnection().Values.TryGetString(TEXT("lobby"), Lobby))
    {
        JoinSteamLobby(Lobby);
    }
});

// The player proves its seat with a token, good for 60 seconds, sent over your own networking.
Session->RequestJoinToken([this](TFlockResult<FFlockMultiplayerSessionJoinToken> Token)
{
    if (Token.IsSuccess()) { SendToHost(Token.Value.Token); }
});

// The host checks it and learns who the player is; a bad token fails with MultiplayerInvalidJoinToken.
Session->VerifyJoinToken(TokenFromPlayer, [this](TFlockResult<FString> PlayerId) { /* PlayerId.Value */ });
```

Travel To Session also ends `not_direct` when the host published a connection of its own,
holding it in the result's `Connection`. `PublishDirectConnection(Port)` publishes this device's
own address, and a player's `FindDirectAddress` picks the one to use: the host's local address
on a shared network, its public address otherwise. Outside a local network, players reach that
address only through a port forwarded to the host.

In Blueprint these are **Flock Publish Connection**, **Flock Wait For Connection** (On
Connection, On Session Ended, On Timed Out, On Failure), **Flock Request Join Token**, **Flock
Verify Join Token**, **Flock Publish Direct Connection** and **Flock Find Direct Address**.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.