# Recipes

> End-to-end: port the ECS racing sample onto the FirstByte transport, and stand up a headless dedicated server with matchmaking.

This page has longer, worked examples. They combine the install, the transport
swap, lane selection, and the lifecycle callbacks into programs that you can
run. For the per-method reference, see
[SDK methods](/modalities/netcode/sdk-methods). For short snippets, see the
[Cookbook](/modalities/netcode/cookbook).

## Recipe 1 — Port the ECS racing sample onto FirstByte

Unity's
[ECS-Network-Racing-Sample](https://github.com/Unity-Technologies/ECS-Network-Racing-Sample)
is a DOTS / Netcode for Entities project. The transport swap is the fastest
way to check that an end-to-end match runs over the FirstByte relay mesh.

  1. **Add the package**
Open the sample in Unity. Add the transport via **Package Manager → Add
    package from git URL**:

    ```text
    https://github.com/clutchcall/core-sdk.git?path=unity/com.clutchcall.transport
    ```
  2. **Swap the transport on the NetworkManager**
Find the prefab that holds the `NetworkManager`. Remove `UnityTransport`.
    Add `ClutchCallTransport`. Then point Netcode at it:

    ```csharp
    var nm = manager.GetComponent<NetworkManager>();
    nm.NetworkConfig.NetworkTransport = nm.GetComponent<ClutchCallTransport>();
    ```
  3. **Configure relay + token**
Drive the inspector fields from a small bootstrap. Then the host and the
    clients agree on the room:

    ```csharp
    var t = nm.GetComponent<ClutchCallTransport>();
    t.RelayHost = "relay.firstbyte.fast";
    t.RoomId    = "race-01";
    t.Token     = await FetchRoomTokenAsync("race-01", localPlayerId);
    ```
  4. **Assign host vs client roles**
Keep the sample's own "Go in game" flow. The host opens the room's
    authority namespace. Each client announces a discovery namespace and joins
    the room:

    ```csharp
    if (isHost) nm.StartHost();
    else        nm.StartClient();
    ```
  5. **Build players (not the editor)**
Editor batch mode does not drive Netcode for Entities. Build a **player**
    for each role. Start the players. The `[Server]` / `[Client]` connection
    lifecycle completes. Netcode assigns `NetworkId`. The cars sync.

What flows where:

- **Car transforms / physics ticks** use the **unreliable** lane (QUIC
  datagrams). This traffic is high frequency and latest-wins, with no
  head-of-line blocking.
- **Spawns, despawns, and race-start / finish RPCs** use the **reliable** lane
  (MoQT subgroup streams). Delivery is ordered and guaranteed.

Over a full match, tens of thousands of datagrams flow through the relay. You
can check link health from the transport:

```csharp
foreach (var id in nm.ConnectedClientsIds)
    Debug.Log($"peer {id}: rtt={nm.GetComponent<ClutchCallTransport>().GetCurrentRtt(id):F0}ms");
```

> **NOTE:**
> If a client does not appear in the host roster, check that both built players
> share the **same `RoomId`** and a valid token. The discovery namespace
> announcement drives roster membership. If the rooms do not match, the host
> never sees the announce.

## Recipe 2 — Headless dedicated server with matchmaking

This is a common production shape. A **dedicated server** player runs the
authority for a room. Clients get a room-scoped token from your control-plane
API before they connect. The server has no display and starts on launch.

### Server bootstrap

```csharp
using Unity.Netcode;
using FirstByte.Transport;
using UnityEngine;

public class DedicatedServer : MonoBehaviour {
    [SerializeField] NetworkManager nm;

    async void Start() {
        if (!Application.isBatchMode) return; // server build only

        var roomId = Args.Get("room", "lobby-1");
        var t = nm.GetComponent<ClutchCallTransport>();
        t.RelayHost = "relay.firstbyte.fast";
        t.RoomId    = roomId;
        t.Token     = await MintServerTokenAsync(roomId);

        nm.OnClientConnectedCallback  += id => Debug.Log($"join  {id} ({nm.ConnectedClientsIds.Count} in room)");
        nm.OnClientDisconnectCallback += id => Debug.Log($"leave {id}");
        nm.OnTransportFailure         += () => { Debug.LogError("relay session failed"); Application.Quit(1); };

        nm.StartServer();
        Debug.Log($"authority up for room {roomId}");
    }
}
```

### Client matchmaking + join

The client asks the control-plane API for a room and a token. It configures the
transport. Then it connects:

```csharp
public class Matchmaker : MonoBehaviour {
    [SerializeField] NetworkManager nm;

    public async Task JoinAsync(string playerId) {
        // 1. control-plane API hands back a room + short-lived token
        var match = await Api.PostAsync<MatchResp>(
            "https://api.firstbyte.fast/matchmake",
            new { player = playerId });

        // 2. point the transport at the assigned room
        var t = nm.GetComponent<ClutchCallTransport>();
        t.RelayHost = match.RelayHost;     // nearest POP
        t.RoomId    = match.RoomId;
        t.Token     = match.Token;         // room-scoped, short-lived

        // 3. join — datagram lane carries inputs, reliable carries spawns
        nm.OnTransportFailure += () => SceneManager.LoadScene("Lobby");
        nm.StartClient();
    }
}
```

### Why this shape works

  **No self-hosted relay**

Both the dedicated server and the clients connect **outbound** to the
    nearest relay POP. There is no inbound port to open and no relay
    infrastructure to run. The mesh fans state and input between regions.

  **One QUIC connection per process**

The server holds a single QUIC connection for the authority. Each client
    holds one connection for its session. Reliable and unreliable lanes
    multiplex over it. There is no second socket and no separate reliability
    layer to tune.

  **Tokens are room-scoped and short-lived**

The control-plane API mints a token bound to a room and a short TTL. Thus a
    leaked client token cannot join arbitrary matches. The server mints its own
    authority token the same way.

## Recipe 3 — Lane discipline for a fast-paced shooter

When you control the gameplay code, route traffic onto the correct lane
explicitly. The rule: **send latest-wins, high-frequency traffic on the
unreliable lane. Send anything that you cannot drop on the reliable lane.**

```csharp
public class PlayerSync : NetworkBehaviour {
    // 60 Hz transform updates — drop-safe, latest-wins → unreliable datagram
    [Rpc(SendTo.Server, Delivery = RpcDelivery.Unreliable)]
    void MoveRpc(Vector3 pos, Quaternion rot, ulong tick) =>
        server.IntegrateMove(OwnerClientId, pos, rot, tick);

    // hit registration — must arrive, must be ordered → reliable subgroup stream
    [Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)]
    void FireRpc(ulong targetTick, Vector3 origin, Vector3 dir) =>
        server.ResolveShot(OwnerClientId, targetTick, origin, dir);

    void Update() {
        if (!IsOwner) return;
        // guard the datagram budget for the move payload
        if (transform.HasMovedSince(lastTick))
            MoveRpc(transform.position, transform.rotation, NetworkTickSystem.Tick);
    }
}
```

> **WARNING:**
> Keep unreliable payloads under `MaxPayloadSize()`. The transport **rejects** a
> QUIC datagram larger than the negotiated max. It does **not fragment** it. Thus
> an oversized snapshot silently fails to send. It does not split across packets.
> For large authoritative state, send a reliable baseline and unreliable deltas.

## Related

  - **[Details](/modalities/netcode/details)** — This page covers the wire model, the lanes, and the architecture.
  - **[Games modality](/modalities/games/details)** — This is the pure-MoQT face of the same wire model, for non-Unity engines.
