PlanyDocs
API reference

PlayerSessionService

api.plany.cloud:443Regional5 methods

See who is playing on your game servers, and who has. Plany records every connection made to a server and every player session — who joined, when, and when they left — worked out from the server's own output, with no SDK to integrate. GetPresence answers "who is on this server right now" in one call; ListPlayerSessions and ListConnections give the history.

ReportConnection

plany.session.v1.PlayerSessionService/ReportConnection

Records that a connection to a game server was opened, forwarded or closed.

Called by Plany's connection proxy; you do not call this yourself. Idempotent on connection_id, so a repeated report updates one connection rather than recording a second.

Parameters

  • parentstringRequired

    projects/{p}/locations/{l}/gameServers/{s}.

  • connection_idstringRequired

    The proxy's own id for this connection, stable across the open report and the close report. A ULID, minted when the socket was accepted.

  • stateConnection.StateRequired
    STATE_OPENSTATE_CLOSED
  • port_namestringRequired

    Which of the game's ports it arrived on.

  • protocolstring

    "tcp" or "udp".

  • client_addressstring

    Source address as the proxy saw it, host:port.

  • upstream_addressstring

    Where it was forwarded to, once there was somewhere to forward it.

  • woke_serverbool

    Whether the game server was not running when this connection arrived. See Connection.woke_server — it is an observation, not a claim about which connection won the race to start it.

  • open_timeTimestamp

    When the socket was accepted. Set on both reports so a close that arrives without its open — a proxy that restarted, a report that was dropped — is still a complete record rather than a connection that began at close time.

  • close_timeTimestamp
  • CLOSE_REASON_CLIENT_CLOSEDCLOSE_REASON_SERVER_CLOSEDCLOSE_REASON_WAKE_TIMEOUTCLOSE_REASON_UPSTREAM_UNREACHABLECLOSE_REASON_EXPIRED
  • wait_durationDuration

    How long the player waited to be spliced onto a server.

Returns

Connection.

grpcurl -H "authorization: Bearer $PLANY_API_KEY" \
-d '{
"parent": "projects//locations//gameServers/",
"connection_id": "my-connection",
"state": "STATE_OPEN",
"port_name": ""
}' \
api.plany.cloud:443 plany.session.v1.PlayerSessionService/ReportConnection
Example response
{
"name": "projects//locations//gameServers//connections/my-connection",
"game_server": "projects//locations//gameServers/",
"state": "STATE_OPEN",
"port_name": "",
"protocol": "",
"client_address": "",
"client_ip": "",
"upstream_address": "",
"woke_server": false,
"open_time": "2026-10-04T12:00:00Z",
"close_time": "2026-10-04T12:00:00Z",
"close_reason": "CLOSE_REASON_CLIENT_CLOSED",
"wait_duration": "300s",
"player_session": "projects//locations//gameServers//playerSessions/my-player-session"
}

ListConnections

Read
plany.session.v1.PlayerSessionService/ListConnections

Lists the connections made to one game server.

Parameters

  • parentstringRequired

    projects/{p}/locations/{l}/gameServers/{s}.

    One server, and no wildcard. A region's connections are the highest-churn rows this service holds, and projects/- over them is a query that answers slowly and tells nobody what they asked.

  • page_sizeint32
  • page_tokenstring
  • time_rangeInterval

    Restrict by open time.

  • active_onlybool

    Only connections still open.

  • port_namestring

    Only connections that arrived on this port.

Returns

ListConnectionsResponse.

grpcurl -H "authorization: Bearer $PLANY_API_KEY" \
-d '{
"parent": "projects//locations//gameServers/",
"page_size": 50
}' \
api.plany.cloud:443 plany.session.v1.PlayerSessionService/ListConnections
Example response
{
"connections": [
{
"name": "projects//locations//gameServers//connections/my-connection",
"game_server": "projects//locations//gameServers/",
"state": "STATE_OPEN",
"port_name": "",
"protocol": "",
"client_address": "",
"client_ip": "",
"upstream_address": "",
"woke_server": false,
"open_time": "2026-10-04T12:00:00Z",
"close_time": "2026-10-04T12:00:00Z",
"close_reason": "CLOSE_REASON_CLIENT_CLOSED",
"wait_duration": "300s",
"player_session": "projects//locations//gameServers//playerSessions/my-player-session"
}
],
"next_page_token": ""
}

ListPlayerSessions

Read
plany.session.v1.PlayerSessionService/ListPlayerSessions

Lists the player sessions on one game server, newest first.

Parameters

  • parentstringRequired

    projects/{p}/locations/{l}/gameServers/{s}.

  • page_sizeint32
  • page_tokenstring
  • time_rangeInterval

    Restrict by start time. A session that began before the window and is still running is included, because "who was on my server yesterday evening" is not answered by excluding the person who never left.

  • active_onlybool

    Only sessions still active.

  • player_keystring

    One player, by the key their sessions are recorded under. See PlayerSession.player_key.

  • include_provisionalbool

    Include sessions built from templates that have not been promoted.

    Off by default, and that default is the point: a caller who did not think about provenance gets only sessions the platform is prepared to stand behind. Somebody watching a new game being learned asks for them deliberately.

Returns

ListPlayerSessionsResponse.

grpcurl -H "authorization: Bearer $PLANY_API_KEY" \
-d '{
"parent": "projects//locations//gameServers/",
"page_size": 50
}' \
api.plany.cloud:443 plany.session.v1.PlayerSessionService/ListPlayerSessions
Example response
{
"player_sessions": [
{
"name": "projects//locations//gameServers//playerSessions/my-player-session",
"game_server": "projects//locations//gameServers/",
"game": "",
"player": {
"display_name": "My server",
"player_id": "my-player",
"id_type": "",
"address": ""
},
"player_key": "",
"state": "STATE_ACTIVE",
"start_time": "2026-10-04T12:00:00Z",
"end_time": "2026-10-04T12:00:00Z",
"end_reason": "END_REASON_DISCONNECTED",
"duration": "300s",
"confidence": "CONFIDENCE_CONFIRMED",
"connection": "projects//locations//gameServers//connections/my-connection",
"origin_event": ""
}
],
"next_page_token": ""
}

GetPresence

Read
plany.session.v1.PlayerSessionService/GetPresence

Returns who is on one game server right now.

The read everything else actually wants. A singleton rather than a list, because a caller asking "should I keep this server running" wants one number and would otherwise page through sessions to compute it — three times, in three services, differently.

Parameters

  • namestringRequired

    projects/{p}/locations/{l}/gameServers/{s}/presence.

Returns

Presence.

grpcurl -H "authorization: Bearer $PLANY_API_KEY" \
-d '{
"name": "projects//locations//gameServers//presence"
}' \
api.plany.cloud:443 plany.session.v1.PlayerSessionService/GetPresence
Example response
{
"name": "projects//locations//gameServers//presence",
"game_server": "projects//locations//gameServers/",
"player_count": 0,
"connection_count": 0,
"waiting_connection_count": 0,
"players": [
{
"display_name": "My server",
"player_id": "my-player",
"id_type": "",
"address": ""
}
],
"last_activity_time": "2026-10-04T12:00:00Z",
"has_provisional_sessions": false
}

ListPresence

Read
plany.session.v1.PlayerSessionService/ListPresence

Returns presence for every game server in a project.

Parameters

  • parentstringRequired

    projects/{p}/locations/{l}, or projects/-/locations/{l} for every project in this region — internal callers only.

  • page_sizeint32
  • page_tokenstring
  • occupied_onlybool

    Only servers with somebody on them or somebody waiting.

    Almost always what a caller wants: the fleet is mostly hibernated servers with nobody on them, which is the product working.

Returns

ListPresenceResponse.

grpcurl -H "authorization: Bearer $PLANY_API_KEY" \
-d '{
"parent": "",
"page_size": 50
}' \
api.plany.cloud:443 plany.session.v1.PlayerSessionService/ListPresence
Example response
{
"presences": [
{
"name": "projects//locations//gameServers//presence",
"game_server": "projects//locations//gameServers/",
"player_count": 0,
"connection_count": 0,
"waiting_connection_count": 0,
"players": [
{
"display_name": "My server",
"player_id": "my-player",
"id_type": "",
"address": ""
}
],
"last_activity_time": "2026-10-04T12:00:00Z",
"has_provisional_sessions": false
}
],
"next_page_token": ""
}

The PlayerSession object

projects//locations//gameServers//playerSessions/{{player_session}}

PlayerSession is one person's stay on one game server.

Worked out from the server's own output, which is what makes this work for a game Plany has never seen: the server already announces every join and part, and Plany learns to read them.

Check confidence. While Plany is still proving the patterns for a new game, its sessions are provisional.

Attributes

  • namestringIdentifier

    projects/{p}/locations/{l}/gameServers/{s}/playerSessions/{session}.

    The id is derived from the game server, the player and the join time, so one stay always has the same name.

  • game_serverstringOutput only
  • gamestringOutput only

    The game that was running, carried through so a consumer can reason about a session without resolving the server.

  • playerPlayerOutput only

    Who. Every field on Player is optional because games disagree on what a player is; see plany.gamelog.v1.Player.

    Show child attributes
    • display_namestring

      Display name as the server printed it. Unstable — Minecraft names change and Steam personas change hourly — so it is for humans, not for joins.

    • player_idstring

      Stable identifier, when the game logs one: a Minecraft UUID, a SteamID64, an EOS product id. This is what a session is keyed on when it exists.

    • id_typestring

      Which namespace player_id belongs to, e.g. "steam", "minecraft", "eos". Two games can both log a 17-digit number and mean different things.

    • addressstring

      Source address, when logged.

      A personal datum, and a game server prints it unasked. It is kept because it is often the only stable identifier a game offers, and it is the field most likely to need suppressing per account — which is why it is separate rather than folded into attributes.

  • player_keystringOutput only

    What this session is keyed on: the player's stable id where the game logs one, and its display name where it does not.

    Surfaced rather than left implicit, because it is what "the same player" means here — and on a game that logs only a name, that is a weaker claim than it looks. A rename mid-session becomes two sessions.

  • statePlayerSession.StateOutput only
    STATE_ACTIVESTATE_ENDED
  • start_timeTimestampOutput only

    When the server said they joined — the log line's own time, not when the platform got round to reading it. Extraction can lag by minutes after an outage, and a session timed by extraction is wrong by exactly that much.

  • end_timeTimestampOutput only

    Unset while active.

  • end_reasonPlayerSession.EndReasonOutput only
    END_REASON_DISCONNECTEDEND_REASON_SUPERSEDEDEND_REASON_SERVER_STOPPEDEND_REASON_SERVER_RESTARTEDEND_REASON_EXPIRED
  • durationDurationOutput only

    end_time - start_time, for an active session measured to now.

  • confidenceConfidenceOutput only

    Whether this session may be acted on.

    Copied from the event that opened it. PROVISIONAL means the template that recognised the join has not been promoted, so the session is published for anyone who wants to watch early and must not reach a bill.

    CONFIDENCE_CONFIRMEDCONFIDENCE_PROVISIONAL
  • connectionstringOutput only

    The connection this session is believed to have arrived on.

    Matched on source address within a window, and therefore a belief rather than a fact — which is why it is a link between two stored records and not a merge. Empty whenever the game did not log an address, which is most games.

  • origin_eventstringOutput only

    The event this session was opened from, so a surprising session can be traced back to the line that caused it.

PlayerSession
{
"name": "projects//locations//gameServers//playerSessions/my-player-session",
"game_server": "projects//locations//gameServers/",
"game": "",
"player": {
"display_name": "My server",
"player_id": "my-player",
"id_type": "",
"address": ""
},
"player_key": "",
"state": "STATE_ACTIVE",
"start_time": "2026-10-04T12:00:00Z",
"end_time": "2026-10-04T12:00:00Z",
"end_reason": "END_REASON_DISCONNECTED",
"duration": "300s",
"confidence": "CONFIDENCE_CONFIRMED",
"connection": "projects//locations//gameServers//connections/my-connection",
"origin_event": ""
}

Connection

Connection is one network connection made to a game server's address.

Transport-level: it records the connection, not who made it. Plany never reads a game's protocol, so a connection has no player, account or game data of its own — that comes from the matching player session.

  • namestringIdentifier

    projects/{p}/locations/{l}/gameServers/{s}/connections/{connection}.

  • game_serverstringOutput only

    The game server this connection was made to.

  • stateConnection.StateOutput only
    STATE_OPENSTATE_CLOSED
  • port_namestringOutput only

    Which of the game's ports the connection arrived on — "game", "query", "rcon". A server browser probing the query port every thirty seconds is not somebody playing, and without this the two are the same row.

  • protocolstringOutput only

    "tcp" or "udp".

  • client_addressstringOutput only

    Source address as the proxy saw it, host:port.

    A personal datum and treated as one: it is here because it is the only thing a connection and a log line have in common, which makes it the only way the two halves of this service correlate at all. See client_ip.

  • client_ipstringOutput only

    The host part of client_address, stored separately because it is what correlation matches on — a game announces a player's address without the ephemeral port, so the full pair never joins to anything.

  • upstream_addressstringOutput only

    Where the proxy forwarded it, host:port on the game server's own private address. Empty while the server is still starting, which is the state a connection to a hibernated server spends most of its life in.

  • woke_serverboolOutput only

    Whether the game server was not running when this connection arrived.

    The demand signal, and named for what the proxy can actually observe. It is not "this connection is the one that started the server" — which single connection won that race is Playpute's to know, and asking the proxy would be asking it to guess. Twenty friends joining a hibernated server at once all carry this, which is the honest description of what happened.

    What it separates is the thing that matters: demand for a stopped server, from traffic to a running one. Almost every connection is the latter.

  • open_timeTimestampOutput only
  • close_timeTimestampOutput only

    Unset while the connection is open.

  • close_reasonConnection.CloseReasonOutput only
    CLOSE_REASON_CLIENT_CLOSEDCLOSE_REASON_SERVER_CLOSEDCLOSE_REASON_WAKE_TIMEOUTCLOSE_REASON_UPSTREAM_UNREACHABLECLOSE_REASON_EXPIRED
  • wait_durationDurationOutput only

    How long the player waited before being spliced to a server: the gap between accepting the socket and having somewhere to send it.

    Recorded because it is the number the product is judged on. Hibernation only works if a restore is faster than a boot, and this is the only place the platform observes what a player actually experienced rather than what a snapshot benchmark says.

  • player_sessionstringOutput only

    The player session this connection is believed to belong to, when one was matched. Empty is the common case and not an error — see the package comment, and PlayerSession.connection.

ListConnectionsResponse

  • Newest first.

    Show child attributes
    • namestringIdentifier

      projects/{p}/locations/{l}/gameServers/{s}/connections/{connection}.

    • game_serverstringOutput only

      The game server this connection was made to.

    • stateConnection.StateOutput only
      STATE_OPENSTATE_CLOSED
    • port_namestringOutput only

      Which of the game's ports the connection arrived on — "game", "query", "rcon". A server browser probing the query port every thirty seconds is not somebody playing, and without this the two are the same row.

    • protocolstringOutput only

      "tcp" or "udp".

    • client_addressstringOutput only

      Source address as the proxy saw it, host:port.

      A personal datum and treated as one: it is here because it is the only thing a connection and a log line have in common, which makes it the only way the two halves of this service correlate at all. See client_ip.

    • client_ipstringOutput only

      The host part of client_address, stored separately because it is what correlation matches on — a game announces a player's address without the ephemeral port, so the full pair never joins to anything.

    • upstream_addressstringOutput only

      Where the proxy forwarded it, host:port on the game server's own private address. Empty while the server is still starting, which is the state a connection to a hibernated server spends most of its life in.

    • woke_serverboolOutput only

      Whether the game server was not running when this connection arrived.

      The demand signal, and named for what the proxy can actually observe. It is not "this connection is the one that started the server" — which single connection won that race is Playpute's to know, and asking the proxy would be asking it to guess. Twenty friends joining a hibernated server at once all carry this, which is the honest description of what happened.

      What it separates is the thing that matters: demand for a stopped server, from traffic to a running one. Almost every connection is the latter.

    • open_timeTimestampOutput only
    • close_timeTimestampOutput only

      Unset while the connection is open.

    • close_reasonConnection.CloseReasonOutput only
      CLOSE_REASON_CLIENT_CLOSEDCLOSE_REASON_SERVER_CLOSEDCLOSE_REASON_WAKE_TIMEOUTCLOSE_REASON_UPSTREAM_UNREACHABLECLOSE_REASON_EXPIRED
    • wait_durationDurationOutput only

      How long the player waited before being spliced to a server: the gap between accepting the socket and having somewhere to send it.

      Recorded because it is the number the product is judged on. Hibernation only works if a restore is faster than a boot, and this is the only place the platform observes what a player actually experienced rather than what a snapshot benchmark says.

    • player_sessionstringOutput only

      The player session this connection is believed to belong to, when one was matched. Empty is the common case and not an error — see the package comment, and PlayerSession.connection.

  • next_page_tokenstring

ListPlayerSessionsResponse

  • player_sessionsrepeated PlayerSession

    Newest first.

    Show child attributes
    • namestringIdentifier

      projects/{p}/locations/{l}/gameServers/{s}/playerSessions/{session}.

      The id is derived from the game server, the player and the join time, so one stay always has the same name.

    • game_serverstringOutput only
    • gamestringOutput only

      The game that was running, carried through so a consumer can reason about a session without resolving the server.

    • playerPlayerOutput only

      Who. Every field on Player is optional because games disagree on what a player is; see plany.gamelog.v1.Player.

      Show child attributes
      • display_namestring

        Display name as the server printed it. Unstable — Minecraft names change and Steam personas change hourly — so it is for humans, not for joins.

      • player_idstring

        Stable identifier, when the game logs one: a Minecraft UUID, a SteamID64, an EOS product id. This is what a session is keyed on when it exists.

      • id_typestring

        Which namespace player_id belongs to, e.g. "steam", "minecraft", "eos". Two games can both log a 17-digit number and mean different things.

      • addressstring

        Source address, when logged.

        A personal datum, and a game server prints it unasked. It is kept because it is often the only stable identifier a game offers, and it is the field most likely to need suppressing per account — which is why it is separate rather than folded into attributes.

    • player_keystringOutput only

      What this session is keyed on: the player's stable id where the game logs one, and its display name where it does not.

      Surfaced rather than left implicit, because it is what "the same player" means here — and on a game that logs only a name, that is a weaker claim than it looks. A rename mid-session becomes two sessions.

    • statePlayerSession.StateOutput only
      STATE_ACTIVESTATE_ENDED
    • start_timeTimestampOutput only

      When the server said they joined — the log line's own time, not when the platform got round to reading it. Extraction can lag by minutes after an outage, and a session timed by extraction is wrong by exactly that much.

    • end_timeTimestampOutput only

      Unset while active.

    • end_reasonPlayerSession.EndReasonOutput only
      END_REASON_DISCONNECTEDEND_REASON_SUPERSEDEDEND_REASON_SERVER_STOPPEDEND_REASON_SERVER_RESTARTEDEND_REASON_EXPIRED
    • durationDurationOutput only

      end_time - start_time, for an active session measured to now.

    • confidenceConfidenceOutput only

      Whether this session may be acted on.

      Copied from the event that opened it. PROVISIONAL means the template that recognised the join has not been promoted, so the session is published for anyone who wants to watch early and must not reach a bill.

      CONFIDENCE_CONFIRMEDCONFIDENCE_PROVISIONAL
    • connectionstringOutput only

      The connection this session is believed to have arrived on.

      Matched on source address within a window, and therefore a belief rather than a fact — which is why it is a link between two stored records and not a merge. Empty whenever the game did not log an address, which is most games.

    • origin_eventstringOutput only

      The event this session was opened from, so a surprising session can be traced back to the line that caused it.

  • next_page_tokenstring

ListPresenceResponse

  • Counts only. Presence.players is left empty here on purpose — a region's worth of player names is a response measured in megabytes answering a question about occupancy.

    Show child attributes
    • namestringIdentifier

      projects/{p}/locations/{l}/gameServers/{s}/presence. A singleton.

    • game_serverstringOutput only
    • player_countint32Output only

      Active sessions: players the server said were on it and did not say left.

    • connection_countint32Output only

      Open connections, which is a different and usually larger number. A query socket is a connection and not a player, and a player whose game opens two sockets is one player.

    • waiting_connection_countint32Output only

      Connections open against a server with no active sessions at all.

      The signal that matters while a server is starting: somebody is waiting and nobody is playing yet. Zero players and a non-zero count here is exactly "a hibernated server has demand", which is the state Playpute will act on.

    • playersrepeated PlayerOutput only

      Who, capped — see ListPresenceResponse. Empty on a fleet-wide listing, where the counts are the point and the names would be the response.

      Show child attributes
      • display_namestring

        Display name as the server printed it. Unstable — Minecraft names change and Steam personas change hourly — so it is for humans, not for joins.

      • player_idstring

        Stable identifier, when the game logs one: a Minecraft UUID, a SteamID64, an EOS product id. This is what a session is keyed on when it exists.

      • id_typestring

        Which namespace player_id belongs to, e.g. "steam", "minecraft", "eos". Two games can both log a 17-digit number and mean different things.

      • addressstring

        Source address, when logged.

        A personal datum, and a game server prints it unasked. It is kept because it is often the only stable identifier a game offers, and it is the field most likely to need suppressing per account — which is why it is separate rather than folded into attributes.

    • last_activity_timeTimestampOutput only

      The most recent thing this service observed about the server: a join, a part, a connection either way.

      The field an idle-shutdown policy should read, and deliberately not "last_session_end": a server with a player who joined an hour ago and has said nothing since is busy, and one whose last event was a disconnect two hours ago is not.

    • has_provisional_sessionsboolOutput only

      Whether any active session came from a template still being shadowed. A caller about to act on player_count — bill it, or keep a server alive on the strength of it — should refuse when this is true.

  • next_page_tokenstring

Presence

Presence is who is on a game server right now.

A derived view rather than a stored one: it is the answer to the question every other service actually asks, which is never "list me the sessions" but "is anybody playing". Idle shutdown, the console's server list and the meter that will bill for play rather than uptime all want this one number, and each computing it from a session list is three places to get it wrong.

  • namestringIdentifier

    projects/{p}/locations/{l}/gameServers/{s}/presence. A singleton.

  • game_serverstringOutput only
  • player_countint32Output only

    Active sessions: players the server said were on it and did not say left.

  • connection_countint32Output only

    Open connections, which is a different and usually larger number. A query socket is a connection and not a player, and a player whose game opens two sockets is one player.

  • waiting_connection_countint32Output only

    Connections open against a server with no active sessions at all.

    The signal that matters while a server is starting: somebody is waiting and nobody is playing yet. Zero players and a non-zero count here is exactly "a hibernated server has demand", which is the state Playpute will act on.

  • playersrepeated PlayerOutput only

    Who, capped — see ListPresenceResponse. Empty on a fleet-wide listing, where the counts are the point and the names would be the response.

    Show child attributes
    • display_namestring

      Display name as the server printed it. Unstable — Minecraft names change and Steam personas change hourly — so it is for humans, not for joins.

    • player_idstring

      Stable identifier, when the game logs one: a Minecraft UUID, a SteamID64, an EOS product id. This is what a session is keyed on when it exists.

    • id_typestring

      Which namespace player_id belongs to, e.g. "steam", "minecraft", "eos". Two games can both log a 17-digit number and mean different things.

    • addressstring

      Source address, when logged.

      A personal datum, and a game server prints it unasked. It is kept because it is often the only stable identifier a game offers, and it is the field most likely to need suppressing per account — which is why it is separate rather than folded into attributes.

  • last_activity_timeTimestampOutput only

    The most recent thing this service observed about the server: a join, a part, a connection either way.

    The field an idle-shutdown policy should read, and deliberately not "last_session_end": a server with a player who joined an hour ago and has said nothing since is busy, and one whose last event was a disconnect two hours ago is not.

  • has_provisional_sessionsboolOutput only

    Whether any active session came from a template still being shadowed. A caller about to act on player_count — bill it, or keep a server alive on the strength of it — should refuse when this is true.

Enums

Connection.CloseReason

Why a connection ended. Reported by the proxy, which is the only component that watched it close.

ValueDescription
CLOSE_REASON_UNSPECIFIED
CLOSE_REASON_CLIENT_CLOSEDOrdinary: one side closed and the splice ended.
CLOSE_REASON_SERVER_CLOSED
CLOSE_REASON_WAKE_TIMEOUTThe server never came up inside the proxy's wake timeout, so a player who was waiting was let go. The one close reason that is a product failure rather than somebody leaving.
CLOSE_REASON_UPSTREAM_UNREACHABLEThe proxy could not reach the server it was told to forward to.
CLOSE_REASON_EXPIREDNobody reported a close and the record aged out. See PlayerSession's END_REASON_EXPIRED, which carries the same warning: this is an upper bound on when it ended, not a measurement of it.

Connection.State

ValueDescription
STATE_UNSPECIFIED
STATE_OPENThe socket is open. For a hibernated server this is a player waiting on an accepted connection that has not been forwarded anywhere yet.
STATE_CLOSEDThe socket is closed, either end.

PlayerSession.EndReason

How a session ended, which is not decoration: three of these five mean "nobody told us it ended and we inferred it", and a consumer that treats them as equivalent to a clean disconnect is one that bills for time nobody played.

ValueDescription
END_REASON_UNSPECIFIED
END_REASON_DISCONNECTEDThe server announced the player left. The only reason whose `end_time` is something the platform observed rather than inferred.
END_REASON_SUPERSEDEDThe same player joined again with no leave in between, so the earlier session's end was never printed — a crash on the client, a lost line, or a game that does not announce every part. Ended at the later join.
END_REASON_SERVER_STOPPEDThe server stopped, which ends every session on it whether or not anybody's departure was logged. A server being torn down is the single most common way a session really ends on this platform, because that is what happens when the last player leaves.
END_REASON_SERVER_RESTARTEDThe server came back up. Anything still open belonged to the run before it and was never closed — a crash, an OOM kill, a host that went away.
END_REASON_EXPIREDNothing closed it and it outlived the maximum session age. `end_time` here is a **ceiling, not a measurement**: it says the session had ended by then, not that it ended then. Billing must clamp rather than charge it, and a support answer built on one should say so.

PlayerSession.State

ValueDescription
STATE_UNSPECIFIED
STATE_ACTIVEThe player is believed to be on the server now.
STATE_ENDED