// Package transport defines the wire format between clients and the server. // // PoC: JSON for fast iteration. Specified by proto/client_server.proto for // the production binary protobuf format. // // TODO PoC: replace JSON with protobuf-generated structs (see // ../proto/client_server.proto). package transport import ( "encoding/json" "fmt" "time" ) // MessageType identifies the kind of payload. type MessageType string const ( // Client -> Server. TypeClientHello MessageType = "client_hello" TypeJoinRace MessageType = "join_race" TypeLeaveRace MessageType = "leave_race" TypeInputState MessageType = "input_state" TypeChatMessage MessageType = "chat_message" TypePing MessageType = "ping" // Client -> Server (lobby). TypeLobbyList MessageType = "lobby_list" TypeLobbyCreate MessageType = "lobby_create" TypeLobbyQuickPlay MessageType = "lobby_quick_play" TypeLobbyJoinRace MessageType = "lobby_join_race" TypeLobbyLeaveRace MessageType = "lobby_leave_race" TypeLobbyKickDriver MessageType = "lobby_kick_driver" TypeLobbyDelete MessageType = "lobby_delete" // Client -> Server (catalog: tracks). TypeTrackList MessageType = "track_list" TypeTrackGet MessageType = "track_get" TypeTrackCreate MessageType = "track_create" TypeTrackUpdate MessageType = "track_update" TypeTrackDelete MessageType = "track_delete" // Client -> Server (catalog: cars). TypeCarList MessageType = "car_list" TypeCarGet MessageType = "car_get" TypeCarCreate MessageType = "car_create" TypeCarUpdate MessageType = "car_update" TypeCarDelete MessageType = "car_delete" // Client -> Server (stats). TypeStatsRequest MessageType = "stats_request" // Server -> Client. TypeServerHello MessageType = "server_hello" TypeRaceSnapshot MessageType = "race_snapshot" TypeInputAck MessageType = "input_ack" TypeCorrection MessageType = "correction" TypeRaceEvent MessageType = "race_event" TypePong MessageType = "pong" TypeError MessageType = "error" // Server -> Client (lobby). TypeLobbySnapshot MessageType = "lobby_snapshot" // Server -> Client (catalog). TypeTrackSnapshot MessageType = "track_snapshot" TypeTrackAck MessageType = "track_ack" TypeCarSnapshot MessageType = "car_snapshot" TypeCarAck MessageType = "car_ack" TypeCatalogSummary MessageType = "catalog_summary" // Server -> Client (stats). TypeStatsSnapshot MessageType = "stats_snapshot" TypeDriverProfile MessageType = "driver_profile" TypeLapRecorded MessageType = "lap_recorded" TypeRaceResult MessageType = "race_result" TypeRaceStandings MessageType = "race_standings" ) // Envelope wraps every WS frame with a type discriminator. type Envelope struct { Type MessageType `json:"type"` Seq uint64 `json:"seq,omitempty"` TSMs int64 `json:"ts_ms,omitempty"` Payload any `json:"payload,omitempty"` } // ClientHello is the first message from a client. type ClientHello struct { Version string `json:"version"` Token string `json:"token,omitempty"` // empty in dev mode Locale string `json:"locale,omitempty"` ClientID string `json:"client_id,omitempty"` // for reconnect } // ServerHello is the response to ClientHello. type ServerHello struct { SessionID string `json:"session_id"` RaceTick uint64 `json:"race_tick"` ServerVersion string `json:"server_version"` Config struct { TickRate int `json:"tick_rate_hz"` SnapshotRate int `json:"snapshot_rate_hz"` MaxCars int `json:"max_cars"` } `json:"config"` } // JoinRace is sent by the client to enter a race session. type JoinRace struct { RaceID string `json:"race_id"` CarSlot int `json:"car_slot"` // 0-3 for PoC (4 cars) CarName string `json:"car_name,omitempty"` } // LeaveRace is sent when leaving a race. type LeaveRace struct { RaceID string `json:"race_id"` } // InputState is the high-rate control packet (60-120 Hz). type InputState struct { Steering float64 `json:"steering"` // -1.0 .. 1.0 Throttle float64 `json:"throttle"` // 0.0 .. 1.0 Brake float64 `json:"brake"` // 0.0 .. 1.0 Gear int `json:"gear"` // -1 reverse, 0 neutral, 1..6 forward Buttons uint32 `json:"buttons,omitempty"` } // ChatMessage for lobby chat (rate-limited). type ChatMessage struct { Text string `json:"text"` } // Ping for latency measurement. type Ping struct { ClientTSMs int64 `json:"client_ts_ms"` } // RaceSnapshot is the authoritative state of the race, broadcast N times/sec. type RaceSnapshot struct { Tick uint64 `json:"tick"` TSMs int64 `json:"ts_ms"` Elapsed float64 `json:"elapsed_s"` Cars []CarInfo `json:"cars"` } // CarInfo describes one car in the race. type CarInfo struct { ID string `json:"id"` DriverName string `json:"driver_name"` X float64 `json:"x"` // metres Y float64 `json:"y"` // metres Heading float64 `json:"heading"` // radians Speed float64 `json:"speed"` // m/s Lap int `json:"lap"` Sector int `json:"sector"` // 0..2 LastLapMs int64 `json:"last_lap_ms"` BestLapMs int64 `json:"best_lap_ms"` InputApplied struct { Steering float64 `json:"steering"` Throttle float64 `json:"throttle"` Brake float64 `json:"brake"` } `json:"input_applied"` DNF bool `json:"dnf"` } // InputAck acknowledges an input packet (lightweight). type InputAck struct { Seq uint64 `json:"seq"` AppliedAtMs int64 `json:"applied_at_ms"` ServerTickMs int64 `json:"server_tick_ms"` } // Correction is sent when client prediction diverged from server. type Correction struct { Tick uint64 `json:"tick"` DeltaSteering float64 `json:"delta_steering"` DeltaThrottle float64 `json:"delta_throttle"` Reason string `json:"reason"` } // RaceEvent for major state transitions. type RaceEvent struct { RaceID string `json:"race_id"` Event string `json:"event"` // "start", "lap", "sector", "dnf", "finish" CarID string `json:"car_id,omitempty"` Sector int `json:"sector,omitempty"` LapMs int64 `json:"lap_ms,omitempty"` TSMs int64 `json:"ts_ms"` } // Pong for RTT measurement. type Pong struct { ClientTSMs int64 `json:"client_ts_ms"` ServerTSMs int64 `json:"server_ts_ms"` } // ErrorMsg is sent on protocol or validation errors. type ErrorMsg struct { Code string `json:"code"` Message string `json:"message"` } // ------------------------------------------------------------------------- // Lobby payloads // ------------------------------------------------------------------------- // LobbyRace is the wire form of lobby.RaceMeta (kept separate to avoid // a circular import between transport and lobby). type LobbyRace struct { ID string `json:"id"` Name string `json:"name"` TrackID string `json:"track_id"` MaxCars int `json:"max_cars"` Laps int `json:"laps"` TimeLimitS int `json:"time_limit_s"` DriverIDs []string `json:"driver_ids"` Status string `json:"status"` CreatedMs int64 `json:"created_ms"` StartedMs int64 `json:"started_ms,omitempty"` // Podium is populated only for finished races. Empty for live ones. Podium []RacePodiumEntry `json:"podium,omitempty"` } // RacePodiumEntry is one row of a finished race's top-3. type RacePodiumEntry struct { Position int `json:"position" example:"1"` DriverID string `json:"driver_id" example:"driver-alice"` Name string `json:"name" example:"driver-alice"` TotalTimeMs int64 `json:"total_time_ms" example:"123456"` Nickname string `json:"nickname" example:"driver-alice"` AvatarUrl string `json:"avatar_url"` ClanID string `json:"clan_id" example:"clan-bob"` } // VersionResponse is the body of the version API. type VersionResponse struct { Version string `json:"version" example:"0.1.0-poc"` } // WebRTCConnectRequest is the body of the WebRTC connection API. type WebRTCConnectRequest struct { SDP string `json:"sdp"` Type string `json:"type" example:"offer"` DriverID string `json:"driver_id" example:"driver-alice"` } // WebRTCConnectResponse is the response to WebRTCConnectRequest. type WebRTCConnectResponse struct { SDP string `json:"sdp"` Type string `json:"type" example:"answer"` } // LobbyDriver is the wire form of lobby.DriverMeta. type LobbyDriver struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` // idle | racing | offline RaceID string `json:"race_id,omitempty"` ConnectedMs int64 `json:"connected_ms"` LastSeenMs int64 `json:"last_seen_ms"` Country string `json:"country,omitempty"` AvatarHue int `json:"avatar_hue,omitempty"` } // LobbySnapshot is the full lobby state, broadcast on change. type LobbySnapshot struct { GeneratedMs int64 `json:"generated_ms"` Version uint64 `json:"version"` Races []LobbyRace `json:"races"` Drivers []LobbyDriver `json:"drivers"` } // LobbyCreate is sent by a client to create a race (becomes host). type LobbyCreate struct { RaceID string `json:"race_id,omitempty"` Name string `json:"name"` TrackID string `json:"track_id,omitempty"` MaxCars int `json:"max_cars,omitempty"` Laps int `json:"laps,omitempty"` TimeLimitS int `json:"time_limit_s,omitempty"` } // LobbyJoinRace is sent by a client to join a specific race. type LobbyJoinRace struct { RaceID string `json:"race_id"` CarSlot int `json:"car_slot,omitempty"` CarName string `json:"car_name,omitempty"` } // LobbyLeaveRace moves a driver from race back to lobby. type LobbyLeaveRace struct { RaceID string `json:"race_id,omitempty"` } // LobbyKickDriver is sent by the host to remove another driver. type LobbyKickDriver struct { RaceID string `json:"race_id"` DriverID string `json:"driver_id"` } // LobbyDelete removes the race entirely (host only). type LobbyDelete struct { RaceID string `json:"race_id"` } // LobbyCreatedAck confirms a successful lobby_create. type LobbyCreatedAck struct { Ok bool `json:"ok"` Race LobbyRace `json:"race,omitempty"` Error string `json:"error,omitempty"` Driver LobbyDriver `json:"driver,omitempty"` } // LobbyJoinAck confirms lobby_join_race / lobby_quick_play. type LobbyJoinAck struct { Ok bool `json:"ok"` Race LobbyRace `json:"race,omitempty"` Error string `json:"error,omitempty"` } // ------------------------------------------------------------------------- // Catalog payloads (tracks and cars) // ------------------------------------------------------------------------- // TrackWaypoint is the wire form of catalog.Waypoint. type TrackWaypoint struct { X float64 `json:"x"` Y float64 `json:"y"` HeadingRad float64 `json:"heading_rad"` SpeedMs float64 `json:"speed_ms"` Curvature float64 `json:"curvature"` } // TrackBounds is the wire form of catalog.Bounds. type TrackBounds struct { MinX float64 `json:"min_x"` MinY float64 `json:"min_y"` MaxX float64 `json:"max_x"` MaxY float64 `json:"max_y"` LengthM float64 `json:"length_m"` WidthM float64 `json:"width_m"` } // TrackWire is the wire form of catalog.TrackMeta. type TrackWire struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description,omitempty"` AuthorID string `json:"author_id"` Visibility string `json:"visibility"` Scale int `json:"scale"` LengthM float64 `json:"length_m"` WidthM float64 `json:"width_m"` LaneWidthM float64 `json:"lane_width_m"` Bounds TrackBounds `json:"bounds"` Surface string `json:"surface"` Tags []string `json:"tags,omitempty"` Centerline []TrackWaypoint `json:"centerline"` BestLapMs int64 `json:"best_lap_ms"` BestLapHolder string `json:"best_lap_holder,omitempty"` CreatedMs int64 `json:"created_ms"` UpdatedMs int64 `json:"updated_ms"` } // TrackListRequest: payload { "scope": "all|system|public|private", "tag": "..." }. type TrackListRequest struct { Scope string `json:"scope,omitempty"` Tag string `json:"tag,omitempty"` } // TrackGetRequest: payload { "id": "..." }. type TrackGetRequest struct { ID string `json:"id"` } // TrackCreateRequest: full TrackWire (server fills ID if empty). type TrackCreateRequest struct { Track TrackWire `json:"track"` } // TrackUpdateRequest: ID + patch fields. type TrackUpdateRequest struct { ID string `json:"id"` Patch TrackPatch `json:"patch"` } // TrackPatch: any field may be omitted (empty string / zero means unchanged). type TrackPatch struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Visibility *string `json:"visibility,omitempty"` Scale *int `json:"scale,omitempty"` LengthM *float64 `json:"length_m,omitempty"` WidthM *float64 `json:"width_m,omitempty"` LaneWidthM *float64 `json:"lane_width_m,omitempty"` Surface *string `json:"surface,omitempty"` Tags *[]string `json:"tags,omitempty"` Centerline *[]TrackWaypoint `json:"centerline,omitempty"` } // TrackDeleteRequest: payload { "id": "..." }. type TrackDeleteRequest struct { ID string `json:"id"` } // TrackSnapshot: full catalog tracks feed (broadcast on change). type TrackSnapshot struct { GeneratedMs int64 `json:"generated_ms"` Version uint64 `json:"version"` Tracks []TrackWire `json:"tracks"` } // TrackAck: response to create/update/delete operations. type TrackAck struct { Ok bool `json:"ok"` ID string `json:"id,omitempty"` Error string `json:"error,omitempty"` Track TrackWire `json:"track,omitempty"` } // MotorWire is the wire form of catalog.MotorSpec. type MotorWire struct { Kind string `json:"kind"` Class string `json:"class"` KV int `json:"kv"` PowerW float64 `json:"power_w"` } // BatteryWire is the wire form of catalog.BatterySpec. type BatteryWire struct { VoltageV float64 `json:"voltage_v"` CapacityMah int `json:"capacity_mah"` Cells int `json:"cells"` Chemistry string `json:"chemistry"` } // ChassisWire is the wire form of catalog.ChassisSpec. type ChassisWire struct { Model string `json:"model"` Material string `json:"material"` Printed bool `json:"printed"` WheelbaseMm float64 `json:"wheelbase_mm"` TrackMm float64 `json:"track_mm"` } // CarWire is the wire form of catalog.CarMeta. type CarWire struct { ID string `json:"id"` Name string `json:"name"` OwnerID string `json:"owner_id"` Visibility string `json:"visibility"` Scale int `json:"scale"` LengthMm float64 `json:"length_mm"` WidthMm float64 `json:"width_mm"` HeightMm float64 `json:"height_mm"` WeightG float64 `json:"weight_g"` Chassis ChassisWire `json:"chassis"` Motor MotorWire `json:"motor"` Battery BatteryWire `json:"battery"` Drive string `json:"drive"` TopSpeedMs float64 `json:"top_speed_ms"` ColorHex string `json:"color_hex"` AvatarURL string `json:"avatar_url,omitempty"` Active bool `json:"active"` TotalDistanceM float64 `json:"total_distance_m"` TotalRaces int `json:"total_races"` TotalLaps int `json:"total_laps"` BestLapMs int64 `json:"best_lap_ms"` CreatedMs int64 `json:"created_ms"` UpdatedMs int64 `json:"updated_ms"` } // CarListRequest: payload { "scope": "all|system|public|private", "owner_id": "..." }. type CarListRequest struct { Scope string `json:"scope,omitempty"` OwnerID string `json:"owner_id,omitempty"` } // CarGetRequest: payload { "id": "..." }. type CarGetRequest struct { ID string `json:"id"` } // CarCreateRequest: full CarWire (server fills ID if empty). type CarCreateRequest struct { Car CarWire `json:"car"` } // CarUpdateRequest: ID + patch fields. type CarUpdateRequest struct { ID string `json:"id"` Patch CarPatch `json:"patch"` } // CarPatch: any field may be omitted (nil pointer = unchanged). type CarPatch struct { Name *string `json:"name,omitempty"` Visibility *string `json:"visibility,omitempty"` Scale *int `json:"scale,omitempty"` LengthMm *float64 `json:"length_mm,omitempty"` WidthMm *float64 `json:"width_mm,omitempty"` HeightMm *float64 `json:"height_mm,omitempty"` WeightG *float64 `json:"weight_g,omitempty"` Chassis *ChassisWire `json:"chassis,omitempty"` Motor *MotorWire `json:"motor,omitempty"` Battery *BatteryWire `json:"battery,omitempty"` Drive *string `json:"drive,omitempty"` TopSpeedMs *float64 `json:"top_speed_ms,omitempty"` ColorHex *string `json:"color_hex,omitempty"` AvatarURL *string `json:"avatar_url,omitempty"` Active *bool `json:"active,omitempty"` } // CarDeleteRequest: payload { "id": "..." }. type CarDeleteRequest struct { ID string `json:"id"` } // CarSnapshot: full catalog cars feed (broadcast on change). type CarSnapshot struct { GeneratedMs int64 `json:"generated_ms"` Version uint64 `json:"version"` Cars []CarWire `json:"cars"` } // CarAck: response to create/update/delete operations. type CarAck struct { Ok bool `json:"ok"` ID string `json:"id,omitempty"` Error string `json:"error,omitempty"` Car CarWire `json:"car,omitempty"` } // CatalogSummary: lightweight counters (for header chips). type CatalogSummary struct { TracksTotal int `json:"tracks_total"` TracksSystem int `json:"tracks_system"` TracksPublic int `json:"tracks_public"` TracksPrivate int `json:"tracks_private"` CarsTotal int `json:"cars_total"` CarsSystem int `json:"cars_system"` CarsPublic int `json:"cars_public"` CarsPrivate int `json:"cars_private"` CarsActive int `json:"cars_active"` TSMs int64 `json:"ts_ms"` } // ------------------------------------------------------------------------- // Races: list / upcoming / queue / plans // ------------------------------------------------------------------------- // RaceListResponse is the keyset-paginated body returned by /api/races. type RaceListResponse struct { Items []LobbyRace `json:"items"` NextCursor string `json:"next_cursor,omitempty" example:"MTcwMDAwMDAwMDAwMDpyYWNlLWFiYzpmaW5pc2hlZA"` Count int `json:"count"` } // RaceUpcomingItem is one row in the upcoming list. type RaceUpcomingItem struct { Meta LobbyRace `json:"meta"` QueueLen int `json:"queue_len"` PlanID string `json:"plan_id,omitempty"` StartAtMs int64 `json:"start_at_ms"` } // RaceUpcomingResponse is the body returned by /api/races/upcoming. type RaceUpcomingResponse struct { Items []RaceUpcomingItem `json:"items"` Count int `json:"count"` } // RaceQueueEntry is one (driver, race) subscription. type RaceQueueEntry struct { DriverID string `json:"driver_id" example:"driver-42"` RaceID string `json:"race_id" example:"race-1730000-7"` PlanID string `json:"plan_id,omitempty" example:"plan-1730000-1"` EnqueuedMs int64 `json:"enqueued_ms" example:"1730000000000"` } // RaceQueueJoinRequest is the body of POST /api/races/queue/join. // Either driver_id (body) or X-Driver-Id header is accepted. type RaceQueueJoinRequest struct { DriverID string `json:"driver_id" example:"driver-42"` Limit int `json:"limit,omitempty" example:"3"` } // RaceQueueJoinResponse is what /api/races/queue/join returns. type RaceQueueJoinResponse struct { Joined []RaceQueueEntry `json:"joined"` Skipped []string `json:"skipped,omitempty" example:"race-1,race-2"` } // RaceQueueListResponse is what GET /api/races/queue returns. type RaceQueueListResponse struct { Items []RaceQueueEntry `json:"items"` Count int `json:"count"` } // RacePlan is the wire form of races.RacePlan. type RacePlan struct { ID string `json:"id" example:"plan-1730000-1"` Name string `json:"name" example:"Daily Monza"` TrackID string `json:"track_id" example:"default"` MaxCars int `json:"max_cars" example:"4"` Laps int `json:"laps" example:"10"` TimeLimitS int `json:"time_limit_s,omitempty"` StartAtMs int64 `json:"start_at_ms" example:"1730000000000"` IntervalS int `json:"interval_s,omitempty" example:"3600"` Count int `json:"count,omitempty" example:"0"` Enabled bool `json:"enabled" example:"true"` CreatedMs int64 `json:"created_ms"` UpdatedMs int64 `json:"updated_ms"` NextFireMs int64 `json:"next_fire_ms"` FiresDone int `json:"fires_done"` } // RacePlanCreateRequest is the body of POST /api/races/plans. type RacePlanCreateRequest struct { ID string `json:"id,omitempty" example:"plan-1730000-1"` Name string `json:"name" example:"Daily Monza"` TrackID string `json:"track_id,omitempty" example:"default"` MaxCars int `json:"max_cars" example:"4"` Laps int `json:"laps,omitempty" example:"10"` TimeLimitS int `json:"time_limit_s,omitempty"` StartAtMs int64 `json:"start_at_ms" example:"1730000000000"` IntervalS int `json:"interval_s,omitempty" example:"3600"` Count int `json:"count,omitempty" example:"0"` Enabled *bool `json:"enabled,omitempty" example:"true"` } // RacePlanListResponse is the keyset-paginated body returned by GET /api/races/plans. type RacePlanListResponse struct { Items []RacePlan `json:"items"` NextCursor string `json:"next_cursor,omitempty"` Count int `json:"count"` } // ------------------------------------------------------------------------- // Drivers and clans // ------------------------------------------------------------------------- // Driver is the wire form of a driver record. type Driver struct { ID string `json:"id" example:"driver-1782000-123"` Nickname string `json:"nickname" example:"ACE"` Name string `json:"name" example:"Alice"` AvatarURL string `json:"avatar_url,omitempty" example:"https://cdn.example.com/u/ace.png"` ClanID string `json:"clan_id,omitempty" example:"clan-1782000-1"` CreatedMs int64 `json:"created_ms"` UpdatedMs int64 `json:"updated_ms"` } // DriverCreateRequest is the body of POST /api/drivers. type DriverCreateRequest struct { ID string `json:"id,omitempty" example:"driver-1782000-123"` Nickname string `json:"nickname" example:"ACE"` Name string `json:"name" example:"Alice"` AvatarURL string `json:"avatar_url,omitempty" example:"https://cdn.example.com/u/ace.png"` ClanID string `json:"clan_id,omitempty" example:"clan-1782000-1"` } // DriverUpdateRequest is the body of PUT /api/drivers/{id}. type DriverUpdateRequest struct { Name string `json:"name,omitempty" example:"Alice"` AvatarURL string `json:"avatar_url,omitempty" example:"https://cdn.example.com/u/ace.png"` ClanID *string `json:"clan_id,omitempty" example:"clan-1782000-1"` } // DriverListResponse is the body returned by GET /api/drivers. type DriverListResponse struct { Items []Driver `json:"items"` Count int `json:"count"` } // Clan is the wire form of a clan record. type Clan struct { ID string `json:"id" example:"clan-1782000-1"` Tag string `json:"tag" example:"ACE"` Name string `json:"name" example:"Ace Racing"` AvatarURL string `json:"avatar_url,omitempty" example:"https://cdn.example.com/c/ace.png"` CreatedMs int64 `json:"created_ms"` UpdatedMs int64 `json:"updated_ms"` } // ClanCreateRequest is the body of POST /api/clans. type ClanCreateRequest struct { ID string `json:"id,omitempty" example:"clan-1782000-1"` Tag string `json:"tag" example:"ACE"` Name string `json:"name" example:"Ace Racing"` AvatarURL string `json:"avatar_url,omitempty" example:"https://cdn.example.com/c/ace.png"` } // ClanUpdateRequest is the body of PUT /api/clans/{id}. type ClanUpdateRequest struct { Name string `json:"name,omitempty" example:"Ace Racing"` AvatarURL string `json:"avatar_url,omitempty" example:"https://cdn.example.com/c/ace.png"` } // ClanListResponse is the body returned by GET /api/clans. type ClanListResponse struct { Items []Clan `json:"items"` Count int `json:"count"` } // ------------------------------------------------------------------------- // Stats payloads // ------------------------------------------------------------------------- // StatsServer mirrors stats.ServerStats for the wire. type StatsServer struct { StartedMs int64 `json:"started_ms"` UptimeMs int64 `json:"uptime_ms"` CurrentClients int64 `json:"current_clients"` PeakClients int64 `json:"peak_clients"` TotalConnections uint64 `json:"total_connections"` TotalDisconnects uint64 `json:"total_disconnects"` TotalRacesCreated uint64 `json:"total_races_created"` TotalRacesStarted uint64 `json:"total_races_started"` TotalRacesFinished uint64 `json:"total_races_finished"` TotalLapsRecorded uint64 `json:"total_laps_recorded"` TotalInputsRecv uint64 `json:"total_inputs_received"` TotalSnapshotsSent uint64 `json:"total_snapshots_sent"` SnapshotsDropped uint64 `json:"snapshots_dropped"` AvgRTTMs float64 `json:"avg_rtt_ms"` } // StatsDriver mirrors stats.DriverProfile for the wire. type StatsDriver struct { ID string `json:"id"` Name string `json:"name"` TotalRacesStarted uint64 `json:"total_races_started"` TotalRacesFinished uint64 `json:"total_races_finished"` TotalLaps uint64 `json:"total_laps"` BestLapMs int64 `json:"best_lap_ms"` LastLapMs int64 `json:"last_lap_ms"` TotalDistanceM float64 `json:"total_distance_m"` TotalPlaytimeMs int64 `json:"total_playtime_ms"` Wins uint64 `json:"wins"` Podiums uint64 `json:"podiums"` LastSeenMs int64 `json:"last_seen_ms"` } // StatsLap is one lap in the recent-laps feed. type StatsLap struct { RaceID string `json:"race_id"` DriverID string `json:"driver_id"` LapMs int64 `json:"lap_ms"` Sector1Ms int64 `json:"sector1_ms"` Sector2Ms int64 `json:"sector2_ms"` Sector3Ms int64 `json:"sector3_ms"` Position int `json:"position,omitempty"` TSMs int64 `json:"ts_ms"` } // StatsRaceResult is the final classification of a race for one driver. type StatsRaceResult struct { RaceID string `json:"race_id"` DriverID string `json:"driver_id"` FinishedAtMs int64 `json:"finished_at_ms"` Position int `json:"position"` TotalLaps int `json:"total_laps"` BestLapMs int64 `json:"best_lap_ms"` TotalTimeMs int64 `json:"total_time_ms"` DNF bool `json:"dnf"` } // StatsSnapshot is the full server-side stats payload. type StatsSnapshot struct { Server StatsServer `json:"server"` Drivers []StatsDriver `json:"drivers"` Laps []StatsLap `json:"laps"` Results []StatsRaceResult `json:"results"` TSMs int64 `json:"ts_ms"` } // StatsRequest is sent by clients to request a stats snapshot. // Scope: "server" | "driver" | "race". type StatsRequest struct { Scope string `json:"scope"` TargetID string `json:"target_id,omitempty"` } // RaceStanding is one row in mid-race standings. type RaceStanding struct { Position int `json:"position"` CarID string `json:"car_id"` DriverID string `json:"driver_id"` DriverName string `json:"driver_name"` Lap int `json:"lap"` Sector int `json:"sector"` BestLapMs int64 `json:"best_lap_ms"` LastLapMs int64 `json:"last_lap_ms"` GapMs int64 `json:"gap_ms"` DNF bool `json:"dnf"` } // RaceStandingsMsg is broadcast periodically during a race. type RaceStandingsMsg struct { RaceID string `json:"race_id"` Tick uint64 `json:"tick"` TSMs int64 `json:"ts_ms"` Lap int `json:"lap"` // leader lap Elapsed float64 `json:"elapsed_s"` Rows []RaceStanding `json:"rows"` } // LapRecordedMsg is sent when a driver completes a lap. type LapRecordedMsg struct { RaceID string `json:"race_id"` DriverID string `json:"driver_id"` Lap int `json:"lap"` LapMs int64 `json:"lap_ms"` Sector1Ms int64 `json:"sector1_ms"` Sector2Ms int64 `json:"sector2_ms"` Sector3Ms int64 `json:"sector3_ms"` TSMs int64 `json:"ts_ms"` Position int `json:"position,omitempty"` } // RaceResultMsg is sent for each driver when a race finishes. type RaceResultMsg struct { RaceID string `json:"race_id"` DriverID string `json:"driver_id"` Position int `json:"position"` TotalLaps int `json:"total_laps"` BestLapMs int64 `json:"best_lap_ms"` TotalTimeMs int64 `json:"total_time_ms"` DNF bool `json:"dnf"` FinishedAtMs int64 `json:"finished_at_ms"` } // Encode marshals an envelope to JSON bytes. func Encode(env *Envelope) ([]byte, error) { if env.TSMs == 0 { env.TSMs = time.Now().UnixMilli() } return json.Marshal(env) } // Decode unmarshals JSON bytes into an envelope (payload kept as json.RawMessage). func Decode(data []byte) (*Envelope, error) { var env Envelope if err := json.Unmarshal(data, &env); err != nil { return nil, fmt.Errorf("decode envelope: %w", err) } return &env, nil }