> For the complete documentation index, see [llms.txt](https://kinematicsoup.gitbook.io/reactor/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kinematicsoup.gitbook.io/reactor/tutorials/reconnecting.md).

# Handling Disconnects and Reconnecting

## Summary

Connections drop. A player walks into a lift, a laptop sleeps, a router reboots. Reactor tells you when it happens and then waits for you to decide what to do, because only your game knows whether a dropped player should be held for thirty seconds or removed at once.

This tutorial covers detecting a disconnect, reconnecting with sensible backoff, recognising a player who comes back, and cleaning up correctly on the server when one does not.

### Requirements

* [Reactor Requirements](/reactor/overview.md#requirements)
* Completion of [Running Rooms Locally and Online](/reactor/tutorials/basics.md)
* [Authentication](/reactor/tutorials/authentication.md) if you want returning players recognised

## What Reactor Does and Does Not Do

Reactor detects the drop, raises `OnDisconnect`, and destroys the player's server side player object. It does not retry, and it does not preserve anything for you.

The consequence that surprises people is on the server. A reconnecting player is a **new player**, with a new player id, not a resumed session. Reconnecting a client twice in a row produces this:

```
CONNECT #1  status=SUCCESS  localPlayerId=3
DISCONNECT  status=ABORTED
CONNECT #2  status=SUCCESS  localPlayerId=4
```

The same person on the same machine came back as player 4. Any state you keyed on player id is gone, and any entity they owned has already been dealt with under the rules below. Preserving a session is something you build, and the rest of this page is how.

## Detecting a Disconnect

`ksConnect` exposes `OnConnect`, `OnDisconnect` and `OnGetRooms` as `UnityEvent` fields, so you can wire them in the inspector or in code. The disconnect event carries the room and a status.

```c#
using System.Collections;
using UnityEngine;
using KS.Reactor.Client;
using KS.Reactor.Client.Unity;

public class ceReconnect : MonoBehaviour
{
    [SerializeField] private int m_maxAttempts = 5;

    private ksConnect m_connect;
    private bool m_leaving;
    private int m_attempt;

    private void Awake()
    {
        m_connect = GetComponent<ksConnect>();
        m_connect.OnConnect.AddListener(OnConnect);
        m_connect.OnDisconnect.AddListener(OnDisconnect);
    }

    /// <summary>Call this when the player chooses to quit, before disconnecting.</summary>
    public void Leave()
    {
        m_leaving = true;
        m_connect.Disconnect(false);
    }

    private void OnConnect(ksConnect.ConnectEvent e)
    {
        if (e.Status == ksBaseRoom.ConnectStatus.SUCCESS)
        {
            m_attempt = 0;
            return;
        }
        // A failed attempt is also a reason to retry.
        StartCoroutine(RetryAfterBackoff());
    }

    private void OnDisconnect(ksConnect.DisconnectEvent e)
    {
        // A disconnect the player asked for must not trigger a reconnect. The status cannot
        // tell you which is which, so track the intent yourself. See below.
        if (m_leaving)
        {
            return;
        }
        StartCoroutine(RetryAfterBackoff());
    }

    private IEnumerator RetryAfterBackoff()
    {
        if (m_attempt >= m_maxAttempts)
        {
            Debug.LogWarning("Giving up after " + m_attempt + " attempts.");
            yield break;
        }
        // Back off so a server that is down does not get hammered by every client at once.
        float delay = Mathf.Min(30f, Mathf.Pow(2f, m_attempt));
        m_attempt++;
        yield return new WaitForSeconds(delay);
        m_connect.BeginConnect();
    }
}
```

### The status will not tell you whether the player meant it

This is the trap. Calling `Disconnect` yourself raises `OnDisconnect` with status **`ABORTED`**, and so do several genuine failures. There is no status that means "the local side asked for this".

A reconnect handler that keys off the status alone will therefore reconnect a player who just pressed Quit. Set a flag before you disconnect deliberately, as `m_leaving` does above, and check it first.

The full set of statuses is `SUCCESS`, `TIMEOUT`, `ABORTED`, `REFUSED`, `RESET`, `UNSUPPORTED_PROTOCOL`, `ROOM_INITIALIZE_ERROR`, `INVALID_ADDRESS`, `AUTH_ERR_USER_DEFINED`, `AUTH_ERR_INVALID_PROTOCOL`, `AUTH_ERR_CONNECTION_LIMIT`, `AUTH_ERR_TIMEOUT`, `AUTH_ERR_UNKNOWN`, `GET_ROOMS_ERROR` and `UNKNOWN_ERROR`.

Worth separating: the `AUTH_ERR_*` group and `REFUSED` usually mean retrying will fail the same way, so surface them to the player instead of looping. `TIMEOUT` and `RESET` are the ones worth retrying.

> *`ksConnect.Room` is not a reliable handle on the connected room. It can be null while the room is fully connected, so a reconnect routine that reads it will stall waiting for a value that never arrives. Use the `Room` carried on the `ConnectEvent` and `DisconnectEvent` arguments, and keep your own reference.*

## Watching the Connection State

For UI, the room raises `OnStateChange` as it moves through `NOT_CONNECTED`, `CONNECTING`, `HANDSHAKE`, `VALIDATING_MODEL`, `AUTHENTICATING`, `CONNECTED`, `DISCONNECTING`, `DISCONNECTED` and `ABORTING`.

```c#
room.OnStateChange += (ksBaseRoom r, ksConnectionStates state) =>
{
    m_statusLabel.text = state.ToString();
};
```

`IsConnecting`, `IsConnected` and `IsDisconnecting` cover the common checks without matching on the enum.

## What Happens on the Server

When a client drops, the server destroys its player object and raises `Room.OnPlayerLeave`, and `ksIServerPlayer.OnLeave` on the player itself. Entities the player owned are handled by `DestroyOnOwnerDisconnect` on `ksEntityComponent`: destroyed when it is checked, left in place with no owner when it is not.

Two things about `OnPlayerLeave` catch people out.

**Ownership is already cleared.** By the time your handler runs, `Entity.Owner` is null. Cleanup written as `if (Entity.Owner == player)` never runs, and fails silently rather than erroring. Clear your own state unconditionally instead.

**Update handlers can still run against a destroyed entity.** A script that writes properties every frame will write one more time after its entity is gone, producing:

```
[WARNING; Reactor.ServerEntity]  Cannot set property 0 on destroyed entity 10.
```

Reactor refuses the write and continues, so it is noise rather than breakage, but it fires on every disconnect. Return early on `Entity.IsDestroyed` in any handler that writes properties.

## Recognising a Returning Player

Player id is assigned per connection and changes on reconnect, so it cannot identify a person. The identity that survives is `ksIServerPlayer.AuthenticatedId`, which is populated by authentication.

A project with no authentication has no stable identity to key on, and cannot tell a returning player from a new one. If you want sessions to survive a drop, authentication is a prerequisite rather than an optional extra. See [Authentication](/reactor/tutorials/authentication.md).

With it, the pattern is to park state on leave and adopt it on join:

```c#
public class srSessions : ksServerRoomScript
{
    private class Parked
    {
        public ksVector3 Position;
        public int Score;
        public float ExpiresAt;
    }

    // Keyed by AuthenticatedId, because player ids do not survive a reconnect.
    private Dictionary<string, Parked> m_parked = new Dictionary<string, Parked>();

    private const float HoldSeconds = 60f;

    public override void Initialize()
    {
        Room.OnPlayerJoin += PlayerJoin;
        Room.OnPlayerLeave += PlayerLeave;
        Room.OnUpdate[0] += Update;
    }

    public override void Detached()
    {
        Room.OnPlayerJoin -= PlayerJoin;
        Room.OnPlayerLeave -= PlayerLeave;
        Room.OnUpdate[0] -= Update;
    }

    private void PlayerLeave(ksIServerPlayer player)
    {
        // Do not gate this on Entity.Owner == player. Ownership is already gone.
        if (string.IsNullOrEmpty(player.AuthenticatedId))
        {
            return;
        }
        ksIServerEntity avatar = FindAvatar(player);
        if (avatar == null)
        {
            return;
        }
        m_parked[player.AuthenticatedId] = new Parked()
        {
            Position = avatar.Transform.Position,
            Score = player.Properties[PlayerProp.SCORE],
            ExpiresAt = (float)Room.Time.Time + HoldSeconds
        };
        avatar.Destroy();
    }

    private void PlayerJoin(ksIServerPlayer player)
    {
        Parked parked;
        if (!string.IsNullOrEmpty(player.AuthenticatedId)
            && m_parked.TryGetValue(player.AuthenticatedId, out parked))
        {
            m_parked.Remove(player.AuthenticatedId);
            SpawnAvatar(player, parked.Position);
            player.Properties[PlayerProp.SCORE] = parked.Score;
            return;
        }
        SpawnAvatar(player, RandomSpawn());
    }

    // Parked state must expire, or a room that runs for days accumulates every player
    // who ever left.
    private void Update()
    {
        if (m_parked.Count == 0)
        {
            return;
        }
        float now = (float)Room.Time.Time;
        List<string> expired = null;
        foreach (KeyValuePair<string, Parked> pair in m_parked)
        {
            if (pair.Value.ExpiresAt <= now)
            {
                if (expired == null)
                {
                    expired = new List<string>();
                }
                expired.Add(pair.Key);
            }
        }
        if (expired != null)
        {
            foreach (string key in expired)
            {
                m_parked.Remove(key);
            }
        }
    }
}
```

## Deciding What to Preserve

Parking state and destroying the entity, as above, is the safer default. It frees the physics and sync cost immediately, and it cannot leave an unowned body standing in the world being shot at.

Keeping the entity alive instead, by unchecking `Destroy On Owner Disconnect`, suits games where the body should remain, such as a vehicle a player was driving or a character that should stay vulnerable while they scramble to reconnect. If you do that, decide explicitly what happens when the hold expires and nobody has come back, or you will accumulate abandoned entities.

Either way, set the hold to something you can defend. Long holds keep a slot occupied against players who are trying to join.

> *`Player.IsIdle` and `IdleTime` report a connection that is still open but has gone quiet, which is a different condition from a disconnect and often the earlier signal. See the Idle Connections example.*

## Testing It

A local server will not drop you on its own, so force the cases:

* Call `Disconnect` on the room to test the deliberate path and confirm your intent flag stops the reconnect loop.
* Stop the local server while a client is connected to test an unexpected drop, then start it again and confirm the client comes back.
* Watch `KinematicSoup/Reactor/logs` while you do it. The server records `Client(id = N, address = ...) disconnected: 2-End of file` followed by the new connection and its new id, which is the clearest confirmation that the returning player is a different player as far as the server is concerned.

## Common Problems

| Symptom                                                         | Cause                                                                                                                                                     |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Quitting reconnects the player straight back in                 | The handler treated `ABORTED` as a failure. A deliberate disconnect reports the same status as several real failures, so track intent with your own flag. |
| Reconnect routine hangs forever, never retrying                 | It is waiting on `ksConnect.Room`, which can be null while connected. Use the room from the connect and disconnect event arguments.                       |
| A returning player is treated as new                            | Player id changes on reconnect. Key on `AuthenticatedId`, which requires authentication.                                                                  |
| Disconnect cleanup never runs                                   | It was gated on `Entity.Owner == player`. Ownership is already cleared when `OnPlayerLeave` runs.                                                         |
| "Cannot set property N on destroyed entity" on every disconnect | An update handler wrote a property after its entity was destroyed. Return early on `Entity.IsDestroyed`.                                                  |
| Reconnect storms after a server restart                         | Every client retried on the same schedule. Back off exponentially and cap the attempts.                                                                   |
| Memory grows in long lived rooms                                | Parked session state has no expiry.                                                                                                                       |

## Where to Go Next

* [Authentication](/reactor/tutorials/authentication.md): where `AuthenticatedId` comes from.
* [Client Connection Process](/reactor/architecture/connection-process.md): connect modes, protocols and the full status list.
* [Idle Connections](/reactor/examples/idle_connections.md): connections that are open but silent.
* [Shared Entities and Ownership Transfer](/reactor/tutorials/shared_entities.md): what happens to a shared object when its driver drops.
