mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-16 20:47:55 +00:00
858 lines
29 KiB
Go
858 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"`
|
|
}
|
|
|
|
// 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
|
|
}
|