This guide explains the meaning and limits of data in SDK 0.3.0-preview.2 with HUD API 3. It complements the HUD API guide, the SDK runtime reference and the complete type definitions. Examples use the actual public field names; they do not introduce additional message kinds.
Unknown, empty and null are different states
| Value or shape | Meaning | HUD rule |
|---|---|---|
null | Not supplied, unknown or unavailable. | Use a placeholder. Do not infer a zero position, death or empty equipment. |
[] | A present collection is explicitly empty. | Remove old entries; do not keep drawing the previous list. |
0 | A numeric zero that was actually supplied. | Compare with null, not with a truthiness test. health: 0 and money: 0 are known values. |
false | An explicitly false boolean value. | helmet: false is known; helmet: null is unknown. |
"" | A supplied empty string, such as an intentionally blank panel field. | Replace it only when your presentation explicitly requires a fallback. |
unknown | A present but semantically unknown enum state. | Do not invent a known map, weapon or phase. An unknown message discriminator is instead a protocol error. |
These rules apply at different levels: an omitted object replacement in objects leaves the existing object unchanged. A present replacement contains a complete typed object; its nullable fields are accepted rather than filled from previous field values. removed: true removes an object and carries no payload. A snapshot replaces the entire object baseline.
Which source supplies each domain?
| Domain | Source and interpretation | Important limit |
|---|---|---|
Info / info | GSI: map, phases, round, sides and available source sections. | A present map block does not guarantee a complete ten-player roster. |
| player parts | GSI player blocks split into identity, vitals, equipment, runtime, statistics and pose objects. | Observed, current and roster players may be separate source roles for the same account. |
ManagedMatch / managedMatch | Selected library match with teams, series maps and competition; may exist before the first GSI packet. | Management changes do not refresh the GSI receive timestamp. |
GameMatch / gameManagedMatch | Separate match block attached to the game graph. | Choose an explicit precedence rule when supporting both match sources. |
Radar / radar | Optional HLAE player markers with their own connection, epoch, sequence and receive time. | HLAE poses do not overwrite a player part's GSI pose. |
KillOrder / killOrder | Bounded set of kill events captured through the HLAE connection. | Neither a complete match history nor a general stream of shot or damage events. |
Panel / panel; Options / options | Package-bound panel state and typed display options from the shared HUD session. | They do not establish whether game data is fresh. |
HudClockSample | Host server-time sample, independent of GSI and HLAE. | A synchronized clock and fresh game data are separate requirements. |
availableSections lists present GSI sections. It is neither a permission list nor a promise that every field or player within those sections was supplied. Missing sections remain unknown. Details: GSI sections.
Identities, source roles and ordering
Player keys are opaque and scoped to a game session. Use Roster.Players / roster.players in the supplied order and resolve each key with Player(key) / player(key). Dictionary or Map iteration order is not roster order. Do not construct keys from Steam IDs, names or list positions. The same Steam ID may occur in several source roles.
| Reference | Use | Edge case |
|---|---|---|
roster.players | Ordered list of roster players. | [] is a known empty roster; do not add ten substitute players. |
roster.observed | Key of the actually supplied observed player, or unknown. | Do not automatically select the first roster player. |
roster.current | Key of the current source player. | May describe a menu identity even when no in-match player is being observed. |
identity.observerSlot | Already normalized observer digit: 1 through 9, then 0. | Do not convert it again. After coach filtering, it may differ from the native CS2 number-key assignment. |
grenadeOrder.keys; killOrder.keys; radar.players | Explicit reference order for grenades, kills and radar markers. | Use the matching lookup; do not join objects by a similar-looking identifier. |
Names are display text, not identity or HTML. Use Blazor text binding or textContent; do not decode HTML again or use innerHTML. Keep Steam IDs as strings so JavaScript cannot round them. The Roster reference and Identity describe the complete shape.
Reading the six player parts
| Part and type | Fields and units | Presentation rule |
|---|---|---|
| Identity | steamId, name, side, clan, observer digit and explicit spectation target. | CT/T is the current game side, not a persistent team ID. |
| Vitals | health and armor are supplied points; helmet is boolean. flashed, smoked and burning remain integer source values. | Do not interpret effect values as seconds or assume they are percentages. Unknown HP does not mean death; do not universally cap values at 100. |
| Equipment | money and equipmentValue are in-game money values; defuseKit is boolean. weapons contains inventory definitions. | Unknown money does not imply a free purchase, and an unknown kit value does not prove that no kit is present. |
| Weapon runtime | weapons contains state, ammoClip, ammoClipMax and ammoReserve per slot. | Ammunition values are counts. For grenades they may represent the carried quantity; an unknown capacity is not zero shots. |
| Statistics | Match statistics, recorded statistics and roundKills, roundHeadshotKills, roundDamage. | Label round values, match values and observed partial periods separately. |
| Pose | position is a GSI world vector; forward is a viewing-direction vector. | World coordinates are not screen pixels or metres. A direction vector is not an angle in degrees. |
Example: preserve 0 HP and leave unknown HP unavailable
This complete HudPlayerVitalsV3 object describes known 0 HP, a known absent helmet and an unknown smoke value. It does not itself create a kill event.
{
"activity": "playing",
"health": 0,
"armor": 0,
"helmet": false,
"flashed": 0,
"smoked": null,
"burning": 0
}
The following pure formatting functions take an already committed SDK session state. They display a known 0 and use a placeholder for missing state. C# uses the session's generic options type; JavaScript receives the same HudSessionSnapshot concept.
using SOU.Hud.Sdk;
static string HealthLabel<TOptions>(HudSessionSnapshot<TOptions> state)
{
if (!state.Ready) return "—";
var health = state.Game?.ObservedPlayer?.Vitals.Health;
return health?.ToString(System.Globalization.CultureInfo.InvariantCulture) ?? "—";
}
function healthLabel(state) {
if (!state.ready) return '—';
const game = state.game;
const key = game?.roster?.observed;
const player = key == null ? null : game.player(key);
const health = player?.vitals.health;
return health == null ? '—' : String(health);
}
Joining inventory definitions and weapon runtime
The definition tells you which weapon a slot contains; runtime tells you what it is doing. HudWeaponDefinitionV3 and HudWeaponRuntimeV3 share the same slot. Both lists are available together or both are null; the reducer checks equal length and matching slot order. Still join them by slot and do not assume fixed indices for primary weapon, pistol or knife.
These two matching JSON objects are complete HudPlayerEquipmentV3 and HudPlayerWeaponsV3 payloads. An ammunition change can replace only the second object.
{
"defuseKit": false,
"money": 0,
"equipmentValue": 2700,
"weapons": [
{ "slot": "weapon_0", "name": "weapon_ak47", "type": "rifle", "paintKit": null, "kind": "ak47" }
]
}
{
"weapons": [
{ "slot": "weapon_0", "state": "reloading", "ammoClip": 0, "ammoClipMax": 30, "ammoReserve": 90 }
]
}
For an active-weapon display, select a supplied active or reloading state. holstered is not an active weapon. When death is known, do not present old equipment as current possession. A known empty list clears the display; use bounded text or a neutral symbol for an unknown weapon name. kind is the known classification and name is the open source identifier; a name prefix is not proof of a particular weapon.
Map, round, sides and managed match
| Data | Meaning and limit |
|---|---|
info.map; info.mapKind | Open source identifier and known map classification. An unknown classification does not invalidate the supplied map name. |
info.phase; info.roundPhase | Match phase and round phase are separate states. A round timer alone does not establish the match phase. |
info.roundNumber | Already one-based round number, when supplied. Do not add one again. |
info.ct; info.t | Current GSI sides with scores and optional additional values; not persistent team identities. |
managedMatch.teamA; managedMatch.teamB | Persistent opponents. Check the supplied match side resolutions; leave ambiguous or conflicting assignments unresolved. |
managedMatch.maps; managedMatch.vetos | Series order, map decisions and results. Score A/B belongs to Team A/B, not automatically CT/T. A list position does not identify the current live map. |
info.roundWins | Supplied history ordered by round number, or unknown. Do not reconstruct missing history backwards from the total score. |
managedMatch.competition | Assigned competition, season, section path, optional structure, logo and sponsors. Do not retain missing media or assignments from the previous match. |
Additional fields: Game information, managed match and side resolution.
Labelling statistics and coverage correctly
| Statistics | Scope | When the basis is missing |
|---|---|---|
statistics.stats | Directly supplied match values such as kills, assists, deaths, MVPs and score. | Check each nullable field separately. Do not equate these values with locally recorded statistics. |
statistics.roundKills; statistics.roundHeadshotKills; statistics.roundDamage | Values for the currently supplied round; damage is in damage points. | Do not keep using previous round counters after a round transition. |
statistics.recordedStats | Only fully observed completed rounds. Round count, accumulated damage, kills and headshots belong to exactly that coverage. | Joining late, interruptions and restarts may produce partial coverage. Do not fill in historical data. |
coversWholeMatch | Indicates whether the recorded period covers the relevant match so far. | When false, label the display as the observed period rather than complete match statistics. |
averageDamagePerRound; headshotPercentage | ADR is damage divided by recorded rounds; headshot percentage is 100 times headshots divided by kills. | Without a recorded round, ADR is unknown; without a positive kill count, headshot percentage is unknown. An unknown ratio is not 0 percent. |
Complete shapes: MatchStats and RecordedPlayerStats. Recording counts observed round results once; a rerender must not increment statistics.
Geometry, HLAE radar, grenades and kills
CS2 world coordinates require calibration for the particular map; they are not pixels. forward is a direction vector. Missing positions remain missing even when the display expects a marker. Radar uses the HLAE connection, epoch, sequence and receivedAt as its own source context. The temporary pawnHandle is neither a replacement for a Steam ID nor a persistent identity across respawns.
Use a fresh HLAE frame as the common marker source. Do not fill in a missing player with their older GSI pose. Only when the HLAE frame as a whole is unusable may the HUD fall back to available GSI poses. The current desktop removes radar frames aged at least 750 ms; this is not a general freshness guarantee of the WebSocket. Reset interpolation when the connection, epoch, map or game session changes.
Grenades supply optional position, velocity, lifetimeSeconds, effectTimeSeconds and flame points. The two named time values are seconds; do not construct an unsupported absolute deadline or playback speed from them. Vectors remain source geometry; do not claim SI units or a floor without documented calibration. Grenade kind and weapon kind are different enums.
A kill is identified by connectionId, epoch and id together. Its gameTimeSeconds belongs to the HLAE game timeline. The supplied set is bounded and refreshed on context changes; after reconnect it is not an archive of missed events. Rereading or rerendering the same entry does not create a new animation. Do not infer additional kills or suicide from health: 0, an unknown attacker or an unknown weapon identifier.
Radar references, Radar markers, Grenades and Kill feed list their individual fields.
Do not mix timestamps and units
| Value | Unit and origin | Appropriate use |
|---|---|---|
HudV3Graph.ReceivedAt / receivedAt | ISO-8601 timestamp with an offset from the sOU server: last accepted GSI reception. | GSI data age and the anchor for supplied countdown seconds. Radar or management updates do not refresh it. |
provider.timestampUnixSeconds | Optional Unix time in seconds from the GSI provider block. | Source information; not a replacement for server reception time or the synchronized HUD clock. |
radar.receivedAt | Server reception time of the HLAE radar frame, ISO-8601 with an offset. | Check radar age separately from GSI. |
gameTimeSeconds | Numeric HLAE game time in seconds; not Unix time. | Source ordering and game context; do not subtract server epoch milliseconds from it. |
HudClockSample.serverTime | Server time as an ISO-8601 timestamp with an offset. | Synchronization using measured round-trip time and monotonic advancement. |
hudClock.now() | Synchronized server epoch milliseconds in the JavaScript SDK. | As the now argument for countdownRemaining; do not subtract values expressed in seconds directly. |
HudClock.UtcNow; HudRenderClock.Now | Synchronized server time as a C# DateTimeOffset. | Use as the time basis for HudCountdown.Remaining and custom displays. |
secondsRemaining | Optional remaining seconds from the countdown or bomb. | Calculate using the original receive anchor, valid server time and an explicitly selected pause state. |
Clock readiness, expiry and countdown
HudClockSample contains only the server-time sample. The host measures round-trip time; the C# clock core and browser integration reject samples with a round trip above two seconds. An accepted anchor advances monotonically and expires after more than 90 seconds without a valid renewal. Visibility changes, suspended browser documents and connection changes can also invalidate readiness. These are clock limits, not game-data freshness limits.
{
"serverTime": "2026-10-28T12:00:00+00:00"
}
A failed renewal attempt does not replace an unexpired anchor. An old request or one completing after invalidation must not make a new generation ready. Use hudClock.isReady() before hudClock.now(); C# can check HudClock.TryGetUtcNow. Directly reading an unready strict clock throws. The Blazor host gates its content on clock readiness. The data session's ready property is independent.
Countdown helpers take the original source anchor. Without a valid seconds value the result remains unknown. Without receive time a running countdown remains unknown; an explicitly paused calculation instead holds the supplied seconds value. Negative results are clamped to zero, and a receive time in the future must not increase the source value. Do not infer a pause merely from an old or missing packet.
This JavaScript example runs inside an already initialized SDK HUD. It displays a placeholder when the session is unready, GSI data is not live or the clock is unready. countdownRemaining takes seconds and returns seconds, while hudClock.now() returns milliseconds. The clock's subscribe reports readiness changes and is not an animation cadence.
import { hudClock, countdownRemaining } from './sdk/sou-hud.js';
function countdownLabel(state) {
const graph = state.game;
if (!state.ready || graph?.freshness !== 'live' || !hudClock.isReady()) return '—';
const countdown = graph.clock?.countdown;
const paused = countdown?.phase === 'paused';
const seconds = countdownRemaining(
countdown?.secondsRemaining, graph.receivedAt, hudClock.now(), paused);
return seconds == null ? '—' : String(Math.ceil(seconds));
}
Distinguishing session and game revisions
| Revision | Rule | Consequence |
|---|---|---|
| session message | The outer snapshot message starts with revision: 1 and baseRevision: null; subsequent revisions must increase by exactly one. | Game, panel, view and options share this order. A panel message need not change the game revision. |
| game message | For update, baseRevision must equal the last accepted game revision; revision must be greater and may jump. A new game baseline carries its actual current revision. | A jump with the correct base is different from a mismatched base. The inner game revision does not necessarily start at one. |
HudPanelState.revision; HudViewStateV3.revision | Independent state revisions for the panel and confirmed manager view. | Do not use them as a global game clock or compare them with game or session revisions. |
HudSessionSnapshot.Sequence / sequence | Local publication sequence of the SDK runtime, including status and reset transitions. | Not a transport counter, message acknowledgement or match identity. |
The outer session ID and inner game session ID are separate identities. A reconnect receives a new outer baseline; the inner game session may remain the same. A new map or another game context may change the game session. Revision values remain within the shared exactly comparable integer range up to 9007199254740991.
The following complete inner update requires an already accepted game baseline at revision 7 with a complete player under player-7. Jumping to revision 10 is valid with the matching base. It is not an outer HudSessionMessageV3 message.
{
"schemaVersion": 3,
"kind": "update",
"sessionId": "22222222-2222-2222-2222-222222222222",
"baseRevision": 7,
"revision": 10,
"receivedAt": "2026-10-28T12:00:00+00:00",
"freshness": "live",
"objects": [{
"kind": "playerVitals",
"key": "player-7",
"removed": false,
"vitals": {
"activity": "playing", "health": 37, "armor": 85,
"helmet": true, "flashed": 0, "smoked": null, "burning": 0
}
}]
}
Atomic commits and invalid batches
A batch replaces or removes whole typed objects. A referenced player requires all six parts; roster, grenade, radar and kill references must match the final batch. New references and their objects may arrive in the same message. Their order within objects is not a sequence of visible intermediate states: the reducer validates the complete candidate before publication.
Duplicate object keys, mismatched payload kinds, missing reference targets, orphaned objects, inconsistent weapon lists, nonfinite numbers and a mismatched revision base can invalidate a batch. No valid subset is published. When the high-level SDK runtime reconnects after such a failure, it resets all four session domains and waits for a complete baseline. A custom adapter must not skip arbitrary deltas after a gap and continue calculating.
Committed graphs and JavaScript data are immutable; old states do not observe future reducer changes. Unchanged parts may retain the same reference. Treat C# options as your own immutable records. Keep UI state, animations and selection caches separate and reset them when the source or session changes.
Check readiness, connection and freshness separately
| State | Meaning | Appropriate display |
|---|---|---|
connecting / reconnecting | A complete new session baseline has not yet arrived. | Do not present game, panel, view or options from the old transport as current state. |
connected; ready: false | Transport connected; baseline not fully committed yet. | Keep waiting for readiness; a WebSocket handshake is insufficient. |
ready: true | A complete baseline of all four session domains has been committed. | Check freshness and clock separately; without GSI the graph may still contain no live game block. |
freshness: waiting / unknown | No usable GSI freshness yet, or freshness is unknown. | Prepared match metadata may still be shown; leave live game values unavailable. |
freshness: stale / disconnected | GSI is stale or disconnected under the host policy; the session transport may remain connected. | Mark values as stale, hide them or hold them under an explicit presentation rule. Do not treat them as a new GSI sample. |
hudClock.isReady() / HudClock.IsSynchronized | Valid synchronized clock anchor; independent of session and GSI. | Only then calculate time-dependent running displays. |
stopped | Session ended, locally cancelled or stopped after a terminal options error. | Do not infer an automatic new user session; handle the error and lifetime explicitly. |
Typical configurable GSI host thresholds are 15 seconds to stale and 45 seconds to disconnected. Use the supplied freshness status; do not assume every host has the same configuration. HLAE, panel, view and options messages do not refresh GSI. A valid clock anchor likewise does not make an old round timer fresh.
Panel values, references and one-time actions
Panel state contains values and references, each keyed by section and field name. Text / text preserves an explicitly empty string; Flag / flag distinguishes actual booleans from other JSON types. A fallback is a local presentation decision, not a change to the received value. A reference with missing: true means the selected target is no longer available; do not show data from the previous target.
A HudPanelAction is an individual command with an ID, section, name and optional value, not a persistent checkbox field. Its handler runs after state commit. Actions are not part of the baseline and are not replayed on reconnect. An interruption can miss past actions; the session does not promise later delivery or a durable event history. Avoid replaying commands during rendering.
Panel state, Panel reference and Panel action describe the data; the SDK runtime reference explains subscriptions and error events.
Errors, reset, reconnect and disposal
| Fault or transition | SDK runtime behaviour | HUD responsibility |
|---|---|---|
transport | Connection or transfer is interrupted; all four domains lose readiness and are cleared. | Do not present old live values as new. Wait for a complete fresh baseline. |
protocol | Invalid message or revision base; no partial commit, new baseline through reconnect. | Expose the failure appropriately or record diagnostics; do not guess deltas yourself. |
options | Local deserialization, synchronous options parsing or validation failed. Terminal stop instead of endless retries. | Fix the configuration or options type and create a new session. |
SubscriberFailed / subscriber | A custom selector or listener error is isolated; other subscribers and the valid commit remain intact. | Fix the handler; C# reports through SubscriberFailed, JavaScript through the error channel with kind subscriber. |
DisposeAsync / dispose | Stop the session's owned connection, pending attempts and subscriptions. | Also release your own UI timers, clock listeners and event handlers. Do not restart a disposed instance. |
StartAsync / start launches an instance once without waiting for an online server. Completion / completion tracks its lifetime. Transport and protocol errors wait 1, 2, 5 and then at most 10 seconds between attempts; an accepted baseline resets the retry progression. The SDK channel stays on v3 and does not silently fall back to v2 or periodic game-state requests.
A connection restart does not prove a new map, and a new game context does not replace resetting your own caches. A high render or subscription rate does not create additional source measurements. Use small selectors for data displays, a separate clock cadence for timers and explicit rules for map, session, epoch and freshness in animated transitions.