Files
x0gp/server/internal/transport/codec.go
T

846 lines
29 KiB
Go

// 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"`
}
// 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"`
DeviceID *int `json:"device_id,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
}