Party and players

Plot.party is the latest immutable view of everyone in the game session:

import Plot from '@plot.game/sdk'

await Plot.ready()

const party = Plot.party.get()
console.log(`${party.self.displayName} is player ${party.self.playerId}`)
console.log(`${party.host.displayName} is hosting`)

get() is synchronous after readiness. During a temporary disconnection it keeps returning the latest cached snapshot, which may be stale.

The party snapshot

PartySnapshot contains:

PropertyMeaning
revisionIncreases when the party changes.
playersEveryone added during this session, in stable player-ID order.
hostThe host, always players[0].
selfThe player represented by this game iframe.

A captured snapshot never changes. Call get() again for the latest one or use subscribe() to render updates. Plot does not expose its platform party ID to games; use each player's session-scoped playerId.

Each PartyPlayer is also an immutable plain value:

PropertyMeaning
playerIdStable numeric slot for this game session.
statejoining, connected, disconnected, or left.
displayNameCurrent Plot display name.
colorCurrent color as an exact #RRGGBB CSS value.
iconCurrent built-in icon name.

Names, colors, and icons can change and are not unique identities. Use playerId for UI keys, maps, Player Data lookups, and channel recipients. The exported PartyPlayerState and PlayerIcon values avoid string literals in game code.

Get the party join URL

Use Plot.party.getJoinUrl() when your game wants to render its own invite link or QR code:

const joinUrl = await Plot.party.getJoinUrl()

inviteLink.href = joinUrl
inviteLink.textContent = 'Join this party'
renderJoinQrCode(joinUrl)

Call it after Plot.ready() resolves. It returns the current authoritative, absolute party URL from the trusted Plot host; it is not a field in the cached Party snapshot. Each call asks Plot again, so do not construct a URL from player data or an assumed join-code format. Production URLs use HTTPS. HTTP is accepted only for loopback development.

Retrieving the URL is read-only and does not require a player gesture. The URL contains no embedded credentials and does not bypass Plot's normal join and admission checks. Browser features you use afterward, such as native sharing, may impose their own gesture requirements.

A call before readiness rejects with PlotNotReadyError; a call during a disconnection rejects with not-connected. service-unavailable, communication-failed, internal-error, and operation-timeout are also possible. Because retrieval has no remote side effect, you may deliberately try again when the runtime is connected and the link is still useful. See Error handling for the common error pattern.

Stable IDs and player states

Player IDs are zero-based array indexes, assigned in order and never reused for another person during the session. Player 0 is always the host.

import { PartyPlayerState } from '@plot.game/sdk'

const connectedPlayers = Plot.party
  .get()
  .players.filter(
    (player) => player.state === PartyPlayerState.Connected,
  )

The player list is append-only:

  • Joining means Plot has assigned the player a stable slot, but their game copy has not installed its complete synchronized view and been activated yet.
  • Connected means their game copy has installed the complete synchronized state and can participate.
  • Disconnected means a previously connected game copy was interrupted or is resynchronizing and may return.
  • Left means a non-host player left but remains in the session history.

These states describe whether a game copy can participate through the SDK. Connected does not promise that the player's React tree has rendered, animations have finished, or every optional asset has loaded. If a game needs everyone to confirm a rule or finish a game-specific preparation step, model that separately.

Plot may show an incoming person in its own party interface before the game sees them. Once admitted to the game, other game copies see the player as Joining with a stable ID and an initial {} Player Data object. Plot changes that same player to Connected only after their copy has installed a complete synchronized view and can receive game traffic. On that player's device, Plot.ready() resolves after the final connected snapshot and initial snapshot callbacks are installed. Several players can join at once without IDs moving.

You do not need a Player Data flag or channel message to decide whether a connected player has a usable SDK snapshot. Add a player-owned flag only when the game itself asks for a deliberate action such as reading the rules and tapping Ready to start.

A disconnected player keeps the same slot and Player Data if they reconnect. A refreshed iframe calls Plot.ready() again as a new page; a page that stays open during a temporary interruption is resynchronized automatically and does not call it again. To rebuild from Party and every synchronized record after startup or recovery, read their getters in the connected Plot.onLifecycleChange() callback. Normal browser reconnects preserve current state; unexpected service crashes can restore an older checkpoint, identified by Plot.connection.recovery.kind.

A non-host player who leaves and later rejoins also keeps the slot. Plot changes them from Left to Joining, resets only their Player Data to {}, clears their old transient delivery state, and later changes them to Connected once the new copy is synchronized. This is a player change, not a second onPlayerAdded() event.

The host may be joining or disconnected but never becomes left while the party is open. A temporary host disconnect or refresh preserves the active session and its synchronized state. If the host leaves, Plot closes the party and ends the game session.

Use Plot.party.getPlayer(playerId) when you have stored an ID. It returns the latest player or undefined for an unknown valid slot. IDs must be integers from 0 through 1023; any other value throws TypeError.

Open Plot's Profile or Party UI

Profile editing and party controls belong to Plot outside the game iframe. A game can direct a player to either trusted surface without recreating it:

import Plot, { SystemUi } from '@plot.game/sdk'

profileButton.addEventListener('click', async () => {
  try {
    await Plot.showSystemUi(SystemUi.Profile)
  } catch (error) {
    console.error('Plot could not show the profile', error)
  }
})

Call showSystemUi() directly from the click, tap, or keyboard handler. Do not delay it behind a timer or unrelated asynchronous work: Plot verifies the player interaction before opening parent UI. Await Plot.ready() during normal startup before enabling the control.

  • SystemUi.Profile opens the player's Profile, where they can update their display name, color, and icon or log in to Plot.
  • SystemUi.Party opens the Party surface with the join code and link, voice-chat controls, and the party-management controls available to that player's role.

The promise resolves when Plot accepts the request and invokes the surface. It does not wait for the surface to close and does not report what the player did there. The player remains in control of dismissal. Your game receives no profile record, account action, parent-page access, or party-management API.

Keep an in-game fallback when the runtime is disconnected or Plot reports user-activation-required or service-unavailable. Do not automatically retry an operation-timeout: the requested surface may already have opened before its confirmation was lost. Plot also limits rapid repeats to at most one accepted request per second, so call from deliberate controls rather than an update loop. See Error handling for the complete failure model.

When a player changes their display name, color, or icon, Plot publishes the accepted fields through the normal Party replica in real time. Every active game copy in the party receives the updated snapshot; subscribe() and onPlayerChanged() run with the corresponding fields. Render those public fields from the latest snapshot instead of caching them as permanent identity. Logging in and other account details stay private to Plot.

Subscribe to complete snapshots

Use subscribe() as the main source for party UI:

import { SnapshotReason } from '@plot.game/sdk'

const unsubscribe = Plot.party.subscribe((party, info) => {
  renderParty(party.players)

  if (info.reason === SnapshotReason.Resync) {
    rebuildPartyDerivedUi(party)
  }
})

// When the owning screen is permanently removed:
unsubscribe()

A subscription registered before startup receives its first snapshot as Plot finishes startup, just before ready() resolves. One registered later receives the current snapshot immediately. Later revisions arrive in order, and the returned unsubscribe function is safe to call more than once.

SnapshotReason is SnapshotReason.Initial for that first delivery, SnapshotReason.Change for a normal party update, and SnapshotReason.Resync when Plot replaces its cached state during recovery. A resync is delivered even when the values happen to compare equal, so stateful UI can rebuild from the complete snapshot.

Subscribe to focused events

Use focused callbacks for effects tied to one normal party transition:

type PlayerData = { score?: number }
const playerData = Plot.playerData.as<PlayerData>()

Plot.party.onPlayerAdded(({ player }) => {
  // The new player's initial data is already installed.
  initializePlayerUi(player, playerData.get(player.playerId))
  showJoiningMessage(player.displayName)
})

Plot.party.onPlayerChanged((event) => {
  if (event.changedFields.includes('state')) {
    updatePresence(
      event.player.playerId,
      event.previousPlayer.state,
      event.player.state,
    )
  }
})

onPlayerAdded() runs only when a slot first enters the list. It does not replay players from an initial or resynchronized snapshot. onPlayerChanged() supplies the previous and current player plus all changed fields: state, displayName, color, and icon. If one update affects several players, focused events run in player-ID order.

Plot installs a complete party update before invoking any callback. Inside a listener, Plot.party.get() and the event's party already contain the new values. For a new or rejoining player, their Player Data is also installed first. A newly added player is still Joining and cannot receive game messages yet. Their later change to Connected means the copy can participate; no additional SDK connection flag is required in game code.

If an update changes the current player from Connected to Disconnected or Left, Plot changes Plot.connection to Disconnected before running Party callbacks. Reads inside those callbacks already show the complete new Party. Cached state may be stale while Plot recovers; see Error handling for write and send behavior.

Subscriptions stay registered through recoverable disconnections. If Plot cannot prove every individual transition, it emits one complete Resync snapshot rather than inventing player-added or player-changed events.

All party subscription methods throw TypeError for a non-function listener and return an idempotent unsubscribe function. An exception in game listener code is reported to the browser without stopping other listeners or SDK cleanup.