# ADR-0001: Relay Transport Integration Strategy

**Status:** Accepted  
**Date:** 2026-06-23  
**Deciders:** Mahmoud Aglan  
**Engine:** Unity 6 + FishNet v4 + Synapse UDP  

---

## Context

All game traffic between players in IStalk flows through a WebSocket relay server (`wss://istalk-relay.caprover.al-arcade.com`). There is no direct peer-to-peer connection and no dedicated game server. FishNet v4 uses Synapse as its transport, which normally operates over raw UDP sockets.

The problem: Synapse expects UDP endpoints. The relay speaks WebSocket binary frames. These are incompatible without a bridge layer.

Three options were evaluated in SYS-33 (FishNet Replication Layer GDD):

- **Option A:** Use Synapse's WebSocket mode (if it exists in this version) — point `SynapseConfig._clientAddress` at the relay URL.
- **Option B:** Write a custom `RelayTransport : Transport` class that implements FishNet's abstract `Transport` interface, routing all FishNet packets through `RelayClient.SendGamePacket()`.
- **Option C:** Use Synapse's NAT traversal (`NatTraversalConfig`, `FullConeNatConfig`) for direct UDP between players; relay handles lobby only.

---

## Decision

**Option B — Custom `RelayTransport : Transport` class.**

If during implementation Synapse is found to have a working WebSocket mode that can target the relay URL directly, switch to Option A as a simplification. Option C is deferred to post-MVP as a performance optimization.

---

## Rationale

| Criterion | Option A | Option B (chosen) | Option C |
|-----------|----------|-------------------|----------|
| Certainty of existence | Unknown — Synapse WebSocket mode not confirmed in project's Synapse version | Guaranteed — FishNet `Transport` is a stable abstract class | Guaranteed |
| Relay compatibility | Would need relay to speak Synapse's exact WebSocket subprotocol | Full control — we format packets to match relay's binary protocol exactly | N/A (bypasses relay) |
| Sender ID handling | Unknown — would Synapse strip the 2-byte prepended sender ID? | Explicit — `RelayTransport.HandleReceive()` strips 2-byte header before passing to FishNet | N/A |
| NAT compatibility | N/A (uses relay) | N/A (uses relay) | Fails on symmetric NAT (~15-30% of consumer networks) |
| Implementation effort | Low if it works, unknown if not | Medium — ~200 lines, well-defined interface | Low, but unreliable |
| Debugging control | Opaque (Synapse internals) | Full visibility into every packet | Full visibility |

Option B wins because:
1. It is **guaranteed to work** regardless of Synapse version — the `Transport` abstract class is FishNet's stable extensibility point.
2. We get **explicit control** over the 2-byte sender ID header stripping (per SYS-20 Section 4.2).
3. The relay's binary protocol is dead simple (first byte = target, payload = verbatim FishNet data), so routing it through a custom Transport is straightforward.
4. If Option A turns out to work later, migrating is trivial (swap Transport reference on NetworkManager).

---

## Implementation Specification

### Class Structure

```csharp
public class RelayTransport : Transport
{
    private RelayClient _relayClient;
    private bool _isServer;
    private bool _isClient;

    // FishNet Transport interface
    public override void Initialize(NetworkManager networkManager, int transportIndex) { }
    public override bool StartConnection(bool server) { }
    public override bool StopConnection(bool server) { }
    public override void SendToServer(byte channelId, ArraySegment<byte> segment) { }
    public override void SendToClient(byte channelId, int connectionId, ArraySegment<byte> segment) { }
    public override void IterateIncoming(bool server) { }
    public override void IterateOutgoing(bool server) { }
}
```

### Packet Flow — Client → Server (Runner sends input)

```
RunnerController [ServerRpc]
    → FishNet serializes → calls RelayTransport.SendToServer(channel, payload)
    → RelayTransport prepends target byte 0x01 (toHost)
    → RelayClient.SendGamePacket(0x01, payload)
    → WebSocket binary frame → relay server
    → Relay reads byte[0] == 0x01, forwards to host
    → Host's RelayClient.OnGamePacket fires
    → RelayTransport.HandleReceive(): strip 2-byte sender ID prefix
    → Feed remaining bytes to FishNet server ingress
```

### Packet Flow — Server → Client (Eye broadcasts state)

```
GameManager SyncVar change
    → FishNet serializes → calls RelayTransport.SendToClient(channel, connId, payload)
    → RelayTransport maps FishNet connectionId → relay playerId
    → Prepends target byte (0x02-0xFE for specific client, 0x00 for broadcast)
    → RelayClient.SendGamePacket(targetByte, payload)
    → WebSocket binary frame → relay server
    → Relay reads byte[0], routes to target(s), prepends 2-byte sender ID
    → Each client's RelayClient.OnGamePacket fires
    → RelayTransport.HandleReceive(): strip 2-byte sender ID
    → Feed to FishNet client ingress
```

### Sender ID Stripping

```csharp
private void HandleReceive(byte[] rawPacket)
{
    // Relay prepends 2-byte uint16LE sender playerId
    if (rawPacket.Length < 3) return; // minimum: 2-byte header + 1 byte data

    ushort senderRelayId = BitConverter.ToUInt16(rawPacket, 0);
    var fishNetPayload = new ArraySegment<byte>(rawPacket, 2, rawPacket.Length - 2);

    if (_isServer)
    {
        int connectionId = _relayClient.RelayIdToFishNetId(senderRelayId);
        HandleServerReceive(fishNetPayload, connectionId);
    }
    else
    {
        HandleClientReceive(fishNetPayload);
    }
}
```

### Connection ID Mapping

Stored in `RelayClient` (per SYS-21). Established when each client connects and self-reports via `ServerRpc`:

```csharp
// On server, when a new FishNet client connects:
// Client sends: [ServerRpc] void ServerRegisterRelayId(int relayPlayerId)
// Server stores: _relayToFishNet[relayPlayerId] = sender.ClientId
// Server stores: _fishNetToRelay[sender.ClientId] = relayPlayerId
```

---

## Consequences

**Positive:**
- Full control over packet routing — easy to debug, log, and profile.
- Relay protocol compatibility is explicit and testable in isolation.
- FishNet's channel system (reliable/unreliable) maps cleanly — WebSocket binary frames are inherently reliable (TCP), so both FishNet channels are delivered reliably. This is acceptable for MVP (relay already adds latency; TCP reliability doesn't significantly worsen it for a 2-5 player game).
- Migration path to Option A or C is clean — swap the Transport reference on `NetworkManager`.

**Negative:**
- All game traffic is TCP (WebSocket over TLS). No true UDP unreliable channel. For a 2-5 player game with 30Hz tick rate, this is acceptable but not ideal.
- ~200 lines of custom transport code to write and maintain.
- Head-of-line blocking possible under packet loss — mitigated by the relay being geographically close (same region as players for MVP testing).

**Risks:**
- FishNet v4 `Transport` abstract class signature must be verified against the actual FishNet v4 package in the project. Method names and signatures are from training data — cross-reference with the imported package before implementation. ⚡ MEDIUM RISK.

---

## GDD Requirements Addressed

- TR-NET-003: Binary game packet with 1-byte target routing
- TR-NET-004: 2-byte uint16LE sender ID prepended by relay
- TR-NET-007: Transport-relay bridge strategy (RESOLVED)
- TR-NET-008: RelayPlayerId → FishNetConnectionId mapping

---

## Alternatives Considered

See Options A and C above. Option A remains a valid simplification if Synapse WebSocket mode is discovered. Option C (NAT punch) is a post-MVP performance optimization that eliminates the relay from the game-phase data path.

---

## References

- `design/gdd/sys-20-relay-transport.md` — Binary packet protocol specification
- `design/gdd/sys-33-fishnet-replication.md` — Section 3: Transport-Relay Integration
- `design/gdd/sys-21-lobby-matchmaking.md` — Connection ID mapping
