Synchronized time

Every device clock differs, and messages do not reach every player at the same instant. Plot.time gives every game copy one shared time reference so those differences do not create different game results.

After await Plot.ready(), read game time synchronously:

const gameTimeMs = Plot.time.now()

Plot.time.now() returns a safe integer number of milliseconds since this game session's fixed epoch. The epoch is arbitrary; what matters is that a value such as 34_000 identifies the same moment for every player in this session.

Build timers around a deadline

For a ten-second round, have the host persist the moment it ends. Do not ask each device to start its own ten-second counter:

type GameState = {
  round?: {
    endsAtGameTimeMs: number
  }
}

const gameState = Plot.gameState.as<GameState>()

if (
  Plot.party.get().self.playerId === Plot.party.get().host.playerId &&
  gameState.get().round === undefined
) {
  await gameState.set({
    round: { endsAtGameTimeMs: Plot.time.now() + 10_000 },
  })
}

Every game copy renders the remaining time from that shared deadline. The snapshot subscription handles initial state, ordinary changes, and recovery:

let timerFrame: number | undefined

gameState.subscribe((state, info) => {
  if (!info.affects('round')) return
  if (timerFrame !== undefined) cancelAnimationFrame(timerFrame)

  const endsAt = state.round?.endsAtGameTimeMs
  if (endsAt === undefined) {
    timer.textContent = '—'
    return
  }

  function renderTimer() {
    const remainingMs = Math.max(0, endsAt - Plot.time.now())
    timer.textContent = (remainingMs / 1_000).toFixed(1)
    if (remainingMs > 0) timerFrame = requestAnimationFrame(renderTimer)
  }

  renderTimer()
})

Plot.gameState.as<T>() creates the typed shared-state view, gameState.get() reads the deadline, gameState.set() persists it, and gameState.subscribe() rebuilds the timer after a late join or in-session resynchronization. The info.affects() matcher prevents unrelated Plot.gameState changes from restarting the render loop.

A shared deadline avoids starting a fresh countdown on each message's arrival. Clock synchronization remains an estimate, so small differences are possible. A browser that pauses and resumes computes the current remainder instead of continuing an old local counter.

Use Plot.gameState for a deadline that should survive supported in-session recovery or reach late joiners. It is still session state, not permanent saved progress. A reliable realtime channel can accompany the state change with a transient sound or animation for players already present. Persist the deadline first; Plot's channel causal ordering then ensures a receiver installs that state before its message listener runs.

For audio cues and loops, Toolkit: synced audio handles loading, scheduling against this clock, catching up after delays, and correcting playback drift. Store one shared cue timestamp in Game State and pass it to each local object; the toolkit does not distribute playback commands itself.

Convert timestamps correctly

Most game logic needs only now(). Use the conversion helpers when interoperating with JavaScript dates or an existing timestamp:

const capturedAtGameTimeMs = Plot.time.fromLocalClockMs(Date.now())
const scheduledAtGameTimeMs = Plot.time.fromUnixMs(scheduledUnixMs)
const scheduledDate = new Date(Plot.time.toUnixMs(scheduledAtGameTimeMs))

fromUnixMs() assumes its argument is already a Unix timestamp from Plot or another trusted source. It does not apply a local-device correction.

fromLocalClockMs() is for a timestamp produced by this device's Date.now() clock. It applies the current localClockOffsetMs before converting to game time.

toUnixMs() converts in the other direction and is the inverse of fromUnixMs() while results remain inside JavaScript's safe-integer range.

At approximately the same instant:

Plot.time.unixNow()
  ≈ Date.now() + Plot.time.localClockOffsetMs

Plot.time.now()
  ≈ Plot.time.unixNow() - Plot.time.gameTimeEpochUnixMs

The localClockOffsetMs estimate can change after synchronization. Do not cache it; read it when converting a local timestamp, or use fromLocalClockMs().

Use client timestamps for cooperative gameplay

A client can submit its own Plot.time.now() value for an answer, reaction, or speed score. The shared clock makes that value comparable within the session, which is sufficient for Plot's cooperative game model. It does not prove that modified client code captured the value at the claimed moment. Validate that a submitted timestamp is a safe integer in a sensible range for the current round, but do not build a second clock-sync or anti-cheat protocol into the game.

Guarantees, recovery, and limits

  • now() and unixNow() never decrease within one running SDK instance. Plot filters noisy samples and applies ordinary corrections gradually.
  • gameTimeEpochUnixMs is fixed for the game session. Game-time values from different sessions are not comparable unless first converted to Unix time.
  • Plot.time continues advancing from its last synchronized anchor during a temporary disconnection. Accuracy can drift during a long outage; Plot refreshes it automatically after recovery and when a suspended page becomes visible or returns online. An advancing clock does not prove the connection is healthy; use Plot.connection for an optional connection indicator.
  • Every public timestamp, property value, and conversion result is a safe integer number of milliseconds. The conversion methods throw TypeError for a non-safe-integer argument or result.
  • Calling a TimeApi method with valid arguments, or reading either time property before Plot installs the complete ready state, throws PlotNotReadyError. Internal clock failures use the shared error model. Time is already available inside a pre-registered initial snapshot callback.
  • Clock values are gameplay references, not event sequence numbers. They do not decide synchronized-state or channel order and are not authority for authentication, purchases, or other platform security decisions.

There is no clock event, refresh method, reconnect method, or uncertainty value for game code to manage. Plot owns synchronization; games read the current TimeApi when needed.