# Events reference

Events emitted by the Wavedash SDK

Source: https://docs.wavedash.com/sdk/events

The SDK emits events when things happen — a player joins a lobby, a peer connects, the backend goes offline. Your game listens for these to react in real time.

## Listening for events

<CodeGroup>
```gdscript Godot
WavedashSDK.lobby_joined.connect(_on_lobby_joined)
WavedashSDK.lobby_message.connect(_on_lobby_message)
WavedashSDK.p2p_connection_established.connect(_on_peer_connected)
WavedashSDK.backend_connected.connect(_on_backend_connected)
```

```csharp Unity
Wavedash.SDK.OnLobbyJoined += OnLobbyJoined;
Wavedash.SDK.OnLobbyMessage += OnLobbyMessage;
Wavedash.SDK.OnP2PConnectionEstablished += OnPeerConnected;
Wavedash.SDK.OnBackendConnected += OnBackendConnected;
```

```lua Defold
function init(self)
    wavedash.init({}, function(_, event, payload)
        if event == wavedash.EVENT_LOBBY_JOINED then
            print("Joined lobby", payload.lobbyId)
        elseif event == wavedash.EVENT_LOBBY_MESSAGE then
            print(payload.username .. ": " .. payload.message)
        elseif event == wavedash.EVENT_P2P_CONNECTION_ESTABLISHED then
            print("Peer connected:", payload.username)
        elseif event == wavedash.EVENT_BACKEND_CONNECTED then
            print("Backend connected (attempt " .. payload.connectionCount .. ")")
        end
    end)
end
```

```javascript JavaScript
// Strongly typed payload
// .on() returns an unsubscribe function.
const unsubscribeLobbyJoined = Wavedash.on(Wavedash.Events.LOBBY_JOINED, (payload) => {
  console.log(`Joined lobby ${payload.lobbyId}`);
});
Wavedash.on(Wavedash.Events.LOBBY_MESSAGE, (payload) => {
  console.log(`${payload.username}: ${payload.message}`);
});
Wavedash.on(Wavedash.Events.P2P_CONNECTION_ESTABLISHED, (payload) => {
  console.log(`Peer connected: ${payload.username}`);
});
Wavedash.on(Wavedash.Events.BACKEND_CONNECTED, (payload) => {
  console.log(`Backend connected (attempt ${payload.connectionCount})`);
});

// Later, to detach:
unsubscribeLobbyJoined();                            // via the unsubscribe fn
Wavedash.off(Wavedash.Events.LOBBY_JOINED, handler); // or using .off
```
</CodeGroup>

## Lobby events

| Event | Fires when | Payload |
|-------|-----------|---------|
| `LOBBY_JOINED` | You join or create a lobby | `lobbyId`, `hostId`, `users`, `metadata` |
| `LOBBY_USERS_UPDATED` | A user joins or leaves | `lobbyId`, `userId`, `username`, `userAvatarUrl`, `isHost`, `changeType` ("JOINED" or "LEFT") |
| `LOBBY_MESSAGE` | A chat message arrives | `messageId`, `lobbyId`, `userId`, `username`, `message`, `timestamp` |
| `LOBBY_DATA_UPDATED` | Lobby metadata changes | The full metadata object |
| `LOBBY_KICKED` | You're removed from the lobby | `lobbyId`, `reason` ("KICKED" or "ERROR") |
| `LOBBY_INVITE` | You receive a lobby invite | `notificationId`, `lobbyId`, `sender` (object with `_id`, `username`, `avatarUrl`), `_creationTime` |

### Payload examples

`LOBBY_JOINED` — fires once when you create or join. `users` includes you. `metadata` is whatever the host has set with `setLobbyData()`; it's an empty object on a fresh lobby.

```json
{
  "lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
  "hostId": "k7m1n3p5q7r9s2t4v6w8x0y2z4a6b8c0",
  "users": [
    {
      "userId": "k7m1n3p5q7r9s2t4v6w8x0y2z4a6b8c0",
      "username": "alice",
      "userAvatarUrl": "https://cdn.wavedash.gg/avatars/alice.png",
      "lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
      "isHost": true
    },
    {
      "userId": "n2p4q6r8s0t2v4w6x8y0z2a4b6c8d0e2",
      "username": "bob",
      "lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
      "isHost": false
    }
  ],
  "metadata": {
    "gameMode": "deathmatch",
    "mapName": "arena",
    "round": "2"
  }
}
```

`LOBBY_USERS_UPDATED` — one event per join/leave. Use `changeType` to branch.

```json
{
  "userId": "n2p4q6r8s0t2v4w6x8y0z2a4b6c8d0e2",
  "username": "bob",
  "userAvatarUrl": "https://cdn.wavedash.gg/avatars/bob.png",
  "lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
  "isHost": false,
  "changeType": "JOINED"
}
```

`LOBBY_MESSAGE` — `timestamp` is `Date.now()`-style ms since epoch.

```json
{
  "messageId": "m1n3p5q7r9s1t3v5w7x9y1z3a5b7c9d1",
  "lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
  "userId": "k7m1n3p5q7r9s2t4v6w8x0y2z4a6b8c0",
  "username": "alice",
  "message": "ready when you are",
  "timestamp": 1714296245000
}
```

`LOBBY_DATA_UPDATED` — the entire metadata object, not a diff. Values are strings or numbers.

```json
{
  "gameMode": "deathmatch",
  "mapName": "arena",
  "round": "3",
  "scoreLimit": 10
}
```

`LOBBY_KICKED`:

```json
{
  "lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
  "reason": "KICKED"
}
```

`LOBBY_INVITE` — note the `sender` object uses `_id` and `avatarUrl` (not `userId`/`userAvatarUrl` like the lobby user shape above).

```json
{
  "notificationId": "p3q5r7s9t1v3w5x7y9z1a3b5c7d9e1f3",
  "lobbyId": "j570h2k8p3m7n5q1r4s9t6v2w8x0y3z4",
  "sender": {
    "_id": "k7m1n3p5q7r9s2t4v6w8x0y2z4a6b8c0",
    "username": "alice",
    "avatarUrl": "https://cdn.wavedash.gg/avatars/alice.png"
  },
  "_creationTime": 1714296245000
}
```

## P2P events

| Event | Fires when | Payload |
|-------|-----------|---------|
| `P2P_CONNECTION_ESTABLISHED` | Both data channels to a peer are open and the peer is ready to send/receive | `userId`, `username` |
| `P2P_PEER_RECONNECTING` | A peer's ICE connection failed and the SDK is attempting an ICE restart. Fires on both sides of the link. | `userId`, `username` |
| `P2P_PEER_RECONNECTED` | A peer that was reconnecting is back online. Fires on both sides. | `userId`, `username` |
| `P2P_PEER_DISCONNECTED` | A peer's data channel closed — they left the lobby, their browser tab exited, or the SDK gave up after too many failed ICE restarts | `userId`, `username` |
| `P2P_CONNECTION_FAILED` | A peer connection failed terminally (no TURN credentials, data-channel error, or ICE restart attempts exhausted) | `userId`, `username`, `error` |
| `P2P_PACKET_DROPPED` | The SDK dropped an outgoing or incoming P2P packet (see [Packet drop reasons](#packet-drop-reasons)) | `channel`, `direction` (`"SEND"` or `"RECEIVE"`), `reason`, `droppedCount`, `droppedTotal` |

### Peer connection lifecycle

A healthy peer lifecycle is `P2P_CONNECTION_ESTABLISHED` → (traffic) → `P2P_PEER_DISCONNECTED`. When the network hiccups — a Wi-Fi switch, a brief NAT rebinding — the SDK does an ICE restart and you'll see `P2P_PEER_RECONNECTING` → `P2P_PEER_RECONNECTED` in between. Treat that pair as "this peer is unreachable right now" → "go ahead and resume sending". It's still the same peer, no re-handshake is needed, and you don't need to tear down any per-peer state.

If ICE restarts exceed the SDK's retry budget (3 attempts), the SDK emits `P2P_CONNECTION_FAILED` and then `P2P_PEER_DISCONNECTED` as the channel closes. For broadcast sends, this is the signal to drop the peer from your "reachable" set — see [Multiplayer networking](/multiplayer/networking#broadcast-is-best-effort) for the full pattern.

### Packet drop reasons

The `reason` field on `P2P_PACKET_DROPPED` tells you what remedy, if any, is needed:

| Reason | Direction | Happens when | What to do |
|--------|-----------|--------------|------------|
| `QUEUE_FULL` | `RECEIVE` | The receive-side ring buffer is full | Drain channels more often, throttle send rate, or raise `p2p.maxIncomingMessages` in `init()` |
| `PAYLOAD_TOO_LARGE` | Either | Payload exceeds the configured slot size | Reduce payload or raise `p2p.messageSize` in `init()` |
| `INVALID_PAYLOAD_SIZE` | `SEND` | `payloadSize` was ≤ 0, larger than the buffer, or payload was null | Programming error — fix the call site |
| `INVALID_CHANNEL` | Either | Channel index was outside `[0, 8)` | programming error, SDK version skew or a malicious peer. |
| `MALFORMED` | `RECEIVE` | Incoming wire data was too short to parse a channel header | Almost always SDK version skew; `channel` is `-1` |
| `PEER_NOT_READY` | `SEND` | Target peer's channel wasn't open, or P2P wasn't initialized. **Only emitted for unicast `sendP2PMessage`; broadcasts silently skip unready peers.** | Wait for `P2P_CONNECTION_ESTABLISHED` and watch the reconnect/disconnect events for reachability |

Events are rate-limited per `(direction, channel, reason)` tuple. The first drop on an idle tuple fires immediately; further drops within 500 ms are coalesced into a single event at the end of the window. `droppedCount` is the number of drops in the current event, `droppedTotal` is the cumulative count for that tuple since P2P init.

### Payload examples

The lifecycle events all share the same shape:

```json
{
  "userId": "n2p4q6r8s0t2v4w6x8y0z2a4b6c8d0e2",
  "username": "bob"
}
```

`P2P_CONNECTION_FAILED` adds an `error` string:

```json
{
  "userId": "n2p4q6r8s0t2v4w6x8y0z2a4b6c8d0e2",
  "username": "bob",
  "error": "ICE restart attempts exhausted"
}
```

`P2P_PACKET_DROPPED`:

```json
{
  "channel": 0,
  "direction": "SEND",
  "reason": "QUEUE_FULL",
  "droppedCount": 5,
  "droppedTotal": 47
}
```

## Stats events

| Event | Fires when | Payload |
|-------|-----------|---------|
| `STATS_STORED` | Stats are persisted to the server | `success`, `message` (optional) |

### Payload examples

```json
{ "success": true }
```

On failure, `message` describes what went wrong:

```json
{ "success": false, "message": "Network error while persisting stats" }
```

## Backend events

| Event | Fires when | Payload |
|-------|-----------|---------|
| `BACKEND_CONNECTED` | Connected to Wavedash | `isConnected`, `hasEverConnected`, `connectionCount`, `connectionRetries` |
| `BACKEND_DISCONNECTED` | Lost connection | Same as above |
| `BACKEND_RECONNECTING` | Attempting to reconnect | Same as above |

### Payload examples

All three events share this shape. `connectionCount` increments on each successful connect; `connectionRetries` counts failed attempts in the current outage.

```json
{
  "isConnected": true,
  "hasEverConnected": true,
  "connectionCount": 2,
  "connectionRetries": 0
}
```

## Fullscreen events

| Event | Fires when | Payload |
|-------|-----------|---------|
| `FULLSCREEN_CHANGED` | Host page enters or exits fullscreen | `isFullscreen` (boolean) |

See [Fullscreen](/sdk/fullscreen) for the request/toggle API and overlay-aware integration.

## Mute events

| Event | Fires when | Payload |
|-------|-----------|---------|
| `MUTE_CHANGED` | Host page mutes or unmutes the game | `isMuted` (boolean) |

Wavedash already handles muting and unmuting audio for you. You only need to listen for `MUTE_CHANGED` if you want to keep an in-game UI element (ie a speaker icon) in sync with the host page's mute state.

```javascript
Wavedash.on(Wavedash.Events.MUTE_CHANGED, (payload) => {
  speakerIcon.setMuted(payload.isMuted);
});
```

```json
{ "isMuted": true }
```

See [Audio](/sdk/audio) for the request/toggle API and the player-mute permission rules.

## Per-language names

Each event is exposed as a JS constant, a Unity C# event, and a Godot signal. In Defold, compare the `event` argument from `wavedash.init(callback)` against the corresponding `wavedash.EVENT_*` constant. The payload fields are the same across bindings.

| Event | JavaScript constant | Unity event | Godot signal |
|-------|---------------------|-------------|--------------|
| Lobby joined | `Wavedash.Events.LOBBY_JOINED` | `OnLobbyJoined` | `lobby_joined` |
| Lobby users updated | `Wavedash.Events.LOBBY_USERS_UPDATED` | `OnLobbyUsersUpdated` | `lobby_users_updated` |
| Lobby message | `Wavedash.Events.LOBBY_MESSAGE` | `OnLobbyMessage` | `lobby_message` |
| Lobby data updated | `Wavedash.Events.LOBBY_DATA_UPDATED` | `OnLobbyDataUpdated` | `lobby_data_updated` |
| Lobby kicked | `Wavedash.Events.LOBBY_KICKED` | `OnLobbyKicked` | `lobby_kicked` |
| Lobby invite | `Wavedash.Events.LOBBY_INVITE` | `OnLobbyInvite` | `lobby_invite` |
| P2P connection established | `Wavedash.Events.P2P_CONNECTION_ESTABLISHED` | `OnP2PConnectionEstablished` | `p2p_connection_established` |
| P2P peer reconnecting | `Wavedash.Events.P2P_PEER_RECONNECTING` | `OnP2PPeerReconnecting` | `p2p_peer_reconnecting` |
| P2P peer reconnected | `Wavedash.Events.P2P_PEER_RECONNECTED` | `OnP2PPeerReconnected` | `p2p_peer_reconnected` |
| P2P peer disconnected | `Wavedash.Events.P2P_PEER_DISCONNECTED` | `OnP2PPeerDisconnected` | `p2p_peer_disconnected` |
| P2P connection failed | `Wavedash.Events.P2P_CONNECTION_FAILED` | `OnP2PConnectionFailed` | `p2p_connection_failed` |
| P2P packet dropped | `Wavedash.Events.P2P_PACKET_DROPPED` | `OnP2PPacketDropped` | `p2p_packet_dropped` |
| Stats stored | `Wavedash.Events.STATS_STORED` | `OnStatsStored` | `stats_stored` |
| Backend connected | `Wavedash.Events.BACKEND_CONNECTED` | `OnBackendConnected` | `backend_connected` |
| Backend disconnected | `Wavedash.Events.BACKEND_DISCONNECTED` | `OnBackendDisconnected` | `backend_disconnected` |
| Backend reconnecting | `Wavedash.Events.BACKEND_RECONNECTING` | `OnBackendReconnecting` | `backend_reconnecting` |
| Fullscreen changed | `Wavedash.Events.FULLSCREEN_CHANGED` | `OnFullscreenChanged` | `fullscreen_changed` |
| Mute changed | `Wavedash.Events.MUTE_CHANGED` | `OnMuteChanged` | `mute_changed` |

Godot additionally emits per-call signals (`got_leaderboard`, `got_leaderboard_entries`, `posted_leaderboard_score`, `ugc_item_created`, `ugc_item_updated`, `ugc_item_downloaded`, `remote_file_uploaded`, `remote_file_downloaded`, `remote_directory_downloaded`, `got_remote_directory_listing`, `got_lobbies`, `sent_lobby_invite`, `got_lobby_invite_link`, `lobby_created`, `lobby_left`, `got_friends`, `got_user_jwt`, `user_avatar_loaded`). These fire with the response of the matching async call — use them if you prefer signal-based flow over `await`.

## Deferring events

If your game needs time to load before handling events, pass `deferEvents: true` to `init()`. The SDK queues all events until you call `readyForEvents()`.

<CodeGroup>
```gdscript Godot
func _ready():
    WavedashSDK.init({"deferEvents": true})

    # set up listeners, load assets, etc.

    WavedashSDK.ready_for_events()  # flush the queue
```

```csharp Unity
using System.Collections.Generic;

void Awake()
{
    Wavedash.SDK.Init(new Dictionary<string, object>
    {
        { "deferEvents", true }
    });

    // set up listeners, load assets, etc.

    Wavedash.SDK.ReadyForEvents(); // flush the queue
}
```

```lua Defold
function init(self)
    wavedash.init({ deferEvents = true }, function(_, event, payload)
        if event == wavedash.EVENT_LOBBY_JOINED then
            print("Joined lobby", payload.lobbyId)
        end
    end)

    -- set up listeners, load assets, etc.

    wavedash.ready_for_events() -- flush the queue
end
```

```javascript JavaScript
Wavedash.init({ deferEvents: true });

// set up listeners, load assets, etc.

Wavedash.readyForEvents(); // flush the queue
```
</CodeGroup>
