10 Commits
54 changed files with 6426 additions and 1485 deletions
+2
View File
@@ -11,3 +11,5 @@ server/poc-server.exe
.grepai/ .grepai/
*.blend1 *.blend1
*.blend2 *.blend2
node_modules/
+15 -1
View File
@@ -43,6 +43,17 @@
- Go 1.26, бинарь `cmd/poc-server`. Сборка: `go build ./...` или `go build -o poc-server.exe ./cmd/poc-server`. - Go 1.26, бинарь `cmd/poc-server`. Сборка: `go build ./...` или `go build -o poc-server.exe ./cmd/poc-server`.
- HTTP+WebSocket, роутер `http.NewServeMux`. Запуск требует `DATABASE_URL` (см. `.env`). - HTTP+WebSocket, роутер `http.NewServeMux`. Запуск требует `DATABASE_URL` (см. `.env`).
- **База данных (Supabase)**:
- Используется self-hosted Supabase на порту `3402` (хост: `80.234.93.161:3402`).
- Расширение `timescaledb` отключено (закомментировано в `001_init.sql`), так как Supabase не поставляет его из коробки.
- Нативная папка `supabase/` содержит `config.toml`, объединенный дамп схемы `supabase/migrations/20260717000000_schema.sql` и каталог семян `supabase/seed.sql` (включает рескейл Монако).
- Объединенный дамп схемы автоматически заполняет таблицу `schema_migrations`, чтобы предотвратить ошибки повторного применения миграций Go-сервером на старте.
- **Абстракция данных (Store)**:
- Пакеты `clans` и `drivers` используют интерфейсы `Store` вместо прямого импорта `*PgStore`.
- Это позволяет писать юнит-тесты сервисов без подключения к реальной БД (через `memoryStore`).
- **Слепок структуры (Graphify)**:
- Утилита `scripts/graphify/main.go` собирает описание всех экспортируемых пакетов, структур, интерфейсов и функций в компактный файл [server/CODEBASE.md](file:///d:/x0gp/server/CODEBASE.md).
- Рекомендуется запускать `go run ./scripts/graphify/main.go` после значимых изменений API для поддержания карты проекта в актуальном состоянии и экономии токенов при чтении агентом.
- **Swagger UI**: `GET /swagger/index.html`, спека `GET /swagger/doc.json` (14 paths, 29 definitions). - **Swagger UI**: `GET /swagger/index.html`, спека `GET /swagger/doc.json` (14 paths, 29 definitions).
- Генерация спеки: `swag init -g cmd/poc-server/main.go -o docs --parseInternal` из `D:/x0gp/server`. CLI ставится через `go install github.com/swaggo/swag/cmd/swag@v1.16.4`. - Генерация спеки: `swag init -g cmd/poc-server/main.go -o docs --parseInternal` из `D:/x0gp/server`. CLI ставится через `go install github.com/swaggo/swag/cmd/swag@v1.16.4`.
- Аннотации swag живут над хендлерами в `cmd/poc-server/main.go`, `catalog_handlers.go`, `races_handlers.go`. При добавлении нового REST-эндпоинта — добавить `@Router`/`@Param`/`@Success` и перегенерить `docs/`. - Аннотации swag живут над хендлерами в `cmd/poc-server/main.go`, `catalog_handlers.go`, `races_handlers.go`. При добавлении нового REST-эндпоинта — добавить `@Router`/`@Param`/`@Success` и перегенерить `docs/`.
@@ -56,6 +67,8 @@
### API гонок (keyset-пагинация) ### API гонок (keyset-пагинация)
- `GET /api/races` — список (live + finished), фильтры `status,track_id`, `cursor`, `limit` (1..200, default 50). Status enum: `all|lobby|racing|finished`. **Кастомных лобби нет** — гонки привязаны к физическим трекам, фильтр только по `track_id`. Cursor — base64(`ms:id[:src]`). - `GET /api/races` — список (live + finished), фильтры `status,track_id`, `cursor`, `limit` (1..200, default 50). Status enum: `all|lobby|racing|finished`. **Кастомных лобби нет** — гонки привязаны к физическим трекам, фильтр только по `track_id`. Cursor — base64(`ms:id[:src]`).
- `GET /api/races/last` — результаты (podium/details) последнего законченного заезда.
- `GET /api/races/{id}` — результаты (podium/details) или статус конкретного заезда по ID (работает для активных и завершенных гонок).
- `GET /api/races/upcoming?limit=3` — ближайшие lobby|countdown. - `GET /api/races/upcoming?limit=3` — ближайшие lobby|countdown.
- `POST /api/races/queue/join` — встать в очередь на следующие N (body: `driver_id,limit` или header `X-Driver-Id`). - `POST /api/races/queue/join` — встать в очередь на следующие N (body: `driver_id,limit` или header `X-Driver-Id`).
- `GET /api/races/queue?driver_id=...` — посмотреть. - `GET /api/races/queue?driver_id=...` — посмотреть.
@@ -69,7 +82,8 @@
- CRUD: - CRUD:
- `POST/GET /api/clans`, `GET/PUT/DELETE /api/clans/{id}`, `GET /api/clans/by-tag/{tag}` - `POST/GET /api/clans`, `GET/PUT/DELETE /api/clans/{id}`, `GET /api/clans/by-tag/{tag}`
- `POST/GET /api/drivers`, `GET/PUT/DELETE /api/drivers/{id}`, `GET /api/drivers/by-nick/{nick}` - `POST/GET /api/drivers`, `GET/PUT/DELETE /api/drivers/{id}`, `GET /api/drivers/by-nick/{nick}`
- `PUT /api/drivers/{id}` принимает `clan_id` как `*string` (nil = оставить, "" = очистить, иначе = назначить). - `GET /api/drivers/{id}/profile` — профиль с агрегированной статистикой (всего стартов/финишей, победы, подиумы, лучшее время круга, суммарное время на треке) и сводками лучшей и последней завершенной гонки. Можно вызвать как `/api/drivers/me/profile` для получения профиля авторизованного гонщика.
- `PUT /api/drivers/{id}` принимает `nickname` (3 уникальные заглавные латинские буквы), `name`, `avatar_url` и `clan_id` как `*string` (nil = оставить, "" = очистить, иначе = назначить). Редактирование и удаление чужого профиля запрещено при `DevMode = false`.
- Сидер (через `--seed-races`) заполняет 3 клана и 8 водителей, треки берутся из `tracks` каталога (никаких выдуманных id в `finished_races`/`race_plans`). - Сидер (через `--seed-races`) заполняет 3 клана и 8 водителей, треки берутся из `tracks` каталога (никаких выдуманных id в `finished_races`/`race_plans`).
- Podium в `LobbyRace` заполняется только для finished-гонок (top-3: position, driver_id, name, total_time_ms). - Podium в `LobbyRace` заполняется только для finished-гонок (top-3: position, driver_id, name, total_time_ms).
+93 -36
View File
@@ -1,6 +1,16 @@
#include "esp_camera.h" #include "esp_camera.h"
#include <WiFi.h> #include <WiFi.h>
#include <WiFiUdp.h> #include <WiFiUdp.h>
#include <ESP32Servo.h>
// === НАСТРОЙКА РЕЖИМА ЗАГЛУШКИ ===
const bool USE_MOCK_IF_NO_CAMERA = true; // true - слать черный экран, false - ничего не слать
Servo myServo;
const int servoPin = 13; // ПРИОРИТЕТ: Пин жестко закреплен за сервоприводом
// !!! УНИКАЛЬНЫЙ ID ДЛЯ КАЖДОЙ ПЛАТЫ
const uint8_t DEVICE_ID = 1;
// Конфигурация пинов (пример для Seeed Studio XIAO ESP32S3 Sense) // Конфигурация пинов (пример для Seeed Studio XIAO ESP32S3 Sense)
#define PWDN_GPIO_NUM -1 #define PWDN_GPIO_NUM -1
@@ -10,7 +20,7 @@
#define SIOC_GPIO_NUM 39 #define SIOC_GPIO_NUM 39
#define Y9_GPIO_NUM 48 #define Y9_GPIO_NUM 48
#define Y8_GPIO_NUM 47 #define Y8_GPIO_NUM 47
#define Y7_GPIO_NUM 13 #define Y7_GPIO_NUM 18 // ИЗМЕНЕНО: Отключаем этот пин для камеры, отдаем под Серво
#define Y6_GPIO_NUM 14 #define Y6_GPIO_NUM 14
#define Y5_GPIO_NUM 12 #define Y5_GPIO_NUM 12
#define Y4_GPIO_NUM 11 #define Y4_GPIO_NUM 11
@@ -20,24 +30,45 @@
#define HREF_GPIO_NUM 38 #define HREF_GPIO_NUM 38
#define PCLK_GPIO_NUM 37 #define PCLK_GPIO_NUM 37
const char* ssid = "RT-WiFi-1712"; const char* ssid = "RT-WiFi-1712";
const char* password = "yt3xihY2Ci"; const char* password = "yt3xihY2Ci";
const char* serverIP = "192.168.0.11"; const char* serverIP = "192.168.0.10";
const int serverPort = 9999; const int serverPort = 9999;
const int localPort = 8888; const int localPort = 8888;
// !!! УНИКАЛЬНЫЙ ID ДЛЯ КАЖДОЙ ПЛАТЫ (например, 1 для первой, 2 для второй и т.д.)
const uint8_t DEVICE_ID = 1;
WiFiUDP udpVideo; WiFiUDP udpVideo;
WiFiUDP udpCmd; WiFiUDP udpCmd;
camera_config_t config; camera_config_t config;
// Флаг успешной инициализации камеры
bool isCameraInitialized = false;
// Моковый минимальный валидный JPEG черного цвета
const uint8_t mockBlackJpeg[] = {
0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x00, 0x60,
0x00, 0x60, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x0B, 0x08, 0x00, 0x08,
0x00, 0x08, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00, 0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0xFF, 0xC4, 0x00, 0x14,
0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3F, 0x00, 0x00, 0x3F, 0xFF, 0xD9
};
const size_t mockSize = sizeof(mockBlackJpeg);
void setup() { void setup() {
Serial.begin(115200); Serial.begin(115200);
// Настройка камеры (QVGA для стабильности)
// Инициализируем сервопривод в первую очередь
myServo.attach(servoPin, 500, 2500);
Serial.println("Сервопривод инициализирован на пине 13");
// Настройка конфигурации камеры
config.ledc_channel = LEDC_CHANNEL_0; config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0; config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM; config.pin_d0 = Y2_GPIO_NUM;
@@ -45,7 +76,7 @@ void setup() {
config.pin_d2 = Y4_GPIO_NUM; config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM; config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM; config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM; config.pin_d5 = Y7_GPIO_NUM; // Передаст -1, библиотека не тронет 13 пин
config.pin_d6 = Y8_GPIO_NUM; config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM; config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM; config.pin_xclk = XCLK_GPIO_NUM;
@@ -62,7 +93,15 @@ void setup() {
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_count = 2; config.fb_count = 2;
esp_camera_init(&config); // Проверяем статус инициализации камеры
esp_err_t err = esp_camera_init(&config);
if (err == ESP_OK) {
isCameraInitialized = true;
Serial.println("Камера успешно инициализирована!");
} else {
isCameraInitialized = false;
Serial.printf("Камера не найдена (код 0x%x). Режим мок-данных: %s\n", err, USE_MOCK_IF_NO_CAMERA ? "ВКЛ" : "ВЫКЛ");
}
// Подключение к Wi-Fi // Подключение к Wi-Fi
WiFi.begin(ssid, password); WiFi.begin(ssid, password);
@@ -79,43 +118,58 @@ void setup() {
void checkIncomingCommands() { void checkIncomingCommands() {
int packetSize = udpCmd.parsePacket(); int packetSize = udpCmd.parsePacket();
if (packetSize > 0) { if (packetSize == 5) {
char packetBuffer[255]; uint8_t packetBuffer[5];
int len = udpCmd.read(packetBuffer, 255); int len = udpCmd.read(packetBuffer, 5);
if (len > 0) { packetBuffer[len] = 0; } if (len == 5) {
uint8_t steer = packetBuffer[0];
uint8_t throttle = packetBuffer[1];
uint8_t brake = packetBuffer[2];
uint8_t gear = packetBuffer[3];
uint8_t flags = packetBuffer[4];
String command = String(packetBuffer); // ИСПРАВЛЕНО: Инвертирован диапазон (180, 0 вместо 0, 180) для исправления направления
command.trim(); int servoAngle = map(steer, 0, 200, 180, 0);
myServo.write(servoAngle);
Serial.print("Получена команда: "); Serial.printf("Got control packet: Steer=%d (Angle=%d), Throttle=%d%%, Brake=%d%%, Gear=%d, Flags=0x%02X\n",
Serial.println(command); steer, servoAngle, throttle, brake, gear, flags);
Serial.print("Получена команда: ");
Serial.println(command);
// Логика управления
if (command == "left") {
// Ваш код поворота налево
} else if (command == "right") {
// Ваш код поворота направо
} else if (command == "start") {
// Ваш код запуска моторов / логики
} else if (command == "stop") {
// Ваш код остановки
} }
} else if (packetSize > 0) {
char garbage[255];
udpCmd.read(garbage, sizeof(garbage));
} }
} }
void loop() { void loop() {
checkIncomingCommands(); checkIncomingCommands();
camera_fb_t * fb = esp_camera_fb_get(); uint8_t * buf = nullptr;
if (!fb) return; size_t bufferSize = 0;
camera_fb_t * fb = nullptr;
size_t bufferSize = fb->len; // Пытаемся получить кадр, если камера запущена
uint8_t * buf = fb->buf; if (isCameraInitialized) {
fb = esp_camera_fb_get();
if (fb) {
bufferSize = fb->len;
buf = fb->buf;
}
}
// Размер куска данных уменьшаем до 1023, чтобы вместе с 1 байтом ID пакет был 1024 байта // Если кадра с камеры нет
if (!buf) {
// Если отправка заглушки отключена — выходим из цикла, продолжая слушать команды
if (!USE_MOCK_IF_NO_CAMERA) {
delay(10);
return;
}
// Если включена — подставляем моковый черный кадр
bufferSize = mockSize;
buf = (uint8_t *)mockBlackJpeg;
}
// Нарезка и отправка по UDP (оригинальная логика)
size_t chunkSize = 1023; size_t chunkSize = 1023;
uint8_t packetBuffer[1024]; uint8_t packetBuffer[1024];
@@ -125,15 +179,18 @@ void loop() {
sendSize = bufferSize - i; sendSize = bufferSize - i;
} }
// Формируем пакет: первый байт — ID, дальше — видеоданные
packetBuffer[0] = DEVICE_ID; packetBuffer[0] = DEVICE_ID;
memcpy(packetBuffer + 1, buf + i, sendSize); memcpy(packetBuffer + 1, buf + i, sendSize);
udpVideo.beginPacket(serverIP, serverPort); udpVideo.beginPacket(serverIP, serverPort);
udpVideo.write(packetBuffer, sendSize + 1); // Отправляем данные вместе с ID udpVideo.write(packetBuffer, sendSize + 1);
udpVideo.endPacket(); udpVideo.endPacket();
} }
// Возвращаем буфер только если он реальный
if (fb) {
esp_camera_fb_return(fb); esp_camera_fb_return(fb);
}
delay(10); delay(10);
} }
+233
View File
@@ -0,0 +1,233 @@
{
"name": "x0gp",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {
"supabase": "^2.109.1"
}
},
"node_modules/@ecies/ciphers": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz",
"integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==",
"dev": true,
"license": "MIT",
"engines": {
"bun": ">=1",
"deno": ">=2.7.10",
"node": ">=16"
},
"peerDependencies": {
"@noble/ciphers": "^1.0.0"
}
},
"node_modules/@noble/ciphers": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/curves": {
"version": "1.9.7",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz",
"integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.8.0"
},
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@supabase/cli-darwin-arm64": {
"version": "2.109.1",
"resolved": "https://registry.npmjs.org/@supabase/cli-darwin-arm64/-/cli-darwin-arm64-2.109.1.tgz",
"integrity": "sha512-tkn8tfunyqIL7RE+7DVjg6Ql2cJLPkGgh9cPafp2LbXI0qDgds0TaS+UOTHQEjci8JQXXe2wS00+122ko2QI8A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@supabase/cli-darwin-x64": {
"version": "2.109.1",
"resolved": "https://registry.npmjs.org/@supabase/cli-darwin-x64/-/cli-darwin-x64-2.109.1.tgz",
"integrity": "sha512-0Q5ZAoWhOyIv4ZzQOU8QbjjdB2JmznnDNyJ3VrIeuLMWoifVfUWaLZhHBNYzr4xXoxBpKrggomuAT8BaEhY3lg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@supabase/cli-linux-arm64": {
"version": "2.109.1",
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64/-/cli-linux-arm64-2.109.1.tgz",
"integrity": "sha512-MS1djJjq5laD99+jYJUoARfSZhzRX9aXmo5piZ1yl2JXb9IXTuhiTD5olkFKQcgNckRcAZqMf4fucQGTYC767A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@supabase/cli-linux-arm64-musl": {
"version": "2.109.1",
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.109.1.tgz",
"integrity": "sha512-cXNOsSU7MS+jmslGQATTD4DfWBEIHiFi7LaBWyBxHmR7tzIiZXimUXgN26ZiQjfAxU+RZq52C3k0qhi7vmqXQw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@supabase/cli-linux-x64": {
"version": "2.109.1",
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64/-/cli-linux-x64-2.109.1.tgz",
"integrity": "sha512-svFmamF/vIq4/oinwY50jDi869itC9/GWrPaGtsHFkK4NUBcQtl1T37WWIivGsXwbBKNC4FjZD3dGqjL7bfW1g==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@supabase/cli-linux-x64-musl": {
"version": "2.109.1",
"resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64-musl/-/cli-linux-x64-musl-2.109.1.tgz",
"integrity": "sha512-zeD9MrZpEKJrjkSPcYp4nZeGJ9FQt0i/kiRij4ZprPP6R5l+SLi6Pk20ln+Vj/GZuWTVxX7IHJ11pgViaReAWw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@supabase/cli-windows-arm64": {
"version": "2.109.1",
"resolved": "https://registry.npmjs.org/@supabase/cli-windows-arm64/-/cli-windows-arm64-2.109.1.tgz",
"integrity": "sha512-NeKzgWAOpglnLwMTggTp5t44fbkfxACLkQNlCRx0q332HMM3WqsKQUsHv1MHc6ZbEc5bcMwaYjqPNI/aOWnP5g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@supabase/cli-windows-x64": {
"version": "2.109.1",
"resolved": "https://registry.npmjs.org/@supabase/cli-windows-x64/-/cli-windows-x64-2.109.1.tgz",
"integrity": "sha512-L9/pDLM+4IR8646aT69ZDVcnJeg1lZZsB26ZgI775S3f8jOHP41+1UH9Oq1g9CvKiAQviuaLgtwkuvi0I/cc/A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/eciesjs": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.5.0.tgz",
"integrity": "sha512-s0J9SEVYAEPg7J63GFMApLYzPH9VNIQIyC6s15JpnqVc0TqcKWdbgFlnAweEBRyMmko2dcs2sfC83Hj4J43tuA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ecies/ciphers": "^0.2.6",
"@noble/ciphers": "^1.3.0",
"@noble/curves": "^1.9.7",
"@noble/hashes": "^1.8.0"
},
"engines": {
"bun": ">=1",
"deno": ">=2.7.10",
"node": ">=16"
}
},
"node_modules/jose": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/supabase": {
"version": "2.109.1",
"resolved": "https://registry.npmjs.org/supabase/-/supabase-2.109.1.tgz",
"integrity": "sha512-N2yP2MHTxOxXBWhfn3poudpJn4pkPosAUo7J/46FTou/l7wOwFi9tox8NSN6HljWkfM0zhwPRimNNGC9XBMoxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"eciesjs": "^0.5.0",
"jose": "^6.2.3"
},
"bin": {
"supabase": "dist/supabase.js"
},
"optionalDependencies": {
"@supabase/cli-darwin-arm64": "2.109.1",
"@supabase/cli-darwin-x64": "2.109.1",
"@supabase/cli-linux-arm64": "2.109.1",
"@supabase/cli-linux-arm64-musl": "2.109.1",
"@supabase/cli-linux-x64": "2.109.1",
"@supabase/cli-linux-x64-musl": "2.109.1",
"@supabase/cli-windows-arm64": "2.109.1",
"@supabase/cli-windows-x64": "2.109.1"
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"devDependencies": {
"supabase": "^2.109.1"
}
}
+8 -4
View File
@@ -9,7 +9,7 @@
# postgres://x0gp:x0gp@localhost:5432/x0gp?sslmode=disable # postgres://x0gp:x0gp@localhost:5432/x0gp?sslmode=disable
# postgres://x0gp:x0gp@postgres:5432/x0gp?sslmode=disable (inside compose) # postgres://x0gp:x0gp@postgres:5432/x0gp?sslmode=disable (inside compose)
DATABASE_URL=postgres://x0gp:x0gp@localhost:5432/x0gp?sslmode=disable DATABASE_URL=postgres://postgres:postgres@localhost:3402/postgres?sslmode=disable
# HTTP server. # HTTP server.
X0GP_HTTP_ADDR=:8080 X0GP_HTTP_ADDR=:8080
@@ -19,9 +19,13 @@ X0GP_LOG_LEVEL=info
X0GP_TICK_RATE=60 X0GP_TICK_RATE=60
X0GP_SNAPSHOT_RATE=30 X0GP_SNAPSHOT_RATE=30
X0GP_MAX_CLIENTS=100 X0GP_MAX_CLIENTS=100
X0GP_SEED_RACES=0
# PoC: skip auth, allow multiple clients on the same track. X0GP_SEED_RESET=0
X0GP_DEV_MODE=true
# Optional: override path(s) to dotenv files (colon-separated). # Optional: override path(s) to dotenv files (colon-separated).
# X0GP_DOTENV=.env:./configs/local.env # X0GP_DOTENV=.env:./configs/local.env
# PoC: skip auth, allow multiple clients on the same track.
X0GP_DEV_MODE=true
# JWT secret (Supabase service role or JWT secret). Required when X0GP_DEV_MODE=false.
X0GP_JWT_SECRET=your-supabase-jwt-secret-here
+764
View File
@@ -0,0 +1,764 @@
# x0gp Go Codebase Map (Generated)
This file contains a compiled map of all packages, structures, interfaces, and public functions in the `server` directory. It is optimized for token-efficiency so AI agents and humans can understand the codebase layout without reading all files.
## Package `main` (`cmd/poc-server`)
HTTP handlers for the catalog (tracks + cars).
All HTTP routes live under /api/*:
GET /api/tracks list tracks (optional ?scope=system|public|private&tag=)
GET /api/tracks/{id} get a single track
POST /api/tracks create a track
PUT /api/tracks/{id} update a track
DELETE /api/tracks/{id} delete a track
GET /api/cars list cars (optional ?scope=...&owner_id=...)
GET /api/cars/{id} get a single car
POST /api/cars create a car
PUT /api/cars/{id} update a car
DELETE /api/cars/{id} delete a car
GET /api/catalog combined snapshot + summary
WebSocket access is wired up directly in main.go's readPump switch.
### Structs
- **struct Hub**
- **struct UDPVideoService**
- **struct WebRTCService**
### Functions
- func NewHub(*slog.Logger) *Hub
- func NewUDPVideoService(*slog.Logger, string, int) *UDPVideoService
- func NewWebRTCService(*slog.Logger, *control.Engine, *lobby.Service, *UDPVideoService, *catalog.Service) *WebRTCService
### Methods
- func (*catalogBroadcaster) Run()
- func (*catalogBroadcaster) Stop()
- func (*Hub) BroadcastSnapshot(transport.RaceSnapshot)
- func (*statusRecorder) WriteHeader(int)
- func (*statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error)
- func (*UDPVideoService) Start(context.Context, *sync.WaitGroup)
- func (*UDPVideoService) SendControlPacket(uint8, []byte) error
- func (*UDPVideoService) SendCommand(uint8, string) error
- func (*UDPVideoService) StreamHandler() http.HandlerFunc
- func (*UDPVideoService) ControlHandler() http.HandlerFunc
- func (*WebRTCService) Start(context.Context, *sync.WaitGroup)
- func (*WebRTCService) ConnectHandler() http.HandlerFunc
- func (*WebRTCService) CloseSession(string)
- func (*WebRTCService) CloseAll()
---
## Package `clans` (`internal/clans`)
Package clans owns persistence and business logic for clans.
A clan has a unique 3-letter tag (A-Z, uppercase, exactly 3 chars),
a name, and an optional avatar URL. Drivers reference a clan via a
nullable FK (see internal/drivers).
### Interfaces
- **interface Store** { Exec, Create, Get, GetByTag, List, Delete, Update, Count }
### Structs
- **struct Service**
- **struct CreateInput** { ID, Tag, Name, AvatarURL }
- **struct UpdateInput** { Name, AvatarURL }
- **struct Clan** { ID, Tag, Name, AvatarURL, CreatedMs, UpdatedMs }
- **struct PgStore**
### Functions
- func NewService(Store) *Service
- func IsValidTag(string) bool
- func NewPgStore(*pgxpool.Pool) *PgStore
### Methods
- func (*Service) Create(context.Context, CreateInput) (Clan, error)
- func (*Service) Get(context.Context, string) (Clan, error)
- func (*Service) GetByTag(context.Context, string) (Clan, error)
- func (*Service) List(context.Context, int, int) ([]Clan, error)
- func (*Service) Update(context.Context, string, UpdateInput) (Clan, error)
- func (*Service) Delete(context.Context, string) error
- func (*PgStore) Exec(context.Context, string) (int64, error)
- func (*PgStore) Create(context.Context, Clan) error
- func (*PgStore) Get(context.Context, string) (Clan, error)
- func (*PgStore) GetByTag(context.Context, string) (Clan, error)
- func (*PgStore) List(context.Context, int, int) ([]Clan, error)
- func (*PgStore) Delete(context.Context, string) error
- func (*PgStore) Update(context.Context, Clan) error
- func (*PgStore) Count(context.Context) (int, error)
---
## Package `lobby` (`internal/lobby`)
Package lobby owns the "outside any race" state of the server: which
races are open, which drivers are connected but idle, who is the host
of each race.
The lobby is the single source of truth for matchmaking and for
rendering the "what races are available right now" list. It does NOT
own per-race physics — that's the Engine (internal/control).
All operations are thread-safe. Mutations broadcast a snapshot event
to subscribers (see Service.Subscribe).
### Interfaces
- **interface Persistence** { UpsertRace, DeleteRace, AddDriver, RemoveDriver, SetRaceStatus, UpsertDriver, DeleteDriver }
### Structs
- **struct CreateRaceOptions** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS }
- **struct RaceMeta** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, DriverIDs, Status, CreatedMs, StartedMs }
- **struct DriverMeta** { ID, Name, Nickname, AvatarURL, ClanID, ClanTag, Status, RaceID, ConnectedMs, LastSeenMs, Country, AvatarHue, CarID }
- **struct Snapshot** { GeneratedMs, Races, Drivers, Version }
- **struct Stats** { RacesTotal, RacesOpen, RacesActive, DriversIdle, DriversTotal }
- **struct Service**
### Functions
- func NewService() *Service
### Methods
- func (*Service) CreateRace(CreateRaceOptions) (RaceMeta, error)
- func (*Service) AddRace(RaceMeta)
- func (*Service) AddDriver(string, string, string) (DriverMeta, error)
- func (*Service) RemoveDriver(string)
- func (*Service) Heartbeat(string)
- func (*Service) SetDriverProfile(string, string, string, string, string)
- func (*Service) AddDriverToRace(string, string) error
- func (*Service) RemoveDriverFromRace(string)
- func (*Service) SetRaceStatus(string, RaceStatus) error
- func (*Service) DeleteRace(string) error
- func (*Service) QuickPlay(string, int) (RaceMeta, error)
- func (*Service) GetRace(string) (RaceMeta, error)
- func (*Service) GetDriver(string) (DriverMeta, error)
- func (*Service) SelectCar(string, *string) error
- func (*Service) ListRaces() []RaceMeta
- func (*Service) ListDrivers() []DriverMeta
- func (*Service) SetPersistence(Persistence)
- func (*Service) SetRaceLifecycleHook(func, func)
- func (*Service) Subscribe() (?, func)
- func (*Service) Stop()
- func (*Service) Done() ?
- func (*Service) Snapshot() Snapshot
- func (*Service) Stats() Stats
---
## Package `races` (`internal/races`)
Package races owns the cross-cutting "race list" surface: keyset-paginated
listing of live and finished races, upcoming (next-N) selection, the
driver-to-race queue and the recurring race-plan scheduler.
The package composes two storage backends:
- lobby.Service (in-memory): authoritative source for active races
(status = lobby | countdown | racing). Used directly for the live
page of /api/races, for the upcoming view and for the scheduler
materialisation.
- Postgres (pgxpool): authoritative for finished races (snapshotted
on SetRaceStatus(finished)), for race plans and for the queue.
Finished races are read with keyset pagination on
(finished_ms DESC, id DESC).
### Structs
- **struct Cursor** { Ms, ID }
- **struct LeaderboardEntry** { DriverID, Name, Nickname, AvatarURL, ClanID, ClanTag, Points, BestPos, BestTimeMs, Rank }
- **struct LeaderboardResult** { Items, Total, CurrentDriver }
- **struct TrackCalendarInfo** { ID, TrackID, TrackName, StartAt, EndAt }
- **struct TrackCalendarLeaderboard** { TrackID, TrackName, StartAt, EndAt, Status, Podium, CurrentDriver }
- **struct LiveStore**
- **struct Service**
- **struct ListFilter** { Statuses, TrackID, Cursor, Limit }
- **struct RaceItem** { Source, Meta, Podium }
- **struct ListResult** { Items, NextCursor, TotalCount }
- **struct UpcomingItem** { Meta, QueueLen, PlanID, StartAtMs }
- **struct JoinUpcomingResult** { Joined, Skipped }
- **struct SnapshotFinished** { Meta, FinishedMs, TotalLaps, WinnerDriverID, WinnerName, BestLapMs, Results }
- **struct CreatePlanInput** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, StartAtMs, IntervalS, Count, Enabled }
- **struct Scheduler**
- **struct PodiumEntry** { Position, DriverID, Name, TotalTimeMs, Nickname, AvatarUrl, ClanID }
- **struct DriverResult** { DriverID, Name, TotalTimeMs, BestLapMs, Position, Nickname, AvatarUrl, ClanID }
- **struct FinishedRace** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, DriverIDs, Status, CreatedMs, StartedMs, FinishedMs, DurationMs, TotalLaps, TotalDrivers, Results, WinnerDriverID, WinnerName, BestLapMs, Podium }
- **struct RacePlan** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, StartAtMs, IntervalS, Count, Enabled, CreatedMs, UpdatedMs, NextFireMs, FiresDone }
- **struct QueueEntry** { DriverID, RaceID, PlanID, EnqueuedMs }
- **struct PgStore**
- **struct DriverStats** { TotalRacesStarted, TotalRacesFinished, Wins, Podiums, BestLapMs, TotalPlaytimeMs }
- **struct DriverRaceSummary** { RaceID, RaceName, FinishedMs, Position, TotalTimeMs, BestLapMs, TrackID, TrackName }
### Functions
- func DecodeCursor(string) (Cursor, error)
- func NewLiveStore(*PgStore) *LiveStore
- func NewService(*PgStore, *lobby.Service) *Service
- func NewScheduler(*PgStore, *lobby.Service, time.Duration) *Scheduler
- func NewPgStore(*pgxpool.Pool) *PgStore
- func ParseStatusFilter(string) ([]StatusFilter, error)
### Methods
- func (Cursor) String() string
- func (Cursor) IsZero() bool
- func (Cursor) Encode() string
- func (*PgStore) GetTrackLeaderboard(context.Context, string, int, int, int) ([]LeaderboardEntry, int, error)
- func (*PgStore) GetTrackLeaderboardDriver(context.Context, string, int, string) (*LeaderboardEntry, error)
- func (*PgStore) GetOverallLeaderboard(context.Context, int, int, int) ([]LeaderboardEntry, int, error)
- func (*PgStore) GetOverallLeaderboardDriver(context.Context, int, string) (*LeaderboardEntry, error)
- func (*PgStore) GetDriverLeaderboardProfile(context.Context, string) (*LeaderboardEntry, error)
- func (*PgStore) GetCalendarLeaderboardTracks(context.Context) ([]TrackCalendarInfo, error)
- func (*LiveStore) UpsertRace(context.Context, lobby.RaceMeta) error
- func (*LiveStore) DeleteRace(context.Context, string) error
- func (*LiveStore) AddDriver(context.Context, string, string, int) error
- func (*LiveStore) RemoveDriver(context.Context, string, string) error
- func (*LiveStore) SetRaceStatus(context.Context, string, lobby.RaceStatus, int64) error
- func (*LiveStore) UpsertDriver(context.Context, lobby.DriverMeta) error
- func (*LiveStore) DeleteDriver(context.Context, string) error
- func (*LiveStore) ListAllRaces(context.Context) ([]lobby.RaceMeta, error)
- func (*LiveStore) ListAllDrivers(context.Context) ([]lobby.DriverMeta, error)
- func (*LiveStore) ListLivePaged(context.Context, []string, string, Cursor, int) ([]lobby.RaceMeta, error)
- func (*LiveStore) ListUpcoming(context.Context, int) ([]lobby.RaceMeta, error)
- func (*LiveStore) Exec(context.Context, string) (int64, error)
- func (*Service) SetLiveStore(*LiveStore)
- func (*Service) RestoreFromDB(context.Context) (int, int, error)
- func (*Service) SetMaxUpcoming(int)
- func (*Service) ListRaces(context.Context, ListFilter) (ListResult, error)
- func (*Service) Upcoming(context.Context, int) ([]UpcomingItem, error)
- func (*Service) JoinUpcoming(context.Context, string, int) (JoinUpcomingResult, error)
- func (*Service) LeaveQueue(context.Context, string, string) error
- func (*Service) ListQueue(context.Context, string) ([]QueueEntry, error)
- func (*Service) PersistFinished(context.Context, SnapshotFinished) error
- func (*Service) CreatePlan(context.Context, CreatePlanInput) (RacePlan, error)
- func (*Service) ListPlans(context.Context, Cursor, int) ([]RacePlan, error)
- func (*Service) CountPlans(context.Context) (int, error)
- func (*Service) DeletePlan(context.Context, string) error
- func (*Service) GetLeaderboard(context.Context, string, int, int, int, string) (*LeaderboardResult, error)
- func (*Service) GetCalendarLeaderboard(context.Context, int, string) ([]TrackCalendarLeaderboard, error)
- func (*Service) GetRace(context.Context, string) (FinishedRace, error)
- func (*Service) GetLastFinished(context.Context) (FinishedRace, error)
- func (*Service) GetDriverProfileStats(context.Context, string) (DriverStats, *DriverRaceSummary, *DriverRaceSummary, error)
- func (*Scheduler) Run(context.Context)
- func (*Scheduler) Stop()
- func (*Scheduler) Done() ?
- func (*PgStore) Exec(context.Context, string, ?) (int64, error)
- func (*PgStore) ListTrackIDs(context.Context) ([]string, error)
- func (*PgStore) UpsertLive(context.Context, lobby.RaceMeta) error
- func (*PgStore) DeleteLive(context.Context, string) error
- func (*PgStore) AddLiveDriver(context.Context, string, string, int) error
- func (*PgStore) RemoveLiveDriver(context.Context, string, string) error
- func (*PgStore) SetLiveStatus(context.Context, string, lobby.RaceStatus, int64) error
- func (*PgStore) ListLivePaged(context.Context, []string, string, Cursor, int) ([]lobby.RaceMeta, error)
- func (*PgStore) ListLiveUpcoming(context.Context, int) ([]lobby.RaceMeta, error)
- func (*PgStore) GetLive(context.Context, string) (lobby.RaceMeta, error)
- func (*PgStore) ListAllRaces(context.Context) ([]lobby.RaceMeta, error)
- func (*PgStore) UpsertLobbyDriver(context.Context, lobby.DriverMeta) error
- func (*PgStore) DeleteLobbyDriver(context.Context, string) error
- func (*PgStore) ListLobbyDrivers(context.Context) ([]lobby.DriverMeta, error)
- func (*PgStore) InsertFinished(context.Context, FinishedRace) error
- func (*PgStore) ListFinished(context.Context, string, Cursor, int) ([]FinishedRace, error)
- func (*PgStore) CountRaces(context.Context, []string, string) (int, error)
- func (*PgStore) GetFinished(context.Context, string) (FinishedRace, error)
- func (*PgStore) GetLastFinished(context.Context) (FinishedRace, error)
- func (*PgStore) CreatePlan(context.Context, RacePlan) error
- func (*PgStore) GetPlan(context.Context, string) (RacePlan, error)
- func (*PgStore) ListPlans(context.Context, Cursor, int) ([]RacePlan, error)
- func (*PgStore) CountPlans(context.Context) (int, error)
- func (*PgStore) DeletePlan(context.Context, string) error
- func (*PgStore) SetPlanEnabled(context.Context, string, bool) error
- func (*PgStore) AdvancePlan(context.Context, string, int64) error
- func (*PgStore) DuePlans(context.Context, int64, int) ([]RacePlan, error)
- func (*PgStore) Enqueue(context.Context, string, string, string) (QueueEntry, error)
- func (*PgStore) Dequeue(context.Context, string, string) error
- func (*PgStore) ListQueueByDriver(context.Context, string) ([]QueueEntry, error)
- func (*PgStore) ListQueueByRace(context.Context, string) ([]QueueEntry, error)
- func (*PgStore) CountQueueByRace(context.Context, string) (int, error)
- func (FinishedRace) ToLobbyMeta() lobby.RaceMeta
- func (*PgStore) GetDriverStats(context.Context, string) (DriverStats, error)
- func (*PgStore) GetDriverLastRace(context.Context, string) (*DriverRaceSummary, error)
- func (*PgStore) GetDriverBestRace(context.Context, string) (*DriverRaceSummary, error)
---
## Package `main` (`scripts/genseed`)
Package main contains the genseed utility. It is built into a standalone
binary via `go build ./scripts/genseed` and is not part of the runtime.
Usage:
go run ./scripts/genseed seeds.json > 002_seed.sql
It reads the JSON dump produced by `DUMP_SEEDS=1 go test ./internal/catalog`
and emits ON CONFLICT-safe INSERTs for the catalog tables.
---
## Package `main` (`scripts/genseed-json`)
Package main contains a small helper that prints the default seeds
(5 F1 tracks + 5 F1 cars) as JSON to stdout. It is run as:
go run ./scripts/genseed-json > seeds.json
which is then fed into scripts/genseed to regenerate the SQL.
---
## Package `main` (`scripts/graphify`)
Package main is the graphify utility. It walks the codebase, parses Go AST,
and outputs a highly compact symbol summary to server/CODEBASE.md.
This is used for token savings and helping developers/AI agents quickly map out imports and structures.
### Structs
- **struct PackageSummary** { Name, Path, Doc, Structs, Interfaces, Functions, Methods }
---
## Package `auth` (`internal/auth`)
Package auth handles Supabase JWT validation and HTTP middleware.
### Structs
- **struct Claims** { DriverID, Email }
### Functions
- func ValidateJWT(string, string) (Claims, error)
- func Middleware(string, bool) func
- func DriverIDFromContext(context.Context) (string, bool)
- func EmailFromContext(context.Context) (string, bool)
---
## Package `main` (`.`)
Smoke test: ws client connects, exchanges a few messages, exits.
Run: go run ./smoke_test.go
---
## Package `seeddefs` (`internal/catalog/seeddefs`)
Package seeddefs holds the canonical seed definitions for tracks and
cars. It lives in its own package (no postgres dependencies) so both
the catalog tests and the scripts/genseed SQL generator can consume
the same source-of-truth data.
### Structs
- **struct Waypoint** { X, Y, HeadingRad, SpeedMs, Curvature }
- **struct Bounds** { MinX, MinY, MaxX, MaxY, LengthM, WidthM }
- **struct Track** { ID, Name, Description, AuthorID, Visibility, Scale, LengthM, WidthM, LaneWidthM, Surface, Tags, Centerline, Bounds, BestLapMs, BestLapHolder }
- **struct Chassis** { Model, Material, Printed, WheelbaseMm, TrackMm }
- **struct Motor** { Kind, Class, KV, PowerW }
- **struct Battery** { VoltageV, CapacityMah, Cells, Chemistry }
- **struct Car** { ID, Name, OwnerID, Visibility, Scale, LengthMm, WidthMm, HeightMm, WeightG, Chassis, Motor, Battery, Drive, TopSpeedMs, ColorHex, Active, TotalDistanceM, TotalRaces, TotalLaps, BestLapMs }
### Functions
- func DefaultTracks() []Track
- func DefaultCars() []Car
- func Monaco() Track
- func BarcelonaCatalunya() Track
- func AustriaRedBullRing() Track
- func GreatBritainSilverstone() Track
- func BelgiumSpa() Track
- func RedBullRB20() Car
- func FerrariSF24() Car
- func McLarenMCL38() Car
- func MercedesW15() Car
- func AstonMartinAMR24() Car
- func ComputeBounds([]Waypoint) Bounds
---
## Package `control` (`internal/control`)
Package control owns the authoritative race state.
For PoC, the physics model is intentionally simple (no tire slip, no
collision): each car integrates position from (throttle, brake, steering).
Realistic physics, anti-cheat checks, and CV-based ground truth will be
added in MVP / production phases.
### Structs
- **struct Car** { ID, DriverID, DriverName, DeviceID, X, Y, Heading, Speed, Lap, Sector, LastLapMs, BestLapMs, DNF, AppliedSteering, AppliedThrottle, AppliedBrake }
- **struct Engine**
### Functions
- func NewEngine(int) *Engine
### Methods
- func (RacePhase) String() string
- func (*Car) ApplyInput(transport.InputState, float64)
- func (*Car) Snapshot() transport.CarInfo
- func (*Engine) Snapshots() ?
- func (*Engine) Phase() RacePhase
- func (*Engine) SetPhase(RacePhase)
- func (*Engine) AddCar(string, string, int, *int) (*Car, error)
- func (*Engine) RemoveCar(string)
- func (*Engine) GetCarByDriver(string) *Car
- func (*Engine) Run(context.Context)
- func (*Engine) Stop()
---
## Package `drivers` (`internal/drivers`)
Package drivers owns persistence and business logic for drivers
(racers). Each driver has a unique 3-letter nickname (A-Z,
uppercase, exactly 3 chars), a name, an optional avatar URL and an
optional clan reference.
### Interfaces
- **interface Store** { Exec, Create, Get, GetByNickname, List, Delete, Update, Count }
### Structs
- **struct Service**
- **struct CreateInput** { ID, Nickname, Name, AvatarURL, ClanID }
- **struct UpdateInput** { Nickname, Name, AvatarURL, ClanID }
- **struct Driver** { ID, Nickname, Name, AvatarURL, ClanID, CreatedMs, UpdatedMs }
- **struct PgStore**
### Functions
- func NewService(Store) *Service
- func IsValidNickname(string) bool
- func NewPgStore(*pgxpool.Pool) *PgStore
### Methods
- func (*Service) Create(context.Context, CreateInput) (Driver, error)
- func (*Service) Get(context.Context, string) (Driver, error)
- func (*Service) GetByNickname(context.Context, string) (Driver, error)
- func (*Service) List(context.Context, int, int, string) ([]Driver, error)
- func (*Service) Update(context.Context, string, UpdateInput) (Driver, error)
- func (*Service) Delete(context.Context, string) error
- func (*PgStore) Exec(context.Context, string) (int64, error)
- func (*PgStore) Create(context.Context, Driver) error
- func (*PgStore) Get(context.Context, string) (Driver, error)
- func (*PgStore) GetByNickname(context.Context, string) (Driver, error)
- func (*PgStore) List(context.Context, int, int, string) ([]Driver, error)
- func (*PgStore) Update(context.Context, Driver) error
- func (*PgStore) Delete(context.Context, string) error
- func (*PgStore) Count(context.Context) (int, error)
---
## Package `realtime` (`internal/realtime`)
Package realtime manages WebSocket connections: registration, fan-out of
race snapshots, and per-client send queues with backpressure handling.
### Structs
- **struct Client** { ID, Send, SessionID, Done, DriverID, LastUDPCommand }
- **struct Hub**
- **struct Stats** { Connections, DropsTotal }
### Functions
- func NewHub() *Hub
- func MustEnvelope(transport.MessageType, any) *transport.Envelope
### Methods
- func (*Client) Close()
- func (*Hub) Stats() Stats
- func (*Hub) Register(*Client)
- func (*Hub) Unregister(*Client)
- func (*Hub) Run(context.Context)
- func (*Hub) Stop()
- func (*Hub) Publish(*transport.Envelope)
---
## Package `catalog` (`internal/catalog`)
Package catalog owns the static configuration entities: tracks and
cars. Both are first-class resources that drivers browse, pick, and
(in later phases) author.
PoC: in-memory cache backed by a pluggable Store. The runtime uses
postgres.PgStore so tracks / cars survive restarts. The cache is the
source of truth for read paths; the Store is the source of truth for
durability. Mutations go through the Store, then refresh the cache.
Design constraints come from the user's apartment layout:
- largest room: 4.46 m × 3.19 m (446 × 319 cm)
- car scale: 1/27 (Compact)
- printer: Bambu A1 (220×220×250 mm build volume)
Tracks are sized to fit a 4.5 × 3.2 m bounding box with a safety
margin, which keeps them physically realizable in the apartment.
Cars are sized to a 1/27 touring envelope (~80 × 35 × 25 mm).
### Interfaces
- **interface Store** { Load, UpsertTrack, DeleteTrack, UpsertCar, DeleteCar, UpdateCarStats, GetTrackCalendar, CreateTrackCalendar, DeleteTrackCalendar }
### Structs
- **struct CreateTrackOptions** { ID, Name, Description, AuthorID, Visibility, Scale, LengthM, WidthM, LaneWidthM, Surface, Tags, Centerline }
- **struct UpdateTrackOptions** { Name, Description, Visibility, LengthM, WidthM, LaneWidthM, Surface, Tags, Centerline }
- **struct CreateCarOptions** { ID, Name, OwnerID, Visibility, Scale, LengthMm, WidthMm, HeightMm, WeightG, Chassis, Motor, Battery, Drive, TopSpeedMs, ColorHex, AvatarURL, Active, DeviceID }
- **struct UpdateCarOptions** { Name, Visibility, LengthMm, WidthMm, HeightMm, WeightG, TopSpeedMs, ColorHex, Active, Chassis, Motor, Battery, Drive, DeviceID }
- **struct Waypoint** { X, Y, HeadingRad, SpeedMs, Curvature }
- **struct Bounds** { MinX, MinY, MaxX, MaxY, LengthM, WidthM }
- **struct TrackMeta** { ID, Name, Description, AuthorID, Visibility, Scale, LengthM, WidthM, LaneWidthM, Bounds, Surface, Tags, Centerline, BestLapMs, BestLapHolder, CreatedMs, UpdatedMs }
- **struct TrackCalendarEntry** { ID, TrackID, StartAt, EndAt }
- **struct MotorSpec** { Kind, Class, KV, PowerW }
- **struct BatterySpec** { VoltageV, CapacityMah, Cells, Chemistry }
- **struct ChassisSpec** { Model, Material, Printed, WheelbaseMm, TrackMm }
- **struct CarMeta** { ID, Name, OwnerID, Visibility, Scale, LengthMm, WidthMm, HeightMm, WeightG, Chassis, Motor, Battery, Drive, DeviceID, TopSpeedMs, ColorHex, AvatarURL, Active, TotalDistanceM, TotalRaces, TotalLaps, BestLapMs, CreatedMs, UpdatedMs }
- **struct Snapshot** { GeneratedMs, Version, Tracks, Cars }
- **struct Stats** { TracksTotal, TracksSystem, TracksPublic, TracksPrivate, CarsTotal, CarsSystem, CarsPublic, CarsPrivate, CarsActive }
- **struct Event** { Kind, TrackID, CarID, Snapshot }
- **struct Service**
### Functions
- func NewService(context.Context, Store) (*Service, error)
### Methods
- func (CreateTrackOptions) Validate() error
- func (*Service) CreateTrack(context.Context, CreateTrackOptions) (TrackMeta, error)
- func (*Service) UpdateTrack(context.Context, string, UpdateTrackOptions) (TrackMeta, error)
- func (*Service) DeleteTrack(context.Context, string) error
- func (CreateCarOptions) Validate() error
- func (*Service) CreateCar(context.Context, CreateCarOptions) (CarMeta, error)
- func (*Service) UpdateCar(context.Context, string, UpdateCarOptions) (CarMeta, error)
- func (*Service) DeleteCar(context.Context, string) error
- func (*Service) SetCarStats(context.Context, string, func) error
- func (*Service) ListTracksByVisibility(Visibility) []TrackMeta
- func (*Service) ListCarsByVisibility(Visibility) []CarMeta
- func (*Service) ListCarsByOwner(string) []CarMeta
- func (*Service) GetTrackCalendar(context.Context) ([]TrackCalendarEntry, error)
- func (*Service) CreateTrackCalendar(context.Context, string, time.Time, time.Time) (TrackCalendarEntry, error)
- func (*Service) DeleteTrackCalendar(context.Context, int) error
- func (*memoryStore) Load(context.Context) ([]TrackMeta, []CarMeta, error)
- func (*memoryStore) UpsertTrack(context.Context, TrackMeta) error
- func (*memoryStore) DeleteTrack(context.Context, string) error
- func (*memoryStore) UpsertCar(context.Context, CarMeta) error
- func (*memoryStore) DeleteCar(context.Context, string) error
- func (*memoryStore) UpdateCarStats(context.Context, string, func) error
- func (*memoryStore) GetTrackCalendar(context.Context) ([]TrackCalendarEntry, error)
- func (*memoryStore) CreateTrackCalendar(context.Context, TrackCalendarEntry) (TrackCalendarEntry, error)
- func (*memoryStore) DeleteTrackCalendar(context.Context, int) error
- func (*Service) Subscribe() (?, func)
- func (*Service) Stop()
- func (*Service) Done() ?
- func (*Service) Snapshot() Snapshot
- func (*Service) Stats() Stats
- func (*Service) ListTracks() []TrackMeta
- func (*Service) ListCars() []CarMeta
- func (*Service) GetTrack(string) (TrackMeta, bool)
- func (*Service) GetCar(string) (CarMeta, bool)
---
## Package `config` (`internal/config`)
Package config loads runtime configuration from environment variables.
On startup Load() reads a .env file (if present) from the current
working directory and merges its values into the process environment.
Existing process env vars take precedence over .env so deployments
can override .env without modifying the file.
### Structs
- **struct Config** { HTTPAddr, TickRate, TickInterval, LogLevel, MaxClients, SnapshotRate, DevMode, DatabaseURL, JWTSecret, UDPVideoAddr, ESPCmdPort }
### Functions
- func Load() (*Config, error)
---
## Package `seed` (`internal/races/seed`)
### Structs
- **struct DefaultCounts** { Finished, Live, Plans }
- **struct Options** { Counts, Seed, Reset, Now }
- **struct Summary** { FinishedInserted, LiveCreated, PlansInserted, QueueInserted, Duration }
- **struct Runner**
### Functions
- func NewRunner(*races.PgStore, *lobby.Service, *races.LiveStore, *clans.PgStore, *drivers.PgStore) *Runner
### Methods
- func (*Runner) Run(context.Context, Options) (Summary, error)
---
## Package `stats` (`internal/stats`)
Package stats collects and exposes server and per-driver metrics.
PoC: in-memory only, atomic counters where possible, mutex around
driver and race records. The data lives for the lifetime of the
process. A future storage layer will swap this for Postgres/Redis.
Public surface is split into:
- lifecycle hooks (Connect/Disconnect/Race*/Lap*/Input*/RTT) —
called from cmd/poc-server and from internal/control and lobby
- Snapshot() — full server snapshot for /stats/detailed
- DriverProfile(id) — per-driver summary
- RecentLaps(n) — last N laps across all races
### Structs
- **struct ServerStats** { StartedMs, UptimeMs, CurrentClients, PeakClients, TotalConnections, TotalDisconnects, TotalRacesCreated, TotalRacesStarted, TotalRacesFinished, TotalLapsRecorded, TotalInputsRecv, TotalSnapshotsSent, SnapshotsDropped, AvgRTTMs, RecentRTTs }
- **struct DriverProfile** { ID, Name, TotalRacesStarted, TotalRacesFinished, TotalLaps, BestLapMs, LastLapMs, TotalDistanceM, TotalPlaytimeMs, Wins, Podiums, LastSeenMs }
- **struct LapRecord** { RaceID, DriverID, LapMs, Sector1Ms, Sector2Ms, Sector3Ms, Position, TSMs }
- **struct RaceResult** { RaceID, DriverID, FinishedAtMs, Position, TotalLaps, BestLapMs, TotalTimeMs, DNF }
- **struct Collector**
### Functions
- func NewCollector() *Collector
### Methods
- func (*Collector) Start()
- func (*Collector) OnConnect(string, string)
- func (*Collector) OnDisconnect(string, int64)
- func (*Collector) OnRaceCreated()
- func (*Collector) OnRaceStarted([]string)
- func (*Collector) OnRaceFinished(RaceResult)
- func (*Collector) OnLapRecorded(LapRecord)
- func (*Collector) OnInputProcessed()
- func (*Collector) OnSnapshotSent(uint64, uint64)
- func (*Collector) OnRTT(int64)
- func (*Collector) OnDistance(string, float64)
- func (*Collector) Server() ServerStats
- func (*Collector) Driver(string, string) DriverProfile
- func (*Collector) Drivers() []DriverProfile
- func (*Collector) RecentLaps(int) []LapRecord
- func (*Collector) RecentResults(int) []RaceResult
---
## Package `postgres` (`internal/storage/postgres`)
Package postgres owns the persistence layer of the catalog.
It exposes a *pgxpool.Pool plus a small set of repository helpers used by
internal/catalog. Migrations are applied at startup from the SQL files in
the migrations/ subdirectory.
### Structs
- **struct PgStore**
- **struct Config** { URL, Host, Port, User, Password, Database, SSLMode, MaxConns, MinConns, ConnectTimeout }
### Functions
- func NewPgStore(*pgxpool.Pool) *PgStore
- func Open(context.Context, Config) (*pgxpool.Pool, error)
- func Migrate(context.Context, *pgxpool.Pool) error
### Methods
- func (*PgStore) Load(context.Context) ([]catalog.TrackMeta, []catalog.CarMeta, error)
- func (*PgStore) UpsertTrack(context.Context, catalog.TrackMeta) error
- func (*PgStore) DeleteTrack(context.Context, string) error
- func (*PgStore) UpsertCar(context.Context, catalog.CarMeta) error
- func (*PgStore) DeleteCar(context.Context, string) error
- func (*PgStore) UpdateCarStats(context.Context, string, func) error
- func (*PgStore) GetTrackCalendar(context.Context) ([]catalog.TrackCalendarEntry, error)
- func (*PgStore) CreateTrackCalendar(context.Context, catalog.TrackCalendarEntry) (catalog.TrackCalendarEntry, error)
- func (*PgStore) DeleteTrackCalendar(context.Context, int) error
- func (Config) DSN() (string, error)
---
## Package `transport` (`internal/transport`)
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).
### Structs
- **struct Envelope** { Type, Seq, TSMs, Payload }
- **struct ClientHello** { Version, Token, Locale, ClientID }
- **struct ServerHello** { SessionID, RaceTick, ServerVersion, Config }
- **struct JoinRace** { RaceID, CarSlot, CarName }
- **struct LeaveRace** { RaceID }
- **struct InputState** { Steering, Throttle, Brake, Gear, Buttons }
- **struct ChatMessage** { Text }
- **struct Ping** { ClientTSMs }
- **struct RaceSnapshot** { Tick, TSMs, Elapsed, Cars }
- **struct CarInfo** { ID, DriverName, X, Y, Heading, Speed, Lap, Sector, LastLapMs, BestLapMs, InputApplied, DNF }
- **struct InputAck** { Seq, AppliedAtMs, ServerTickMs }
- **struct Correction** { Tick, DeltaSteering, DeltaThrottle, Reason }
- **struct RaceEvent** { RaceID, Event, CarID, Sector, LapMs, TSMs }
- **struct Pong** { ClientTSMs, ServerTSMs }
- **struct ErrorMsg** { Code, Message }
- **struct LobbyRace** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, DriverIDs, Status, CreatedMs, StartedMs, Podium }
- **struct RacePodiumEntry** { Position, DriverID, Name, TotalTimeMs, Nickname, AvatarUrl, ClanID }
- **struct VersionResponse** { Version }
- **struct WebRTCConnectRequest** { SDP, Type, DriverID }
- **struct WebRTCConnectResponse** { SDP, Type }
- **struct LobbyDriver** { ID, Name, Status, RaceID, ConnectedMs, LastSeenMs, Country, AvatarHue }
- **struct LobbySnapshot** { GeneratedMs, Version, Races, Drivers }
- **struct LobbyCreate** { RaceID, Name, TrackID, MaxCars, Laps, TimeLimitS }
- **struct LobbyJoinRace** { RaceID, CarSlot, CarName }
- **struct LobbyLeaveRace** { RaceID }
- **struct LobbyKickDriver** { RaceID, DriverID }
- **struct LobbyDelete** { RaceID }
- **struct LobbyCreatedAck** { Ok, Race, Error, Driver }
- **struct LobbyJoinAck** { Ok, Race, Error }
- **struct TrackWaypoint** { X, Y, HeadingRad, SpeedMs, Curvature }
- **struct TrackBounds** { MinX, MinY, MaxX, MaxY, LengthM, WidthM }
- **struct TrackWire** { ID, Name, Description, AuthorID, Visibility, Scale, LengthM, WidthM, LaneWidthM, Bounds, Surface, Tags, Centerline, BestLapMs, BestLapHolder, CreatedMs, UpdatedMs, IsActive }
- **struct TrackListRequest** { Scope, Tag }
- **struct TrackGetRequest** { ID }
- **struct TrackCreateRequest** { Track }
- **struct TrackUpdateRequest** { ID, Patch }
- **struct TrackPatch** { Name, Description, Visibility, Scale, LengthM, WidthM, LaneWidthM, Surface, Tags, Centerline }
- **struct TrackDeleteRequest** { ID }
- **struct TrackSnapshot** { GeneratedMs, Version, Tracks }
- **struct TrackAck** { Ok, ID, Error, Track }
- **struct MotorWire** { Kind, Class, KV, PowerW }
- **struct BatteryWire** { VoltageV, CapacityMah, Cells, Chemistry }
- **struct ChassisWire** { Model, Material, Printed, WheelbaseMm, TrackMm }
- **struct CarWire** { ID, Name, OwnerID, Visibility, Scale, LengthMm, WidthMm, HeightMm, WeightG, Chassis, Motor, Battery, Drive, TopSpeedMs, ColorHex, AvatarURL, Active, TotalDistanceM, TotalRaces, TotalLaps, BestLapMs, CreatedMs, UpdatedMs, DeviceID }
- **struct CarListRequest** { Scope, OwnerID }
- **struct CarGetRequest** { ID }
- **struct CarCreateRequest** { Car }
- **struct CarUpdateRequest** { ID, Patch }
- **struct CarPatch** { Name, Visibility, Scale, LengthMm, WidthMm, HeightMm, WeightG, Chassis, Motor, Battery, Drive, TopSpeedMs, ColorHex, AvatarURL, Active, DeviceID }
- **struct CarDeleteRequest** { ID }
- **struct CarSnapshot** { GeneratedMs, Version, Cars }
- **struct CarAck** { Ok, ID, Error, Car }
- **struct CatalogSummary** { TracksTotal, TracksSystem, TracksPublic, TracksPrivate, CarsTotal, CarsSystem, CarsPublic, CarsPrivate, CarsActive, TSMs }
- **struct RaceListResponse** { Items, NextCursor, Count }
- **struct RaceUpcomingItem** { Meta, QueueLen, PlanID, StartAtMs }
- **struct RaceUpcomingResponse** { Items, Count }
- **struct RaceQueueEntry** { DriverID, RaceID, PlanID, EnqueuedMs }
- **struct RaceQueueJoinRequest** { DriverID, Limit }
- **struct RaceQueueJoinResponse** { Joined, Skipped }
- **struct RaceQueueListResponse** { Items, Count }
- **struct RacePlan** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, StartAtMs, IntervalS, Count, Enabled, CreatedMs, UpdatedMs, NextFireMs, FiresDone }
- **struct RacePlanCreateRequest** { ID, Name, TrackID, MaxCars, Laps, TimeLimitS, StartAtMs, IntervalS, Count, Enabled }
- **struct RacePlanListResponse** { Items, NextCursor, Count }
- **struct Driver** { ID, Nickname, Name, AvatarURL, ClanID, CreatedMs, UpdatedMs }
- **struct DriverCreateRequest** { ID, Nickname, Name, AvatarURL, ClanID }
- **struct DriverUpdateRequest** { Nickname, Name, AvatarURL, ClanID }
- **struct DriverProfileResponse** { Driver, Stats, LastRace, BestRace }
- **struct DriverStats** { TotalRacesStarted, TotalRacesFinished, Wins, Podiums, BestLapMs, TotalPlaytimeMs }
- **struct DriverRaceSummary** { RaceID, RaceName, FinishedMs, Position, TotalTimeMs, BestLapMs, TrackID, TrackName }
- **struct DriverListResponse** { Items, Count }
- **struct Clan** { ID, Tag, Name, AvatarURL, CreatedMs, UpdatedMs }
- **struct ClanCreateRequest** { ID, Tag, Name, AvatarURL }
- **struct ClanUpdateRequest** { Name, AvatarURL }
- **struct ClanListResponse** { Items, Count }
- **struct StatsServer** { StartedMs, UptimeMs, CurrentClients, PeakClients, TotalConnections, TotalDisconnects, TotalRacesCreated, TotalRacesStarted, TotalRacesFinished, TotalLapsRecorded, TotalInputsRecv, TotalSnapshotsSent, SnapshotsDropped, AvgRTTMs }
- **struct StatsDriver** { ID, Name, TotalRacesStarted, TotalRacesFinished, TotalLaps, BestLapMs, LastLapMs, TotalDistanceM, TotalPlaytimeMs, Wins, Podiums, LastSeenMs }
- **struct StatsLap** { RaceID, DriverID, LapMs, Sector1Ms, Sector2Ms, Sector3Ms, Position, TSMs }
- **struct StatsRaceResult** { RaceID, DriverID, FinishedAtMs, Position, TotalLaps, BestLapMs, TotalTimeMs, DNF }
- **struct StatsSnapshot** { Server, Drivers, Laps, Results, TSMs }
- **struct StatsRequest** { Scope, TargetID }
- **struct RaceStanding** { Position, CarID, DriverID, DriverName, Lap, Sector, BestLapMs, LastLapMs, GapMs, DNF }
- **struct RaceStandingsMsg** { RaceID, Tick, TSMs, Lap, Elapsed, Rows }
- **struct LapRecordedMsg** { RaceID, DriverID, Lap, LapMs, Sector1Ms, Sector2Ms, Sector3Ms, TSMs, Position }
- **struct RaceResultMsg** { RaceID, DriverID, Position, TotalLaps, BestLapMs, TotalTimeMs, DNF, FinishedAtMs }
- **struct LeaderboardEntry** { DriverID, Nickname, Name, AvatarURL, ClanID, ClanTag, Points, BestPos, BestTimeMs, Rank }
- **struct LeaderboardResponse** { Items, Total, Limit, Offset, CurrentDriver }
- **struct TrackCalendarEntry** { ID, TrackID, StartAt, EndAt }
- **struct TrackCalendarCreateRequest** { TrackID, StartAt, EndAt }
- **struct TrackCalendarResponse** { Items }
- **struct TrackCalendarLeaderboard** { TrackID, TrackName, StartAt, EndAt, Status, Podium, CurrentDriver }
- **struct TrackCalendarLeaderboardResponse** { Items }
### Functions
- func Encode(*Envelope) ([]byte, error)
- func Decode([]byte) (*Envelope, error)
---
+5
View File
@@ -84,4 +84,9 @@ compose-down:
clean: clean:
rm -rf $(BIN_DIR) rm -rf $(BIN_DIR)
.PHONY: graphify
graphify:
$(GO) run ./scripts/graphify/main.go
.DEFAULT_GOAL := build .DEFAULT_GOAL := build
@@ -189,6 +189,7 @@ func carToWire(c catalog.CarMeta) transport.CarWire {
ColorHex: c.ColorHex, ColorHex: c.ColorHex,
AvatarURL: c.AvatarURL, AvatarURL: c.AvatarURL,
Active: c.Active, Active: c.Active,
DeviceID: c.DeviceID,
// Stats are read-mostly; the server fills them on read. // Stats are read-mostly; the server fills them on read.
TotalDistanceM: c.TotalDistanceM, TotalDistanceM: c.TotalDistanceM,
TotalRaces: c.TotalRaces, TotalRaces: c.TotalRaces,
@@ -241,6 +242,7 @@ func carFromWire(w transport.CarWire) catalog.CarMeta {
Active: w.Active, Active: w.Active,
CreatedMs: w.CreatedMs, CreatedMs: w.CreatedMs,
UpdatedMs: w.UpdatedMs, UpdatedMs: w.UpdatedMs,
DeviceID: w.DeviceID,
} }
} }
@@ -292,6 +294,7 @@ func carPatchFromWire(p transport.CarPatch) catalog.UpdateCarOptions {
Chassis: chassisFromPtr(p.Chassis), Chassis: chassisFromPtr(p.Chassis),
Motor: motorFromPtr(p.Motor), Motor: motorFromPtr(p.Motor),
Battery: batteryFromPtr(p.Battery), Battery: batteryFromPtr(p.Battery),
DeviceID: p.DeviceID,
} }
} }
+133 -19
View File
@@ -24,9 +24,12 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/x0gp/server/internal/auth"
"github.com/x0gp/server/internal/clans" "github.com/x0gp/server/internal/clans"
"github.com/x0gp/server/internal/config"
"github.com/x0gp/server/internal/drivers" "github.com/x0gp/server/internal/drivers"
"github.com/x0gp/server/internal/lobby" "github.com/x0gp/server/internal/lobby"
"github.com/x0gp/server/internal/races"
"github.com/x0gp/server/internal/transport" "github.com/x0gp/server/internal/transport"
) )
@@ -283,16 +286,105 @@ func driversListHandler(svc *drivers.Service) http.HandlerFunc {
// @Router /api/drivers/{id} [get] // @Router /api/drivers/{id} [get]
// @Router /api/drivers/{id} [put] // @Router /api/drivers/{id} [put]
// @Router /api/drivers/{id} [delete] // @Router /api/drivers/{id} [delete]
// @Router /api/drivers/{id}/device [put] // @Router /api/drivers/{id}/car [put]
func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.HandlerFunc { func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service, racesSvc *races.Service, cfg *config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/drivers/") id := strings.TrimPrefix(r.URL.Path, "/api/drivers/")
if id == "" { if id == "" {
writeError(w, http.StatusBadRequest, "bad_request", "missing driver id") writeError(w, http.StatusBadRequest, "bad_request", "missing driver id")
return return
} }
if strings.HasSuffix(id, "/device") {
driverID := strings.TrimSuffix(id, "/device") if strings.HasSuffix(id, "/profile") {
driverID := strings.TrimSuffix(id, "/profile")
if driverID == "" {
writeError(w, http.StatusBadRequest, "bad_request", "missing driver id")
return
}
if driverID == "me" {
authedID, ok := auth.DriverIDFromContext(r.Context())
if !ok {
if cfg.DevMode {
driverID = "driver-alice"
} else {
writeError(w, http.StatusUnauthorized, "unauthorized", "missing or invalid auth token")
return
}
} else {
driverID = authedID
}
}
if r.Method != http.MethodGet {
w.Header().Set("Allow", "GET")
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", r.Method)
return
}
drv, err := svc.Get(r.Context(), driverID)
if err != nil {
if errors.Is(err, drivers.ErrNotFound) {
writeError(w, http.StatusNotFound, "not_found", "driver not found")
return
}
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
dbStats, dbLast, dbBest, err := racesSvc.GetDriverProfileStats(r.Context(), driverID)
if err != nil {
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
var lastSummary *transport.DriverRaceSummary
if dbLast != nil {
lastSummary = &transport.DriverRaceSummary{
RaceID: dbLast.RaceID,
RaceName: dbLast.RaceName,
FinishedMs: dbLast.FinishedMs,
Position: dbLast.Position,
TotalTimeMs: dbLast.TotalTimeMs,
BestLapMs: dbLast.BestLapMs,
TrackID: dbLast.TrackID,
TrackName: dbLast.TrackName,
}
}
var bestSummary *transport.DriverRaceSummary
if dbBest != nil {
bestSummary = &transport.DriverRaceSummary{
RaceID: dbBest.RaceID,
RaceName: dbBest.RaceName,
FinishedMs: dbBest.FinishedMs,
Position: dbBest.Position,
TotalTimeMs: dbBest.TotalTimeMs,
BestLapMs: dbBest.BestLapMs,
TrackID: dbBest.TrackID,
TrackName: dbBest.TrackName,
}
}
profile := transport.DriverProfileResponse{
Driver: driverToWire(drv),
Stats: transport.DriverStats{
TotalRacesStarted: dbStats.TotalRacesStarted,
TotalRacesFinished: dbStats.TotalRacesFinished,
Wins: dbStats.Wins,
Podiums: dbStats.Podiums,
BestLapMs: dbStats.BestLapMs,
TotalPlaytimeMs: dbStats.TotalPlaytimeMs,
},
LastRace: lastSummary,
BestRace: bestSummary,
}
writeJSON(w, http.StatusOK, profile)
return
}
if strings.HasSuffix(id, "/car") {
driverID := strings.TrimSuffix(id, "/car")
if driverID == "" { if driverID == "" {
writeError(w, http.StatusBadRequest, "bad_request", "missing driver id") writeError(w, http.StatusBadRequest, "bad_request", "missing driver id")
return return
@@ -303,29 +395,26 @@ func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.Hand
return return
} }
// Read device_id from JSON body or query param // Read car_id from JSON body or query param
var deviceID *int var carID *string
type deviceRequest struct { type carRequest struct {
DeviceID *int `json:"device_id"` CarID *string `json:"car_id"`
} }
var req deviceRequest var req carRequest
if r.ContentLength > 0 { if r.ContentLength > 0 {
if err := json.NewDecoder(r.Body).Decode(&req); err == nil { if err := json.NewDecoder(r.Body).Decode(&req); err == nil {
deviceID = req.DeviceID carID = req.CarID
} }
} }
// Fallback to query parameter if body didn't provide it // Fallback to query parameter if body didn't provide it
if deviceID == nil { if carID == nil {
qVal := r.URL.Query().Get("device_id") qVal := r.URL.Query().Get("car_id")
if qVal != "" { if qVal != "" {
if qVal == "null" { if qVal == "null" {
deviceID = nil carID = nil
} else if idInt, err := strconv.Atoi(qVal); err == nil {
deviceID = &idInt
} else { } else {
writeError(w, http.StatusBadRequest, "bad_request", "invalid device_id query parameter") carID = &qVal
return
} }
} }
} }
@@ -345,8 +434,8 @@ func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.Hand
_, _ = lobbySvc.AddDriver(driverID, drv.Name, "") _, _ = lobbySvc.AddDriver(driverID, drv.Name, "")
lobbySvc.SetDriverProfile(driverID, drv.Nickname, drv.AvatarURL, drv.ClanID, "") lobbySvc.SetDriverProfile(driverID, drv.Nickname, drv.AvatarURL, drv.ClanID, "")
// Select device in lobby (handles constraints and write-through) // Select car in lobby (handles constraints and write-through)
if err := lobbySvc.SelectDevice(driverID, deviceID); err != nil { if err := lobbySvc.SelectCar(driverID, carID); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", err.Error()) writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return return
} }
@@ -392,12 +481,21 @@ func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.Hand
} }
writeJSON(w, http.StatusOK, driverToWire(d)) writeJSON(w, http.StatusOK, driverToWire(d))
case http.MethodPut: case http.MethodPut:
if !cfg.DevMode {
authedID, ok := auth.DriverIDFromContext(r.Context())
if !ok || authedID != id {
writeError(w, http.StatusForbidden, "forbidden", "cannot update another driver's profile")
return
}
}
var req transport.DriverUpdateRequest var req transport.DriverUpdateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", err.Error()) writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return return
} }
d, err := svc.Update(r.Context(), id, drivers.UpdateInput{ d, err := svc.Update(r.Context(), id, drivers.UpdateInput{
Nickname: req.Nickname,
Name: req.Name, Name: req.Name,
AvatarURL: req.AvatarURL, AvatarURL: req.AvatarURL,
ClanID: req.ClanID, ClanID: req.ClanID,
@@ -407,11 +505,27 @@ func driversByIDHandler(svc *drivers.Service, lobbySvc *lobby.Service) http.Hand
writeError(w, http.StatusNotFound, "not_found", "driver not found") writeError(w, http.StatusNotFound, "not_found", "driver not found")
return return
} }
if errors.Is(err, drivers.ErrAlreadyExists) {
writeError(w, http.StatusConflict, "conflict", fmt.Sprintf("nickname %s is already taken", req.Nickname))
return
}
if errors.Is(err, drivers.ErrInvalidInput) {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
writeError(w, http.StatusInternalServerError, "internal", err.Error()) writeError(w, http.StatusInternalServerError, "internal", err.Error())
return return
} }
writeJSON(w, http.StatusOK, driverToWire(d)) writeJSON(w, http.StatusOK, driverToWire(d))
case http.MethodDelete: case http.MethodDelete:
if !cfg.DevMode {
authedID, ok := auth.DriverIDFromContext(r.Context())
if !ok || authedID != id {
writeError(w, http.StatusForbidden, "forbidden", "cannot delete another driver's profile")
return
}
}
if err := svc.Delete(r.Context(), id); err != nil { if err := svc.Delete(r.Context(), id); err != nil {
if errors.Is(err, drivers.ErrNotFound) { if errors.Is(err, drivers.ErrNotFound) {
writeError(w, http.StatusNotFound, "not_found", "driver not found") writeError(w, http.StatusNotFound, "not_found", "driver not found")
+185 -58
View File
@@ -27,15 +27,18 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"encoding/hex"
"encoding/json" "encoding/json"
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"log/slog" "log/slog"
"math"
"net" "net"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
"strings"
"sync" "sync"
"syscall" "syscall"
"time" "time"
@@ -49,6 +52,7 @@ import (
// the // @... annotations on the handlers. // the // @... annotations on the handlers.
_ "github.com/x0gp/server/docs" _ "github.com/x0gp/server/docs"
"github.com/x0gp/server/internal/auth"
"github.com/x0gp/server/internal/catalog" "github.com/x0gp/server/internal/catalog"
"github.com/x0gp/server/internal/clans" "github.com/x0gp/server/internal/clans"
"github.com/x0gp/server/internal/config" "github.com/x0gp/server/internal/config"
@@ -197,7 +201,7 @@ func main() {
driversSvc := drivers.NewService(drivers.NewPgStore(pool)) driversSvc := drivers.NewService(drivers.NewPgStore(pool))
// WebRTC 1-to-1 driver-car proxy service // WebRTC 1-to-1 driver-car proxy service
webrtcSvc := NewWebRTCService(logger, engine, lobbySvc, videoSvc) webrtcSvc := NewWebRTCService(logger, engine, lobbySvc, videoSvc, cat)
// Persist finished races so the /api/races list and keyset pagination // Persist finished races so the /api/races list and keyset pagination
// can serve historical data across restarts. The snapshot is best- // can serve historical data across restarts. The snapshot is best-
@@ -286,33 +290,40 @@ func main() {
// Start WebRTC service // Start WebRTC service
webrtcSvc.Start(ctx, &wg) webrtcSvc.Start(ctx, &wg)
apiMux := http.NewServeMux()
apiMux.HandleFunc("/api/version", versionHandler())
apiMux.HandleFunc("/api/catalog", catalogHandler(cat))
apiMux.HandleFunc("/api/tracks", tracksRouter(cat))
apiMux.HandleFunc("/api/tracks/", trackByIDHandler(cat))
apiMux.HandleFunc("/api/tracks/calendar", trackCalendarRouter(cat))
apiMux.HandleFunc("/api/tracks/calendar/", deleteTrackCalendarHandler(cat))
apiMux.HandleFunc("/api/cars", carsRouter(cat))
apiMux.HandleFunc("/api/cars/", carByIDHandler(cat))
apiMux.HandleFunc("/api/races", racesListHandler(racesSvc))
apiMux.HandleFunc("/api/races/last", racesGetLastHandler(racesSvc))
apiMux.HandleFunc("/api/races/", racesGetHandler(racesSvc))
apiMux.HandleFunc("/api/races/upcoming", racesUpcomingHandler(racesSvc))
apiMux.HandleFunc("/api/races/queue", racesQueueRouter(racesSvc))
apiMux.HandleFunc("/api/races/queue/join", racesQueueJoinHandler(racesSvc))
apiMux.HandleFunc("/api/races/plans", racePlansRouter(racesSvc))
apiMux.HandleFunc("/api/races/plans/", racePlansDeleteHandler(racesSvc))
apiMux.HandleFunc("/api/leaderboard", leaderboardHandler(racesSvc))
apiMux.HandleFunc("/api/leaderboard/tracks", calendarLeaderboardHandler(racesSvc))
apiMux.HandleFunc("/api/clans", clansRouter(clansSvc))
apiMux.HandleFunc("/api/clans/", clansByIDHandler(clansSvc))
apiMux.HandleFunc("/api/drivers", driversRouter(driversSvc))
apiMux.HandleFunc("/api/drivers/", driversByIDHandler(driversSvc, lobbySvc, racesSvc, cfg))
apiMux.HandleFunc("/api/video/stream", videoSvc.StreamHandler())
apiMux.HandleFunc("/api/video/control", videoSvc.ControlHandler())
apiMux.HandleFunc("/api/webrtc/connect", webrtcSvc.ConnectHandler())
authMW := auth.Middleware(cfg.JWTSecret, cfg.DevMode)
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler(hub, engine)) mux.HandleFunc("/health", healthHandler(hub, engine))
mux.HandleFunc("/api/version", versionHandler())
mux.HandleFunc("/stats", statsHandler(hub, engine)) mux.HandleFunc("/stats", statsHandler(hub, engine))
mux.HandleFunc("/api/catalog", catalogHandler(cat)) mux.Handle("/api/", authMW(apiMux))
mux.HandleFunc("/api/tracks", tracksRouter(cat)) mux.HandleFunc("/ws", wsHandler(cfg, engine, hub, cat, lobbySvc, driversSvc, clansSvc, videoSvc, logger))
mux.HandleFunc("/api/tracks/", trackByIDHandler(cat))
mux.HandleFunc("/api/tracks/calendar", trackCalendarRouter(cat))
mux.HandleFunc("/api/tracks/calendar/", deleteTrackCalendarHandler(cat))
mux.HandleFunc("/api/cars", carsRouter(cat))
mux.HandleFunc("/api/cars/", carByIDHandler(cat))
mux.HandleFunc("/api/races", racesListHandler(racesSvc))
mux.HandleFunc("/api/races/upcoming", racesUpcomingHandler(racesSvc))
mux.HandleFunc("/api/races/queue", racesQueueRouter(racesSvc))
mux.HandleFunc("/api/races/queue/join", racesQueueJoinHandler(racesSvc))
mux.HandleFunc("/api/races/plans", racePlansRouter(racesSvc))
mux.HandleFunc("/api/races/plans/", racePlansDeleteHandler(racesSvc))
mux.HandleFunc("/api/leaderboard", leaderboardHandler(racesSvc))
mux.HandleFunc("/api/leaderboard/tracks", calendarLeaderboardHandler(racesSvc))
mux.HandleFunc("/api/clans", clansRouter(clansSvc))
mux.HandleFunc("/api/clans/", clansByIDHandler(clansSvc))
mux.HandleFunc("/api/drivers", driversRouter(driversSvc))
mux.HandleFunc("/api/drivers/", driversByIDHandler(driversSvc, lobbySvc))
mux.HandleFunc("/api/video/stream", videoSvc.StreamHandler())
mux.HandleFunc("/api/video/control", videoSvc.ControlHandler())
mux.HandleFunc("/api/webrtc/connect", webrtcSvc.ConnectHandler())
mux.HandleFunc("/ws", wsHandler(cfg, engine, hub, cat, lobbySvc, driversSvc, videoSvc, logger))
// Static client files (resolving TODO) // Static client files (resolving TODO)
mux.Handle("/", http.FileServer(http.Dir("./web"))) mux.Handle("/", http.FileServer(http.Dir("./web")))
@@ -481,7 +492,7 @@ func statsHandler(h *Hub, e *control.Engine) http.HandlerFunc {
// @Tags websocket // @Tags websocket
// @Success 101 {object} transport.Envelope "Switching protocols. Subsequent frames follow the envelope schema." // @Success 101 {object} transport.Envelope "Switching protocols. Subsequent frames follow the envelope schema."
// @Router /ws [get] // @Router /ws [get]
func wsHandler(cfg *config.Config, e *control.Engine, h *Hub, cat *catalog.Service, lobbySvc *lobby.Service, driversSvc *drivers.Service, videoSvc *UDPVideoService, logger *slog.Logger) http.HandlerFunc { func wsHandler(cfg *config.Config, e *control.Engine, h *Hub, cat *catalog.Service, lobbySvc *lobby.Service, driversSvc *drivers.Service, clansSvc *clans.Service, videoSvc *UDPVideoService, logger *slog.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil) conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
@@ -499,7 +510,7 @@ func wsHandler(cfg *config.Config, e *control.Engine, h *Hub, cat *catalog.Servi
"remote", r.RemoteAddr, "remote", r.RemoteAddr,
) )
go writePump(client, conn, logger) go writePump(client, conn, logger)
go readPump(cfg, client, conn, e, h, cat, lobbySvc, driversSvc, videoSvc, logger) go readPump(cfg, client, conn, e, h, cat, lobbySvc, driversSvc, clansSvc, videoSvc, logger)
} }
} }
@@ -541,7 +552,7 @@ func writePump(c *realtime.Client, conn *websocket.Conn, logger *slog.Logger) {
} }
} }
func readPump(cfg *config.Config, c *realtime.Client, conn *websocket.Conn, e *control.Engine, h *Hub, cat *catalog.Service, lobbySvc *lobby.Service, driversSvc *drivers.Service, videoSvc *UDPVideoService, logger *slog.Logger) { func readPump(cfg *config.Config, c *realtime.Client, conn *websocket.Conn, e *control.Engine, h *Hub, cat *catalog.Service, lobbySvc *lobby.Service, driversSvc *drivers.Service, clansSvc *clans.Service, videoSvc *UDPVideoService, logger *slog.Logger) {
defer func() { defer func() {
h.Unregister(c) h.Unregister(c)
logger.Info("client disconnected", "client_id", c.ID) logger.Info("client disconnected", "client_id", c.ID)
@@ -573,9 +584,9 @@ func readPump(cfg *config.Config, c *realtime.Client, conn *websocket.Conn, e *c
switch env.Type { switch env.Type {
case transport.TypeClientHello: case transport.TypeClientHello:
handleClientHello(c, e, cfg, lobbySvc, driversSvc, env) handleClientHello(c, e, cfg, lobbySvc, driversSvc, clansSvc, env)
case transport.TypeJoinRace: case transport.TypeJoinRace:
handleJoinRace(c, e, lobbySvc, env) handleJoinRace(c, e, lobbySvc, cat, env)
case transport.TypeLeaveRace: case transport.TypeLeaveRace:
handleLeaveRace(c, e, env) handleLeaveRace(c, e, env)
case transport.TypeInputState: case transport.TypeInputState:
@@ -604,7 +615,7 @@ func readPump(cfg *config.Config, c *realtime.Client, conn *websocket.Conn, e *c
// Message handlers -------------------------------------------------------- // Message handlers --------------------------------------------------------
func handleClientHello(c *realtime.Client, e *control.Engine, cfg *config.Config, lobbySvc *lobby.Service, driversSvc *drivers.Service, env *transport.Envelope) { func handleClientHello(c *realtime.Client, e *control.Engine, cfg *config.Config, lobbySvc *lobby.Service, driversSvc *drivers.Service, clansSvc *clans.Service, env *transport.Envelope) {
hello := transport.ServerHello{ hello := transport.ServerHello{
SessionID: c.ID, SessionID: c.ID,
RaceTick: 0, RaceTick: 0,
@@ -615,25 +626,114 @@ func handleClientHello(c *realtime.Client, e *control.Engine, cfg *config.Config
hello.Config.MaxCars = 4 hello.Config.MaxCars = 4
c.SessionID = c.ID c.SessionID = c.ID
// Extract client_id (DriverID) if present in payload
payload, _ := env.Payload.(map[string]any) payload, _ := env.Payload.(map[string]any)
if payload != nil { var clientID string
if clientID, ok := payload["client_id"].(string); ok && clientID != "" { var email string
if cfg.DevMode {
// In devMode, client_id is optional and can be arbitrary
if cid, ok := payload["client_id"].(string); ok && cid != "" {
clientID = cid
} else {
clientID = c.ID
}
} else {
// In production, require valid Supabase JWT
tokenStr, _ := payload["token"].(string)
if tokenStr == "" {
sendError(c, "unauthorized", "missing auth token in client_hello")
c.Close()
return
}
claims, err := auth.ValidateJWT(tokenStr, cfg.JWTSecret)
if err != nil {
sendError(c, "unauthorized", fmt.Sprintf("invalid auth token: %v", err))
c.Close()
return
}
clientID = claims.DriverID
email = claims.Email
}
c.DriverID = clientID c.DriverID = clientID
// Pre-populate driver in the lobby
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) // Resolve/ensure profile and join lobby
if drv, err := driversSvc.Get(ctx, clientID); err == nil { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
drv, clanTag, err := ensureDriverProfile(ctx, clientID, email, driversSvc, clansSvc)
if err == nil {
_, _ = lobbySvc.AddDriver(clientID, drv.Name, "") _, _ = lobbySvc.AddDriver(clientID, drv.Name, "")
lobbySvc.SetDriverProfile(clientID, drv.Nickname, drv.AvatarURL, drv.ClanID, "") lobbySvc.SetDriverProfile(clientID, drv.Nickname, drv.AvatarURL, drv.ClanID, clanTag)
} } else {
cancel() // Fallback placeholder profile if database error
} _, _ = lobbySvc.AddDriver(clientID, "Driver", "")
lobbySvc.SetDriverProfile(clientID, "DRV", "", "", "")
} }
c.Send <- mustEncode(transport.TypeServerHello, hello) c.Send <- mustEncode(transport.TypeServerHello, hello)
} }
func handleJoinRace(c *realtime.Client, e *control.Engine, lobbySvc *lobby.Service, env *transport.Envelope) { func ensureDriverProfile(ctx context.Context, id string, email string, driversSvc *drivers.Service, clansSvc *clans.Service) (drivers.Driver, string, error) {
drv, err := driversSvc.Get(ctx, id)
if err == nil {
var clanTag string
if drv.ClanID != "" {
if cl, err := clansSvc.Get(ctx, drv.ClanID); err == nil {
clanTag = cl.Tag
}
}
return drv, clanTag, nil
}
// Profile not found: auto-create a default profile
name := "Driver"
if email != "" {
if idx := strings.Index(email, "@"); idx > 0 {
name = email[:idx]
}
}
// Generate unique 3-letter nickname
nick := "DRV"
if len(name) >= 3 {
candidate := strings.ToUpper(name[:3])
if isAlphaOnly(candidate) {
nick = candidate
}
}
// Check if nickname already exists
for i := 0; i < 20; i++ {
_, err := driversSvc.GetByNickname(ctx, nick)
if err != nil && errors.Is(err, drivers.ErrNotFound) {
break
}
nick = fmt.Sprintf("D%02d", i)
}
created, err := driversSvc.Create(ctx, drivers.CreateInput{
ID: id,
Nickname: nick,
Name: name,
})
if err != nil {
return drivers.Driver{}, "", err
}
return created, "", nil
}
func isAlphaOnly(s string) bool {
for _, c := range s {
if (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') {
return false
}
}
return true
}
func handleJoinRace(c *realtime.Client, e *control.Engine, lobbySvc *lobby.Service, cat *catalog.Service, env *transport.Envelope) {
payload, _ := env.Payload.(map[string]any) payload, _ := env.Payload.(map[string]any)
slot := 0 slot := 0
name := "anon" name := "anon"
@@ -652,8 +752,10 @@ func handleJoinRace(c *realtime.Client, e *control.Engine, lobbySvc *lobby.Servi
driverID = c.ID driverID = c.ID
} }
var deviceID *int var deviceID *int
if d, err := lobbySvc.GetDriver(driverID); err == nil { if d, err := lobbySvc.GetDriver(driverID); err == nil && d.CarID != nil && *d.CarID != "" {
deviceID = d.DeviceID if carMeta, ok := cat.GetCar(*d.CarID); ok {
deviceID = carMeta.DeviceID
}
} }
car, err := e.AddCar(c.ID, name, slot, deviceID) car, err := e.AddCar(c.ID, name, slot, deviceID)
@@ -685,6 +787,9 @@ func handleInput(c *realtime.Client, e *control.Engine, lobbySvc *lobby.Service,
if v, ok := payload["gear"].(float64); ok { if v, ok := payload["gear"].(float64); ok {
in.Gear = int(v) in.Gear = int(v)
} }
if v, ok := payload["buttons"].(float64); ok {
in.Buttons = uint32(v)
}
car := e.GetCarByDriver(c.ID) car := e.GetCarByDriver(c.ID)
if car == nil { if car == nil {
return return
@@ -693,21 +798,43 @@ func handleInput(c *realtime.Client, e *control.Engine, lobbySvc *lobby.Service,
// Forward command transitions to ESP32 physical car if car has a device_id assigned // Forward command transitions to ESP32 physical car if car has a device_id assigned
if car.DeviceID != nil { if car.DeviceID != nil {
var cmd string var packet [5]byte
if in.Throttle > 0.1 {
cmd = "start"
} else {
cmd = "stop"
}
if in.Steering < -0.2 {
cmd = "left"
} else if in.Steering > 0.2 {
cmd = "right"
}
if cmd != "" && cmd != c.LastUDPCommand { // 1. Steer: mapping float64 [-1.0, 1.0] to [0..200] (100 is center)
c.LastUDPCommand = cmd steerVal := int(math.Round((in.Steering + 1.0) * 100.0))
_ = videoSvc.SendCommand(uint8(*car.DeviceID), cmd) if steerVal < 0 { steerVal = 0 }
if steerVal > 200 { steerVal = 200 }
packet[0] = uint8(steerVal)
// 2. Throttle: mapping float64 [0.0, 1.0] to [0..100]
throttleVal := int(math.Round(in.Throttle * 100.0))
if throttleVal < 0 { throttleVal = 0 }
if throttleVal > 100 { throttleVal = 100 }
packet[1] = uint8(throttleVal)
// 3. Brake: mapping float64 [0.0, 1.0] to [0..100]
brakeVal := int(math.Round(in.Brake * 100.0))
if brakeVal < 0 { brakeVal = 0 }
if brakeVal > 100 { brakeVal = 100 }
packet[2] = uint8(brakeVal)
// 4. Gear: map -1 to 0 (R), 0 to 1 (N), >=1 to 2 (D)
gearVal := 1 // Neutral by default
if in.Gear < 0 {
gearVal = 0 // R
} else if in.Gear > 0 {
gearVal = 2 // D
}
packet[3] = uint8(gearVal)
// 5. Flags: bit 0 (DRS), bit 1 (Pit Limiter)
packet[4] = uint8(in.Buttons & 0x03)
// Deduplicate UDP transmissions
hexCmd := hex.EncodeToString(packet[:])
if hexCmd != c.LastUDPCommand {
c.LastUDPCommand = hexCmd
_ = videoSvc.SendControlPacket(uint8(*car.DeviceID), packet)
} }
} }
+94
View File
@@ -90,6 +90,100 @@ func racesListHandler(svc *races.Service) http.HandlerFunc {
} }
} }
// racesGetLastHandler godoc
// @Summary Get the results of the last finished race
// @Description Returns the details and podium/results of the most recently finished race.
// @Tags races
// @Produce json
// @Success 200 {object} transport.LobbyRace "Last race results"
// @Failure 404 {object} map[string]interface{} "No finished races found"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /api/races/last [get]
func racesGetLastHandler(svc *races.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
res, err := svc.GetLastFinished(r.Context())
if err != nil {
if errors.Is(err, races.ErrNotFound) {
writeError(w, http.StatusNotFound, "not_found", "no finished races found")
return
}
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
var out transport.LobbyRace
meta := lobby.RaceMeta{
ID: res.ID,
Name: res.Name,
TrackID: res.TrackID,
MaxCars: res.MaxCars,
Laps: res.Laps,
TimeLimitS: res.TimeLimitS,
DriverIDs: res.DriverIDs,
Status: lobby.RaceStatus(res.Status),
CreatedMs: res.CreatedMs,
StartedMs: res.StartedMs,
}
if len(res.Podium) > 0 {
out = lobbyToWireFinished(meta, res.Podium)
} else {
out = lobbyToWire(meta)
}
writeJSON(w, http.StatusOK, out)
}
}
// racesGetHandler godoc
// @Summary Get results of a specific race by ID
// @Description Returns details of a single race by its ID. Works for both live and finished races.
// @Tags races
// @Produce json
// @Param id path string true "Race ID"
// @Success 200 {object} transport.LobbyRace "Race details"
// @Failure 400 {object} map[string]interface{} "Missing race id"
// @Failure 404 {object} map[string]interface{} "Race not found"
// @Failure 500 {object} map[string]interface{} "Internal server error"
// @Router /api/races/{id} [get]
func racesGetHandler(svc *races.Service) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := strings.TrimPrefix(r.URL.Path, "/api/races/")
if id == "" || strings.Contains(id, "/") {
writeError(w, http.StatusBadRequest, "bad_request", "missing race id")
return
}
res, err := svc.GetRace(r.Context(), id)
if err != nil {
if errors.Is(err, races.ErrNotFound) {
writeError(w, http.StatusNotFound, "not_found", "race not found")
return
}
writeError(w, http.StatusInternalServerError, "internal", err.Error())
return
}
var out transport.LobbyRace
meta := lobby.RaceMeta{
ID: res.ID,
Name: res.Name,
TrackID: res.TrackID,
MaxCars: res.MaxCars,
Laps: res.Laps,
TimeLimitS: res.TimeLimitS,
DriverIDs: res.DriverIDs,
Status: lobby.RaceStatus(res.Status),
CreatedMs: res.CreatedMs,
StartedMs: res.StartedMs,
}
if len(res.Podium) > 0 {
out = lobbyToWireFinished(meta, res.Podium)
} else {
out = lobbyToWire(meta)
}
writeJSON(w, http.StatusOK, out)
}
}
func parseRaceListFilter(r *http.Request) (races.ListFilter, error) { func parseRaceListFilter(r *http.Request) (races.ListFilter, error) {
q := r.URL.Query() q := r.URL.Query()
cur, err := races.DecodeCursor(q.Get("cursor")) cur, err := races.DecodeCursor(q.Get("cursor"))
+138 -18
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"bufio" "bufio"
"context" "context"
"encoding/json"
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
@@ -172,7 +173,6 @@ func (s *UDPVideoService) Start(ctx context.Context, wg *sync.WaitGroup) {
} }
s.mu.RUnlock() s.mu.RUnlock()
} }
if !hasDevice { if !hasDevice {
s.logger.Warn("cannot send command: no ESP32 devices discovered yet") s.logger.Warn("cannot send command: no ESP32 devices discovered yet")
continue continue
@@ -188,13 +188,12 @@ func (s *UDPVideoService) Start(ctx context.Context, wg *sync.WaitGroup) {
}() }()
} }
// SendCommand sends a command string to the ESP32 representing deviceID. // SendControlPacket sends a 5-byte binary control packet to the ESP32 representing deviceID.
func (s *UDPVideoService) SendCommand(deviceID uint8, cmd string) error { func (s *UDPVideoService) SendControlPacket(deviceID uint8, packet [5]byte) error {
s.mu.RLock() s.mu.RLock()
dev, ok := s.devices[deviceID] dev, ok := s.devices[deviceID]
conn := s.conn conn := s.conn
s.mu.RUnlock() s.mu.RUnlock()
if !ok || dev.addr == nil { if !ok || dev.addr == nil {
return fmt.Errorf("ESP32 device %d address unknown (no video packets received yet)", deviceID) return fmt.Errorf("ESP32 device %d address unknown (no video packets received yet)", deviceID)
} }
@@ -207,10 +206,35 @@ func (s *UDPVideoService) SendCommand(deviceID uint8, cmd string) error {
Port: s.espCmdPort, Port: s.espCmdPort,
} }
_, err := conn.WriteToUDP([]byte(cmd), cmdAddr) _, err := conn.WriteToUDP(packet[:], cmdAddr)
return err return err
} }
// SendCommand sends a command string to the ESP32 representing deviceID by mapping it to a 5-byte packet.
func (s *UDPVideoService) SendCommand(deviceID uint8, cmd string) error {
var packet [5]byte
packet[0] = 100 // Steer: center
packet[1] = 0 // Throttle: 0%
packet[2] = 0 // Brake: 0%
packet[3] = 1 // Gear: Neutral
packet[4] = 0 // Flags: 0
switch cmd {
case "start":
packet[1] = 100 // Throttle: 100%
packet[3] = 2 // Gear: Drive
case "stop":
packet[2] = 100 // Brake: 100%
packet[3] = 1 // Gear: Neutral
case "left":
packet[0] = 0 // Steer: full left
case "right":
packet[0] = 200 // Steer: full right
}
return s.SendControlPacket(deviceID, packet)
}
func (s *UDPVideoService) broadcastFrame(deviceID uint8, frame []byte) { func (s *UDPVideoService) broadcastFrame(deviceID uint8, frame []byte) {
// dev.latestFrame is updated inside the caller's lock (Start loop) to avoid races // dev.latestFrame is updated inside the caller's lock (Start loop) to avoid races
if dev, ok := s.devices[deviceID]; ok { if dev, ok := s.devices[deviceID]; ok {
@@ -328,13 +352,14 @@ func (s *UDPVideoService) StreamHandler() http.HandlerFunc {
// ControlHandler godoc // ControlHandler godoc
// @Summary Send command to ESP32 // @Summary Send command to ESP32
// @Description Sends a text command (e.g. start, stop, left, right) to the ESP32 command port (UDP 8888). // @Description Sends a text command or a JSON control packet to the ESP32 command port (UDP 8888).
// @Tags video // @Tags video
// @Param id query int false "Device ID (0..127)" // @Param id query int false "Device ID (0..127)"
// @Param cmd query string true "Command text" // @Param cmd query string false "Command text (legacy)"
// @Param body body map[string]int false "Control packet JSON (steer, throttle, brake, gear, flags)"
// @Produce json // @Produce json
// @Success 200 {object} map[string]interface{} "Command sent" // @Success 200 {object} map[string]interface{} "Command sent"
// @Failure 400 {string} string "Missing cmd parameter" // @Failure 400 {string} string "Missing parameter or invalid body"
// @Failure 500 {string} string "Internal error" // @Failure 500 {string} string "Internal error"
// @Router /api/video/control [post] // @Router /api/video/control [post]
func (s *UDPVideoService) ControlHandler() http.HandlerFunc { func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
@@ -343,14 +368,6 @@ func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed") writeError(w, http.StatusMethodNotAllowed, "method_not_allowed", "Only POST is allowed")
return return
} }
cmd := r.URL.Query().Get("cmd")
if cmd == "" {
cmd = r.FormValue("cmd")
}
if cmd == "" {
writeError(w, http.StatusBadRequest, "bad_request", "Missing 'cmd' parameter")
return
}
idVal := r.URL.Query().Get("id") idVal := r.URL.Query().Get("id")
if idVal == "" { if idVal == "" {
@@ -366,7 +383,109 @@ func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
} }
} }
err := s.SendCommand(deviceID, cmd) var packet [5]byte
packet[0] = 100 // Steer: center
packet[1] = 0 // Throttle: 0%
packet[2] = 0 // Brake: 0%
packet[3] = 1 // Gear: Neutral
packet[4] = 0 // Flags: 0
isJSON := strings.Contains(r.Header.Get("Content-Type"), "application/json")
var cmdUsed string
if isJSON {
var req struct {
Steer *int `json:"steer"`
Throttle *int `json:"throttle"`
Brake *int `json:"brake"`
Gear *int `json:"gear"`
Flags *int `json:"flags"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", "invalid JSON payload")
return
}
if req.Steer != nil {
val := *req.Steer
if val < 0 {
val = 0
}
if val > 200 {
val = 200
}
packet[0] = uint8(val)
}
if req.Throttle != nil {
val := *req.Throttle
if val < 0 {
val = 0
}
if val > 100 {
val = 100
}
packet[1] = uint8(val)
}
if req.Brake != nil {
val := *req.Brake
if val < 0 {
val = 0
}
if val > 100 {
val = 100
}
packet[2] = uint8(val)
}
if req.Gear != nil {
val := *req.Gear
if val < 0 {
val = 0
}
if val > 2 {
val = 2
}
packet[3] = uint8(val)
}
if req.Flags != nil {
val := *req.Flags
if val < 0 {
val = 0
}
if val > 255 {
val = 255
}
packet[4] = uint8(val)
}
cmdUsed = fmt.Sprintf("json(steer=%d,throttle=%d,brake=%d,gear=%d,flags=%d)", packet[0], packet[1], packet[2], packet[3], packet[4])
} else {
cmd := r.URL.Query().Get("cmd")
if cmd == "" {
cmd = r.FormValue("cmd")
}
if cmd == "" {
writeError(w, http.StatusBadRequest, "bad_request", "Missing 'cmd' parameter")
return
}
cmdUsed = cmd
switch cmd {
case "start":
packet[1] = 100
packet[3] = 2
case "stop":
packet[2] = 100
packet[3] = 1
case "left":
packet[0] = 0
case "right":
packet[0] = 200
default:
writeError(w, http.StatusBadRequest, "bad_request", "Unknown command: "+cmd)
return
}
}
err := s.SendControlPacket(deviceID, packet)
if err != nil { if err != nil {
writeError(w, http.StatusInternalServerError, "internal_error", err.Error()) writeError(w, http.StatusInternalServerError, "internal_error", err.Error())
return return
@@ -375,7 +494,8 @@ func (s *UDPVideoService) ControlHandler() http.HandlerFunc {
writeJSON(w, http.StatusOK, map[string]any{ writeJSON(w, http.StatusOK, map[string]any{
"status": "success", "status": "success",
"device_id": deviceID, "device_id": deviceID,
"command": cmd, "command": cmdUsed,
"packet": packet[:],
}) })
} }
} }
+58 -23
View File
@@ -2,13 +2,16 @@ package main
import ( import (
"context" "context"
"encoding/hex"
"encoding/json" "encoding/json"
"log/slog" "log/slog"
"math"
"net/http" "net/http"
"sync" "sync"
"time" "time"
"github.com/pion/webrtc/v4" "github.com/pion/webrtc/v4"
"github.com/x0gp/server/internal/catalog"
"github.com/x0gp/server/internal/control" "github.com/x0gp/server/internal/control"
"github.com/x0gp/server/internal/lobby" "github.com/x0gp/server/internal/lobby"
"github.com/x0gp/server/internal/transport" "github.com/x0gp/server/internal/transport"
@@ -29,16 +32,18 @@ type WebRTCService struct {
engine *control.Engine engine *control.Engine
lobbySvc *lobby.Service lobbySvc *lobby.Service
videoSvc *UDPVideoService videoSvc *UDPVideoService
catalogSvc *catalog.Service
sessions map[string]*driverSession sessions map[string]*driverSession
sessionsMu sync.RWMutex sessionsMu sync.RWMutex
} }
func NewWebRTCService(logger *slog.Logger, e *control.Engine, lobbySvc *lobby.Service, videoSvc *UDPVideoService) *WebRTCService { func NewWebRTCService(logger *slog.Logger, e *control.Engine, lobbySvc *lobby.Service, videoSvc *UDPVideoService, catalogSvc *catalog.Service) *WebRTCService {
return &WebRTCService{ return &WebRTCService{
logger: logger, logger: logger,
engine: e, engine: e,
lobbySvc: lobbySvc, lobbySvc: lobbySvc,
videoSvc: videoSvc, videoSvc: videoSvc,
catalogSvc: catalogSvc,
sessions: make(map[string]*driverSession), sessions: make(map[string]*driverSession),
} }
} }
@@ -91,10 +96,14 @@ func (s *WebRTCService) ConnectHandler() http.HandlerFunc {
// Resolve device ID for this driver // Resolve device ID for this driver
deviceID := uint8(1) // default fallback deviceID := uint8(1) // default fallback
if driver, err := s.lobbySvc.GetDriver(req.DriverID); err == nil && driver.DeviceID != nil { if driver, err := s.lobbySvc.GetDriver(req.DriverID); err == nil && driver.CarID != nil && *driver.CarID != "" {
deviceID = uint8(*driver.DeviceID) if car, ok := s.catalogSvc.GetCar(*driver.CarID); ok && car.DeviceID != nil {
deviceID = uint8(*car.DeviceID)
} else { } else {
s.logger.Warn("webrtc client connected but no physical device_id assigned in lobbySvc. Defaulting to device 1", "driver_id", req.DriverID) s.logger.Warn("webrtc client connected but car has no physical device_id assigned in catalog. Defaulting to device 1", "driver_id", req.DriverID, "car_id", *driver.CarID)
}
} else {
s.logger.Warn("webrtc client connected but no car_id assigned in lobbySvc. Defaulting to device 1", "driver_id", req.DriverID)
} }
// Create PeerConnection // Create PeerConnection
@@ -269,6 +278,10 @@ func (s *WebRTCService) handleDataChannelMessage(session *driverSession, data []
in.Gear = int(v) in.Gear = int(v)
hasInput = true hasInput = true
} }
if v, ok := payload["buttons"].(float64); ok {
in.Buttons = uint32(v)
hasInput = true
}
if hasInput { if hasInput {
s.logger.Debug("received inputs on webrtc data channel", "driver_id", session.driverID, "throttle", in.Throttle, "steering", in.Steering) s.logger.Debug("received inputs on webrtc data channel", "driver_id", session.driverID, "throttle", in.Throttle, "steering", in.Steering)
@@ -277,19 +290,6 @@ func (s *WebRTCService) handleDataChannelMessage(session *driverSession, data []
car.ApplyInput(in, 1.0/60.0) car.ApplyInput(in, 1.0/60.0)
} }
// Send commands to physical car
var cmd string
if in.Throttle > 0.1 {
cmd = "start"
} else {
cmd = "stop"
}
if in.Steering < -0.2 {
cmd = "left"
} else if in.Steering > 0.2 {
cmd = "right"
}
// Resolve destination device // Resolve destination device
var destDeviceID *uint8 var destDeviceID *uint8
if car != nil && car.DeviceID != nil { if car != nil && car.DeviceID != nil {
@@ -300,14 +300,49 @@ func (s *WebRTCService) handleDataChannelMessage(session *driverSession, data []
destDeviceID = &session.deviceID destDeviceID = &session.deviceID
} }
if destDeviceID != nil && cmd != "" && cmd != session.lastCommand { if destDeviceID != nil {
s.logger.Info("webrtc control state transition", "driver_id", session.driverID, "old_cmd", session.lastCommand, "new_cmd", cmd) var packet [5]byte
session.lastCommand = cmd
err := s.videoSvc.SendCommand(*destDeviceID, cmd) // 1. Steer: mapping float64 [-1.0, 1.0] to [0..200] (100 is center)
steerVal := int(math.Round((in.Steering + 1.0) * 100.0))
if steerVal < 0 { steerVal = 0 }
if steerVal > 200 { steerVal = 200 }
packet[0] = uint8(steerVal)
// 2. Throttle: mapping float64 [0.0, 1.0] to [0..100]
throttleVal := int(math.Round(in.Throttle * 100.0))
if throttleVal < 0 { throttleVal = 0 }
if throttleVal > 100 { throttleVal = 100 }
packet[1] = uint8(throttleVal)
// 3. Brake: mapping float64 [0.0, 1.0] to [0..100]
brakeVal := int(math.Round(in.Brake * 100.0))
if brakeVal < 0 { brakeVal = 0 }
if brakeVal > 100 { brakeVal = 100 }
packet[2] = uint8(brakeVal)
// 4. Gear: map -1 to 0 (R), 0 to 1 (N), >=1 to 2 (D)
gearVal := 1 // Neutral by default
if in.Gear < 0 {
gearVal = 0 // R
} else if in.Gear > 0 {
gearVal = 2 // D
}
packet[3] = uint8(gearVal)
// 5. Flags: bit 0 (DRS), bit 1 (Pit Limiter)
packet[4] = uint8(in.Buttons & 0x03)
hexCmd := hex.EncodeToString(packet[:])
if hexCmd != session.lastCommand {
s.logger.Info("webrtc control state transition", "driver_id", session.driverID, "old_hex", session.lastCommand, "new_hex", hexCmd)
session.lastCommand = hexCmd
err := s.videoSvc.SendControlPacket(*destDeviceID, packet)
if err != nil { if err != nil {
s.logger.Error("failed to forward command to ESP32", "device_id", *destDeviceID, "cmd", cmd, "err", err) s.logger.Error("failed to forward control packet to ESP32", "device_id", *destDeviceID, "packet", packet, "err", err)
} else { } else {
s.logger.Info("forwarded command to ESP32 via UDP", "device_id", *destDeviceID, "cmd", cmd) s.logger.Info("forwarded control packet to ESP32 via UDP", "device_id", *destDeviceID, "packet", packet)
}
} }
} }
} }
+110 -6
View File
@@ -948,7 +948,7 @@ const docTemplate = `{
} }
} }
}, },
"/api/drivers/{id}/device": { "/api/drivers/{id}/car": {
"put": { "put": {
"description": "Dispatches on HTTP method. Path ` + "`" + `/api/drivers/by-nick/{nick}` + "`" + ` is a convenience that returns the driver by 3-letter nickname.", "description": "Dispatches on HTTP method. Path ` + "`" + `/api/drivers/by-nick/{nick}` + "`" + ` is a convenience that returns the driver by 3-letter nickname.",
"produces": [ "produces": [
@@ -1192,6 +1192,40 @@ const docTemplate = `{
} }
} }
}, },
"/api/races/last": {
"get": {
"description": "Returns the details and podium/results of the most recently finished race.",
"produces": [
"application/json"
],
"tags": [
"races"
],
"summary": "Get the results of the last finished race",
"responses": {
"200": {
"description": "Last race results",
"schema": {
"$ref": "#/definitions/transport.LobbyRace"
}
},
"404": {
"description": "No finished races found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/races/plans": { "/api/races/plans": {
"get": { "get": {
"description": "Returns race plans in ascending start_at order. Pass back the ` + "`" + `next_cursor` + "`" + ` to get the next page.", "description": "Returns race plans in ascending start_at order. Pass back the ` + "`" + `next_cursor` + "`" + ` to get the next page.",
@@ -1534,6 +1568,56 @@ const docTemplate = `{
} }
} }
}, },
"/api/races/{id}": {
"get": {
"description": "Returns details of a single race by its ID. Works for both live and finished races.",
"produces": [
"application/json"
],
"tags": [
"races"
],
"summary": "Get results of a specific race by ID",
"parameters": [
{
"type": "string",
"description": "Race ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Race details",
"schema": {
"$ref": "#/definitions/transport.LobbyRace"
}
},
"400": {
"description": "Missing race id",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Race not found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/tracks": { "/api/tracks": {
"get": { "get": {
"description": "Returns all tracks visible to the caller. Optional query filters: ` + "`" + `scope=system|public|private` + "`" + `, ` + "`" + `tag=\u003cstring\u003e` + "`" + `.", "description": "Returns all tracks visible to the caller. Optional query filters: ` + "`" + `scope=system|public|private` + "`" + `, ` + "`" + `tag=\u003cstring\u003e` + "`" + `.",
@@ -2003,7 +2087,7 @@ const docTemplate = `{
}, },
"/api/video/control": { "/api/video/control": {
"post": { "post": {
"description": "Sends a text command (e.g. start, stop, left, right) to the ESP32 command port (UDP 8888).", "description": "Sends a text command or a JSON control packet to the ESP32 command port (UDP 8888).",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -2020,10 +2104,20 @@ const docTemplate = `{
}, },
{ {
"type": "string", "type": "string",
"description": "Command text", "description": "Command text (legacy)",
"name": "cmd", "name": "cmd",
"in": "query", "in": "query"
"required": true },
{
"description": "Control packet JSON (steer, throttle, brake, gear, flags)",
"name": "body",
"in": "body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "integer"
}
}
} }
], ],
"responses": { "responses": {
@@ -2035,7 +2129,7 @@ const docTemplate = `{
} }
}, },
"400": { "400": {
"description": "Missing cmd parameter", "description": "Missing parameter or invalid body",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@@ -2232,6 +2326,9 @@ const docTemplate = `{
"color_hex": { "color_hex": {
"type": "string" "type": "string"
}, },
"device_id": {
"type": "integer"
},
"drive": { "drive": {
"type": "string" "type": "string"
}, },
@@ -2299,6 +2396,9 @@ const docTemplate = `{
"created_ms": { "created_ms": {
"type": "integer" "type": "integer"
}, },
"device_id": {
"type": "integer"
},
"drive": { "drive": {
"type": "string" "type": "string"
}, },
@@ -2528,6 +2628,10 @@ const docTemplate = `{
"name": { "name": {
"type": "string", "type": "string",
"example": "Alice" "example": "Alice"
},
"nickname": {
"type": "string",
"example": "ALI"
} }
} }
}, },
+110 -6
View File
@@ -946,7 +946,7 @@
} }
} }
}, },
"/api/drivers/{id}/device": { "/api/drivers/{id}/car": {
"put": { "put": {
"description": "Dispatches on HTTP method. Path `/api/drivers/by-nick/{nick}` is a convenience that returns the driver by 3-letter nickname.", "description": "Dispatches on HTTP method. Path `/api/drivers/by-nick/{nick}` is a convenience that returns the driver by 3-letter nickname.",
"produces": [ "produces": [
@@ -1190,6 +1190,40 @@
} }
} }
}, },
"/api/races/last": {
"get": {
"description": "Returns the details and podium/results of the most recently finished race.",
"produces": [
"application/json"
],
"tags": [
"races"
],
"summary": "Get the results of the last finished race",
"responses": {
"200": {
"description": "Last race results",
"schema": {
"$ref": "#/definitions/transport.LobbyRace"
}
},
"404": {
"description": "No finished races found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/races/plans": { "/api/races/plans": {
"get": { "get": {
"description": "Returns race plans in ascending start_at order. Pass back the `next_cursor` to get the next page.", "description": "Returns race plans in ascending start_at order. Pass back the `next_cursor` to get the next page.",
@@ -1532,6 +1566,56 @@
} }
} }
}, },
"/api/races/{id}": {
"get": {
"description": "Returns details of a single race by its ID. Works for both live and finished races.",
"produces": [
"application/json"
],
"tags": [
"races"
],
"summary": "Get results of a specific race by ID",
"parameters": [
{
"type": "string",
"description": "Race ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "Race details",
"schema": {
"$ref": "#/definitions/transport.LobbyRace"
}
},
"400": {
"description": "Missing race id",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"404": {
"description": "Race not found",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/api/tracks": { "/api/tracks": {
"get": { "get": {
"description": "Returns all tracks visible to the caller. Optional query filters: `scope=system|public|private`, `tag=\u003cstring\u003e`.", "description": "Returns all tracks visible to the caller. Optional query filters: `scope=system|public|private`, `tag=\u003cstring\u003e`.",
@@ -2001,7 +2085,7 @@
}, },
"/api/video/control": { "/api/video/control": {
"post": { "post": {
"description": "Sends a text command (e.g. start, stop, left, right) to the ESP32 command port (UDP 8888).", "description": "Sends a text command or a JSON control packet to the ESP32 command port (UDP 8888).",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@@ -2018,10 +2102,20 @@
}, },
{ {
"type": "string", "type": "string",
"description": "Command text", "description": "Command text (legacy)",
"name": "cmd", "name": "cmd",
"in": "query", "in": "query"
"required": true },
{
"description": "Control packet JSON (steer, throttle, brake, gear, flags)",
"name": "body",
"in": "body",
"schema": {
"type": "object",
"additionalProperties": {
"type": "integer"
}
}
} }
], ],
"responses": { "responses": {
@@ -2033,7 +2127,7 @@
} }
}, },
"400": { "400": {
"description": "Missing cmd parameter", "description": "Missing parameter or invalid body",
"schema": { "schema": {
"type": "string" "type": "string"
} }
@@ -2230,6 +2324,9 @@
"color_hex": { "color_hex": {
"type": "string" "type": "string"
}, },
"device_id": {
"type": "integer"
},
"drive": { "drive": {
"type": "string" "type": "string"
}, },
@@ -2297,6 +2394,9 @@
"created_ms": { "created_ms": {
"type": "integer" "type": "integer"
}, },
"device_id": {
"type": "integer"
},
"drive": { "drive": {
"type": "string" "type": "string"
}, },
@@ -2526,6 +2626,10 @@
"name": { "name": {
"type": "string", "type": "string",
"example": "Alice" "example": "Alice"
},
"nickname": {
"type": "string",
"example": "ALI"
} }
} }
}, },
+78 -6
View File
@@ -39,6 +39,8 @@ definitions:
$ref: '#/definitions/transport.ChassisWire' $ref: '#/definitions/transport.ChassisWire'
color_hex: color_hex:
type: string type: string
device_id:
type: integer
drive: drive:
type: string type: string
height_mm: height_mm:
@@ -83,6 +85,8 @@ definitions:
type: string type: string
created_ms: created_ms:
type: integer type: integer
device_id:
type: integer
drive: drive:
type: string type: string
height_mm: height_mm:
@@ -241,6 +245,9 @@ definitions:
name: name:
example: Alice example: Alice
type: string type: string
nickname:
example: ALI
type: string
type: object type: object
transport.Envelope: transport.Envelope:
properties: properties:
@@ -1484,7 +1491,7 @@ paths:
summary: Get / update / delete a driver by id summary: Get / update / delete a driver by id
tags: tags:
- drivers - drivers
/api/drivers/{id}/device: /api/drivers/{id}/car:
put: put:
description: Dispatches on HTTP method. Path `/api/drivers/by-nick/{nick}` is description: Dispatches on HTTP method. Path `/api/drivers/by-nick/{nick}` is
a convenience that returns the driver by 3-letter nickname. a convenience that returns the driver by 3-letter nickname.
@@ -1656,6 +1663,65 @@ paths:
summary: List races (keyset paginated) summary: List races (keyset paginated)
tags: tags:
- races - races
/api/races/{id}:
get:
description: Returns details of a single race by its ID. Works for both live
and finished races.
parameters:
- description: Race ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: Race details
schema:
$ref: '#/definitions/transport.LobbyRace'
"400":
description: Missing race id
schema:
additionalProperties: true
type: object
"404":
description: Race not found
schema:
additionalProperties: true
type: object
"500":
description: Internal server error
schema:
additionalProperties: true
type: object
summary: Get results of a specific race by ID
tags:
- races
/api/races/last:
get:
description: Returns the details and podium/results of the most recently finished
race.
produces:
- application/json
responses:
"200":
description: Last race results
schema:
$ref: '#/definitions/transport.LobbyRace'
"404":
description: No finished races found
schema:
additionalProperties: true
type: object
"500":
description: Internal server error
schema:
additionalProperties: true
type: object
summary: Get the results of the last finished race
tags:
- races
/api/races/plans: /api/races/plans:
get: get:
description: Returns race plans in ascending start_at order. Pass back the `next_cursor` description: Returns race plans in ascending start_at order. Pass back the `next_cursor`
@@ -2219,18 +2285,24 @@ paths:
- system - system
/api/video/control: /api/video/control:
post: post:
description: Sends a text command (e.g. start, stop, left, right) to the ESP32 description: Sends a text command or a JSON control packet to the ESP32 command
command port (UDP 8888). port (UDP 8888).
parameters: parameters:
- description: Device ID (0..127) - description: Device ID (0..127)
in: query in: query
name: id name: id
type: integer type: integer
- description: Command text - description: Command text (legacy)
in: query in: query
name: cmd name: cmd
required: true
type: string type: string
- description: Control packet JSON (steer, throttle, brake, gear, flags)
in: body
name: body
schema:
additionalProperties:
type: integer
type: object
produces: produces:
- application/json - application/json
responses: responses:
@@ -2240,7 +2312,7 @@ paths:
additionalProperties: true additionalProperties: true
type: object type: object
"400": "400":
description: Missing cmd parameter description: Missing parameter or invalid body
schema: schema:
type: string type: string
"500": "500":
+1
View File
@@ -15,6 +15,7 @@ require (
github.com/go-openapi/jsonreference v0.20.0 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.6 // indirect github.com/go-openapi/spec v0.20.6 // indirect
github.com/go-openapi/swag v0.19.15 // indirect github.com/go-openapi/swag v0.19.15 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
+2
View File
@@ -14,6 +14,8 @@ github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+105
View File
@@ -0,0 +1,105 @@
// Package auth handles Supabase JWT validation and HTTP middleware.
package auth
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"github.com/golang-jwt/jwt/v5"
)
type contextKey int
const (
driverIDKey contextKey = iota
emailKey
)
// Claims represents the parsed Supabase JWT claims we care about.
type Claims struct {
DriverID string
Email string
}
// ValidateJWT parses and validates a Supabase HMAC-SHA256 JWT using the secret.
func ValidateJWT(tokenStr string, secret string) (Claims, error) {
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(secret), nil
})
if err != nil {
return Claims{}, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
sub, _ := claims["sub"].(string)
email, _ := claims["email"].(string)
if sub == "" {
return Claims{}, errors.New("missing sub claim")
}
return Claims{
DriverID: sub,
Email: email,
}, nil
}
return Claims{}, errors.New("invalid token")
}
// Middleware returns an HTTP middleware that extracts and validates the Bearer token.
// When devMode is enabled, authentication is bypassed entirely.
func Middleware(secret string, devMode bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if devMode {
next.ServeHTTP(w, r)
return
}
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":"unauthorized","message":"missing authorization header"}`))
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(`{"error":"unauthorized","message":"invalid authorization format"}`))
return
}
claims, err := ValidateJWT(parts[1], secret)
if err != nil {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte(fmt.Sprintf(`{"error":"unauthorized","message":%q}`, err.Error())))
return
}
ctx := context.WithValue(r.Context(), driverIDKey, claims.DriverID)
ctx = context.WithValue(ctx, emailKey, claims.Email)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// DriverIDFromContext retrieves the driver ID from the context.
func DriverIDFromContext(ctx context.Context) (string, bool) {
id, ok := ctx.Value(driverIDKey).(string)
return id, ok
}
// EmailFromContext retrieves the email from the context.
func EmailFromContext(ctx context.Context) (string, bool) {
email, ok := ctx.Value(emailKey).(string)
return email, ok
}
+125
View File
@@ -0,0 +1,125 @@
package auth
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
const testSecret = "my-super-secret-supabase-jwt-key"
func generateTestToken(sub string, email string, expiresAt time.Time, secret string) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": sub,
"email": email,
"exp": expiresAt.Unix(),
})
return token.SignedString([]byte(secret))
}
func TestValidateJWT(t *testing.T) {
// 1. Valid token
validToken, err := generateTestToken("driver-uuid-123", "driver@test.com", time.Now().Add(time.Hour), testSecret)
if err != nil {
t.Fatalf("failed to generate valid token: %v", err)
}
claims, err := ValidateJWT(validToken, testSecret)
if err != nil {
t.Errorf("expected no error, got: %v", err)
}
if claims.DriverID != "driver-uuid-123" || claims.Email != "driver@test.com" {
t.Errorf("unexpected claims: %+v", claims)
}
// 2. Expired token
expiredToken, err := generateTestToken("driver-uuid-123", "driver@test.com", time.Now().Add(-time.Hour), testSecret)
if err != nil {
t.Fatalf("failed to generate expired token: %v", err)
}
_, err = ValidateJWT(expiredToken, testSecret)
if err == nil {
t.Error("expected error for expired token, got nil")
}
// 3. Invalid signature
_, err = ValidateJWT(validToken, "wrong-secret-key")
if err == nil {
t.Error("expected error for wrong signature, got nil")
}
}
func TestAuthMiddleware(t *testing.T) {
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
driverID, ok := DriverIDFromContext(r.Context())
if ok {
w.Header().Set("X-Driver-ID", driverID)
}
w.WriteHeader(http.StatusOK)
})
// 1. DevMode = true: bypass authentication
mwDev := Middleware(testSecret, true)(nextHandler)
req := httptest.NewRequest(http.MethodGet, "/api/races", nil)
w := httptest.NewRecorder()
mwDev.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200 OK in DevMode, got %d", w.Code)
}
// 2. DevMode = false: missing Authorization header
mwProd := Middleware(testSecret, false)(nextHandler)
w = httptest.NewRecorder()
mwProd.ServeHTTP(w, req)
if w.Code != http.StatusUnauthorized {
t.Errorf("expected 401 Unauthorized for missing auth header, got %d", w.Code)
}
// 3. DevMode = false: invalid Authorization format
reqFormat := httptest.NewRequest(http.MethodGet, "/api/races", nil)
reqFormat.Header.Set("Authorization", "InvalidFormat token123")
w = httptest.NewRecorder()
mwProd.ServeHTTP(w, reqFormat)
if w.Code != http.StatusUnauthorized {
t.Errorf("expected 401 Unauthorized for invalid format, got %d", w.Code)
}
// 4. DevMode = false: valid token
validToken, err := generateTestToken("driver-uuid-123", "driver@test.com", time.Now().Add(time.Hour), testSecret)
if err != nil {
t.Fatalf("failed to generate valid token: %v", err)
}
reqValid := httptest.NewRequest(http.MethodGet, "/api/races", nil)
reqValid.Header.Set("Authorization", "Bearer "+validToken)
w = httptest.NewRecorder()
mwProd.ServeHTTP(w, reqValid)
if w.Code != http.StatusOK {
t.Errorf("expected 200 OK for valid token, got %d", w.Code)
}
if w.Header().Get("X-Driver-ID") != "driver-uuid-123" {
t.Errorf("expected X-Driver-ID header to be set in context, got %q", w.Header().Get("X-Driver-ID"))
}
}
func TestContextHelpers(t *testing.T) {
ctx := context.Background()
if _, ok := DriverIDFromContext(ctx); ok {
t.Error("expected ok=false for empty context")
}
ctx = context.WithValue(ctx, driverIDKey, "test-driver")
id, ok := DriverIDFromContext(ctx)
if !ok || id != "test-driver" {
t.Errorf("expected test-driver, got %s (ok=%t)", id, ok)
}
}
+10
View File
@@ -247,6 +247,7 @@ type CreateCarOptions struct {
ColorHex string ColorHex string
AvatarURL string AvatarURL string
Active *bool // optional; defaults to true Active *bool // optional; defaults to true
DeviceID *int
} }
// Validate sanity-checks dimensions and required fields. // Validate sanity-checks dimensions and required fields.
@@ -319,6 +320,7 @@ func (s *Service) CreateCar(ctx context.Context, opts CreateCarOptions) (CarMeta
ColorHex: opts.ColorHex, ColorHex: opts.ColorHex,
AvatarURL: opts.AvatarURL, AvatarURL: opts.AvatarURL,
Active: active, Active: active,
DeviceID: opts.DeviceID,
CreatedMs: now, CreatedMs: now,
UpdatedMs: now, UpdatedMs: now,
} }
@@ -345,6 +347,7 @@ type UpdateCarOptions struct {
Motor *MotorSpec Motor *MotorSpec
Battery *BatterySpec Battery *BatterySpec
Drive *Drivetrain Drive *Drivetrain
DeviceID *int // double pointer or *int to clear / set? Let's use *int, but to allow setting to nil (e.g. if we pass -1 or use *int), wait, actually, we can support DeviceID **int or *int. Let's use *int for now or wait, if *int is nil, it means do not update. If they want to set to a device ID, they pass a pointer. Wait! What if they want to clear it? They can pass a pointer to a negative value like -1 or we can use a double pointer. Actually, in Go, *int is very simple. Let's make it *int. If we need to clear, we can pass a device ID of -1 and map it to nil. Let's check how the JSON schema handles it. Usually *int is sufficient. Wait, let's use *int.
} }
// UpdateCar applies a partial update. // UpdateCar applies a partial update.
@@ -403,6 +406,13 @@ func (s *Service) UpdateCar(ctx context.Context, id string, opts UpdateCarOption
if opts.Drive != nil { if opts.Drive != nil {
c.Drive = *opts.Drive c.Drive = *opts.Drive
} }
if opts.DeviceID != nil {
if *opts.DeviceID < 0 {
c.DeviceID = nil
} else {
c.DeviceID = opts.DeviceID
}
}
c.UpdatedMs = time.Now().UnixMilli() c.UpdatedMs = time.Now().UnixMilli()
if c.LengthMm < minCarLengthMm || c.LengthMm > maxCarLengthMm || if c.LengthMm < minCarLengthMm || c.LengthMm > maxCarLengthMm ||
+11 -1
View File
@@ -17,13 +17,23 @@ import "math"
// (1/24 scale), power figures are real (full-scale 1.6L V6 hybrid // (1/24 scale), power figures are real (full-scale 1.6L V6 hybrid
// turbo, ~1000 hp combined MGU-K + ICE). // turbo, ~1000 hp combined MGU-K + ICE).
func DefaultTracks() []Track { func DefaultTracks() []Track {
return []Track{ tracks := []Track{
Monaco(), Monaco(),
BarcelonaCatalunya(), BarcelonaCatalunya(),
AustriaRedBullRing(), AustriaRedBullRing(),
GreatBritainSilverstone(), GreatBritainSilverstone(),
BelgiumSpa(), BelgiumSpa(),
} }
for i := range tracks {
for j := range tracks[i].Centerline {
tracks[i].Centerline[j].Y = -tracks[i].Centerline[j].Y
tracks[i].Centerline[j].HeadingRad = -tracks[i].Centerline[j].HeadingRad
}
tracks[i].Bounds = ComputeBounds(tracks[i].Centerline)
tracks[i].LengthM = tracks[i].Bounds.LengthM
tracks[i].WidthM = tracks[i].Bounds.WidthM
}
return tracks
} }
func DefaultCars() []Car { func DefaultCars() []Car {
+1
View File
@@ -146,6 +146,7 @@ type CarMeta struct {
Motor MotorSpec `json:"motor"` Motor MotorSpec `json:"motor"`
Battery BatterySpec `json:"battery"` Battery BatterySpec `json:"battery"`
Drive Drivetrain `json:"drive"` Drive Drivetrain `json:"drive"`
DeviceID *int `json:"device_id,omitempty"` // mapped physical ESP32 device ID
// Performance envelope. // Performance envelope.
TopSpeedMs float64 `json:"top_speed_ms"` TopSpeedMs float64 `json:"top_speed_ms"`
+3 -3
View File
@@ -10,14 +10,14 @@ import (
"github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgconn"
) )
// Service composes validation on top of PgStore. // Service composes validation on top of Store.
type Service struct { type Service struct {
pg *PgStore pg Store
now func() time.Time now func() time.Time
} }
// NewService wires a service. // NewService wires a service.
func NewService(pg *PgStore) *Service { func NewService(pg Store) *Service {
return &Service{pg: pg, now: time.Now} return &Service{pg: pg, now: time.Now}
} }
+166
View File
@@ -0,0 +1,166 @@
package clans
import (
"context"
"errors"
"testing"
"time"
"github.com/jackc/pgx/v5/pgconn"
)
type memoryStore struct {
clans map[string]Clan
}
func newMemoryStore() *memoryStore {
return &memoryStore{clans: make(map[string]Clan)}
}
func (m *memoryStore) Exec(ctx context.Context, sql string) (int64, error) {
return 0, nil
}
func (m *memoryStore) Create(ctx context.Context, c Clan) error {
for _, existing := range m.clans {
if existing.Tag == c.Tag {
return &pgconn.PgError{Code: "23505", Message: "duplicate key value violates unique constraint"}
}
}
m.clans[c.ID] = c
return nil
}
func (m *memoryStore) Get(ctx context.Context, id string) (Clan, error) {
c, ok := m.clans[id]
if !ok {
return Clan{}, ErrNotFound
}
return c, nil
}
func (m *memoryStore) GetByTag(ctx context.Context, tag string) (Clan, error) {
for _, c := range m.clans {
if c.Tag == tag {
return c, nil
}
}
return Clan{}, ErrNotFound
}
func (m *memoryStore) List(ctx context.Context, limit, offset int) ([]Clan, error) {
out := make([]Clan, 0)
for _, c := range m.clans {
out = append(out, c)
}
return out, nil
}
func (m *memoryStore) Delete(ctx context.Context, id string) error {
if _, ok := m.clans[id]; !ok {
return ErrNotFound
}
delete(m.clans, id)
return nil
}
func (m *memoryStore) Update(ctx context.Context, c Clan) error {
if _, ok := m.clans[c.ID]; !ok {
return ErrNotFound
}
m.clans[c.ID] = c
return nil
}
func (m *memoryStore) Count(ctx context.Context) (int, error) {
return len(m.clans), nil
}
func TestClansService(t *testing.T) {
store := newMemoryStore()
svc := NewService(store)
svc.now = func() time.Time {
return time.Unix(1700000000, 0)
}
ctx := context.Background()
// 1. Create a clan with valid inputs
c1, err := svc.Create(ctx, CreateInput{
ID: "clan-1",
Tag: "SPD",
Name: "Speed Demons",
AvatarURL: "https://example.com/spd.png",
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
if c1.ID != "clan-1" || c1.Tag != "SPD" || c1.Name != "Speed Demons" {
t.Errorf("Unexpected created clan: %+v", c1)
}
// 2. Validate Tag must be 3 uppercase ASCII letters
_, err = svc.Create(ctx, CreateInput{Tag: "SPEED", Name: "toolongtag"})
if !errors.Is(err, ErrInvalidInput) {
t.Errorf("Expected ErrInvalidInput for tag 'SPEED', got: %v", err)
}
_, err = svc.Create(ctx, CreateInput{Tag: "S1D", Name: "withnumbertag"})
if !errors.Is(err, ErrInvalidInput) {
t.Errorf("Expected ErrInvalidInput for tag 'S1D', got: %v", err)
}
// 3. Validate empty Name is rejected
_, err = svc.Create(ctx, CreateInput{Tag: "AAA", Name: ""})
if !errors.Is(err, ErrInvalidInput) {
t.Errorf("Expected ErrInvalidInput for empty name, got: %v", err)
}
// 4. Duplicate tag check
_, err = svc.Create(ctx, CreateInput{
ID: "clan-2",
Tag: "SPD",
Name: "Duplicate SPD Tag",
})
if !errors.Is(err, ErrAlreadyExists) {
t.Errorf("Expected ErrAlreadyExists for duplicate tag, got: %v", err)
}
// 5. Get clan
got, err := svc.Get(ctx, "clan-1")
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if got.Tag != "SPD" {
t.Errorf("Expected SPD, got %s", got.Tag)
}
// 6. Get by tag
gotByTag, err := svc.GetByTag(ctx, "spd") // should normalize to SPD
if err != nil {
t.Fatalf("GetByTag failed: %v", err)
}
if gotByTag.ID != "clan-1" {
t.Errorf("Expected clan-1, got %s", gotByTag.ID)
}
// 7. Update clan
updated, err := svc.Update(ctx, "clan-1", UpdateInput{
Name: "Super Speed Demons",
AvatarURL: "https://example.com/super-spd.png",
})
if err != nil {
t.Fatalf("Update failed: %v", err)
}
if updated.Name != "Super Speed Demons" || updated.AvatarURL != "https://example.com/super-spd.png" {
t.Errorf("Unexpected updated clan fields: %+v", updated)
}
// 8. Delete clan
err = svc.Delete(ctx, "clan-1")
if err != nil {
t.Fatalf("Delete failed: %v", err)
}
_, err = svc.Get(ctx, "clan-1")
if !errors.Is(err, ErrNotFound) {
t.Errorf("Expected ErrNotFound after deletion, got: %v", err)
}
}
+12
View File
@@ -39,6 +39,18 @@ type Clan struct {
UpdatedMs int64 UpdatedMs int64
} }
// Store defines the persistence operations for clans.
type Store interface {
Exec(ctx context.Context, sql string) (int64, error)
Create(ctx context.Context, c Clan) error
Get(ctx context.Context, id string) (Clan, error)
GetByTag(ctx context.Context, tag string) (Clan, error)
List(ctx context.Context, limit, offset int) ([]Clan, error)
Delete(ctx context.Context, id string) error
Update(ctx context.Context, c Clan) error
Count(ctx context.Context) (int, error)
}
// PgStore is the Postgres-backed half of the package. // PgStore is the Postgres-backed half of the package.
type PgStore struct { type PgStore struct {
pool *pgxpool.Pool pool *pgxpool.Pool
+14
View File
@@ -26,6 +26,7 @@ type Config struct {
DevMode bool // If true, allow multiple clients without auth DevMode bool // If true, allow multiple clients without auth
DatabaseURL string // Postgres connection string; required at runtime DatabaseURL string // Postgres connection string; required at runtime
JWTSecret string // Supabase JWT Secret for signature verification
UDPVideoAddr string // UDP address to listen on for video, e.g. ":9999" UDPVideoAddr string // UDP address to listen on for video, e.g. ":9999"
ESPCmdPort int // Port on ESP32 that listens for commands, e.g. 8888 ESPCmdPort int // Port on ESP32 that listens for commands, e.g. 8888
@@ -39,6 +40,14 @@ type Config struct {
func Load() (*Config, error) { func Load() (*Config, error) {
loadDotEnv() loadDotEnv()
jwtSecret := getEnv("X0GP_JWT_SECRET", "")
if jwtSecret == "" {
jwtSecret = getEnv("JWT_SECRET", "")
}
if jwtSecret == "" {
jwtSecret = getEnv("SUPABASE_JWT_SECRET", "")
}
c := &Config{ c := &Config{
HTTPAddr: getEnv("X0GP_HTTP_ADDR", ":8080"), HTTPAddr: getEnv("X0GP_HTTP_ADDR", ":8080"),
TickRate: getEnvInt("X0GP_TICK_RATE", 60), TickRate: getEnvInt("X0GP_TICK_RATE", 60),
@@ -47,10 +56,15 @@ func Load() (*Config, error) {
SnapshotRate: getEnvInt("X0GP_SNAPSHOT_RATE", 30), SnapshotRate: getEnvInt("X0GP_SNAPSHOT_RATE", 30),
DevMode: getEnvBool("X0GP_DEV_MODE", true), DevMode: getEnvBool("X0GP_DEV_MODE", true),
DatabaseURL: getEnv("DATABASE_URL", ""), DatabaseURL: getEnv("DATABASE_URL", ""),
JWTSecret: jwtSecret,
UDPVideoAddr: getEnv("X0GP_UDP_VIDEO_ADDR", ":9999"), UDPVideoAddr: getEnv("X0GP_UDP_VIDEO_ADDR", ":9999"),
ESPCmdPort: getEnvInt("X0GP_ESP_CMD_PORT", 8888), ESPCmdPort: getEnvInt("X0GP_ESP_CMD_PORT", 8888),
} }
if !c.DevMode && c.JWTSecret == "" {
return nil, fmt.Errorf("JWT secret (X0GP_JWT_SECRET / JWT_SECRET) is required when DevMode is disabled")
}
if c.TickRate <= 0 || c.TickRate > 240 { if c.TickRate <= 0 || c.TickRate > 240 {
return nil, fmt.Errorf("X0GP_TICK_RATE must be in (0, 240], got %d", c.TickRate) return nil, fmt.Errorf("X0GP_TICK_RATE must be in (0, 240], got %d", c.TickRate)
} }
+15 -3
View File
@@ -10,14 +10,14 @@ import (
"github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgconn"
) )
// Service composes validation on top of PgStore. // Service composes validation on top of Store.
type Service struct { type Service struct {
pg *PgStore pg Store
now func() time.Time now func() time.Time
} }
// NewService wires a service. // NewService wires a service.
func NewService(pg *PgStore) *Service { func NewService(pg Store) *Service {
return &Service{pg: pg, now: time.Now} return &Service{pg: pg, now: time.Now}
} }
@@ -78,6 +78,7 @@ func (s *Service) List(ctx context.Context, limit, offset int, clanID string) ([
// UpdateInput is the patch payload. // UpdateInput is the patch payload.
type UpdateInput struct { type UpdateInput struct {
Nickname string
Name string Name string
AvatarURL string AvatarURL string
ClanID *string // nil = leave unchanged, &"" = clear, &"..." = set ClanID *string // nil = leave unchanged, &"" = clear, &"..." = set
@@ -89,6 +90,13 @@ func (s *Service) Update(ctx context.Context, id string, in UpdateInput) (Driver
if err != nil { if err != nil {
return Driver{}, err return Driver{}, err
} }
if in.Nickname != "" {
nick := strings.ToUpper(strings.TrimSpace(in.Nickname))
if !IsValidNickname(nick) {
return Driver{}, fmt.Errorf("%w: nickname must be 3 uppercase ASCII letters", ErrInvalidInput)
}
cur.Nickname = nick
}
if in.Name != "" { if in.Name != "" {
cur.Name = in.Name cur.Name = in.Name
} }
@@ -99,6 +107,10 @@ func (s *Service) Update(ctx context.Context, id string, in UpdateInput) (Driver
cur.ClanID = *in.ClanID cur.ClanID = *in.ClanID
} }
if err := s.pg.Update(ctx, cur); err != nil { if err := s.pg.Update(ctx, cur); err != nil {
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
return Driver{}, fmt.Errorf("%w: %s", ErrAlreadyExists, cur.Nickname)
}
return Driver{}, err return Driver{}, err
} }
return s.pg.Get(ctx, id) return s.pg.Get(ctx, id)
+195
View File
@@ -0,0 +1,195 @@
package drivers
import (
"context"
"errors"
"testing"
"time"
"github.com/jackc/pgx/v5/pgconn"
)
type memoryStore struct {
drivers map[string]Driver
}
func newMemoryStore() *memoryStore {
return &memoryStore{drivers: make(map[string]Driver)}
}
func (m *memoryStore) Exec(ctx context.Context, sql string) (int64, error) {
return 0, nil
}
func (m *memoryStore) Create(ctx context.Context, d Driver) error {
for _, existing := range m.drivers {
if existing.Nickname == d.Nickname {
return &pgconn.PgError{Code: "23505", Message: "duplicate key value violates unique constraint"}
}
}
m.drivers[d.ID] = d
return nil
}
func (m *memoryStore) Get(ctx context.Context, id string) (Driver, error) {
d, ok := m.drivers[id]
if !ok {
return Driver{}, ErrNotFound
}
return d, nil
}
func (m *memoryStore) GetByNickname(ctx context.Context, nick string) (Driver, error) {
for _, d := range m.drivers {
if d.Nickname == nick {
return d, nil
}
}
return Driver{}, ErrNotFound
}
func (m *memoryStore) List(ctx context.Context, limit, offset int, clanID string) ([]Driver, error) {
out := make([]Driver, 0)
for _, d := range m.drivers {
if clanID == "" || d.ClanID == clanID {
out = append(out, d)
}
}
return out, nil
}
func (m *memoryStore) Delete(ctx context.Context, id string) error {
if _, ok := m.drivers[id]; !ok {
return ErrNotFound
}
delete(m.drivers, id)
return nil
}
func (m *memoryStore) Update(ctx context.Context, d Driver) error {
if _, ok := m.drivers[d.ID]; !ok {
return ErrNotFound
}
for id, existing := range m.drivers {
if id != d.ID && existing.Nickname == d.Nickname {
return &pgconn.PgError{Code: "23505", Message: "duplicate key value violates unique constraint"}
}
}
m.drivers[d.ID] = d
return nil
}
func (m *memoryStore) Count(ctx context.Context) (int, error) {
return len(m.drivers), nil
}
func TestDriversService(t *testing.T) {
store := newMemoryStore()
svc := NewService(store)
svc.now = func() time.Time {
return time.Unix(1700000000, 0)
}
ctx := context.Background()
// 1. Create a driver
d1, err := svc.Create(ctx, CreateInput{
ID: "driver-1",
Nickname: "HAM",
Name: "Lewis Hamilton",
AvatarURL: "https://example.com/ham.png",
ClanID: "clan-1",
})
if err != nil {
t.Fatalf("Create failed: %v", err)
}
if d1.ID != "driver-1" || d1.Nickname != "HAM" || d1.Name != "Lewis Hamilton" || d1.ClanID != "clan-1" {
t.Errorf("Unexpected created driver: %+v", d1)
}
// 2. Validate Nickname must be 3 uppercase ASCII letters
_, err = svc.Create(ctx, CreateInput{Nickname: "HAMILTON", Name: "toolong"})
if !errors.Is(err, ErrInvalidInput) {
t.Errorf("Expected ErrInvalidInput for tag 'HAMILTON', got: %v", err)
}
// 3. Validate empty Name is rejected
_, err = svc.Create(ctx, CreateInput{Nickname: "BOT", Name: ""})
if !errors.Is(err, ErrInvalidInput) {
t.Errorf("Expected ErrInvalidInput for empty name, got: %v", err)
}
// 4. Duplicate nickname check
_, err = svc.Create(ctx, CreateInput{
ID: "driver-2",
Nickname: "HAM",
Name: "Another Lewis",
})
if !errors.Is(err, ErrAlreadyExists) {
t.Errorf("Expected ErrAlreadyExists for duplicate nickname, got: %v", err)
}
// 5. Get driver
got, err := svc.Get(ctx, "driver-1")
if err != nil {
t.Fatalf("Get failed: %v", err)
}
if got.Nickname != "HAM" {
t.Errorf("Expected HAM, got %s", got.Nickname)
}
// 6. Get by nickname
gotByNick, err := svc.GetByNickname(ctx, "ham") // should normalize to HAM
if err != nil {
t.Fatalf("GetByNickname failed: %v", err)
}
if gotByNick.ID != "driver-1" {
t.Errorf("Expected driver-1, got %s", gotByNick.ID)
}
// 7. Update driver (including nickname)
clanID := "clan-2"
updated, err := svc.Update(ctx, "driver-1", UpdateInput{
Nickname: "VET",
Name: "Lewis Hamilton MBE",
AvatarURL: "https://example.com/ham-mbe.png",
ClanID: &clanID,
})
if err != nil {
t.Fatalf("Update failed: %v", err)
}
if updated.Nickname != "VET" || updated.Name != "Lewis Hamilton MBE" || updated.AvatarURL != "https://example.com/ham-mbe.png" || updated.ClanID != "clan-2" {
t.Errorf("Unexpected updated driver fields: %+v", updated)
}
// 7a. Update with invalid nickname
_, err = svc.Update(ctx, "driver-1", UpdateInput{Nickname: "TOOLONG"})
if !errors.Is(err, ErrInvalidInput) {
t.Errorf("Expected ErrInvalidInput for invalid nickname update, got: %v", err)
}
// 7b. Create driver-2 to test duplicate nickname conflict
_, err = svc.Create(ctx, CreateInput{
ID: "driver-2",
Nickname: "HAM",
Name: "Lewis",
})
if err != nil {
t.Fatalf("Create driver-2 failed: %v", err)
}
// 7c. Update driver-2 to nickname "VET" (should fail, already taken by driver-1)
_, err = svc.Update(ctx, "driver-2", UpdateInput{Nickname: "VET"})
if !errors.Is(err, ErrAlreadyExists) {
t.Errorf("Expected ErrAlreadyExists for duplicate nickname update, got: %v", err)
}
// 8. Delete driver
err = svc.Delete(ctx, "driver-1")
if err != nil {
t.Fatalf("Delete failed: %v", err)
}
_, err = svc.Get(ctx, "driver-1")
if !errors.Is(err, ErrNotFound) {
t.Errorf("Expected ErrNotFound after deletion, got: %v", err)
}
}
+14 -2
View File
@@ -39,6 +39,18 @@ type Driver struct {
UpdatedMs int64 UpdatedMs int64
} }
// Store defines the persistence operations for drivers.
type Store interface {
Exec(ctx context.Context, sql string) (int64, error)
Create(ctx context.Context, d Driver) error
Get(ctx context.Context, id string) (Driver, error)
GetByNickname(ctx context.Context, nick string) (Driver, error)
List(ctx context.Context, limit, offset int, clanID string) ([]Driver, error)
Delete(ctx context.Context, id string) error
Update(ctx context.Context, d Driver) error
Count(ctx context.Context) (int, error)
}
// PgStore is the Postgres-backed half of the package. // PgStore is the Postgres-backed half of the package.
type PgStore struct { type PgStore struct {
pool *pgxpool.Pool pool *pgxpool.Pool
@@ -153,9 +165,9 @@ func (s *PgStore) Update(ctx context.Context, d Driver) error {
d.UpdatedMs = time.Now().UnixMilli() d.UpdatedMs = time.Now().UnixMilli()
tag, err := s.pool.Exec(ctx, ` tag, err := s.pool.Exec(ctx, `
UPDATE drivers UPDATE drivers
SET name = $2, avatar_url = $3, clan_id = $4, updated_ms = $5 SET nickname = $2, name = $3, avatar_url = $4, clan_id = $5, updated_ms = $6
WHERE id = $1`, WHERE id = $1`,
d.ID, d.Name, d.AvatarURL, nullableClan(d.ClanID), d.UpdatedMs) d.ID, d.Nickname, d.Name, d.AvatarURL, nullableClan(d.ClanID), d.UpdatedMs)
if err != nil { if err != nil {
return err return err
} }
+14 -20
View File
@@ -187,7 +187,7 @@ func (s *Service) RemoveDriver(id string) {
d.Status = DriverStatusOffline d.Status = DriverStatusOffline
d.LastSeenMs = time.Now().UnixMilli() d.LastSeenMs = time.Now().UnixMilli()
d.RaceID = "" d.RaceID = ""
d.DeviceID = nil d.CarID = nil
hook := s.onRaceRemoved hook := s.onRaceRemoved
s.mu.Unlock() s.mu.Unlock()
@@ -244,9 +244,9 @@ func (s *Service) AddDriverToRace(driverID, raceID string) error {
s.mu.Unlock() s.mu.Unlock()
return ErrDriverNotFound return ErrDriverNotFound
} }
if d.DeviceID == nil { if d.CarID == nil || *d.CarID == "" {
s.mu.Unlock() s.mu.Unlock()
return fmt.Errorf("driver %s has no device selected", driverID) return fmt.Errorf("driver %s has no car selected", driverID)
} }
r, ok := s.races[raceID] r, ok := s.races[raceID]
if !ok { if !ok {
@@ -306,7 +306,7 @@ func (s *Service) RemoveDriverFromRace(driverID string) {
} }
d.Status = DriverStatusIdle d.Status = DriverStatusIdle
d.RaceID = "" d.RaceID = ""
d.DeviceID = nil d.CarID = nil
prevRaceID := d.RaceID prevRaceID := d.RaceID
hook := s.onRaceRemoved hook := s.onRaceRemoved
s.mu.Unlock() s.mu.Unlock()
@@ -349,7 +349,7 @@ func (s *Service) SetRaceStatus(raceID string, status RaceStatus) error {
if d, ok := s.drivers[did]; ok { if d, ok := s.drivers[did]; ok {
d.Status = DriverStatusIdle d.Status = DriverStatusIdle
d.RaceID = "" d.RaceID = ""
d.DeviceID = nil d.CarID = nil
affected = append(affected, *d) affected = append(affected, *d)
} }
} }
@@ -454,16 +454,10 @@ func (s *Service) GetDriver(driverID string) (DriverMeta, error) {
return *d, nil return *d, nil
} }
// SelectDevice assigns a physical device ID (0..127) to a driver. // SelectCar assigns a catalog car ID to a driver.
// Pass deviceID = nil to release the device. // If carID is nil, it clears the driver's car assignment.
// Returns an error if the device is already taken by another online driver. // It returns an error if the car is already taken by another active driver.
func (s *Service) SelectDevice(driverID string, deviceID *int) error { func (s *Service) SelectCar(driverID string, carID *string) error {
if deviceID != nil {
if *deviceID < 0 || *deviceID > 127 {
return fmt.Errorf("invalid device ID: must be 0..127")
}
}
s.mu.Lock() s.mu.Lock()
d, ok := s.drivers[driverID] d, ok := s.drivers[driverID]
if !ok { if !ok {
@@ -471,17 +465,17 @@ func (s *Service) SelectDevice(driverID string, deviceID *int) error {
return ErrDriverNotFound return ErrDriverNotFound
} }
// Enforce that device cannot be selected if already assigned to another driver. // Enforce that car cannot be selected if already assigned to another driver.
if deviceID != nil { if carID != nil && *carID != "" {
for otherID, otherD := range s.drivers { for otherID, otherD := range s.drivers {
if otherID != driverID && otherD.Status != DriverStatusOffline && otherD.DeviceID != nil && *otherD.DeviceID == *deviceID { if otherID != driverID && otherD.Status != DriverStatusOffline && otherD.CarID != nil && *otherD.CarID == *carID {
s.mu.Unlock() s.mu.Unlock()
return fmt.Errorf("device %d is already taken by driver %s", *deviceID, otherID) return fmt.Errorf("car %s is already taken by driver %s", *carID, otherID)
} }
} }
} }
d.DeviceID = deviceID d.CarID = carID
out := *d out := *d
s.mu.Unlock() s.mu.Unlock()
+3 -3
View File
@@ -75,9 +75,9 @@ func TestQuickPlayJoinsFirstRace(t *testing.T) {
s := NewService() s := NewService()
_, _ = s.AddDriver("d1", "Alice", "") _, _ = s.AddDriver("d1", "Alice", "")
_, _ = s.AddDriver("d2", "Bob", "") _, _ = s.AddDriver("d2", "Bob", "")
dev1, dev2 := 1, 2 car1, car2 := "car-1", "car-2"
_ = s.SelectDevice("d1", &dev1) _ = s.SelectCar("d1", &car1)
_ = s.SelectDevice("d2", &dev2) _ = s.SelectCar("d2", &car2)
r1, _ := s.QuickPlay("d1", 4) r1, _ := s.QuickPlay("d1", 4)
r2, err := s.QuickPlay("d2", 4) r2, err := s.QuickPlay("d2", 4)
if err != nil { if err != nil {
+1 -1
View File
@@ -65,7 +65,7 @@ type DriverMeta struct {
LastSeenMs int64 `json:"last_seen_ms"` LastSeenMs int64 `json:"last_seen_ms"`
Country string `json:"country,omitempty"` // optional, for UI Country string `json:"country,omitempty"` // optional, for UI
AvatarHue int `json:"avatar_hue,omitempty"` // 0..360, deterministic per id AvatarHue int `json:"avatar_hue,omitempty"` // 0..360, deterministic per id
DeviceID *int `json:"-"` CarID *string `json:"car_id,omitempty"`
} }
// Snapshot is the immutable view broadcast to clients. // Snapshot is the immutable view broadcast to clients.
+215 -6
View File
@@ -294,17 +294,226 @@ func TestLeaderboard(t *testing.T) {
t.Fatalf("expected 3 calendar entries, got %d", len(res)) t.Fatalf("expected 3 calendar entries, got %d", len(res))
} }
// 1. Monaco (finished) // 1. Austria (planned)
if res[0].TrackID != "monaco" || res[0].Status != "finished" { if res[0].TrackID != "austria-redbull-ring" || res[0].Status != "planned" {
t.Errorf("expected monaco (finished), got %s (%s)", res[0].TrackID, res[0].Status) t.Errorf("expected austria-redbull-ring (planned), got %s (%s)", res[0].TrackID, res[0].Status)
} }
// 2. Barcelona (active) // 2. Barcelona (active)
if res[1].TrackID != "barcelona-catalunya" || res[1].Status != "active" { if res[1].TrackID != "barcelona-catalunya" || res[1].Status != "active" {
t.Errorf("expected barcelona-catalunya (active), got %s (%s)", res[1].TrackID, res[1].Status) t.Errorf("expected barcelona-catalunya (active), got %s (%s)", res[1].TrackID, res[1].Status)
} }
// 3. Austria (planned) // 3. Monaco (finished)
if res[2].TrackID != "austria-redbull-ring" || res[2].Status != "planned" { if res[2].TrackID != "monaco" || res[2].Status != "finished" {
t.Errorf("expected austria-redbull-ring (planned), got %s (%s)", res[2].TrackID, res[2].Status) t.Errorf("expected monaco (finished), got %s (%s)", res[2].TrackID, res[2].Status)
} }
}) })
} }
func TestGetRaceAndLastFinished(t *testing.T) {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
t.Skip("DATABASE_URL is not set, skipping database integration tests")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("failed to connect to test database: %v", err)
}
defer pool.Close()
store := NewPgStore(pool)
cleanup := func() {
_, _ = pool.Exec(ctx, "DELETE FROM race_drivers WHERE race_id LIKE 'test-get-race-%'")
_, _ = pool.Exec(ctx, "DELETE FROM races WHERE id LIKE 'test-get-race-%'")
}
cleanup()
defer cleanup()
// 1. Insert a finished race
now := time.Now().UnixMilli()
race := FinishedRace{
ID: "test-get-race-1",
Name: "Test Get Race 1",
TrackID: "monaco",
MaxCars: 4,
Laps: 5,
TimeLimitS: 300,
Status: "finished",
CreatedMs: now - 10000,
StartedMs: now - 8000,
FinishedMs: now - 5000,
DurationMs: 3000,
TotalLaps: 20,
TotalDrivers: 1,
}
err = store.InsertFinished(ctx, race)
if err != nil {
t.Fatalf("failed to insert finished race: %v", err)
}
// 2. Fetch the race by ID
got, err := store.GetFinished(ctx, "test-get-race-1")
if err != nil {
t.Fatalf("GetFinished failed: %v", err)
}
if got.ID != "test-get-race-1" || got.Name != "Test Get Race 1" {
t.Errorf("expected test-get-race-1, got: %+v", got)
}
// 3. Insert a second finished race (newer finished_ms)
race2 := FinishedRace{
ID: "test-get-race-2",
Name: "Test Get Race 2",
TrackID: "monaco",
MaxCars: 4,
Laps: 5,
TimeLimitS: 300,
Status: "finished",
CreatedMs: now - 5000,
StartedMs: now - 4000,
FinishedMs: now - 1000,
DurationMs: 3000,
TotalLaps: 20,
TotalDrivers: 1,
}
err = store.InsertFinished(ctx, race2)
if err != nil {
t.Fatalf("failed to insert finished race 2: %v", err)
}
// 4. Fetch the last finished race (should be test-get-race-2)
last, err := store.GetLastFinished(ctx)
if err != nil {
t.Fatalf("GetLastFinished failed: %v", err)
}
if last.ID != "test-get-race-2" {
t.Errorf("expected test-get-race-2 as last race, got: %s", last.ID)
}
}
func TestDriverProfileStats(t *testing.T) {
dsn := os.Getenv("DATABASE_URL")
if dsn == "" {
t.Skip("DATABASE_URL is empty; skipping integration test")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("failed to open pool: %v", err)
}
defer pool.Close()
store := NewPgStore(pool)
// Clean up any test records
_, _ = store.Exec(ctx, `DELETE FROM race_queue WHERE driver_id = 'test-stats-drv'`)
_, _ = store.Exec(ctx, `DELETE FROM race_drivers WHERE driver_id = 'test-stats-drv'`)
_, _ = store.Exec(ctx, `DELETE FROM races WHERE id LIKE 'test-stats-race-%'`)
_, _ = store.Exec(ctx, `DELETE FROM drivers WHERE id = 'test-stats-drv'`)
// 1. Create a driver
_, _ = store.Exec(ctx, `
INSERT INTO drivers (id, nickname, name, avatar_url, created_ms, updated_ms)
VALUES ('test-stats-drv', 'TSD', 'Test Stats Driver', '', 1700000000, 1700000000)`)
// 2. Insert races and race results
// Race 1: finished, TSD took 2nd position, best lap 15000, total time 50000
race1 := FinishedRace{
ID: "test-stats-race-1",
Name: "Race 1",
TrackID: "monaco",
MaxCars: 4,
Laps: 5,
TimeLimitS: 300,
DriverIDs: []string{"test-stats-drv"},
Status: "finished",
CreatedMs: 1700000000,
StartedMs: 1700000500,
FinishedMs: 1700060000,
DurationMs: 59500,
TotalLaps: 5,
TotalDrivers: 2,
Results: []DriverResult{
{
DriverID: "test-stats-drv",
TotalTimeMs: 50000,
BestLapMs: 15000,
Position: ptrInt(2),
},
},
}
if err := store.InsertFinished(ctx, race1); err != nil {
t.Fatalf("InsertFinished 1 failed: %v", err)
}
// Race 2: finished, TSD took 1st position (Win), best lap 14000 (Best Lap), total time 45000
race2 := FinishedRace{
ID: "test-stats-race-2",
Name: "Race 2",
TrackID: "monaco",
MaxCars: 4,
Laps: 5,
TimeLimitS: 300,
DriverIDs: []string{"test-stats-drv"},
Status: "finished",
CreatedMs: 1700100000,
StartedMs: 1700100500,
FinishedMs: 1700160000,
DurationMs: 59500,
TotalLaps: 5,
TotalDrivers: 2,
Results: []DriverResult{
{
DriverID: "test-stats-drv",
TotalTimeMs: 45000,
BestLapMs: 14000,
Position: ptrInt(1),
},
},
}
if err := store.InsertFinished(ctx, race2); err != nil {
t.Fatalf("InsertFinished 2 failed: %v", err)
}
// 3. Query stats
stats, err := store.GetDriverStats(ctx, "test-stats-drv")
if err != nil {
t.Fatalf("GetDriverStats failed: %v", err)
}
if stats.TotalRacesStarted != 2 || stats.TotalRacesFinished != 2 {
t.Errorf("expected 2 races started/finished, got: %+v", stats)
}
if stats.Wins != 1 || stats.Podiums != 2 {
t.Errorf("expected 1 win, 2 podiums, got: %+v", stats)
}
if stats.BestLapMs != 14000 {
t.Errorf("expected best lap 14000, got %d", stats.BestLapMs)
}
if stats.TotalPlaytimeMs != 95000 {
t.Errorf("expected total playtime 95000, got %d", stats.TotalPlaytimeMs)
}
// 4. Query last race (should be Race 2, finished at 1700160000)
last, err := store.GetDriverLastRace(ctx, "test-stats-drv")
if err != nil {
t.Fatalf("GetDriverLastRace failed: %v", err)
}
if last == nil || last.RaceID != "test-stats-race-2" {
t.Errorf("expected last race test-stats-race-2, got: %+v", last)
}
// 5. Query best race (should be Race 2, because position 1 is better than 2)
best, err := store.GetDriverBestRace(ctx, "test-stats-drv")
if err != nil {
t.Fatalf("GetDriverBestRace failed: %v", err)
}
if best == nil || best.RaceID != "test-stats-race-2" {
t.Errorf("expected best race test-stats-race-2, got: %+v", best)
}
}
func ptrInt(v int) *int { return &v }
+40 -34
View File
@@ -30,29 +30,34 @@ var DefaultNow = time.Now
var fallbackTracks = []string{"default"} var fallbackTracks = []string{"default"}
var defaultDriverSeeds = []struct { var defaultDriverSeeds = []struct {
id string
nick string nick string
name string name string
avatarURL string
clanID string
createdMs int64
updatedMs int64
}{ }{
{"ACE", "Alice"}, {"driver-seed-001", "BOR", "Gabriel Bortoleto", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/gabriel-bortoleto-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584003},
{"BOB", "Bob"}, {"driver-seed-002", "HUL", "Nico Hulkenberg", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/nico-hulkenberg-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584007},
{"CAR", "Carol"}, {"driver-seed-003", "ALB", "Alex Albon", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/alex-albon-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584008},
{"DAV", "Dave"}, {"driver-seed-004", "SAI", "Carlos Sainz", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/carlos-sainz-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584011},
{"EVE", "Eve"}, {"driver-seed-005", "BEA", "Oliver Bearman", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/oliver-bearman-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584012},
{"FRA", "Frank"}, {"driver-seed-006", "OCO", "Esteban Ocon", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/esteban-ocon-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584014},
{"GRA", "Grace"}, {"driver-seed-007", "LIN", "Arvid Lindblad", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/arvid-lindblad-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584018},
{"HEI", "Heidi"}, {"driver-seed-008", "LAW", "Liam Lawson", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/liam-lawson-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584018},
{"IVY", "Ivy"}, {"driver-seed-009", "COL", "Franco Colapinto", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/franco-colapinto-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584020},
{"JAC", "Jack"}, {"driver-seed-010", "GAS", "Pierre Gasly", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/pierre-gasly-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584023},
{"KEN", "Ken"}, {"driver-seed-011", "STR", "Lance Stroll", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/lance-stroll-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584023},
{"LEO", "Leo"}, {"driver-seed-012", "ALO", "Fernando Alonso", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/fernando-alonso-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584026},
{"MAX", "Max"}, {"driver-seed-013", "ANT", "Kimi Antonelli", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/kimi-antonelli-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584028},
{"NED", "Ned"}, {"driver-seed-014", "RUS", "George Russel", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/george-russell-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584028},
{"OLI", "Oliver"}, {"driver-seed-015", "HAD", "Isack Hadjar", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/isack-hadjar-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584031},
{"PEP", "Peggy"}, {"driver-seed-016", "VER", "Max Verstappen", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/max-verstappen-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584033},
{"QIN", "Quincy"}, {"driver-seed-017", "HAM", "Lewis Hamilton", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/lewis-hamilton-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584034},
{"ROY", "Roy"}, {"driver-seed-018", "NOR", "Lando Norris", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/lando-norris-f1-driver-profile-picture.webp", "clan-seed-003", 1784120584003, 1784120584036},
{"SAM", "Sam"}, {"driver-seed-019", "LEC", "Charles Leclerc", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/charles-leclerc-f1-driver-profile-picture.webp", "clan-seed-001", 1784120584003, 1784120584038},
{"TED", "Ted"}, {"driver-seed-020", "PIA", "Oscar Piastri", "https://supabasekong.ubu.is1di.ru/storage/v1/object/public/static/avatar/oscar-piastri-f1-driver-profile-picture.webp", "clan-seed-002", 1784120584003, 1784120584039},
} }
// Default clans. // Default clans.
@@ -353,21 +358,15 @@ func (r *Runner) seedDriversAndClans(ctx context.Context) error {
return fmt.Errorf("count drivers: %w", err) return fmt.Errorf("count drivers: %w", err)
} }
if drvCount == 0 { if drvCount == 0 {
now := time.Now().UnixMilli() for _, ds := range defaultDriverSeeds {
// Round-robin: each driver assigned to one of the seeded clans.
clanIDs := make([]string, 0, len(defaultClanSeeds))
for i := range defaultClanSeeds {
clanIDs = append(clanIDs, fmt.Sprintf("clan-seed-%03d", i+1))
}
for i, ds := range defaultDriverSeeds {
d := drivers.Driver{ d := drivers.Driver{
ID: fmt.Sprintf("driver-seed-%03d", i+1), ID: ds.id,
Nickname: ds.nick, Nickname: ds.nick,
Name: ds.name, Name: ds.name,
AvatarURL: fmt.Sprintf("https://cdn.example.com/drivers/%s.png", ds.nick), AvatarURL: ds.avatarURL,
ClanID: clanIDs[i%len(clanIDs)], ClanID: ds.clanID,
CreatedMs: now, CreatedMs: ds.createdMs,
UpdatedMs: now, UpdatedMs: ds.updatedMs,
} }
if err := r.driversPg.Create(ctx, d); err != nil { if err := r.driversPg.Create(ctx, d); err != nil {
return fmt.Errorf("insert driver %s: %w", ds.nick, err) return fmt.Errorf("insert driver %s: %w", ds.nick, err)
@@ -548,8 +547,15 @@ func (r *Runner) createLive(status lobby.RaceStatus, idx int) (lobby.RaceMeta, e
continue continue
} }
r.lb.SetDriverProfile(d.ID, d.Nickname, "", d.ClanID, d.ClanTag) r.lb.SetDriverProfile(d.ID, d.Nickname, "", d.ClanID, d.ClanTag)
devID := i cars := []string{
_ = r.lb.SelectDevice(d.ID, &devID) "f1-2024-redbull-rb20",
"f1-2024-ferrari-sf24",
"f1-2024-mclaren-mcl38",
"f1-2024-mercedes-w15",
"f1-2024-astonmartin-amr24",
}
carID := cars[i%len(cars)]
_ = r.lb.SelectCar(d.ID, &carID)
if err := r.lb.AddDriverToRace(d.ID, meta.ID); err != nil { if err := r.lb.AddDriverToRace(d.ID, meta.ID); err != nil {
// ignore: race may be at capacity // ignore: race may be at capacity
} }
+44 -2
View File
@@ -62,8 +62,8 @@ func (s *Service) RestoreFromDB(ctx context.Context) (int, int, error) {
// AddDriver+SetDriverProfile and accept a no-op mirror. // AddDriver+SetDriverProfile and accept a no-op mirror.
_, _ = s.lobby.AddDriver(d.ID, d.Name, "") _, _ = s.lobby.AddDriver(d.ID, d.Name, "")
s.lobby.SetDriverProfile(d.ID, d.Nickname, d.AvatarURL, d.ClanID, d.ClanTag) s.lobby.SetDriverProfile(d.ID, d.Nickname, d.AvatarURL, d.ClanID, d.ClanTag)
if d.DeviceID != nil { if d.CarID != nil {
_ = s.lobby.SelectDevice(d.ID, d.DeviceID) _ = s.lobby.SelectCar(d.ID, d.CarID)
} }
} }
for _, r := range races { for _, r := range races {
@@ -812,6 +812,48 @@ func (s *Service) GetCalendarLeaderboard(ctx context.Context, year int, currentD
return out, nil return out, nil
} }
// GetRace fetches a single race by id (either live from lobby or finished from DB).
func (s *Service) GetRace(ctx context.Context, id string) (FinishedRace, error) {
meta, err := s.lobby.GetRace(id)
if err == nil {
return FinishedRace{
ID: meta.ID,
Name: meta.Name,
TrackID: meta.TrackID,
MaxCars: meta.MaxCars,
Laps: meta.Laps,
TimeLimitS: meta.TimeLimitS,
DriverIDs: meta.DriverIDs,
Status: string(meta.Status),
CreatedMs: meta.CreatedMs,
StartedMs: meta.StartedMs,
}, nil
}
return s.pg.GetFinished(ctx, id)
}
// GetLastFinished fetches the most recently finished race.
func (s *Service) GetLastFinished(ctx context.Context) (FinishedRace, error) {
return s.pg.GetLastFinished(ctx)
}
// GetDriverProfileStats returns the driver stats and summaries of their last and best races.
func (s *Service) GetDriverProfileStats(ctx context.Context, driverID string) (DriverStats, *DriverRaceSummary, *DriverRaceSummary, error) {
stats, err := s.pg.GetDriverStats(ctx, driverID)
if err != nil {
return DriverStats{}, nil, nil, err
}
last, err := s.pg.GetDriverLastRace(ctx, driverID)
if err != nil {
return DriverStats{}, nil, nil, err
}
best, err := s.pg.GetDriverBestRace(ctx, driverID)
if err != nil {
return DriverStats{}, nil, nil, err
}
return stats, last, best, nil
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Scheduler // Scheduler
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+189 -5
View File
@@ -391,7 +391,7 @@ func (s *PgStore) UpsertLobbyDriver(ctx context.Context, d lobby.DriverMeta) err
_, err := s.pool.Exec(ctx, ` _, err := s.pool.Exec(ctx, `
INSERT INTO lobby_drivers INSERT INTO lobby_drivers
(driver_id, name, nickname, avatar_url, clan_id, clan_tag, (driver_id, name, nickname, avatar_url, clan_id, clan_tag,
status, current_race_id, connected_ms, last_seen_ms, device_id) status, current_race_id, connected_ms, last_seen_ms, car_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT (driver_id) DO UPDATE SET ON CONFLICT (driver_id) DO UPDATE SET
name = EXCLUDED.name, name = EXCLUDED.name,
@@ -402,9 +402,9 @@ func (s *PgStore) UpsertLobbyDriver(ctx context.Context, d lobby.DriverMeta) err
status = EXCLUDED.status, status = EXCLUDED.status,
current_race_id = EXCLUDED.current_race_id, current_race_id = EXCLUDED.current_race_id,
last_seen_ms = EXCLUDED.last_seen_ms, last_seen_ms = EXCLUDED.last_seen_ms,
device_id = EXCLUDED.device_id`, car_id = EXCLUDED.car_id`,
d.ID, d.Name, d.Nickname, d.AvatarURL, d.ClanID, d.ClanTag, d.ID, d.Name, d.Nickname, d.AvatarURL, d.ClanID, d.ClanTag,
string(d.Status), d.RaceID, d.ConnectedMs, d.LastSeenMs, d.DeviceID) string(d.Status), d.RaceID, d.ConnectedMs, d.LastSeenMs, d.CarID)
if err != nil { if err != nil {
return fmt.Errorf("upsert lobby driver %s: %w", d.ID, err) return fmt.Errorf("upsert lobby driver %s: %w", d.ID, err)
} }
@@ -421,7 +421,7 @@ func (s *PgStore) DeleteLobbyDriver(ctx context.Context, driverID string) error
func (s *PgStore) ListLobbyDrivers(ctx context.Context) ([]lobby.DriverMeta, error) { func (s *PgStore) ListLobbyDrivers(ctx context.Context) ([]lobby.DriverMeta, error) {
rows, err := s.pool.Query(ctx, ` rows, err := s.pool.Query(ctx, `
SELECT driver_id, name, nickname, avatar_url, clan_id, clan_tag, SELECT driver_id, name, nickname, avatar_url, clan_id, clan_tag,
status, current_race_id, connected_ms, last_seen_ms, device_id status, current_race_id, connected_ms, last_seen_ms, car_id
FROM lobby_drivers ORDER BY last_seen_ms DESC`) FROM lobby_drivers ORDER BY last_seen_ms DESC`)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -432,7 +432,7 @@ func (s *PgStore) ListLobbyDrivers(ctx context.Context) ([]lobby.DriverMeta, err
var d lobby.DriverMeta var d lobby.DriverMeta
var status string var status string
if err := rows.Scan(&d.ID, &d.Name, &d.Nickname, &d.AvatarURL, &d.ClanID, &d.ClanTag, if err := rows.Scan(&d.ID, &d.Name, &d.Nickname, &d.AvatarURL, &d.ClanID, &d.ClanTag,
&status, &d.RaceID, &d.ConnectedMs, &d.LastSeenMs, &d.DeviceID); err != nil { &status, &d.RaceID, &d.ConnectedMs, &d.LastSeenMs, &d.CarID); err != nil {
return nil, err return nil, err
} }
d.Status = lobby.DriverStatus(status) d.Status = lobby.DriverStatus(status)
@@ -696,6 +696,61 @@ func (s *PgStore) hydrateFinished(ctx context.Context, r *FinishedRace) error {
return nil return nil
} }
// GetFinished fetches a single finished race by id.
func (s *PgStore) GetFinished(ctx context.Context, id string) (FinishedRace, error) {
row := s.pool.QueryRow(ctx, `
SELECT id, name, track_id, max_cars, laps, time_limit_s,
status, created_ms, started_ms, finished_ms, duration_ms,
total_laps, total_drivers
FROM races
WHERE id = $1 AND status IN ('finished', 'cancelled')`, id)
var r FinishedRace
if err := row.Scan(
&r.ID, &r.Name, &r.TrackID,
&r.MaxCars, &r.Laps, &r.TimeLimitS, &r.Status,
&r.CreatedMs, &r.StartedMs, &r.FinishedMs, &r.DurationMs,
&r.TotalLaps, &r.TotalDrivers,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return FinishedRace{}, ErrNotFound
}
return FinishedRace{}, err
}
if err := s.hydrateFinished(ctx, &r); err != nil {
return FinishedRace{}, err
}
return r, nil
}
// GetLastFinished fetches the most recently finished race.
func (s *PgStore) GetLastFinished(ctx context.Context) (FinishedRace, error) {
row := s.pool.QueryRow(ctx, `
SELECT id, name, track_id, max_cars, laps, time_limit_s,
status, created_ms, started_ms, finished_ms, duration_ms,
total_laps, total_drivers
FROM races
WHERE status IN ('finished', 'cancelled')
ORDER BY finished_ms DESC, id DESC
LIMIT 1`)
var r FinishedRace
if err := row.Scan(
&r.ID, &r.Name, &r.TrackID,
&r.MaxCars, &r.Laps, &r.TimeLimitS, &r.Status,
&r.CreatedMs, &r.StartedMs, &r.FinishedMs, &r.DurationMs,
&r.TotalLaps, &r.TotalDrivers,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return FinishedRace{}, ErrNotFound
}
return FinishedRace{}, err
}
if err := s.hydrateFinished(ctx, &r); err != nil {
return FinishedRace{}, err
}
return r, nil
}
func joinAnd(parts []string) string { func joinAnd(parts []string) string {
out := "" out := ""
for i, p := range parts { for i, p := range parts {
@@ -991,3 +1046,132 @@ func (f FinishedRace) ToLobbyMeta() lobby.RaceMeta {
StartedMs: f.StartedMs, StartedMs: f.StartedMs,
} }
} }
// DriverStats holds database-calculated stats.
type DriverStats struct {
TotalRacesStarted int64
TotalRacesFinished int64
Wins int64
Podiums int64
BestLapMs int64
TotalPlaytimeMs int64
}
// DriverRaceSummary holds a summary of a single finished race.
type DriverRaceSummary struct {
RaceID string
RaceName string
FinishedMs int64
Position int
TotalTimeMs int64
BestLapMs int64
TrackID string
TrackName string
}
// GetDriverStats aggregates statistics for a given driver from finished races.
func (s *PgStore) GetDriverStats(ctx context.Context, driverID string) (DriverStats, error) {
row := s.pool.QueryRow(ctx, `
SELECT
COUNT(*)::bigint as total_races_started,
COUNT(CASE WHEN rd.position IS NOT NULL OR rd.total_time_ms > 0 THEN 1 END)::bigint as total_races_finished,
COUNT(CASE WHEN rd.position = 1 THEN 1 END)::bigint as wins,
COUNT(CASE WHEN rd.position >= 1 AND rd.position <= 3 THEN 1 END)::bigint as podiums,
COALESCE(MIN(CASE WHEN rd.best_lap_ms > 0 THEN rd.best_lap_ms END), 0)::bigint as best_lap_ms,
COALESCE(SUM(rd.total_time_ms), 0)::bigint as total_playtime_ms
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE rd.driver_id = $1 AND r.status = 'finished'`, driverID)
var stats DriverStats
err := row.Scan(
&stats.TotalRacesStarted,
&stats.TotalRacesFinished,
&stats.Wins,
&stats.Podiums,
&stats.BestLapMs,
&stats.TotalPlaytimeMs,
)
if err != nil {
return DriverStats{}, fmt.Errorf("query driver stats: %w", err)
}
return stats, nil
}
// GetDriverLastRace returns the last completed race for a driver.
func (s *PgStore) GetDriverLastRace(ctx context.Context, driverID string) (*DriverRaceSummary, error) {
row := s.pool.QueryRow(ctx, `
SELECT
rd.race_id,
r.name,
r.finished_ms,
COALESCE(rd.position, 0) as position,
rd.total_time_ms,
rd.best_lap_ms,
r.track_id,
COALESCE(t.name, '') as track_name
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
LEFT JOIN tracks t ON r.track_id = t.id
WHERE rd.driver_id = $1 AND r.status = 'finished'
ORDER BY r.finished_ms DESC, r.id DESC
LIMIT 1`, driverID)
var sum DriverRaceSummary
err := row.Scan(
&sum.RaceID,
&sum.RaceName,
&sum.FinishedMs,
&sum.Position,
&sum.TotalTimeMs,
&sum.BestLapMs,
&sum.TrackID,
&sum.TrackName,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, fmt.Errorf("query driver last race: %w", err)
}
return &sum, nil
}
// GetDriverBestRace returns the best completed race for a driver (by position, then time).
func (s *PgStore) GetDriverBestRace(ctx context.Context, driverID string) (*DriverRaceSummary, error) {
row := s.pool.QueryRow(ctx, `
SELECT
rd.race_id,
r.name,
r.finished_ms,
COALESCE(rd.position, 0) as position,
rd.total_time_ms,
rd.best_lap_ms,
r.track_id,
COALESCE(t.name, '') as track_name
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
LEFT JOIN tracks t ON r.track_id = t.id
WHERE rd.driver_id = $1 AND r.status = 'finished' AND rd.position IS NOT NULL AND rd.position > 0
ORDER BY rd.position ASC, rd.total_time_ms ASC, r.finished_ms DESC, r.id DESC
LIMIT 1`, driverID)
var sum DriverRaceSummary
err := row.Scan(
&sum.RaceID,
&sum.RaceName,
&sum.FinishedMs,
&sum.Position,
&sum.TotalTimeMs,
&sum.BestLapMs,
&sum.TrackID,
&sum.TrackName,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, fmt.Errorf("query driver best race: %w", err)
}
return &sum, nil
}
@@ -11,7 +11,7 @@
-- (S2). We enable it here so that the extension is provisioned alongside -- (S2). We enable it here so that the extension is provisioned alongside
-- the base schema. -- the base schema.
CREATE EXTENSION IF NOT EXISTS timescaledb; -- CREATE EXTENSION IF NOT EXISTS timescaledb;
-- --------------------------------------------------------------------------- -- ---------------------------------------------------------------------------
-- Tracks -- Tracks
@@ -8,81 +8,81 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('monaco', 'Monaco', 'Circuit de Monaco. Narrow street circuit, tight hairpins, no room for error.', 'system', 'system', 27, VALUES ('monaco', 'Monaco', 'Circuit de Monaco. Narrow street circuit, tight hairpins, no room for error.', 'system', 'system', 27,
3.5500000000000003, 1.1, 0.18, 'tile', 0, 'Verstappen', 3.5500000000000003, 1.1, 0.18, 'tile', 0, 'Verstappen',
0.4, 1.4, 3.95, 2.5, 0.4, -2.5, 3.95, -1.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 0, 0.4, 2.2, 0, 3.2, 0) VALUES ('monaco', 0, 0.4, -2.2, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 1, 1.1, 2.2, 0, 3, 0) VALUES ('monaco', 1, 1.1, -2.2, -0, 3, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 2, 1.5, 2.05, -0.4, 2.4, 1.2) VALUES ('monaco', 2, 1.5, -2.05, 0.4, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 3, 1.7, 1.85, -0.9, 2.2, 1.8) VALUES ('monaco', 3, 1.7, -1.85, 0.9, 2.2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 4, 1.55, 1.55, -1.6, 2, 1.4) VALUES ('monaco', 4, 1.55, -1.55, 1.6, 2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 5, 1.25, 1.4, -2.4, 2.1, 1.2) VALUES ('monaco', 5, 1.25, -1.4, 2.4, 2.1, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 6, 0.95, 1.55, 3.141592653589793, 1.9, 1.6) VALUES ('monaco', 6, 0.95, -1.55, -3.141592653589793, 1.9, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 7, 0.75, 1.85, 2, 1.8, 2.2) VALUES ('monaco', 7, 0.75, -1.85, -2, 1.8, 2.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 8, 0.95, 2.15, 2.6, 1.8, 2.6) VALUES ('monaco', 8, 0.95, -2.15, -2.6, 1.8, 2.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 9, 1.2, 2.4, -2.8, 2.1, 1.4) VALUES ('monaco', 9, 1.2, -2.4, 2.8, 2.1, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 10, 1.6, 2.5, -3, 2.4, 1) VALUES ('monaco', 10, 1.6, -2.5, 3, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 11, 2.6, 2.5, 0, 3.2, 0) VALUES ('monaco', 11, 2.6, -2.5, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 12, 3.4, 2.5, 0, 3.2, 0) VALUES ('monaco', 12, 3.4, -2.5, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 13, 3.6, 2.25, -0.5, 2.6, 1.5) VALUES ('monaco', 13, 3.6, -2.25, 0.5, 2.6, 1.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 14, 3.55, 1.95, 0.4, 2.6, 1.5) VALUES ('monaco', 14, 3.55, -1.95, -0.4, 2.6, 1.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 15, 3.3, 1.75, 1, 2.3, 1.4) VALUES ('monaco', 15, 3.3, -1.75, -1, 2.3, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 16, 2.95, 1.55, 1.7, 2.1, 1.8) VALUES ('monaco', 16, 2.95, -1.55, -1.7, 2.1, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 17, 2.6, 1.65, 2.6, 2.1, 1.8) VALUES ('monaco', 17, 2.6, -1.65, -2.6, 2.1, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 18, 2.4, 1.9, -2.7, 2.3, 1.4) VALUES ('monaco', 18, 2.4, -1.9, 2.7, 2.3, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 19, 2.4, 2.2, -3, 2.2, 1.6) VALUES ('monaco', 19, 2.4, -2.2, 3, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 20, 2.6, 2.4, 2.8, 2.5, 1) VALUES ('monaco', 20, 2.6, -2.4, -2.8, 2.5, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 21, 3.2, 2.4, 0, 3.2, 0) VALUES ('monaco', 21, 3.2, -2.4, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 22, 3.7, 2.4, 0, 3.2, 0) VALUES ('monaco', 22, 3.7, -2.4, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 23, 3.95, 2.3, -0.3, 2.8, 0.8) VALUES ('monaco', 23, 3.95, -2.3, 0.3, 2.8, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'f1')
@@ -102,93 +102,93 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('barcelona-catalunya', 'Barcelona-Catalunya', 'Circuit de Barcelona-Catalunya. Mix of high-speed corners and technical chicanes.', 'system', 'system', 27, VALUES ('barcelona-catalunya', 'Barcelona-Catalunya', 'Circuit de Barcelona-Catalunya. Mix of high-speed corners and technical chicanes.', 'system', 'system', 27,
3.75, 2.3000000000000003, 0.18, 'tile', 0, 'Verstappen', 3.75, 2.3000000000000003, 0.18, 'tile', 0, 'Verstappen',
0.3, 0.4, 4.05, 2.7, 0.3, -2.7, 4.05, -0.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 0, 0.3, 0.4, 0, 4.5, 0) VALUES ('barcelona-catalunya', 0, 0.3, -0.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 1, 1.4, 0.4, 0, 4.5, 0) VALUES ('barcelona-catalunya', 1, 1.4, -0.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 2, 1.7, 0.65, 1.5707963267948966, 2.2, 1.6) VALUES ('barcelona-catalunya', 2, 1.7, -0.65, -1.5707963267948966, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 3, 1.5, 0.95, 3.141592653589793, 2, 2) VALUES ('barcelona-catalunya', 3, 1.5, -0.95, -3.141592653589793, 2, 2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 4, 1.2, 1.1, 3.4415926535897934, 2.6, 1) VALUES ('barcelona-catalunya', 4, 1.2, -1.1, -3.4415926535897934, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 5, 0.95, 1, 4.0415926535897935, 2.6, 1) VALUES ('barcelona-catalunya', 5, 0.95, -1, -4.0415926535897935, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 6, 0.8, 1.2, -1.5707963267948966, 2.4, 1.2) VALUES ('barcelona-catalunya', 6, 0.8, -1.2, 1.5707963267948966, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 7, 1.05, 1.5, 0, 3.2, 0.7) VALUES ('barcelona-catalunya', 7, 1.05, -1.5, -0, 3.2, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 8, 1.3, 1.65, 1.5707963267948966, 2.4, 1) VALUES ('barcelona-catalunya', 8, 1.3, -1.65, -1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 9, 1.15, 1.85, 3.141592653589793, 2.4, 1) VALUES ('barcelona-catalunya', 9, 1.15, -1.85, -3.141592653589793, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 10, 1.4, 2, 2.641592653589793, 3, 0.7) VALUES ('barcelona-catalunya', 10, 1.4, -2, -2.641592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 11, 1.7, 2, 2.141592653589793, 3, 0.7) VALUES ('barcelona-catalunya', 11, 1.7, -2, -2.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 12, 1.95, 2.2, 1.5707963267948966, 2.8, 0.5) VALUES ('barcelona-catalunya', 12, 1.95, -2.2, -1.5707963267948966, 2.8, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 13, 1.65, 2.5, 0.3, 2.8, 0.5) VALUES ('barcelona-catalunya', 13, 1.65, -2.5, -0.3, 2.8, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 14, 2.2, 2.7, 0.7, 3.2, 0.5) VALUES ('barcelona-catalunya', 14, 2.2, -2.7, -0.7, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 15, 2.8, 2.7, 1.1, 3.4, 0.5) VALUES ('barcelona-catalunya', 15, 2.8, -2.7, -1.1, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 16, 3.1, 2.5, 1.9707963267948967, 2.2, 1.4) VALUES ('barcelona-catalunya', 16, 3.1, -2.5, -1.9707963267948967, 2.2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 17, 2.95, 2.2, 3.541592653589793, 2.2, 1.4) VALUES ('barcelona-catalunya', 17, 2.95, -2.2, -3.541592653589793, 2.2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 18, 3.3, 1.9, 0, 4, 0) VALUES ('barcelona-catalunya', 18, 3.3, -1.9, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 19, 4, 1.9, 0, 4, 0) VALUES ('barcelona-catalunya', 19, 4, -1.9, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 20, 4.05, 1.6, -1.5707963267948966, 2.4, 1.4) VALUES ('barcelona-catalunya', 20, 4.05, -1.6, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 21, 3.8, 1.4, 3.141592653589793, 2.4, 1.4) VALUES ('barcelona-catalunya', 21, 3.8, -1.4, -3.141592653589793, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 22, 3.5, 1.5, 3.9269908169872414, 2.6, 1) VALUES ('barcelona-catalunya', 22, 3.5, -1.5, -3.9269908169872414, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 23, 3.1, 1.25, -1.9707963267948967, 3, 0.7) VALUES ('barcelona-catalunya', 23, 3.1, -1.25, 1.9707963267948967, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 24, 2.7, 1, -2.4707963267948965, 3, 0.7) VALUES ('barcelona-catalunya', 24, 2.7, -1, 2.4707963267948965, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 25, 2.3, 0.7, 3.541592653589793, 3.4, 0.5) VALUES ('barcelona-catalunya', 25, 2.3, -0.7, -3.541592653589793, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 26, 1.6, 0.5, 3.3415926535897933, 3.6, 0.4) VALUES ('barcelona-catalunya', 26, 1.6, -0.5, -3.3415926535897933, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 27, 0.9, 0.42, 0, 4, 0) VALUES ('barcelona-catalunya', 27, 0.9, -0.42, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'f1')
@@ -206,84 +206,84 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('austria-redbull-ring', 'Austria (Red Bull Ring)', 'Red Bull Ring. Short, fast, three straights separated by hairpins.', 'system', 'system', 27, VALUES ('austria-redbull-ring', 'Austria (Red Bull Ring)', 'Red Bull Ring. Short, fast, three straights separated by hairpins.', 'system', 'system', 27,
3.5500000000000003, 2, 0.18, 'wood', 0, 'Verstappen', 3.5500000000000003, 2, 0.18, 'wood', 0, 'Verstappen',
0.3, 0.4, 3.85, 2.4, 0.3, -2.4, 3.85, -0.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 0, 0.3, 0.4, 0, 4.8, 0) VALUES ('austria-redbull-ring', 0, 0.3, -0.4, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 1, 1.6, 0.4, 0, 4.8, 0) VALUES ('austria-redbull-ring', 1, 1.6, -0.4, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 2, 1.85, 0.65, 1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 2, 1.85, -0.65, -1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 3, 1.6, 0.95, 3.141592653589793, 2, 1.8) VALUES ('austria-redbull-ring', 3, 1.6, -0.95, -3.141592653589793, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 4, 1.3, 1, 3.641592653589793, 3, 0.7) VALUES ('austria-redbull-ring', 4, 1.3, -1, -3.641592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 5, 1.05, 1.2, -1.5707963267948966, 2.6, 1) VALUES ('austria-redbull-ring', 5, 1.05, -1.2, 1.5707963267948966, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 6, 1.4, 1.4, 0, 3.2, 0.5) VALUES ('austria-redbull-ring', 6, 1.4, -1.4, -0, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 7, 1.85, 1.4, 1.7707963267948965, 3.2, 0.5) VALUES ('austria-redbull-ring', 7, 1.85, -1.4, -1.7707963267948965, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 8, 2.1, 1.65, 3.141592653589793, 3, 0.7) VALUES ('austria-redbull-ring', 8, 2.1, -1.65, -3.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 9, 2, 1.85, -1.5707963267948966, 3, 0.7) VALUES ('austria-redbull-ring', 9, 2, -1.85, 1.5707963267948966, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 10, 2.25, 2.05, 0, 2, 1.8) VALUES ('austria-redbull-ring', 10, 2.25, -2.05, -0, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 11, 2.4, 2, 1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 11, 2.4, -2, -1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 12, 2.4, 2.2, 3.141592653589793, 2, 1.8) VALUES ('austria-redbull-ring', 12, 2.4, -2.2, -3.141592653589793, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 13, 2.2, 2.3, -1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 13, 2.2, -2.3, 1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 14, 2.55, 2.4, -0.2, 3.4, 0.4) VALUES ('austria-redbull-ring', 14, 2.55, -2.4, 0.2, 3.4, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 15, 3, 2.4, 0, 4, 0) VALUES ('austria-redbull-ring', 15, 3, -2.4, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 16, 3.5, 2.4, 0, 4.5, 0) VALUES ('austria-redbull-ring', 16, 3.5, -2.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 17, 3.85, 2.15, 1.7707963267948965, 3.4, 0.6) VALUES ('austria-redbull-ring', 17, 3.85, -2.15, -1.7707963267948965, 3.4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 18, 3.6, 1.8, 3.141592653589793, 3, 0.8) VALUES ('austria-redbull-ring', 18, 3.6, -1.8, -3.141592653589793, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 19, 3.25, 1.85, 3.741592653589793, 3, 0.8) VALUES ('austria-redbull-ring', 19, 3.25, -1.85, -3.741592653589793, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 20, 3, 1.6, -1.9707963267948967, 3.2, 0.6) VALUES ('austria-redbull-ring', 20, 3, -1.6, 1.9707963267948967, 3.2, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 21, 2.6, 1.4, -3.141592653589793, 3.4, 0.5) VALUES ('austria-redbull-ring', 21, 2.6, -1.4, 3.141592653589793, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 22, 2, 1.2, 2.641592653589793, 3.6, 0.4) VALUES ('austria-redbull-ring', 22, 2, -1.2, -2.641592653589793, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 23, 1.4, 0.8, 2.9415926535897934, 3.8, 0.3) VALUES ('austria-redbull-ring', 23, 1.4, -0.8, -2.9415926535897934, 3.8, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 24, 0.8, 0.5, 0, 4.2, 0.2) VALUES ('austria-redbull-ring', 24, 0.8, -0.5, -0, 4.2, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'f1')
@@ -300,79 +300,100 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y, bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('great-britain-silverstone', 'Great Britain (Silverstone)', 'Silverstone Circuit. Fast, flowing corners; home of British motorsport.', 'system', 'system', 27, VALUES ('great-britain-silverstone', 'Great Britain (Silverstone)', 'Silverstone Circuit. Fast, flowing corners; home of British motorsport.', 'system', 'system', 27,
3.5500000000000003, 1.2499999999999998, 0.18, 'carpet', 0, 'Hamilton', 3.5500000000000003, 2.4, 0.18, 'carpet', 0, 'Hamilton',
0.3, 1.8, 3.85, 3.05, 0.3, -2.65, 3.85, -0.25,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 0, 0.3, 2.8, 0, 5, 0) VALUES ('great-britain-silverstone', 0, 0.3, -0.3, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 1, 2.4, 2.8, 0, 5, 0) VALUES ('great-britain-silverstone', 1, 2.4, -0.3, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 2, 2.8, 2.65, -1.5707963267948966, 3, 0.8) VALUES ('great-britain-silverstone', 2, 2.8, -0.45, -1.5707963267948966, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 3, 2.6, 2.3, -3.141592653589793, 3.4, 0.6) VALUES ('great-britain-silverstone', 3, 2.6, -0.8, -3.141592653589793, 3.4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 4, 2.95, 2.05, -2.0707963267948966, 3.2, 0.7) VALUES ('great-britain-silverstone', 4, 2.95, -1.05, -2.0707963267948966, 3.2, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 5, 3.3, 1.8, 0, 2.2, 1.6) VALUES ('great-britain-silverstone', 5, 3.3, -1.3, -0, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 6, 3.6, 1.95, 1.5707963267948966, 2.2, 1.6) VALUES ('great-britain-silverstone', 6, 3.6, -1.15, 1.5707963267948966, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 7, 3.6, 2.3, 3.141592653589793, 3, 0.7) VALUES ('great-britain-silverstone', 7, 3.6, -0.8, 3.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 8, 3.85, 2.55, 1.5707963267948966, 2.6, 1) VALUES ('great-britain-silverstone', 8, 3.85, -0.55, 1.5707963267948966, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 9, 3.55, 2.85, 0, 2.4, 1) VALUES ('great-britain-silverstone', 9, 3.55, -0.25, -0, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 10, 3.25, 2.85, 1.5707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 10, 3.25, -0.25, -1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 11, 3.05, 2.55, -1.8707963267948966, 3.4, 0.5) VALUES ('great-britain-silverstone', 11, 3.05, -0.55, -1.8707963267948966, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 12, 2.85, 2.65, -3.141592653589793, 4, 0.3) VALUES ('great-britain-silverstone', 12, 2.85, -0.45, -3.141592653589793, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 13, 1.8, 2.8, 0, 4.6, 0) VALUES ('great-britain-silverstone', 13, 1.8, -0.3, -0, 4.6, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 14, 1.2, 2.95, 1.7707963267948965, 3.8, 0.4) VALUES ('great-britain-silverstone', 14, 1.2, -0.45, 1.7707963267948965, 3.8, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 15, 1, 2.65, 3.541592653589793, 3.6, 0.5) VALUES ('great-britain-silverstone', 15, 1, -0.85, 3.541592653589793, 3.6, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 16, 0.7, 2.4, -1.8707963267948966, 2.4, 1.2) VALUES ('great-britain-silverstone', 16, 0.7, -1.15, -1.8707963267948966, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 17, 0.5, 2.6, 0, 2.6, 0.9) VALUES ('great-britain-silverstone', 17, 0.5, -0.9, -0, 2.6, 0.9)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 18, 0.7, 2.85, 1.5707963267948966, 3, 0.7) VALUES ('great-britain-silverstone', 18, 0.7, -0.65, 1.5707963267948966, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 19, 0.45, 3.05, 3.541592653589793, 2.6, 1) VALUES ('great-britain-silverstone', 19, 0.45, -0.45, 3.541592653589793, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 20, 0.7, 2.5, -1.1707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 20, 0.75, -1, -1.1707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 21, 0.5, 2.05, 3.541592653589793, 2.6, 0.8) VALUES ('great-britain-silverstone', 21, 0.5, -1.5, 3.541592653589793, 2.6, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 22, 0.3, 2.4, 1.5707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 22, 0.3, -1.15, 1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 23, 0.7, -1.6, -0.7853981633974483, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 24, 2, -1.95, -1.9707963267948967, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 25, 3, -2.4, -3.3415926535897933, 3.4, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 26, 2.4, -2.65, 1.8707963267948966, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 27, 1.4, -2.6, 3.141592653589793, 4.4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 28, 0.5, -2.3, 2.8415926535897933, 4.8, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 29, 0.3, -1.7, 1.2707963267948965, 5, 0.1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'f1')
@@ -390,84 +411,84 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('belgium-spa', 'Belgium (Spa-Francorchamps)', 'Spa-Francorchamps. Long, fast, legendary Eau Rouge / Raidillon.', 'system', 'system', 27, VALUES ('belgium-spa', 'Belgium (Spa-Francorchamps)', 'Spa-Francorchamps. Long, fast, legendary Eau Rouge / Raidillon.', 'system', 'system', 27,
3.85, 2.25, 0.18, 'carpet', 0, 'Verstappen', 3.85, 2.25, 0.18, 'carpet', 0, 'Verstappen',
0.4, 0.5, 4.25, 2.75, 0.4, -2.75, 4.25, -0.5,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 0, 0.4, 0.5, 0, 4.8, 0) VALUES ('belgium-spa', 0, 0.4, -0.5, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 1, 1.6, 0.5, 0, 4.8, 0) VALUES ('belgium-spa', 1, 1.6, -0.5, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 2, 1.95, 0.6, 0.3, 4.2, 0.5) VALUES ('belgium-spa', 2, 1.95, -0.6, -0.3, 4.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 3, 2.2, 0.95, 0.9, 4, 0.6) VALUES ('belgium-spa', 3, 2.2, -0.95, -0.9, 4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 4, 2.1, 1.3, 3.541592653589793, 4, 0.6) VALUES ('belgium-spa', 4, 2.1, -1.3, -3.541592653589793, 4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 5, 2.7, 1.5, 0, 5, 0) VALUES ('belgium-spa', 5, 2.7, -1.5, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 6, 3.5, 1.5, 0, 5, 0) VALUES ('belgium-spa', 6, 3.5, -1.5, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 7, 3.85, 1.3, -1.5707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 7, 3.85, -1.3, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 8, 3.6, 1.1, 3.4415926535897934, 2.6, 1.2) VALUES ('belgium-spa', 8, 3.6, -1.1, -3.4415926535897934, 2.6, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 9, 3.3, 1.25, 4.0415926535897935, 2.6, 1.2) VALUES ('belgium-spa', 9, 3.3, -1.25, -4.0415926535897935, 2.6, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 10, 3.5, 1.55, 1.5707963267948966, 3.6, 0.4) VALUES ('belgium-spa', 10, 3.5, -1.55, -1.5707963267948966, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 11, 3.25, 1.85, 0, 2.2, 1.6) VALUES ('belgium-spa', 11, 3.25, -1.85, -0, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 12, 3.05, 1.7, -1.5707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 12, 3.05, -1.7, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 13, 3.25, 1.5, 0, 3, 0.7) VALUES ('belgium-spa', 13, 3.25, -1.5, -0, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 14, 3.7, 1.65, 1.8707963267948966, 3.4, 0.5) VALUES ('belgium-spa', 14, 3.7, -1.65, -1.8707963267948966, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 15, 4.1, 2, 2.1707963267948966, 4, 0.3) VALUES ('belgium-spa', 15, 4.1, -2, -2.1707963267948966, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 16, 4.25, 2.4, 3.541592653589793, 4.6, 0.3) VALUES ('belgium-spa', 16, 4.25, -2.4, -3.541592653589793, 4.6, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 17, 4, 2.7, -1.8707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 17, 4, -2.7, 1.8707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 18, 3.7, 2.55, 3.141592653589793, 2.4, 1.4) VALUES ('belgium-spa', 18, 3.7, -2.55, -3.141592653589793, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 19, 3.55, 2.75, 3.8415926535897933, 2.4, 1.4) VALUES ('belgium-spa', 19, 3.55, -2.75, -3.8415926535897933, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 20, 3.05, 2.7, -1.5707963267948966, 2, 1.8) VALUES ('belgium-spa', 20, 3.05, -2.7, 1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 21, 2.85, 2.45, 0, 2, 1.8) VALUES ('belgium-spa', 21, 2.85, -2.45, -0, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 22, 2, 2.2, 2.8415926535897933, 3.6, 0.3) VALUES ('belgium-spa', 22, 2, -2.2, -2.8415926535897933, 3.6, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 23, 1.2, 1.6, 2.541592653589793, 4, 0.2) VALUES ('belgium-spa', 23, 1.2, -1.6, -2.541592653589793, 4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 24, 0.6, 1.05, 2.8415926535897933, 4.4, 0.2) VALUES ('belgium-spa', 24, 0.6, -1.05, -2.8415926535897933, 4.4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'f1')
@@ -14,81 +14,81 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('monaco', 'Monaco', 'Circuit de Monaco. Narrow street circuit, tight hairpins, no room for error.', 'system', 'system', 27, VALUES ('monaco', 'Monaco', 'Circuit de Monaco. Narrow street circuit, tight hairpins, no room for error.', 'system', 'system', 27,
3.5500000000000003, 1.1, 0.18, 'tile', 0, 'Verstappen', 3.5500000000000003, 1.1, 0.18, 'tile', 0, 'Verstappen',
0.4, 1.4, 3.95, 2.5, 0.4, -2.5, 3.95, -1.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 0, 0.4, 2.2, 0, 3.2, 0) VALUES ('monaco', 0, 0.4, -2.2, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 1, 1.1, 2.2, 0, 3, 0) VALUES ('monaco', 1, 1.1, -2.2, -0, 3, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 2, 1.5, 2.05, -0.4, 2.4, 1.2) VALUES ('monaco', 2, 1.5, -2.05, 0.4, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 3, 1.7, 1.85, -0.9, 2.2, 1.8) VALUES ('monaco', 3, 1.7, -1.85, 0.9, 2.2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 4, 1.55, 1.55, -1.6, 2, 1.4) VALUES ('monaco', 4, 1.55, -1.55, 1.6, 2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 5, 1.25, 1.4, -2.4, 2.1, 1.2) VALUES ('monaco', 5, 1.25, -1.4, 2.4, 2.1, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 6, 0.95, 1.55, 3.141592653589793, 1.9, 1.6) VALUES ('monaco', 6, 0.95, -1.55, -3.141592653589793, 1.9, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 7, 0.75, 1.85, 2, 1.8, 2.2) VALUES ('monaco', 7, 0.75, -1.85, -2, 1.8, 2.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 8, 0.95, 2.15, 2.6, 1.8, 2.6) VALUES ('monaco', 8, 0.95, -2.15, -2.6, 1.8, 2.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 9, 1.2, 2.4, -2.8, 2.1, 1.4) VALUES ('monaco', 9, 1.2, -2.4, 2.8, 2.1, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 10, 1.6, 2.5, -3, 2.4, 1) VALUES ('monaco', 10, 1.6, -2.5, 3, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 11, 2.6, 2.5, 0, 3.2, 0) VALUES ('monaco', 11, 2.6, -2.5, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 12, 3.4, 2.5, 0, 3.2, 0) VALUES ('monaco', 12, 3.4, -2.5, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 13, 3.6, 2.25, -0.5, 2.6, 1.5) VALUES ('monaco', 13, 3.6, -2.25, 0.5, 2.6, 1.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 14, 3.55, 1.95, 0.4, 2.6, 1.5) VALUES ('monaco', 14, 3.55, -1.95, -0.4, 2.6, 1.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 15, 3.3, 1.75, 1, 2.3, 1.4) VALUES ('monaco', 15, 3.3, -1.75, -1, 2.3, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 16, 2.95, 1.55, 1.7, 2.1, 1.8) VALUES ('monaco', 16, 2.95, -1.55, -1.7, 2.1, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 17, 2.6, 1.65, 2.6, 2.1, 1.8) VALUES ('monaco', 17, 2.6, -1.65, -2.6, 2.1, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 18, 2.4, 1.9, -2.7, 2.3, 1.4) VALUES ('monaco', 18, 2.4, -1.9, 2.7, 2.3, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 19, 2.4, 2.2, -3, 2.2, 1.6) VALUES ('monaco', 19, 2.4, -2.2, 3, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 20, 2.6, 2.4, 2.8, 2.5, 1) VALUES ('monaco', 20, 2.6, -2.4, -2.8, 2.5, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 21, 3.2, 2.4, 0, 3.2, 0) VALUES ('monaco', 21, 3.2, -2.4, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 22, 3.7, 2.4, 0, 3.2, 0) VALUES ('monaco', 22, 3.7, -2.4, -0, 3.2, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('monaco', 23, 3.95, 2.3, -0.3, 2.8, 0.8) VALUES ('monaco', 23, 3.95, -2.3, 0.3, 2.8, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'f1')
@@ -108,93 +108,93 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('barcelona-catalunya', 'Barcelona-Catalunya', 'Circuit de Barcelona-Catalunya. Mix of high-speed corners and technical chicanes.', 'system', 'system', 27, VALUES ('barcelona-catalunya', 'Barcelona-Catalunya', 'Circuit de Barcelona-Catalunya. Mix of high-speed corners and technical chicanes.', 'system', 'system', 27,
3.75, 2.3000000000000003, 0.18, 'tile', 0, 'Verstappen', 3.75, 2.3000000000000003, 0.18, 'tile', 0, 'Verstappen',
0.3, 0.4, 4.05, 2.7, 0.3, -2.7, 4.05, -0.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 0, 0.3, 0.4, 0, 4.5, 0) VALUES ('barcelona-catalunya', 0, 0.3, -0.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 1, 1.4, 0.4, 0, 4.5, 0) VALUES ('barcelona-catalunya', 1, 1.4, -0.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 2, 1.7, 0.65, 1.5707963267948966, 2.2, 1.6) VALUES ('barcelona-catalunya', 2, 1.7, -0.65, -1.5707963267948966, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 3, 1.5, 0.95, 3.141592653589793, 2, 2) VALUES ('barcelona-catalunya', 3, 1.5, -0.95, -3.141592653589793, 2, 2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 4, 1.2, 1.1, 3.4415926535897934, 2.6, 1) VALUES ('barcelona-catalunya', 4, 1.2, -1.1, -3.4415926535897934, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 5, 0.95, 1, 4.0415926535897935, 2.6, 1) VALUES ('barcelona-catalunya', 5, 0.95, -1, -4.0415926535897935, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 6, 0.8, 1.2, -1.5707963267948966, 2.4, 1.2) VALUES ('barcelona-catalunya', 6, 0.8, -1.2, 1.5707963267948966, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 7, 1.05, 1.5, 0, 3.2, 0.7) VALUES ('barcelona-catalunya', 7, 1.05, -1.5, -0, 3.2, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 8, 1.3, 1.65, 1.5707963267948966, 2.4, 1) VALUES ('barcelona-catalunya', 8, 1.3, -1.65, -1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 9, 1.15, 1.85, 3.141592653589793, 2.4, 1) VALUES ('barcelona-catalunya', 9, 1.15, -1.85, -3.141592653589793, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 10, 1.4, 2, 2.641592653589793, 3, 0.7) VALUES ('barcelona-catalunya', 10, 1.4, -2, -2.641592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 11, 1.7, 2, 2.141592653589793, 3, 0.7) VALUES ('barcelona-catalunya', 11, 1.7, -2, -2.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 12, 1.95, 2.2, 1.5707963267948966, 2.8, 0.5) VALUES ('barcelona-catalunya', 12, 1.95, -2.2, -1.5707963267948966, 2.8, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 13, 1.65, 2.5, 0.3, 2.8, 0.5) VALUES ('barcelona-catalunya', 13, 1.65, -2.5, -0.3, 2.8, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 14, 2.2, 2.7, 0.7, 3.2, 0.5) VALUES ('barcelona-catalunya', 14, 2.2, -2.7, -0.7, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 15, 2.8, 2.7, 1.1, 3.4, 0.5) VALUES ('barcelona-catalunya', 15, 2.8, -2.7, -1.1, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 16, 3.1, 2.5, 1.9707963267948967, 2.2, 1.4) VALUES ('barcelona-catalunya', 16, 3.1, -2.5, -1.9707963267948967, 2.2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 17, 2.95, 2.2, 3.541592653589793, 2.2, 1.4) VALUES ('barcelona-catalunya', 17, 2.95, -2.2, -3.541592653589793, 2.2, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 18, 3.3, 1.9, 0, 4, 0) VALUES ('barcelona-catalunya', 18, 3.3, -1.9, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 19, 4, 1.9, 0, 4, 0) VALUES ('barcelona-catalunya', 19, 4, -1.9, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 20, 4.05, 1.6, -1.5707963267948966, 2.4, 1.4) VALUES ('barcelona-catalunya', 20, 4.05, -1.6, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 21, 3.8, 1.4, 3.141592653589793, 2.4, 1.4) VALUES ('barcelona-catalunya', 21, 3.8, -1.4, -3.141592653589793, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 22, 3.5, 1.5, 3.9269908169872414, 2.6, 1) VALUES ('barcelona-catalunya', 22, 3.5, -1.5, -3.9269908169872414, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 23, 3.1, 1.25, -1.9707963267948967, 3, 0.7) VALUES ('barcelona-catalunya', 23, 3.1, -1.25, 1.9707963267948967, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 24, 2.7, 1, -2.4707963267948965, 3, 0.7) VALUES ('barcelona-catalunya', 24, 2.7, -1, 2.4707963267948965, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 25, 2.3, 0.7, 3.541592653589793, 3.4, 0.5) VALUES ('barcelona-catalunya', 25, 2.3, -0.7, -3.541592653589793, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 26, 1.6, 0.5, 3.3415926535897933, 3.6, 0.4) VALUES ('barcelona-catalunya', 26, 1.6, -0.5, -3.3415926535897933, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('barcelona-catalunya', 27, 0.9, 0.42, 0, 4, 0) VALUES ('barcelona-catalunya', 27, 0.9, -0.42, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'f1')
@@ -212,84 +212,84 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('austria-redbull-ring', 'Austria (Red Bull Ring)', 'Red Bull Ring. Short, fast, three straights separated by hairpins.', 'system', 'system', 27, VALUES ('austria-redbull-ring', 'Austria (Red Bull Ring)', 'Red Bull Ring. Short, fast, three straights separated by hairpins.', 'system', 'system', 27,
3.5500000000000003, 2, 0.18, 'wood', 0, 'Verstappen', 3.5500000000000003, 2, 0.18, 'wood', 0, 'Verstappen',
0.3, 0.4, 3.85, 2.4, 0.3, -2.4, 3.85, -0.4,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 0, 0.3, 0.4, 0, 4.8, 0) VALUES ('austria-redbull-ring', 0, 0.3, -0.4, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 1, 1.6, 0.4, 0, 4.8, 0) VALUES ('austria-redbull-ring', 1, 1.6, -0.4, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 2, 1.85, 0.65, 1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 2, 1.85, -0.65, -1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 3, 1.6, 0.95, 3.141592653589793, 2, 1.8) VALUES ('austria-redbull-ring', 3, 1.6, -0.95, -3.141592653589793, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 4, 1.3, 1, 3.641592653589793, 3, 0.7) VALUES ('austria-redbull-ring', 4, 1.3, -1, -3.641592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 5, 1.05, 1.2, -1.5707963267948966, 2.6, 1) VALUES ('austria-redbull-ring', 5, 1.05, -1.2, 1.5707963267948966, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 6, 1.4, 1.4, 0, 3.2, 0.5) VALUES ('austria-redbull-ring', 6, 1.4, -1.4, -0, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 7, 1.85, 1.4, 1.7707963267948965, 3.2, 0.5) VALUES ('austria-redbull-ring', 7, 1.85, -1.4, -1.7707963267948965, 3.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 8, 2.1, 1.65, 3.141592653589793, 3, 0.7) VALUES ('austria-redbull-ring', 8, 2.1, -1.65, -3.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 9, 2, 1.85, -1.5707963267948966, 3, 0.7) VALUES ('austria-redbull-ring', 9, 2, -1.85, 1.5707963267948966, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 10, 2.25, 2.05, 0, 2, 1.8) VALUES ('austria-redbull-ring', 10, 2.25, -2.05, -0, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 11, 2.4, 2, 1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 11, 2.4, -2, -1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 12, 2.4, 2.2, 3.141592653589793, 2, 1.8) VALUES ('austria-redbull-ring', 12, 2.4, -2.2, -3.141592653589793, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 13, 2.2, 2.3, -1.5707963267948966, 2, 1.8) VALUES ('austria-redbull-ring', 13, 2.2, -2.3, 1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 14, 2.55, 2.4, -0.2, 3.4, 0.4) VALUES ('austria-redbull-ring', 14, 2.55, -2.4, 0.2, 3.4, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 15, 3, 2.4, 0, 4, 0) VALUES ('austria-redbull-ring', 15, 3, -2.4, -0, 4, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 16, 3.5, 2.4, 0, 4.5, 0) VALUES ('austria-redbull-ring', 16, 3.5, -2.4, -0, 4.5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 17, 3.85, 2.15, 1.7707963267948965, 3.4, 0.6) VALUES ('austria-redbull-ring', 17, 3.85, -2.15, -1.7707963267948965, 3.4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 18, 3.6, 1.8, 3.141592653589793, 3, 0.8) VALUES ('austria-redbull-ring', 18, 3.6, -1.8, -3.141592653589793, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 19, 3.25, 1.85, 3.741592653589793, 3, 0.8) VALUES ('austria-redbull-ring', 19, 3.25, -1.85, -3.741592653589793, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 20, 3, 1.6, -1.9707963267948967, 3.2, 0.6) VALUES ('austria-redbull-ring', 20, 3, -1.6, 1.9707963267948967, 3.2, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 21, 2.6, 1.4, -3.141592653589793, 3.4, 0.5) VALUES ('austria-redbull-ring', 21, 2.6, -1.4, 3.141592653589793, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 22, 2, 1.2, 2.641592653589793, 3.6, 0.4) VALUES ('austria-redbull-ring', 22, 2, -1.2, -2.641592653589793, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 23, 1.4, 0.8, 2.9415926535897934, 3.8, 0.3) VALUES ('austria-redbull-ring', 23, 1.4, -0.8, -2.9415926535897934, 3.8, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('austria-redbull-ring', 24, 0.8, 0.5, 0, 4.2, 0.2) VALUES ('austria-redbull-ring', 24, 0.8, -0.5, -0, 4.2, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'f1')
@@ -307,99 +307,99 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('great-britain-silverstone', 'Great Britain (Silverstone)', 'Silverstone Circuit. Fast, flowing corners; home of British motorsport.', 'system', 'system', 27, VALUES ('great-britain-silverstone', 'Great Britain (Silverstone)', 'Silverstone Circuit. Fast, flowing corners; home of British motorsport.', 'system', 'system', 27,
3.5500000000000003, 2.4, 0.18, 'carpet', 0, 'Hamilton', 3.5500000000000003, 2.4, 0.18, 'carpet', 0, 'Hamilton',
0.3, 0.25, 3.85, 2.65, 0.3, -2.65, 3.85, -0.25,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 0, 0.3, 0.3, 0, 5, 0) VALUES ('great-britain-silverstone', 0, 0.3, -0.3, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 1, 2.4, 0.3, 0, 5, 0) VALUES ('great-britain-silverstone', 1, 2.4, -0.3, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 2, 2.8, 0.45, 1.5707963267948966, 3, 0.8) VALUES ('great-britain-silverstone', 2, 2.8, -0.45, -1.5707963267948966, 3, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 3, 2.6, 0.8, 3.141592653589793, 3.4, 0.6) VALUES ('great-britain-silverstone', 3, 2.6, -0.8, -3.141592653589793, 3.4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 4, 2.95, 1.05, 2.0707963267948966, 3.2, 0.7) VALUES ('great-britain-silverstone', 4, 2.95, -1.05, -2.0707963267948966, 3.2, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 5, 3.3, 1.3, 0, 2.2, 1.6) VALUES ('great-britain-silverstone', 5, 3.3, -1.3, -0, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 6, 3.6, 1.15, -1.5707963267948966, 2.2, 1.6) VALUES ('great-britain-silverstone', 6, 3.6, -1.15, 1.5707963267948966, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 7, 3.6, 0.8, -3.141592653589793, 3, 0.7) VALUES ('great-britain-silverstone', 7, 3.6, -0.8, 3.141592653589793, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 8, 3.85, 0.55, -1.5707963267948966, 2.6, 1) VALUES ('great-britain-silverstone', 8, 3.85, -0.55, 1.5707963267948966, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 9, 3.55, 0.25, 0, 2.4, 1) VALUES ('great-britain-silverstone', 9, 3.55, -0.25, -0, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 10, 3.25, 0.25, 1.5707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 10, 3.25, -0.25, -1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 11, 3.05, 0.55, 1.8707963267948966, 3.4, 0.5) VALUES ('great-britain-silverstone', 11, 3.05, -0.55, -1.8707963267948966, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 12, 2.85, 0.45, 3.141592653589793, 4, 0.3) VALUES ('great-britain-silverstone', 12, 2.85, -0.45, -3.141592653589793, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 13, 1.8, 0.3, 0, 4.6, 0) VALUES ('great-britain-silverstone', 13, 1.8, -0.3, -0, 4.6, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 14, 1.2, 0.45, -1.7707963267948965, 3.8, 0.4) VALUES ('great-britain-silverstone', 14, 1.2, -0.45, 1.7707963267948965, 3.8, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 15, 1, 0.85, -3.541592653589793, 3.6, 0.5) VALUES ('great-britain-silverstone', 15, 1, -0.85, 3.541592653589793, 3.6, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 16, 0.7, 1.15, 1.8707963267948966, 2.4, 1.2) VALUES ('great-britain-silverstone', 16, 0.7, -1.15, -1.8707963267948966, 2.4, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 17, 0.5, 0.9, 0, 2.6, 0.9) VALUES ('great-britain-silverstone', 17, 0.5, -0.9, -0, 2.6, 0.9)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 18, 0.7, 0.65, -1.5707963267948966, 3, 0.7) VALUES ('great-britain-silverstone', 18, 0.7, -0.65, 1.5707963267948966, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 19, 0.45, 0.45, -3.541592653589793, 2.6, 1) VALUES ('great-britain-silverstone', 19, 0.45, -0.45, 3.541592653589793, 2.6, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 20, 0.75, 1, 1.1707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 20, 0.75, -1, -1.1707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 21, 0.5, 1.5, -3.541592653589793, 2.6, 0.8) VALUES ('great-britain-silverstone', 21, 0.5, -1.5, 3.541592653589793, 2.6, 0.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 22, 0.3, 1.15, -1.5707963267948966, 2.4, 1) VALUES ('great-britain-silverstone', 22, 0.3, -1.15, 1.5707963267948966, 2.4, 1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 23, 0.7, 1.6, 0.7853981633974483, 3.4, 0.5) VALUES ('great-britain-silverstone', 23, 0.7, -1.6, -0.7853981633974483, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 24, 2, 1.95, 1.9707963267948967, 3, 0.7) VALUES ('great-britain-silverstone', 24, 2, -1.95, -1.9707963267948967, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 25, 3, 2.4, 3.3415926535897933, 3.4, 0.4) VALUES ('great-britain-silverstone', 25, 3, -2.4, -3.3415926535897933, 3.4, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 26, 2.4, 2.65, -1.8707963267948966, 4, 0.3) VALUES ('great-britain-silverstone', 26, 2.4, -2.65, 1.8707963267948966, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 27, 1.4, 2.6, -3.141592653589793, 4.4, 0.2) VALUES ('great-britain-silverstone', 27, 1.4, -2.6, 3.141592653589793, 4.4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 28, 0.5, 2.3, -2.8415926535897933, 4.8, 0.2) VALUES ('great-britain-silverstone', 28, 0.5, -2.3, 2.8415926535897933, 4.8, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('great-britain-silverstone', 29, 0.3, 1.7, -1.2707963267948965, 5, 0.1) VALUES ('great-britain-silverstone', 29, 0.3, -1.7, 1.2707963267948965, 5, 0.1)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'f1')
@@ -417,84 +417,84 @@ INSERT INTO tracks (id, name, description, author_id, visibility, scale,
created_ms, updated_ms) created_ms, updated_ms)
VALUES ('belgium-spa', 'Belgium (Spa-Francorchamps)', 'Spa-Francorchamps. Long, fast, legendary Eau Rouge / Raidillon.', 'system', 'system', 27, VALUES ('belgium-spa', 'Belgium (Spa-Francorchamps)', 'Spa-Francorchamps. Long, fast, legendary Eau Rouge / Raidillon.', 'system', 'system', 27,
3.85, 2.25, 0.18, 'carpet', 0, 'Verstappen', 3.85, 2.25, 0.18, 'carpet', 0, 'Verstappen',
0.4, 0.5, 4.25, 2.75, 0.4, -2.75, 4.25, -0.5,
1700000000000, 1700000000000) 1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 0, 0.4, 0.5, 0, 4.8, 0) VALUES ('belgium-spa', 0, 0.4, -0.5, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 1, 1.6, 0.5, 0, 4.8, 0) VALUES ('belgium-spa', 1, 1.6, -0.5, -0, 4.8, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 2, 1.95, 0.6, 0.3, 4.2, 0.5) VALUES ('belgium-spa', 2, 1.95, -0.6, -0.3, 4.2, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 3, 2.2, 0.95, 0.9, 4, 0.6) VALUES ('belgium-spa', 3, 2.2, -0.95, -0.9, 4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 4, 2.1, 1.3, 3.541592653589793, 4, 0.6) VALUES ('belgium-spa', 4, 2.1, -1.3, -3.541592653589793, 4, 0.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 5, 2.7, 1.5, 0, 5, 0) VALUES ('belgium-spa', 5, 2.7, -1.5, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 6, 3.5, 1.5, 0, 5, 0) VALUES ('belgium-spa', 6, 3.5, -1.5, -0, 5, 0)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 7, 3.85, 1.3, -1.5707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 7, 3.85, -1.3, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 8, 3.6, 1.1, 3.4415926535897934, 2.6, 1.2) VALUES ('belgium-spa', 8, 3.6, -1.1, -3.4415926535897934, 2.6, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 9, 3.3, 1.25, 4.0415926535897935, 2.6, 1.2) VALUES ('belgium-spa', 9, 3.3, -1.25, -4.0415926535897935, 2.6, 1.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 10, 3.5, 1.55, 1.5707963267948966, 3.6, 0.4) VALUES ('belgium-spa', 10, 3.5, -1.55, -1.5707963267948966, 3.6, 0.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 11, 3.25, 1.85, 0, 2.2, 1.6) VALUES ('belgium-spa', 11, 3.25, -1.85, -0, 2.2, 1.6)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 12, 3.05, 1.7, -1.5707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 12, 3.05, -1.7, 1.5707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 13, 3.25, 1.5, 0, 3, 0.7) VALUES ('belgium-spa', 13, 3.25, -1.5, -0, 3, 0.7)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 14, 3.7, 1.65, 1.8707963267948966, 3.4, 0.5) VALUES ('belgium-spa', 14, 3.7, -1.65, -1.8707963267948966, 3.4, 0.5)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 15, 4.1, 2, 2.1707963267948966, 4, 0.3) VALUES ('belgium-spa', 15, 4.1, -2, -2.1707963267948966, 4, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 16, 4.25, 2.4, 3.541592653589793, 4.6, 0.3) VALUES ('belgium-spa', 16, 4.25, -2.4, -3.541592653589793, 4.6, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 17, 4, 2.7, -1.8707963267948966, 2.4, 1.4) VALUES ('belgium-spa', 17, 4, -2.7, 1.8707963267948966, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 18, 3.7, 2.55, 3.141592653589793, 2.4, 1.4) VALUES ('belgium-spa', 18, 3.7, -2.55, -3.141592653589793, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 19, 3.55, 2.75, 3.8415926535897933, 2.4, 1.4) VALUES ('belgium-spa', 19, 3.55, -2.75, -3.8415926535897933, 2.4, 1.4)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 20, 3.05, 2.7, -1.5707963267948966, 2, 1.8) VALUES ('belgium-spa', 20, 3.05, -2.7, 1.5707963267948966, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 21, 2.85, 2.45, 0, 2, 1.8) VALUES ('belgium-spa', 21, 2.85, -2.45, -0, 2, 1.8)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 22, 2, 2.2, 2.8415926535897933, 3.6, 0.3) VALUES ('belgium-spa', 22, 2, -2.2, -2.8415926535897933, 3.6, 0.3)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 23, 1.2, 1.6, 2.541592653589793, 4, 0.2) VALUES ('belgium-spa', 23, 1.2, -1.6, -2.541592653589793, 4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature)
VALUES ('belgium-spa', 24, 0.6, 1.05, 2.8415926535897933, 4.4, 0.2) VALUES ('belgium-spa', 24, 0.6, -1.05, -2.8415926535897933, 4.4, 0.2)
ON CONFLICT (track_id, seq) DO NOTHING; ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'f1') INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'f1')
@@ -20,56 +20,56 @@ UPDATE tracks SET
WHERE id = 'monaco'; WHERE id = 'monaco';
-- New centerline (51 waypoints, scale_xy=0.016, v=0.632 m/s) -- New centerline (51 waypoints, scale_xy=0.016, v=0.632 m/s)
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 0, -3.2064, -1.0454, 1.4710, 0.632, -0.1988); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 0, -3.2064, 1.0454, -1.4710, 0.632, -0.1988);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 1, -3.1387, -0.5857, 1.3926, 0.632, -0.1787); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 1, -3.1387, 0.5857, -1.3926, 0.632, -0.1787);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 2, -3.0418, -0.1312, 1.3043, 0.632, -0.2544); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 2, -3.0418, 0.1312, -1.3043, 0.632, -0.2544);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 3, -2.8946, 0.3084, 1.1557, 0.632, -0.4431); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 3, -2.8946, -0.3084, -1.1557, 0.632, -0.4431);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 4, -2.6693, 0.7141, 0.8629, 0.632, -1.0875); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 4, -2.6693, -0.7141, -0.8629, 0.632, -1.0875);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 5, -2.3517, 0.9426, 0.1919, 0.632, -1.2844); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 5, -2.3517, -0.9426, -0.1919, 0.632, -1.2844);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 6, -1.8935, 0.8648, -0.1182, 0.632, -0.2237); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 6, -1.8935, -0.8648, 0.1182, 0.632, -0.2237);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 7, -1.4302, 0.8332, -0.0660, 0.632, 0.0475); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 7, -1.4302, -0.8332, 0.0660, 0.632, 0.0475);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 8, -0.9667, 0.8035, -0.0743, 0.632, 0.0756); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 8, -0.9667, -0.8035, 0.0743, 0.632, 0.0756);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 9, -0.5052, 0.7644, 0.0045, 0.632, 0.0806); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 9, -0.5052, -0.7644, -0.0045, 0.632, 0.0806);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 10, -0.0425, 0.8077, 0.0000, 0.632, -0.0425); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 10, -0.0425, -0.8077, -0.0000, 0.632, -0.0425);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 11, 0.4197, 0.7644, -0.0351, 0.632, 0.3650); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 11, 0.4197, -0.7644, 0.0351, 0.632, 0.3650);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 12, 0.8809, 0.7753, 0.3862, 0.632, 1.3887); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 12, 0.8809, -0.7753, -0.3862, 0.632, 1.3887);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 13, 1.1985, 1.0811, 1.2440, 0.632, 1.4550); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 13, 1.1985, -1.0811, -1.2440, 0.632, 1.4550);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 14, 1.1372, 1.5315, 1.5878, 0.632, -0.2144); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 14, 1.1372, -1.5315, -1.5878, 0.632, -0.2144);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 15, 1.1839, 1.9468, 1.0284, 0.632, -1.2681); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 15, 1.1839, -1.9468, -1.0284, 0.632, -1.2681);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 16, 1.5551, 2.2249, 0.6162, 0.632, -0.4763); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 16, 1.5551, -2.2249, -0.6162, 0.632, -0.4763);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 17, 1.9414, 2.4832, 0.5226, 0.632, -0.1756); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 17, 1.9414, -2.4832, -0.5226, 0.632, -0.1756);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 18, 2.3526, 2.6842, 0.4546, 0.632, -0.1494); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 18, 2.3526, -2.6842, -0.4546, 0.632, -0.1494);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 19, 2.3526, 2.6842, -1.2476, 0.632, -0.0225); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 19, 2.3526, -2.6842, 1.2476, 0.632, -0.0225);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 20, 2.4622, 2.3570, -1.2545, 0.632, 0.1775); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 20, 2.4622, -2.3570, 1.2545, 0.632, 0.1775);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 21, 2.5976, 1.9356, -0.5751, 0.632, 34.1925); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 21, 2.5976, -1.9356, 0.5751, 0.632, 34.1925);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 22, 2.6154, 2.2577, 1.0415, 0.632, -0.8400); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 22, 2.6154, -2.2577, -1.0415, 0.632, -0.8400);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 23, 2.9239, 2.4933, -0.0053, 0.632, -3.1975); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 23, 2.9239, -2.4933, 0.0053, 0.632, -3.1975);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 24, 3.2311, 2.2544, -1.4159, 0.632, -2.7456); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 24, 3.2311, -2.2544, 1.4159, 0.632, -2.7456);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 25, 3.0259, 1.8399, -2.1127, 0.632, -0.6488); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 25, 3.0259, -1.8399, 2.1127, 0.632, -0.6488);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 26, 2.7546, 1.4629, -2.2528, 0.632, -0.2769); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 26, 2.7546, -1.4629, 2.2528, 0.632, -0.2769);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 27, 2.4413, 1.1199, -2.3708, 0.632, -0.3044); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 27, 2.4413, -1.1199, 2.3708, 0.632, -0.3044);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 28, 2.0895, 0.8170, -2.5300, 0.632, -0.3825); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 28, 2.0895, -0.8170, 2.5300, 0.632, -0.3825);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 29, 1.6852, 0.5897, -2.7255, 0.632, -0.4019); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 29, 1.6852, -0.5897, 2.7255, 0.632, -0.4019);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 30, 1.2447, 0.4437, -2.9031, 0.632, -0.3538); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 30, 1.2447, -0.4437, 2.9031, 0.632, -0.3538);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 31, 0.7862, 0.3711, -3.0547, 0.632, -0.3044); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 31, 0.7862, -0.3711, 3.0547, 0.632, -0.3044);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 32, 0.3218, 0.3632, 3.0973, 0.632, -0.0975); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 32, 0.3218, -0.3632, -3.0973, 0.632, -0.0975);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 33, -0.1396, 0.4121, 3.1387, 0.632, -0.0394); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 33, -0.1396, -0.4121, -3.1387, 0.632, -0.0394);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 34, -0.5818, 0.3659, 3.0594, 0.632, -0.2375); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 34, -0.5818, -0.3659, -3.0594, 0.632, -0.2375);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 35, -1.0290, 0.4854, 2.9319, 0.632, -0.0950); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 35, -1.0290, -0.4854, -2.9319, 0.632, -0.0950);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 36, -1.4880, 0.5588, 2.9664, 0.632, -0.2500); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 36, -1.4880, -0.5588, -2.9664, 0.632, -0.2500);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 37, -1.9441, 0.6474, -3.1052, 0.632, 0.8825); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 37, -1.9441, -0.6474, 3.1052, 0.632, 0.8825);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 38, -2.3631, 0.5269, -2.4971, 0.632, 1.3600); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 38, -2.3631, -0.5269, 2.4971, 0.632, 1.3600);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 39, -2.6181, 0.1409, -1.9547, 0.632, 0.9481); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 39, -2.6181, -0.1409, 1.9547, 0.632, 0.9481);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 40, -2.7014, -0.3108, -1.5706, 0.632, 0.3788); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 40, -2.7014, 0.3108, 1.5706, 0.632, 0.3788);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 41, -2.6179, -0.7633, -1.6025, 0.632, -0.3425); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 41, -2.6179, 0.7633, 1.6025, 0.632, -0.3425);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 42, -2.7299, -1.2092, -1.8788, 0.632, -0.0725); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 42, -2.7299, 1.2092, 1.8788, 0.632, -0.0725);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 43, -2.8930, -1.6280, -1.6659, 0.632, 0.7706); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 43, -2.8930, 1.6280, 1.6659, 0.632, 0.7706);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 44, -2.8134, -2.0840, -1.2045, 0.632, 0.2162); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 44, -2.8134, 2.0840, 1.2045, 0.632, 0.2162);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 45, -2.5685, -2.4739, -1.5682, 0.632, -2.4475); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 45, -2.5685, 2.4739, 1.5682, 0.632, -2.4475);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 46, -2.8118, -2.6842, 3.0638, 0.632, -4.0544); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 46, -2.8118, 2.6842, -3.0638, 0.632, -4.0544);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 47, -3.1281, -2.4303, 2.0706, 0.632, -1.2500); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 47, -3.1281, 2.4303, -2.0706, 0.632, -1.2500);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 48, -3.2004, -1.9728, 1.6822, 0.632, -0.4706); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 48, -3.2004, 1.9728, -1.6822, 0.632, -0.4706);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 49, -3.2311, -1.5093, 1.5774, 0.632, -0.2275); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 49, -3.2311, 1.5093, -1.5774, 0.632, -0.2275);
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 50, -3.2064, -1.0454, 1.4710, 0.632, -0.1988); INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 50, -3.2064, 1.0454, -1.4710, 0.632, -0.1988);
COMMIT; COMMIT;
@@ -0,0 +1,8 @@
-- 015_driver_car_id.sql — move device_id to cars table and use car_id in lobby_drivers
--
-- Represents the currently selected physical ESP32 DEVICE_ID (0..127) for a car.
-- And represents the currently selected car ID for a driver in lobby_drivers.
ALTER TABLE cars ADD COLUMN IF NOT EXISTS device_id INT;
ALTER TABLE lobby_drivers ADD COLUMN IF NOT EXISTS car_id TEXT;
ALTER TABLE lobby_drivers DROP COLUMN IF EXISTS device_id;
+9 -8
View File
@@ -142,7 +142,7 @@ func (s *PgStore) loadCars(ctx context.Context) ([]catalog.CarMeta, error) {
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry, battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
drive, top_speed_ms, color_hex, avatar_url, active, drive, top_speed_ms, color_hex, avatar_url, active,
total_distance_m, total_races, total_laps, best_lap_ms, total_distance_m, total_races, total_laps, best_lap_ms,
created_ms, updated_ms created_ms, updated_ms, device_id
FROM cars FROM cars
ORDER BY id`) ORDER BY id`)
if err != nil { if err != nil {
@@ -161,7 +161,7 @@ func (s *PgStore) loadCars(ctx context.Context) ([]catalog.CarMeta, error) {
&c.Battery.VoltageV, &c.Battery.CapacityMah, &c.Battery.Cells, &c.Battery.Chemistry, &c.Battery.VoltageV, &c.Battery.CapacityMah, &c.Battery.Cells, &c.Battery.Chemistry,
&c.Drive, &c.TopSpeedMs, &c.ColorHex, &c.AvatarURL, &c.Active, &c.Drive, &c.TopSpeedMs, &c.ColorHex, &c.AvatarURL, &c.Active,
&c.TotalDistanceM, &c.TotalRaces, &c.TotalLaps, &c.BestLapMs, &c.TotalDistanceM, &c.TotalRaces, &c.TotalLaps, &c.BestLapMs,
&c.CreatedMs, &c.UpdatedMs, &c.CreatedMs, &c.UpdatedMs, &c.DeviceID,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -260,8 +260,8 @@ func (s *PgStore) UpsertCar(ctx context.Context, c catalog.CarMeta) error {
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry, battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
drive, top_speed_ms, color_hex, avatar_url, active, drive, top_speed_ms, color_hex, avatar_url, active,
total_distance_m, total_races, total_laps, best_lap_ms, total_distance_m, total_races, total_laps, best_lap_ms,
created_ms, updated_ms) created_ms, updated_ms, device_id)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33)
ON CONFLICT (id) DO UPDATE SET ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name, name = EXCLUDED.name,
owner_id = EXCLUDED.owner_id, owner_id = EXCLUDED.owner_id,
@@ -293,7 +293,8 @@ func (s *PgStore) UpsertCar(ctx context.Context, c catalog.CarMeta) error {
total_races = EXCLUDED.total_races, total_races = EXCLUDED.total_races,
total_laps = EXCLUDED.total_laps, total_laps = EXCLUDED.total_laps,
best_lap_ms = EXCLUDED.best_lap_ms, best_lap_ms = EXCLUDED.best_lap_ms,
updated_ms = EXCLUDED.updated_ms`, updated_ms = EXCLUDED.updated_ms,
device_id = EXCLUDED.device_id`,
c.ID, c.Name, c.OwnerID, string(c.Visibility), c.Scale, c.ID, c.Name, c.OwnerID, string(c.Visibility), c.Scale,
c.LengthMm, c.WidthMm, c.HeightMm, c.WeightG, c.LengthMm, c.WidthMm, c.HeightMm, c.WeightG,
c.Chassis.Model, c.Chassis.Material, c.Chassis.Printed, c.Chassis.Model, c.Chassis.Material, c.Chassis.Printed,
@@ -302,7 +303,7 @@ func (s *PgStore) UpsertCar(ctx context.Context, c catalog.CarMeta) error {
c.Battery.VoltageV, c.Battery.CapacityMah, c.Battery.Cells, c.Battery.Chemistry, c.Battery.VoltageV, c.Battery.CapacityMah, c.Battery.Cells, c.Battery.Chemistry,
string(c.Drive), c.TopSpeedMs, c.ColorHex, c.AvatarURL, c.Active, string(c.Drive), c.TopSpeedMs, c.ColorHex, c.AvatarURL, c.Active,
c.TotalDistanceM, c.TotalRaces, c.TotalLaps, c.BestLapMs, c.TotalDistanceM, c.TotalRaces, c.TotalLaps, c.BestLapMs,
c.CreatedMs, c.UpdatedMs, c.CreatedMs, c.UpdatedMs, c.DeviceID,
) )
return err return err
} }
@@ -333,7 +334,7 @@ func (s *PgStore) UpdateCarStats(ctx context.Context, id string, fn func(*catalo
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry, battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
drive, top_speed_ms, color_hex, avatar_url, active, drive, top_speed_ms, color_hex, avatar_url, active,
total_distance_m, total_races, total_laps, best_lap_ms, total_distance_m, total_races, total_laps, best_lap_ms,
created_ms, updated_ms created_ms, updated_ms, device_id
FROM cars WHERE id = $1 FOR UPDATE`, id, FROM cars WHERE id = $1 FOR UPDATE`, id,
).Scan( ).Scan(
&c.ID, &c.Name, &c.OwnerID, &c.Visibility, &c.Scale, &c.ID, &c.Name, &c.OwnerID, &c.Visibility, &c.Scale,
@@ -344,7 +345,7 @@ func (s *PgStore) UpdateCarStats(ctx context.Context, id string, fn func(*catalo
&c.Battery.VoltageV, &c.Battery.CapacityMah, &c.Battery.Cells, &c.Battery.Chemistry, &c.Battery.VoltageV, &c.Battery.CapacityMah, &c.Battery.Cells, &c.Battery.Chemistry,
&c.Drive, &c.TopSpeedMs, &c.ColorHex, &c.AvatarURL, &c.Active, &c.Drive, &c.TopSpeedMs, &c.ColorHex, &c.AvatarURL, &c.Active,
&c.TotalDistanceM, &c.TotalRaces, &c.TotalLaps, &c.BestLapMs, &c.TotalDistanceM, &c.TotalRaces, &c.TotalLaps, &c.BestLapMs,
&c.CreatedMs, &c.UpdatedMs, &c.CreatedMs, &c.UpdatedMs, &c.DeviceID,
) )
if err != nil { if err != nil {
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
+33
View File
@@ -478,6 +478,7 @@ type CarWire struct {
CreatedMs int64 `json:"created_ms"` CreatedMs int64 `json:"created_ms"`
UpdatedMs int64 `json:"updated_ms"` UpdatedMs int64 `json:"updated_ms"`
DeviceID *int `json:"device_id,omitempty"`
} }
// CarListRequest: payload { "scope": "all|system|public|private", "owner_id": "..." }. // CarListRequest: payload { "scope": "all|system|public|private", "owner_id": "..." }.
@@ -519,6 +520,7 @@ type CarPatch struct {
ColorHex *string `json:"color_hex,omitempty"` ColorHex *string `json:"color_hex,omitempty"`
AvatarURL *string `json:"avatar_url,omitempty"` AvatarURL *string `json:"avatar_url,omitempty"`
Active *bool `json:"active,omitempty"` Active *bool `json:"active,omitempty"`
DeviceID *int `json:"device_id,omitempty"`
} }
// CarDeleteRequest: payload { "id": "..." }. // CarDeleteRequest: payload { "id": "..." }.
@@ -672,11 +674,42 @@ type DriverCreateRequest struct {
// DriverUpdateRequest is the body of PUT /api/drivers/{id}. // DriverUpdateRequest is the body of PUT /api/drivers/{id}.
type DriverUpdateRequest struct { type DriverUpdateRequest struct {
Nickname string `json:"nickname,omitempty" example:"ALI"`
Name string `json:"name,omitempty" example:"Alice"` Name string `json:"name,omitempty" example:"Alice"`
AvatarURL string `json:"avatar_url,omitempty" example:"https://cdn.example.com/u/ace.png"` AvatarURL string `json:"avatar_url,omitempty" example:"https://cdn.example.com/u/ace.png"`
ClanID *string `json:"clan_id,omitempty" example:"clan-1782000-1"` ClanID *string `json:"clan_id,omitempty" example:"clan-1782000-1"`
} }
// DriverProfileResponse is the body returned by GET /api/drivers/{id}/profile.
type DriverProfileResponse struct {
Driver Driver `json:"driver"`
Stats DriverStats `json:"stats"`
LastRace *DriverRaceSummary `json:"last_race"`
BestRace *DriverRaceSummary `json:"best_race"`
}
// DriverStats contains aggregate driver statistics.
type DriverStats struct {
TotalRacesStarted int64 `json:"total_races_started" example:"10"`
TotalRacesFinished int64 `json:"total_races_finished" example:"8"`
Wins int64 `json:"wins" example:"2"`
Podiums int64 `json:"podiums" example:"5"`
BestLapMs int64 `json:"best_lap_ms" example:"12345"`
TotalPlaytimeMs int64 `json:"total_playtime_ms" example:"1200000"`
}
// DriverRaceSummary describes one race result in a driver's profile.
type DriverRaceSummary struct {
RaceID string `json:"race_id" example:"race-123"`
RaceName string `json:"race_name" example:"Monaco Grand Prix"`
FinishedMs int64 `json:"finished_ms" example:"1700000000000"`
Position int `json:"position" example:"1"`
TotalTimeMs int64 `json:"total_time_ms" example:"123456"`
BestLapMs int64 `json:"best_lap_ms" example:"12345"`
TrackID string `json:"track_id" example:"monaco"`
TrackName string `json:"track_name" example:"Monaco"`
}
// DriverListResponse is the body returned by GET /api/drivers. // DriverListResponse is the body returned by GET /api/drivers.
type DriverListResponse struct { type DriverListResponse struct {
Items []Driver `json:"items"` Items []Driver `json:"items"`
+250
View File
@@ -0,0 +1,250 @@
// Package main is the graphify utility. It walks the codebase, parses Go AST,
// and outputs a highly compact symbol summary to server/CODEBASE.md.
// This is used for token savings and helping developers/AI agents quickly map out imports and structures.
package main
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"strings"
)
type PackageSummary struct {
Name string
Path string
Doc string
Structs []string
Interfaces []string
Functions []string
Methods []string
}
func main() {
rootDir := "."
outputFile := "CODEBASE.md"
var buf bytes.Buffer
buf.WriteString("# x0gp Go Codebase Map (Generated)\n\n")
buf.WriteString("This file contains a compiled map of all packages, structures, interfaces, and public functions in the `server` directory. It is optimized for token-efficiency so AI agents and humans can understand the codebase layout without reading all files.\n\n")
pkgs := make(map[string]*PackageSummary)
fset := token.NewFileSet()
err := filepath.WalkDir(rootDir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
// Skip hidden and build dirs
name := d.Name()
if (name != "." && strings.HasPrefix(name, ".")) || name == "vendor" || name == "bin" || name == "docs" || name == "web" {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(d.Name(), ".go") || strings.HasSuffix(d.Name(), "_test.go") {
return nil
}
dir := filepath.Dir(path)
pkgSum, exists := pkgs[dir]
if !exists {
pkgSum = &PackageSummary{
Path: dir,
}
pkgs[dir] = pkgSum
}
node, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
if err != nil {
return nil // ignore unparseable files defensively
}
pkgSum.Name = node.Name.Name
if node.Doc != nil && pkgSum.Doc == "" {
pkgSum.Doc = strings.TrimSpace(node.Doc.Text())
}
// Inspect AST for declarations
ast.Inspect(node, func(n ast.Node) bool {
switch decl := n.(type) {
case *ast.GenDecl:
for _, spec := range decl.Specs {
if typeSpec, ok := spec.(*ast.TypeSpec); ok {
if !ast.IsExported(typeSpec.Name.Name) {
continue
}
switch typeType := typeSpec.Type.(type) {
case *ast.StructType:
structDesc := fmt.Sprintf("- **struct %s**", typeSpec.Name.Name)
if typeType.Fields != nil && len(typeType.Fields.List) > 0 {
fields := []string{}
for _, f := range typeType.Fields.List {
for _, id := range f.Names {
if ast.IsExported(id.Name) {
fields = append(fields, id.Name)
}
}
}
if len(fields) > 0 {
structDesc += fmt.Sprintf(" { %s }", strings.Join(fields, ", "))
}
}
pkgSum.Structs = append(pkgSum.Structs, structDesc)
case *ast.InterfaceType:
methods := []string{}
if typeType.Methods != nil {
for _, m := range typeType.Methods.List {
for _, id := range m.Names {
if ast.IsExported(id.Name) {
methods = append(methods, id.Name)
}
}
}
}
pkgSum.Interfaces = append(pkgSum.Interfaces, fmt.Sprintf("- **interface %s** { %s }", typeSpec.Name.Name, strings.Join(methods, ", ")))
}
}
}
case *ast.FuncDecl:
if !ast.IsExported(decl.Name.Name) {
return true
}
// Format function signature
params := []string{}
if decl.Type.Params != nil {
for _, p := range decl.Type.Params.List {
for range p.Names {
params = append(params, formatType(p.Type))
}
if len(p.Names) == 0 {
params = append(params, formatType(p.Type))
}
}
}
results := []string{}
if decl.Type.Results != nil {
for _, r := range decl.Type.Results.List {
for range r.Names {
results = append(results, formatType(r.Type))
}
if len(r.Names) == 0 {
results = append(results, formatType(r.Type))
}
}
}
sig := fmt.Sprintf("%s(%s)", decl.Name.Name, strings.Join(params, ", "))
if len(results) > 0 {
if len(results) == 1 {
sig += " " + results[0]
} else {
sig += " (" + strings.Join(results, ", ") + ")"
}
}
if decl.Recv != nil && len(decl.Recv.List) > 0 {
recvType := formatType(decl.Recv.List[0].Type)
pkgSum.Methods = append(pkgSum.Methods, fmt.Sprintf("- func (%s) %s", recvType, sig))
} else {
pkgSum.Functions = append(pkgSum.Functions, fmt.Sprintf("- func %s", sig))
}
}
return true
})
return nil
})
if err != nil {
fmt.Fprintf(os.Stderr, "Error gathering package summaries: %v\n", err)
os.Exit(1)
}
// Write out gathered summaries sorted/grouped by path
// (we can sort keys or print them grouped by internal vs cmd)
paths := []string{}
for p := range pkgs {
paths = append(paths, p)
}
// Simple sort: cmd/ first, then internal/
// We want to present it nicely
for _, path := range paths {
sum := pkgs[path]
cleanPath := filepath.ToSlash(path)
buf.WriteString(fmt.Sprintf("## Package `%s` (`%s`)\n\n", sum.Name, cleanPath))
if sum.Doc != "" {
buf.WriteString(fmt.Sprintf("%s\n\n", sum.Doc))
}
if len(sum.Interfaces) > 0 {
buf.WriteString("### Interfaces\n")
for _, item := range sum.Interfaces {
buf.WriteString(item + "\n")
}
buf.WriteString("\n")
}
if len(sum.Structs) > 0 {
buf.WriteString("### Structs\n")
for _, item := range sum.Structs {
buf.WriteString(item + "\n")
}
buf.WriteString("\n")
}
if len(sum.Functions) > 0 {
buf.WriteString("### Functions\n")
for _, item := range sum.Functions {
buf.WriteString(item + "\n")
}
buf.WriteString("\n")
}
if len(sum.Methods) > 0 {
buf.WriteString("### Methods\n")
for _, item := range sum.Methods {
buf.WriteString(item + "\n")
}
buf.WriteString("\n")
}
buf.WriteString("---\n\n")
}
err = os.WriteFile(outputFile, buf.Bytes(), 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "Error writing CODEBASE.md: %v\n", err)
os.Exit(1)
}
fmt.Printf("Generated %s successfully!\n", outputFile)
}
func formatType(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.Ident:
return t.Name
case *ast.StarExpr:
return "*" + formatType(t.X)
case *ast.SelectorExpr:
return formatType(t.X) + "." + t.Sel.Name
case *ast.ArrayType:
return "[]" + formatType(t.Elt)
case *ast.MapType:
return "map[" + formatType(t.Key) + "]" + formatType(t.Value)
case *ast.InterfaceType:
return "interface{}"
case *ast.FuncType:
return "func"
default:
return "?"
}
}
+29
View File
@@ -79,6 +79,35 @@
<div class="wheel"> <div class="wheel">
<div class="wheel-disc" id="wheelDisc"></div> <div class="wheel-disc" id="wheelDisc"></div>
</div> </div>
<div class="control-extra" style="display: flex; gap: 10px; margin-top: 15px; justify-content: space-around; border-top: 1px solid #333; padding-top: 12px;">
<div class="gear-indicator" style="text-align: center;">
<div style="font-size: 0.8rem; color: #aaa;">GEAR</div>
<div style="display: flex; align-items: center; gap: 5px; margin-top: 4px;">
<button id="btnGearDown" class="btn" style="padding: 2px 8px; font-size: 0.8rem;">-</button>
<b id="lblGear" style="font-size: 1.3rem; min-width: 15px; display: inline-block;">N</b>
<button id="btnGearUp" class="btn" style="padding: 2px 8px; font-size: 0.8rem;">+</button>
</div>
<div style="font-size: 0.7rem; color: #888; margin-top: 2px;">(Z / X)</div>
</div>
<div class="sys-toggle" style="text-align: center;">
<div style="font-size: 0.8rem; color: #aaa;">DRS</div>
<button id="btnDrs" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px;">Toggle</button>
<div id="lblDrs" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ff3b30;">OFF</div>
<div style="font-size: 0.7rem; color: #888; margin-top: 2px;">(Q)</div>
</div>
<div class="sys-toggle" style="text-align: center;">
<div style="font-size: 0.8rem; color: #aaa;">PIT LIMIT</div>
<button id="btnPit" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px;">Toggle</button>
<div id="lblPit" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ff3b30;">OFF</div>
<div style="font-size: 0.7rem; color: #888; margin-top: 2px;">(E)</div>
</div>
</div>
<div style="margin-top: 15px; border-top: 1px solid #333; padding-top: 12px;">
<span style="font-size: 0.8rem; color: #aaa;">Пакет управления (JSON):</span>
<pre id="ctrlJson" style="font-family: monospace; font-size: 0.75rem; background: #111; border: 1px solid #222; padding: 8px; border-radius: 4px; color: #00ff66; margin: 6px 0 0 0; overflow-x: auto; max-height: 250px; text-align: left; line-height: 1.2;">{}</pre>
</div>
</div> </div>
<div class="card"> <div class="card">
+100 -1
View File
@@ -35,6 +35,14 @@
tPos: $('tPos'), tPos: $('tPos'),
tTsm: $('tTsm'), tTsm: $('tTsm'),
tCars: $('tCars'), tCars: $('tCars'),
lblGear: $('lblGear'),
lblDrs: $('lblDrs'),
lblPit: $('lblPit'),
btnDrs: $('btnDrs'),
btnPit: $('btnPit'),
btnGearUp: $('btnGearUp'),
btnGearDown: $('btnGearDown'),
ctrlJson: $('ctrlJson'),
}; };
// -------- State -------- // -------- State --------
@@ -53,7 +61,7 @@
snapshotCount: 0, snapshotCount: 0,
snapshotWindow: [], snapshotWindow: [],
keys: new Set(), keys: new Set(),
input: { throttle: 0, brake: 0, steer: 0, handbrake: false, buttons: 0 }, input: { throttle: 0, brake: 0, steer: 0, handbrake: false, buttons: 0, gear: 0, drs: false, pitLimiter: false },
pingTimer: null, pingTimer: null,
inputTimer: null, inputTimer: null,
slotHint: 0, // сервер выдаёт слот; 0 по умолчанию slotHint: 0, // сервер выдаёт слот; 0 по умолчанию
@@ -113,6 +121,12 @@
state.input.steer = steer; state.input.steer = steer;
state.input.handbrake = handbrake; state.input.handbrake = handbrake;
// Calculate buttons bitmask: bit 0 (DRS), bit 1 (Pit Limiter)
let buttons = 0;
if (state.input.drs) buttons |= 1;
if (state.input.pitLimiter) buttons |= 2;
state.input.buttons = buttons;
// Update UI bars // Update UI bars
ui.lblThrottle.textContent = throttle.toFixed(2); ui.lblThrottle.textContent = throttle.toFixed(2);
ui.lblBrake.textContent = brake.toFixed(2); ui.lblBrake.textContent = brake.toFixed(2);
@@ -129,6 +143,48 @@
ui.barSteer.style.width = '2%'; ui.barSteer.style.width = '2%';
ui.wheelDisc.style.transform = `rotate(${steer * 90}deg)`; ui.wheelDisc.style.transform = `rotate(${steer * 90}deg)`;
// Update Gear, DRS, Pit Limiter UI text & class if elements exist
if (ui.lblGear) {
let gName = 'N';
if (state.input.gear === -1) gName = 'R';
if (state.input.gear === 1) gName = 'D';
ui.lblGear.textContent = gName;
}
if (ui.lblDrs) {
ui.lblDrs.textContent = state.input.drs ? 'ON' : 'OFF';
ui.lblDrs.className = state.input.drs ? 'val active' : 'val';
}
if (ui.lblPit) {
ui.lblPit.textContent = state.input.pitLimiter ? 'ON' : 'OFF';
ui.lblPit.className = state.input.pitLimiter ? 'val active' : 'val';
}
// Format control packet as JSON and show in UI
if (ui.ctrlJson) {
const steerByte = Math.round((steer + 1.0) * 100);
const throttleByte = Math.round(throttle * 100);
const brakeByte = Math.round(brake * 100);
const gearByte = state.input.gear === -1 ? 0 : (state.input.gear === 1 ? 2 : 1);
const jsonStr = JSON.stringify({
sent_json: {
steering: steer,
throttle: throttle,
brake: brake,
gear: state.input.gear,
buttons: buttons
},
binary_packet_bytes: {
byte0_steer: `0x${steerByte.toString(16).toUpperCase().padStart(2, '0')} (${steerByte})`,
byte1_throttle: `0x${throttleByte.toString(16).toUpperCase().padStart(2, '0')} (${throttleByte}%)`,
byte2_brake: `0x${brakeByte.toString(16).toUpperCase().padStart(2, '0')} (${brakeByte}%)`,
byte3_gear: `0x${gearByte.toString(16).toUpperCase().padStart(2, '0')} (${gearByte === 0 ? 'R' : (gearByte === 2 ? 'D' : 'N')})`,
byte4_flags: `0x${buttons.toString(16).toUpperCase().padStart(2, '0')} (DRS=${state.input.drs ? 1 : 0}, Pit=${state.input.pitLimiter ? 1 : 0})`
}
}, null, 2);
ui.ctrlJson.textContent = jsonStr;
}
} }
// -------- Send helpers -------- // -------- Send helpers --------
@@ -450,6 +506,24 @@
if (tag === 'INPUT' || tag === 'TEXTAREA') return; if (tag === 'INPUT' || tag === 'TEXTAREA') return;
if (['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault(); if (['arrowup','arrowdown','arrowleft','arrowright',' '].includes(k)) e.preventDefault();
state.keys.add(k); state.keys.add(k);
// Toggle DRS, Pit Limiter, and shift gears
if (k === 'q') {
state.input.drs = !state.input.drs;
pollInput();
}
if (k === 'e') {
state.input.pitLimiter = !state.input.pitLimiter;
pollInput();
}
if (k === 'z') {
state.input.gear = Math.max(-1, state.input.gear - 1);
pollInput();
}
if (k === 'x') {
state.input.gear = Math.min(1, state.input.gear + 1);
pollInput();
}
}); });
window.addEventListener('keyup', (e) => { window.addEventListener('keyup', (e) => {
state.keys.delete(keyName(e)); state.keys.delete(keyName(e));
@@ -462,6 +536,31 @@
ui.btnJoin.addEventListener('click', joinRace); ui.btnJoin.addEventListener('click', joinRace);
ui.btnLeave.addEventListener('click', leaveRace); ui.btnLeave.addEventListener('click', leaveRace);
if (ui.btnDrs) {
ui.btnDrs.addEventListener('click', () => {
state.input.drs = !state.input.drs;
pollInput();
});
}
if (ui.btnPit) {
ui.btnPit.addEventListener('click', () => {
state.input.pitLimiter = !state.input.pitLimiter;
pollInput();
});
}
if (ui.btnGearUp) {
ui.btnGearUp.addEventListener('click', () => {
state.input.gear = Math.min(1, state.input.gear + 1);
pollInput();
});
}
if (ui.btnGearDown) {
ui.btnGearDown.addEventListener('click', () => {
state.input.gear = Math.max(-1, state.input.gear - 1);
pollInput();
});
}
// -------- Boot -------- // -------- Boot --------
setConn('disconnected', ''); setConn('disconnected', '');
log('x0gp PoC client ready. Press Connect.', 'meta'); log('x0gp PoC client ready. Press Connect.', 'meta');
+395
View File
@@ -0,0 +1,395 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>x0gp — Race Track Viewer</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=Plus+Jakarta+Sans:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0b0f19;
--card-bg: rgba(17, 24, 39, 0.7);
--card-border: rgba(255, 255, 255, 0.08);
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--accent: #3b82f6;
--accent-glow: rgba(59, 130, 246, 0.15);
--gradient-accent: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Plus Jakarta Sans', sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
min-height: 100vh;
padding: 2.5rem;
background-image:
radial-gradient(circle at 10% 20%, rgba(59, 130, 246, 0.05) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, rgba(37, 99, 235, 0.05) 0%, transparent 40%);
background-attachment: fixed;
}
header {
max-width: 1200px;
margin: 0 auto 3rem auto;
display: flex;
justify-content: space-between;
align-items: center;
}
h1 {
font-family: 'Outfit', sans-serif;
font-size: 2.5rem;
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, #ffffff 0%, #a5b4fc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.subtitle {
color: var(--text-secondary);
font-size: 0.95rem;
margin-top: 0.25rem;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
backdrop-filter: blur(12px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(800px circle at var(--x, 0px) var(--y, 0px), rgba(255, 255, 255, 0.06), transparent 40%);
pointer-events: none;
opacity: 0;
transition: opacity 0.5s;
}
.card:hover::before {
opacity: 1;
}
.card:hover {
transform: translateY(-4px);
border-color: rgba(59, 130, 246, 0.3);
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(59, 130, 246, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.track-name {
font-family: 'Outfit', sans-serif;
font-size: 1.35rem;
font-weight: 600;
color: #ffffff;
}
.track-id {
font-family: monospace;
font-size: 0.8rem;
color: var(--text-secondary);
background: rgba(255, 255, 255, 0.05);
padding: 0.2rem 0.5rem;
border-radius: 6px;
}
.canvas-container {
width: 100%;
height: 280px;
background-color: #0b0f19;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 255, 255, 0.03);
position: relative;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
font-size: 0.85rem;
}
.stat-item {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.03);
padding: 0.5rem 0.75rem;
border-radius: 8px;
display: flex;
justify-content: space-between;
}
.stat-label {
color: var(--text-secondary);
}
.stat-value {
color: #ffffff;
font-weight: 600;
}
.tags-container {
display: flex;
gap: 0.4rem;
flex-wrap: wrap;
}
.tag {
font-size: 0.75rem;
padding: 0.2rem 0.6rem;
border-radius: 20px;
background: rgba(59, 130, 246, 0.1);
color: #60a5fa;
border: 1px solid rgba(59, 130, 246, 0.2);
}
.tag-active {
background: rgba(16, 185, 129, 0.1);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.2);
}
.loader {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
width: 100%;
grid-column: 1 / -1;
flex-direction: column;
gap: 1rem;
color: var(--text-secondary);
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(59, 130, 246, 0.1);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<header>
<div>
<h1>x0gp — Race Track Catalog</h1>
<div class="subtitle">Visualizing physical tracks and their generated centerline geometry</div>
</div>
</header>
<main class="grid" id="tracks-grid">
<div class="loader">
<div class="spinner"></div>
<div>Loading tracks from server API...</div>
</div>
</main>
<script>
// Track SVG centerline rendering algorithm
function generateSvgPath(centerline, bounds, padding = 30, size = 260) {
if (!centerline || centerline.length < 2) return null;
const minX = bounds.min_x;
const maxX = bounds.max_x;
const minY = bounds.min_y;
const maxY = bounds.max_y;
const rangeX = maxX - minX || 1;
const rangeY = maxY - minY || 1;
const aspectRatio = rangeX / rangeY;
let width, height;
if (aspectRatio > 1) {
width = size;
height = size / aspectRatio;
} else {
width = size * aspectRatio;
height = size;
}
const canvasWidth = width + padding * 2;
const canvasHeight = height + padding * 2;
const pixelPoints = centerline.map((point) => ({
x: ((point.x - minX) / rangeX) * width + padding,
y: ((point.y - minY) / rangeY) * height + padding,
heading: point.heading_rad,
curvature: point.curvature
}));
let dAttribute = '';
for (let i = 0; i < pixelPoints.length; i++) {
const current = pixelPoints[i];
if (i === 0) {
dAttribute += `M ${current.x.toFixed(2)} ${current.y.toFixed(2)}`;
} else {
const prev = pixelPoints[i - 1];
const dx = current.x - prev.x;
const dy = current.y - prev.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const tension = 0.30 + (prev.curvature * 0.05);
const cpLength = distance * tension;
const cp1x = prev.x + Math.cos(prev.heading) * cpLength;
const cp1y = prev.y + Math.sin(prev.heading) * cpLength;
const cp2x = current.x - Math.cos(current.heading) * cpLength;
const cp2y = current.y - Math.sin(current.heading) * cpLength;
dAttribute += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${current.x.toFixed(2)} ${current.y.toFixed(2)}`;
}
}
if (pixelPoints.length > 2) {
const first = pixelPoints[0];
const last = pixelPoints[pixelPoints.length - 1];
const dx = first.x - last.x;
const dy = first.y - last.y;
const distance = Math.sqrt(dx * dx + dy * dy);
const tension = 0.3 + last.curvature * 0.05;
const cpLength = distance * tension;
const cp1x = last.x + Math.cos(last.heading) * cpLength;
const cp1y = last.y + Math.sin(last.heading) * cpLength;
const cp2x = first.x - Math.cos(first.heading) * cpLength;
const cp2y = first.y - Math.sin(first.heading) * cpLength;
dAttribute += ` C ${cp1x.toFixed(2)} ${cp1y.toFixed(2)}, ${cp2x.toFixed(2)} ${cp2y.toFixed(2)}, ${first.x.toFixed(2)} ${first.y.toFixed(2)} Z`;
}
return {
d: dAttribute,
canvasWidth,
canvasHeight
};
}
async function init() {
const grid = document.getElementById('tracks-grid');
try {
const res = await fetch('/api/tracks');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
grid.innerHTML = '';
if (!data.tracks || data.tracks.length === 0) {
grid.innerHTML = '<div class="loader">No tracks found on the server.</div>';
return;
}
data.tracks.forEach(track => {
const card = document.createElement('div');
card.className = 'card';
// Track hover mouse light effect
card.addEventListener('mousemove', e => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
card.style.setProperty('--x', `${x}px`);
card.style.setProperty('--y', `${y}px`);
});
// Generate SVG
const svgData = generateSvgPath(track.centerline, track.bounds);
let svgHtml = '<div style="color: var(--text-secondary); font-size: 0.85rem;">No centerline coordinates</div>';
if (svgData) {
svgHtml = `
<svg width="100%" height="100%" viewBox="0 0 ${svgData.canvasWidth} ${svgData.canvasHeight}" preserveAspectRatio="xMidYMid meet" style="display: block; max-width: 100%; max-height: 100%; height: auto; transform-origin: center;">
<path d="${svgData.d}" fill="none" stroke="#ffffff" stroke-width="20" stroke-linecap="round" stroke-linejoin="round" />
<path d="${svgData.d}" fill="none" stroke="#111827" stroke-width="10" stroke-linecap="round" stroke-linejoin="round" />
</svg>
`;
}
const isActiveTag = track.is_active ? '<span class="tag tag-active">Active Track</span>' : '';
const bestLapStr = track.best_lap_ms > 0 ? `${(track.best_lap_ms / 1000).toFixed(3)}s` : 'None';
card.innerHTML = `
<div class="card-header">
<div>
<div class="track-name">${track.name}</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-top: 0.2rem;">${track.description || 'No description'}</div>
</div>
<div class="track-id">${track.id}</div>
</div>
<div class="canvas-container">
${svgHtml}
</div>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Length</span>
<span class="stat-value">${track.length_m.toFixed(1)} m</span>
</div>
<div class="stat-item">
<span class="stat-label">Width</span>
<span class="stat-value">${track.width_m.toFixed(1)} m</span>
</div>
<div class="stat-item">
<span class="stat-label">Lane Width</span>
<span class="stat-value">${track.lane_width_m.toFixed(1)} m</span>
</div>
<div class="stat-item">
<span class="stat-label">Best Lap</span>
<span class="stat-value">${bestLapStr}</span>
</div>
</div>
<div class="tags-container">
<span class="tag" style="background: rgba(255,255,255,0.05); color: #fff; border-color: rgba(255,255,255,0.1); text-transform: capitalize;">${track.surface}</span>
${isActiveTag}
</div>
`;
grid.appendChild(card);
});
} catch (err) {
grid.innerHTML = `<div class="loader" style="color: #ef4444;">Failed to load tracks: ${err.message}</div>`;
}
}
init();
</script>
</body>
</html>
+119 -3
View File
@@ -408,6 +408,35 @@
<div id="keyS" class="key-btn">S</div> <div id="keyS" class="key-btn">S</div>
<div id="keyD" class="key-btn">D</div> <div id="keyD" class="key-btn">D</div>
</div> </div>
<div class="control-extra" style="display: flex; gap: 10px; margin-top: 15px; justify-content: space-around; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 12px; width: 100%;">
<div class="gear-indicator" style="text-align: center;">
<div style="font-size: 0.8rem; color: #8a8d9a;">GEAR</div>
<div style="display: flex; align-items: center; gap: 5px; margin-top: 4px;">
<button id="btnGearDown" class="btn" style="padding: 2px 8px; font-size: 0.8rem; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">-</button>
<b id="lblGear" style="font-size: 1.3rem; min-width: 15px; display: inline-block;">N</b>
<button id="btnGearUp" class="btn" style="padding: 2px 8px; font-size: 0.8rem; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">+</button>
</div>
<div style="font-size: 0.7rem; color: #6b7280; margin-top: 2px;">(Z / X)</div>
</div>
<div class="sys-toggle" style="text-align: center;">
<div style="font-size: 0.8rem; color: #8a8d9a;">DRS</div>
<button id="btnDrs" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">Toggle</button>
<div id="lblDrs" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ef4444;">OFF</div>
<div style="font-size: 0.7rem; color: #6b7280; margin-top: 2px;">(Q)</div>
</div>
<div class="sys-toggle" style="text-align: center;">
<div style="font-size: 0.8rem; color: #8a8d9a;">PIT LIMIT</div>
<button id="btnPit" class="btn" style="padding: 2px 6px; font-size: 0.8rem; margin-top: 4px; background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); color: white; border-radius: 4px; cursor: pointer;">Toggle</button>
<div id="lblPit" class="val" style="font-weight: bold; font-size: 0.95rem; margin-top: 4px; color: #ef4444;">OFF</div>
<div style="font-size: 0.7rem; color: #6b7280; margin-top: 2px;">(E)</div>
</div>
</div>
<div style="margin-top: 15px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 12px; width: 100%;">
<span style="font-size: 0.8rem; color: #8a8d9a;">Пакет управления (JSON):</span>
<pre id="ctrlJson" style="font-family: monospace; font-size: 0.75rem; background: rgba(0,0,0,0.5); border: 1px solid rgba(255,255,255,0.05); padding: 8px; border-radius: 4px; color: #00ff66; margin: 6px 0 0 0; overflow-x: auto; max-height: 250px; text-align: left; line-height: 1.2;">{}</pre>
</div>
</div> </div>
</div> </div>
@@ -489,6 +518,10 @@
let currentSteer = 0; // -1 to 1 let currentSteer = 0; // -1 to 1
let currentThrottle = 0; // 0 to 1 let currentThrottle = 0; // 0 to 1
let currentBrake = 0; // 0 to 1 let currentBrake = 0; // 0 to 1
let currentGear = 0; // 0: N, -1: R, 1: D
let drs = false;
let pitLimiter = false;
let buttons = 0;
let pingsReceived = 0; let pingsReceived = 0;
let latencyStart = 0; let latencyStart = 0;
@@ -513,6 +546,22 @@
activeKeys[key] = true; activeKeys[key] = true;
updateKeyVisuals(); updateKeyVisuals();
} }
if (key === 'q') {
drs = !drs;
updateKeyVisuals();
}
if (key === 'e') {
pitLimiter = !pitLimiter;
updateKeyVisuals();
}
if (key === 'z') {
currentGear = Math.max(-1, currentGear - 1);
updateKeyVisuals();
}
if (key === 'x') {
currentGear = Math.min(1, currentGear + 1);
updateKeyVisuals();
}
}); });
window.addEventListener('keyup', (e) => { window.addEventListener('keyup', (e) => {
@@ -542,9 +591,75 @@
if (activeKeys.a) currentSteer = -1.0; if (activeKeys.a) currentSteer = -1.0;
if (activeKeys.d) currentSteer = 1.0; if (activeKeys.d) currentSteer = 1.0;
// Log control changes to UI console buttons = 0;
log(`Controls updated: throttle=${currentThrottle}, steering=${currentSteer}, brake=${currentBrake}`, 'out'); if (drs) buttons |= 1;
if (pitLimiter) buttons |= 2;
// Update UI Gear/DRS/Pit if elements exist
const lblGear = document.getElementById('lblGear');
if (lblGear) {
let gName = 'N';
if (currentGear === -1) gName = 'R';
if (currentGear === 1) gName = 'D';
lblGear.innerText = gName;
} }
const lblDrs = document.getElementById('lblDrs');
if (lblDrs) {
lblDrs.innerText = drs ? 'ON' : 'OFF';
lblDrs.style.color = drs ? '#00ff66' : '#ef4444';
}
const lblPit = document.getElementById('lblPit');
if (lblPit) {
lblPit.innerText = pitLimiter ? 'ON' : 'OFF';
lblPit.style.color = pitLimiter ? '#00ff66' : '#ef4444';
}
const ctrlJson = document.getElementById('ctrlJson');
if (ctrlJson) {
const steerByte = Math.round((currentSteer + 1.0) * 100);
const throttleByte = Math.round(currentThrottle * 100);
const brakeByte = Math.round(currentBrake * 100);
const gearByte = currentGear === -1 ? 0 : (currentGear === 1 ? 2 : 1);
ctrlJson.innerText = JSON.stringify({
sent_json: {
steering: currentSteer,
throttle: currentThrottle,
brake: currentBrake,
gear: currentGear,
buttons: buttons
},
binary_packet_bytes: {
byte0_steer: `0x${steerByte.toString(16).toUpperCase().padStart(2, '0')} (${steerByte})`,
byte1_throttle: `0x${throttleByte.toString(16).toUpperCase().padStart(2, '0')} (${throttleByte}%)`,
byte2_brake: `0x${brakeByte.toString(16).toUpperCase().padStart(2, '0')} (${brakeByte}%)`,
byte3_gear: `0x${gearByte.toString(16).toUpperCase().padStart(2, '0')} (${gearByte === 0 ? 'R' : (gearByte === 2 ? 'D' : 'N')})`,
byte4_flags: `0x${buttons.toString(16).toUpperCase().padStart(2, '0')} (DRS=${drs ? 1 : 0}, Pit=${pitLimiter ? 1 : 0})`
}
}, null, 2);
}
// Log control changes to UI console
log(`Controls updated: throttle=${currentThrottle}, steering=${currentSteer}, brake=${currentBrake}, gear=${currentGear}, buttons=${buttons}`, 'out');
}
// Click listeners for the custom controls
document.getElementById('btnGearDown').addEventListener('click', () => {
currentGear = Math.max(-1, currentGear - 1);
updateKeyVisuals();
});
document.getElementById('btnGearUp').addEventListener('click', () => {
currentGear = Math.min(1, currentGear + 1);
updateKeyVisuals();
});
document.getElementById('btnDrs').addEventListener('click', () => {
drs = !drs;
updateKeyVisuals();
});
document.getElementById('btnPit').addEventListener('click', () => {
pitLimiter = !pitLimiter;
updateKeyVisuals();
});
connectBtn.addEventListener('click', () => { connectBtn.addEventListener('click', () => {
if (pc) { if (pc) {
@@ -742,7 +857,8 @@
throttle: currentThrottle, throttle: currentThrottle,
steering: currentSteer, steering: currentSteer,
brake: currentBrake, brake: currentBrake,
gear: 1, gear: currentGear,
buttons: buttons,
timestamp: Date.now() timestamp: Date.now()
}; };
dataChannel.send(JSON.stringify(payload)); dataChannel.send(JSON.stringify(payload));
+8
View File
@@ -0,0 +1,8 @@
# Supabase
.branches
.temp
# dotenvx
.env.keys
.env.local
.env.*.local
+414
View File
@@ -0,0 +1,414 @@
# For detailed configuration reference documentation, visit:
# https://supabase.com/docs/guides/local-development/cli/config
# A string used to distinguish different Supabase projects on the same host. Defaults to the
# working directory name when running `supabase init`.
project_id = "x0gp"
[api]
enabled = true
# Port to use for the API URL.
port = 54321
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` and `graphql_public` schemas are included by default.
schemas = ["public", "graphql_public"]
# Extra schemas to add to the search_path of every request.
extra_search_path = ["public", "extensions"]
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
# for accidental or malicious requests.
max_rows = 1000
# Controls whether new tables, views, sequences and functions created in the `public` schema by
# `postgres` are reachable through the Data API roles (`anon`, `authenticated`, `service_role`)
# without explicit GRANTs. When unset, new entities are NOT auto-exposed, matching the new cloud
# default. Set to `true` to keep the legacy behaviour of auto-exposing new entities; this is
# deprecated and the field is removed on 2026-10-30 once the always-revoked behaviour is permanent.
# auto_expose_new_tables = true
[api.tls]
# Enable HTTPS endpoints locally using a self-signed certificate.
enabled = false
# Paths to self-signed certificate pair.
# cert_path = "../certs/my-cert.pem"
# key_path = "../certs/my-key.pem"
[db]
# Port to use for the local database URL.
port = 54322
# Port used by db diff command to initialize the shadow database.
shadow_port = 54320
# Maximum amount of time to wait for health check when starting the local database.
health_timeout = "2m"
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
# server_version;` on the remote database to check.
major_version = 17
[db.pooler]
enabled = false
# Port to use for the local connection pooler.
port = 54329
# Specifies when a server connection can be reused by other clients.
# Configure one of the supported pooler modes: `transaction`, `session`.
pool_mode = "transaction"
# How many server connections to allow per user/database pair.
default_pool_size = 20
# Maximum number of client connections allowed.
max_client_conn = 100
# [db.vault]
# secret_key = "env(SECRET_VALUE)"
[db.migrations]
# If disabled, migrations will be skipped during a db push or reset.
enabled = true
# Specifies an ordered list of schema files, directories, or glob patterns that describe your database.
# Supports paths relative to supabase directory: "./schemas/*.sql", "./database".
schema_paths = []
[db.seed]
# If enabled, seeds the database after migrations during a db reset.
enabled = true
# Specifies an ordered list of seed files to load during db reset.
# Supports glob patterns relative to supabase directory: "./seeds/*.sql"
sql_paths = ["./seed.sql"]
[db.network_restrictions]
# Enable management of network restrictions.
enabled = false
# List of IPv4 CIDR blocks allowed to connect to the database.
# Defaults to allow all IPv4 connections. Set empty array to block all IPs.
allowed_cidrs = ["0.0.0.0/0"]
# List of IPv6 CIDR blocks allowed to connect to the database.
# Defaults to allow all IPv6 connections. Set empty array to block all IPs.
allowed_cidrs_v6 = ["::/0"]
# Uncomment to reject non-secure connections to the database.
# [db.ssl_enforcement]
# enabled = true
[realtime]
enabled = true
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
# ip_version = "IPv6"
# The maximum length in bytes of HTTP request headers. (default: 4096)
# max_header_length = 4096
[studio]
enabled = true
# Port to use for Supabase Studio.
port = 54323
# External URL of the API server that frontend connects to.
api_url = "http://127.0.0.1"
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
openai_api_key = "env(OPENAI_API_KEY)"
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
# are monitored, and you can view the emails that would have been sent from the web interface.
[local_smtp]
enabled = true
# Port to use for the email testing server web interface.
port = 54324
# Uncomment to expose additional ports for testing user applications that send emails.
# smtp_port = 54325
# pop3_port = 54326
# admin_email = "admin@email.com"
# sender_name = "Admin"
[storage]
enabled = true
# The maximum file size allowed (e.g. "5MB", "500KB").
file_size_limit = "50MiB"
# Uncomment to configure local storage buckets
# [storage.buckets.images]
# public = false
# file_size_limit = "50MiB"
# allowed_mime_types = ["image/png", "image/jpeg"]
# objects_path = "./images"
# Allow connections via S3 compatible clients
[storage.s3_protocol]
enabled = true
# Image transformation API is available to Supabase Pro plan.
# [storage.image_transformation]
# enabled = true
# Store analytical data in S3 for running ETL jobs over Iceberg Catalog
# This feature is only available on the hosted platform.
[storage.analytics]
enabled = false
max_namespaces = 5
max_tables = 10
max_catalogs = 2
# Analytics Buckets is available to Supabase Pro plan.
# [storage.analytics.buckets.my-warehouse]
# Store vector embeddings in S3 for large and durable datasets
[storage.vector]
enabled = true
max_buckets = 10
max_indexes = 5
# Vector Buckets is available to Supabase Pro plan.
# [storage.vector.buckets.documents-openai]
[auth]
enabled = true
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
# in emails.
site_url = "http://127.0.0.1:3000"
# The public URL that Auth serves on. Defaults to the API external URL with `/auth/v1` appended.
# external_url = ""
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
additional_redirect_urls = ["https://127.0.0.1:3000"]
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
jwt_expiry = 3600
# JWT issuer URL. If not set, defaults to auth.external_url.
# jwt_issuer = ""
# Path to JWT signing key. DO NOT commit your signing keys file to git.
# signing_keys_path = "./signing_keys.json"
# If disabled, the refresh token will never expire.
enable_refresh_token_rotation = true
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
# Requires enable_refresh_token_rotation = true.
refresh_token_reuse_interval = 10
# Allow/disallow new user signups to your project.
enable_signup = true
# Allow/disallow anonymous sign-ins to your project.
enable_anonymous_sign_ins = false
# Allow/disallow testing manual linking of accounts
enable_manual_linking = false
# Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
minimum_password_length = 6
# Passwords that do not meet the following requirements will be rejected as weak. Supported values
# are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
password_requirements = ""
# Configure passkey sign-ins.
# [auth.passkey]
# enabled = false
# Configure WebAuthn relying party settings (required when passkey is enabled).
# [auth.webauthn]
# rp_display_name = "Supabase"
# rp_id = "localhost"
# rp_origins = ["http://127.0.0.1:3000"]
[auth.rate_limit]
# Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled.
email_sent = 2
# Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled.
sms_sent = 30
# Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true.
anonymous_users = 30
# Number of sessions that can be refreshed in a 5 minute interval per IP address.
token_refresh = 150
# Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users).
sign_in_sign_ups = 30
# Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address.
token_verifications = 30
# Number of Web3 logins that can be made in a 5 minute interval per IP address.
web3 = 30
# Configure one of the supported captcha providers: `hcaptcha`, `turnstile`.
# [auth.captcha]
# enabled = true
# provider = "hcaptcha"
# secret = ""
[auth.email]
# Allow/disallow new user signups via email to your project.
enable_signup = true
# If enabled, a user will be required to confirm any email change on both the old, and new email
# addresses. If disabled, only the new email is required to confirm.
double_confirm_changes = true
# If enabled, users need to confirm their email address before signing in.
enable_confirmations = false
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
secure_password_change = false
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
max_frequency = "1s"
# Number of characters used in the email OTP.
otp_length = 6
# Number of seconds before the email OTP expires (defaults to 1 hour).
otp_expiry = 3600
# Use a production-ready SMTP server
# [auth.email.smtp]
# enabled = true
# host = "smtp.sendgrid.net"
# port = 587
# user = "apikey"
# pass = "env(SENDGRID_API_KEY)"
# admin_email = "admin@email.com"
# sender_name = "Admin"
# Uncomment to customize email template
# [auth.email.template.invite]
# subject = "You have been invited"
# content_path = "./supabase/templates/invite.html"
# Uncomment to customize notification email template
# [auth.email.notification.password_changed]
# enabled = true
# subject = "Your password has been changed"
# content_path = "./templates/password_changed_notification.html"
[auth.sms]
# Allow/disallow new user signups via SMS to your project.
enable_signup = false
# If enabled, users need to confirm their phone number before signing in.
enable_confirmations = false
# Template for sending OTP to users
template = "Your code is {{ `{{ .Code }}` }}"
# Controls the minimum amount of time that must pass before sending another sms otp.
max_frequency = "5s"
# Use pre-defined map of phone number to OTP for testing.
# [auth.sms.test_otp]
# 4152127777 = "123456"
# Configure logged in session timeouts.
# [auth.sessions]
# Force log out after the specified duration.
# timebox = "24h"
# Force log out if the user has been inactive longer than the specified duration.
# inactivity_timeout = "8h"
# This hook runs before a new user is created and allows developers to reject the request based on the incoming user object.
# [auth.hook.before_user_created]
# enabled = true
# uri = "pg-functions://postgres/auth/before-user-created-hook"
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
# [auth.hook.custom_access_token]
# enabled = true
# uri = "pg-functions://<database>/<schema>/<hook_name>"
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
[auth.sms.twilio]
enabled = false
account_sid = ""
message_service_sid = ""
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
# Multi-factor-authentication is available to Supabase Pro plan.
[auth.mfa]
# Control how many MFA factors can be enrolled at once per user.
max_enrolled_factors = 10
# Control MFA via App Authenticator (TOTP)
[auth.mfa.totp]
enroll_enabled = false
verify_enabled = false
# Configure MFA via Phone Messaging
[auth.mfa.phone]
enroll_enabled = false
verify_enabled = false
otp_length = 6
template = "Your code is {{ `{{ .Code }}` }}"
max_frequency = "5s"
# Configure MFA via WebAuthn
# [auth.mfa.web_authn]
# enroll_enabled = true
# verify_enabled = true
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
# `twitter`, `x`, `slack`, `spotify`, `workos`, `zoom`.
[auth.external.apple]
enabled = false
client_id = ""
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
# Overrides the default auth callback URL derived from auth.external_url.
redirect_uri = ""
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
# or any other third-party OIDC providers.
url = ""
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
skip_nonce_check = false
# If enabled, it will allow the user to successfully authenticate when the provider does not return an email address.
email_optional = false
# Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard.
# You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting.
[auth.web3.solana]
enabled = false
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
[auth.third_party.firebase]
enabled = false
# project_id = "my-firebase-project"
# Use Auth0 as a third-party provider alongside Supabase Auth.
[auth.third_party.auth0]
enabled = false
# tenant = "my-auth0-tenant"
# tenant_region = "us"
# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
[auth.third_party.aws_cognito]
enabled = false
# user_pool_id = "my-user-pool-id"
# user_pool_region = "us-east-1"
# Use Clerk as a third-party provider alongside Supabase Auth.
[auth.third_party.clerk]
enabled = false
# Obtain from https://clerk.com/setup/supabase
# domain = "example.clerk.accounts.dev"
# OAuth server configuration
[auth.oauth_server]
# Enable OAuth server functionality
enabled = false
# Path for OAuth consent flow UI
authorization_url_path = "/oauth/consent"
# Allow dynamic client registration
allow_dynamic_registration = false
[edge_runtime]
enabled = true
# Supported request policies: `oneshot`, `per_worker`.
# `per_worker` (default) — enables hot reload during local development.
# `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks).
policy = "per_worker"
# Port to attach the Chrome inspector for debugging edge functions.
inspector_port = 8083
# The Deno major version to use.
deno_version = 2
# [edge_runtime.secrets]
# secret_key = "env(SECRET_VALUE)"
[analytics]
enabled = true
port = 54327
# Configure one of the supported backends: `postgres`, `bigquery`.
backend = "postgres"
# Experimental features may be deprecated any time
[experimental]
# Configures Postgres storage engine to use OrioleDB (S3)
orioledb_version = ""
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
s3_host = "env(S3_HOST)"
# Configures S3 bucket region, eg. us-east-1
s3_region = "env(S3_REGION)"
# Configures AWS_ACCESS_KEY_ID for S3 bucket
s3_access_key = "env(S3_ACCESS_KEY)"
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
s3_secret_key = "env(S3_SECRET_KEY)"
# pg-delta is the schema diff engine for db diff / db pull / db remote commit.
# Set enabled = false to fall back to the legacy migra engine.
[experimental.pgdelta]
enabled = true
# Directory under `supabase/` where declarative files are written.
# declarative_schema_path = "./database"
# JSON string passed through to pg-delta SQL formatting.
# format_options = "{\"keywordCase\":\"upper\",\"indent\":2,\"maxWidth\":80,\"commaStyle\":\"trailing\"}"
@@ -0,0 +1,265 @@
-- 20260717000000_schema.sql — Consolidated schema for x0gp catalog, races, and drivers.
-- ---------------------------------------------------------------------------
-- Tracks & Waypoints
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tracks (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
author_id TEXT NOT NULL DEFAULT 'system',
visibility TEXT NOT NULL DEFAULT 'system' CHECK (visibility IN ('system', 'public', 'private')),
scale INT NOT NULL DEFAULT 27,
length_m DOUBLE PRECISION NOT NULL DEFAULT 0,
width_m DOUBLE PRECISION NOT NULL DEFAULT 0,
lane_width_m DOUBLE PRECISION NOT NULL DEFAULT 0.20,
surface TEXT NOT NULL DEFAULT 'carpet' CHECK (surface IN ('carpet', 'tile', 'wood', 'paper', 'mixed')),
best_lap_ms BIGINT NOT NULL DEFAULT 0,
best_lap_holder TEXT NOT NULL DEFAULT '',
bounds_min_x DOUBLE PRECISION NOT NULL DEFAULT 0,
bounds_min_y DOUBLE PRECISION NOT NULL DEFAULT 0,
bounds_max_x DOUBLE PRECISION NOT NULL DEFAULT 0,
bounds_max_y DOUBLE PRECISION NOT NULL DEFAULT 0,
created_ms BIGINT NOT NULL,
updated_ms BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS tracks_visibility_idx ON tracks (visibility);
CREATE INDEX IF NOT EXISTS tracks_author_idx ON tracks (author_id);
CREATE TABLE IF NOT EXISTS track_waypoints (
track_id TEXT NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
seq INT NOT NULL,
x DOUBLE PRECISION NOT NULL,
y DOUBLE PRECISION NOT NULL,
heading_rad DOUBLE PRECISION NOT NULL,
speed_ms DOUBLE PRECISION NOT NULL,
curvature DOUBLE PRECISION NOT NULL,
PRIMARY KEY (track_id, seq)
);
CREATE TABLE IF NOT EXISTS track_tags (
track_id TEXT NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
tag TEXT NOT NULL,
PRIMARY KEY (track_id, tag)
);
CREATE INDEX IF NOT EXISTS track_tags_tag_idx ON track_tags (tag);
-- ---------------------------------------------------------------------------
-- Cars
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS cars (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
owner_id TEXT NOT NULL DEFAULT 'system',
visibility TEXT NOT NULL DEFAULT 'system' CHECK (visibility IN ('system', 'public', 'private')),
scale INT NOT NULL DEFAULT 27,
length_mm DOUBLE PRECISION NOT NULL,
width_mm DOUBLE PRECISION NOT NULL,
height_mm DOUBLE PRECISION NOT NULL DEFAULT 0,
weight_g DOUBLE PRECISION NOT NULL DEFAULT 0,
chassis_model TEXT NOT NULL DEFAULT '',
chassis_material TEXT NOT NULL DEFAULT '',
chassis_printed BOOLEAN NOT NULL DEFAULT FALSE,
chassis_wheelbase_mm DOUBLE PRECISION NOT NULL DEFAULT 0,
chassis_track_mm DOUBLE PRECISION NOT NULL DEFAULT 0,
motor_kind TEXT NOT NULL DEFAULT 'brushed',
motor_class TEXT NOT NULL DEFAULT '',
motor_kv INT NOT NULL DEFAULT 0,
motor_power_w DOUBLE PRECISION NOT NULL DEFAULT 0,
battery_voltage_v DOUBLE PRECISION NOT NULL DEFAULT 0,
battery_capacity_mah INT NOT NULL DEFAULT 0,
battery_cells INT NOT NULL DEFAULT 0,
battery_chemistry TEXT NOT NULL DEFAULT 'li-po',
drive TEXT NOT NULL DEFAULT '2WD' CHECK (drive IN ('2WD', '4WD')),
top_speed_ms DOUBLE PRECISION NOT NULL DEFAULT 0,
color_hex TEXT NOT NULL DEFAULT '#ff6b1a',
avatar_url TEXT NOT NULL DEFAULT '',
active BOOLEAN NOT NULL DEFAULT TRUE,
total_distance_m DOUBLE PRECISION NOT NULL DEFAULT 0,
total_races INT NOT NULL DEFAULT 0,
total_laps INT NOT NULL DEFAULT 0,
best_lap_ms BIGINT NOT NULL DEFAULT 0,
created_ms BIGINT NOT NULL,
updated_ms BIGINT NOT NULL,
device_id INT
);
CREATE INDEX IF NOT EXISTS cars_visibility_idx ON cars (visibility);
CREATE INDEX IF NOT EXISTS cars_owner_idx ON cars (owner_id);
-- ---------------------------------------------------------------------------
-- Clans & Drivers
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS clans (
id TEXT PRIMARY KEY,
tag TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
avatar_url TEXT NOT NULL DEFAULT '',
created_ms BIGINT NOT NULL,
updated_ms BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS clans_tag_idx ON clans (tag);
CREATE TABLE IF NOT EXISTS drivers (
id TEXT PRIMARY KEY,
nickname TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
avatar_url TEXT NOT NULL DEFAULT '',
clan_id TEXT REFERENCES clans(id) ON DELETE SET NULL,
created_ms BIGINT NOT NULL,
updated_ms BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS drivers_nickname_idx ON drivers (nickname);
CREATE INDEX IF NOT EXISTS drivers_clan_idx ON drivers (clan_id);
-- ---------------------------------------------------------------------------
-- Races & Race Drivers
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS races (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
track_id TEXT NOT NULL DEFAULT '',
max_cars INT NOT NULL,
laps INT NOT NULL,
time_limit_s INT NOT NULL,
status TEXT NOT NULL DEFAULT 'lobby' CHECK (status IN ('lobby', 'countdown', 'racing', 'finished', 'cancelled')),
created_ms BIGINT NOT NULL,
started_ms BIGINT NOT NULL DEFAULT 0,
finished_ms BIGINT NOT NULL DEFAULT 0,
duration_ms BIGINT NOT NULL DEFAULT 0,
total_laps INT NOT NULL DEFAULT 0,
total_drivers INT NOT NULL DEFAULT 0,
updated_ms BIGINT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS races_status_finished_idx ON races (status, finished_ms DESC, id DESC);
CREATE INDEX IF NOT EXISTS races_started_idx ON races (status, started_ms DESC, id DESC);
CREATE INDEX IF NOT EXISTS races_track_idx ON races (track_id);
CREATE INDEX IF NOT EXISTS races_status_idx ON races (status);
CREATE TABLE IF NOT EXISTS race_drivers (
race_id TEXT NOT NULL REFERENCES races(id) ON DELETE CASCADE,
driver_id TEXT NOT NULL,
slot INT NOT NULL DEFAULT 0,
joined_ms BIGINT NOT NULL,
total_time_ms BIGINT NOT NULL DEFAULT 0,
best_lap_ms BIGINT NOT NULL DEFAULT 0,
position INT,
PRIMARY KEY (race_id, driver_id)
);
CREATE INDEX IF NOT EXISTS race_drivers_driver_idx ON race_drivers (driver_id);
CREATE INDEX IF NOT EXISTS race_drivers_podium_idx ON race_drivers (race_id, total_time_ms ASC, position ASC) WHERE total_time_ms > 0;
-- ---------------------------------------------------------------------------
-- Race Plans & Queue
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS race_plans (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
track_id TEXT NOT NULL DEFAULT 'default',
max_cars INT NOT NULL,
laps INT NOT NULL DEFAULT 0,
time_limit_s INT NOT NULL DEFAULT 0,
start_at_ms BIGINT NOT NULL,
interval_s INT NOT NULL DEFAULT 0, -- 0 = single-shot
count INT NOT NULL DEFAULT 0, -- 0 = repeat forever
enabled BOOLEAN NOT NULL DEFAULT TRUE,
created_ms BIGINT NOT NULL,
updated_ms BIGINT NOT NULL,
next_fire_ms BIGINT NOT NULL, -- last materialised fire time
fires_done INT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS race_plans_start_idx ON race_plans (start_at_ms ASC, id ASC);
CREATE INDEX IF NOT EXISTS race_plans_enabled_idx ON race_plans (enabled, next_fire_ms);
CREATE TABLE IF NOT EXISTS race_queue (
driver_id TEXT NOT NULL,
race_id TEXT NOT NULL,
plan_id TEXT NOT NULL DEFAULT '',
enqueued_ms BIGINT NOT NULL,
PRIMARY KEY (driver_id, race_id)
);
CREATE INDEX IF NOT EXISTS race_queue_race_idx ON race_queue (race_id);
CREATE INDEX IF NOT EXISTS race_queue_driver_idx ON race_queue (driver_id, enqueued_ms);
-- ---------------------------------------------------------------------------
-- Lobby Drivers
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS lobby_drivers (
driver_id TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
nickname TEXT NOT NULL DEFAULT '',
avatar_url TEXT NOT NULL DEFAULT '',
clan_id TEXT NOT NULL DEFAULT '',
clan_tag TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'idle' CHECK (status IN ('idle', 'racing', 'offline')),
current_race_id TEXT NOT NULL DEFAULT '',
connected_ms BIGINT NOT NULL,
last_seen_ms BIGINT NOT NULL,
car_id TEXT
);
CREATE INDEX IF NOT EXISTS lobby_drivers_status_idx ON lobby_drivers (status);
-- ---------------------------------------------------------------------------
-- Track Calendar
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS track_calendar (
id SERIAL PRIMARY KEY,
track_id TEXT NOT NULL REFERENCES tracks(id) ON DELETE CASCADE,
start_at TIMESTAMP WITH TIME ZONE NOT NULL,
end_at TIMESTAMP WITH TIME ZONE NOT NULL,
CHECK (start_at < end_at)
);
CREATE INDEX IF NOT EXISTS track_calendar_dates_idx ON track_calendar (start_at, end_at);
-- ---------------------------------------------------------------------------
-- Go Migration Metadata (Bridges Go server startup migrations and Supabase)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO schema_migrations (version) VALUES
('001_init.sql'),
('002_seed.sql'),
('003_f1_seed.sql'),
('004_monaco_rescale.sql'),
('005_races.sql'),
('006_drop_host.sql'),
('007_podium.sql'),
('008_drivers_clans.sql'),
('009_live_persistence.sql'),
('010_unify_races.sql'),
('011_drop_driver_ids.sql'),
('012_normalize_race_results.sql'),
('013_driver_device.sql'),
('014_track_calendar.sql'),
('015_driver_car_id.sql')
ON CONFLICT (version) DO NOTHING;
+345
View File
@@ -0,0 +1,345 @@
-- seed.sql — Initial system catalog (tracks + cars) for Supabase.
-- ---------------------------------------------------------------------------
-- Monaco (Rescaled)
-- ---------------------------------------------------------------------------
INSERT INTO tracks (id, name, description, author_id, visibility, scale,
length_m, width_m, lane_width_m, surface, best_lap_ms, best_lap_holder,
bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y,
created_ms, updated_ms)
VALUES ('monaco', 'Monaco', 'Circuit de Monaco. Narrow street circuit, tight hairpins, no room for error.', 'system', 'system', 27,
21.87, 6.46, 0.18, 'tile', 0, 'Verstappen',
-3.231, -2.684, 3.231, 2.684,
1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 0, -3.2064, 1.0454, -1.4710, 0.632, -0.1988) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 1, -3.1387, 0.5857, -1.3926, 0.632, -0.1787) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 2, -3.0418, 0.1312, -1.3043, 0.632, -0.2544) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 3, -2.8946, -0.3084, -1.1557, 0.632, -0.4431) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 4, -2.6693, -0.7141, -0.8629, 0.632, -1.0875) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 5, -2.3517, -0.9426, -0.1919, 0.632, -1.2844) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 6, -1.8935, -0.8648, 0.1182, 0.632, -0.2237) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 7, -1.4302, -0.8332, 0.0660, 0.632, 0.0475) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 8, -0.9667, -0.8035, 0.0743, 0.632, 0.0756) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 9, -0.5052, -0.7644, -0.0045, 0.632, 0.0806) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 10, -0.0425, -0.8077, -0.0000, 0.632, -0.0425) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 11, 0.4197, -0.7644, 0.0351, 0.632, 0.3650) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 12, 0.8809, -0.7753, -0.3862, 0.632, 1.3887) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 13, 1.1985, -1.0811, -1.2440, 0.632, 1.4550) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 14, 1.1372, -1.5315, -1.5878, 0.632, -0.2144) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 15, 1.1839, -1.9468, -1.0284, 0.632, -1.2681) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 16, 1.5551, -2.2249, -0.6162, 0.632, -0.4763) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 17, 1.9414, -2.4832, -0.5226, 0.632, -0.1756) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 18, 2.3526, -2.6842, -0.4546, 0.632, -0.1494) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 19, 2.3526, -2.6842, 1.2476, 0.632, -0.0225) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 20, 2.4622, -2.3570, 1.2545, 0.632, 0.1775) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 21, 2.5976, -1.9356, 0.5751, 0.632, 34.1925) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 22, 2.6154, -2.2577, -1.0415, 0.632, -0.8400) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 23, 2.9239, -2.4933, 0.0053, 0.632, -3.1975) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 24, 3.2311, -2.2544, 1.4159, 0.632, -2.7456) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 25, 3.0259, -1.8399, 2.1127, 0.632, -0.6488) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 26, 2.7546, -1.4629, 2.2528, 0.632, -0.2769) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 27, 2.4413, -1.1199, 2.3708, 0.632, -0.3044) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 28, 2.0895, -0.8170, 2.5300, 0.632, -0.3825) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 29, 1.6852, -0.5897, 2.7255, 0.632, -0.4019) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 30, 1.2447, -0.4437, 2.9031, 0.632, -0.3538) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 31, 0.7862, -0.3711, 3.0547, 0.632, -0.3044) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 32, 0.3218, -0.3632, -3.0973, 0.632, -0.0975) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 33, -0.1396, -0.4121, -3.1387, 0.632, -0.0394) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 34, -0.5818, -0.3659, -3.0594, 0.632, -0.2375) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 35, -1.0290, -0.4854, -2.9319, 0.632, -0.0950) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 36, -1.4880, -0.5588, -2.9664, 0.632, -0.2500) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 37, -1.9441, -0.6474, 3.1052, 0.632, 0.8825) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 38, -2.3631, -0.5269, 2.4971, 0.632, 1.3600) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 39, -2.6181, -0.1409, 1.9547, 0.632, 0.9481) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 40, -2.7014, 0.3108, 1.5706, 0.632, 0.3788) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 41, -2.6179, 0.7633, 1.6025, 0.632, -0.3425) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 42, -2.7299, 1.2092, 1.8788, 0.632, -0.0725) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 43, -2.8930, 1.6280, 1.6659, 0.632, 0.7706) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 44, -2.8134, 2.0840, 1.2045, 0.632, 0.2162) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 45, -2.5685, 2.4739, 1.5682, 0.632, -2.4475) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 46, -2.8118, 2.6842, -3.0638, 0.632, -4.0544) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 47, -3.1281, 2.4303, -2.0706, 0.632, -1.2500) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 48, -3.2004, 1.9728, -1.6822, 0.632, -0.4706) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 49, -3.2311, 1.5093, -1.5774, 0.632, -0.2275) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('monaco', 50, -3.2064, 1.0454, -1.4710, 0.632, -0.1988) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'f1') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'street') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'tight') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'slow') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('monaco', 'monaco') ON CONFLICT DO NOTHING;
-- ---------------------------------------------------------------------------
-- Barcelona-Catalunya
-- ---------------------------------------------------------------------------
INSERT INTO tracks (id, name, description, author_id, visibility, scale,
length_m, width_m, lane_width_m, surface, best_lap_ms, best_lap_holder,
bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y,
created_ms, updated_ms)
VALUES ('barcelona-catalunya', 'Barcelona-Catalunya', 'Circuit de Barcelona-Catalunya. Mix of high-speed corners and technical chicanes.', 'system', 'system', 27,
3.75, 2.30, 0.18, 'tile', 0, 'Verstappen',
0.3, -2.7, 4.05, -0.4,
1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 0, 0.3, -0.4, -0, 4.5, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 1, 1.4, -0.4, -0, 4.5, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 2, 1.7, -0.65, -1.5707963267948966, 2.2, 1.6) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 3, 1.5, -0.95, -3.141592653589793, 2, 2) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 4, 1.2, -1.1, -3.4415926535897934, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 5, 0.95, -1, -4.0415926535897935, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 6, 0.8, -1.2, 1.5707963267948966, 2.4, 1.2) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 7, 1.05, -1.5, -0, 3.2, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 8, 1.3, -1.65, -1.5707963267948966, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 9, 1.15, -1.85, -3.141592653589793, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 10, 1.4, -2, -2.641592653589793, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 11, 1.7, -2, -2.141592653589793, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 12, 1.95, -2.2, -1.5707963267948966, 2.8, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 13, 1.65, -2.5, -0.3, 2.8, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 14, 2.2, -2.7, -0.7, 3.2, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 15, 2.8, -2.7, -1.1, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 16, 3.1, -2.5, -1.9707963267948967, 2.2, 1.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 17, 2.95, -2.2, -3.541592653589793, 2.2, 1.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 18, 3.3, -1.9, -0, 4, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 19, 4, -1.9, -0, 4, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 20, 4.05, -1.6, 1.5707963267948966, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 21, 3.8, -1.4, -3.141592653589793, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 22, 3.5, -1.5, -3.9269908169872414, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 23, 3.1, -1.25, 1.9707963267948967, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 24, 2.7, -1, 2.4707963267948965, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 25, 2.3, -0.7, -3.541592653589793, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 26, 1.6, -0.5, -3.3415926535897933, 3.6, 0.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('barcelona-catalunya', 27, 0.9, -0.42, -0, 4, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'f1') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'balanced') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'testing') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('barcelona-catalunya', 'barcelona') ON CONFLICT DO NOTHING;
-- ---------------------------------------------------------------------------
-- Austria (Red Bull Ring)
-- ---------------------------------------------------------------------------
INSERT INTO tracks (id, name, description, author_id, visibility, scale,
length_m, width_m, lane_width_m, surface, best_lap_ms, best_lap_holder,
bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y,
created_ms, updated_ms)
VALUES ('austria-redbull-ring', 'Austria (Red Bull Ring)', 'Red Bull Ring. Short, fast, three straights separated by hairpins.', 'system', 'system', 27,
3.55, 2.0, 0.18, 'wood', 0, 'Verstappen',
0.3, -2.4, 3.85, -0.4,
1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 0, 0.3, -0.4, -0, 4.8, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 1, 1.6, -0.4, -0, 4.8, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 2, 1.85, -0.65, -1.5707963267948966, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 3, 1.6, -0.95, -3.141592653589793, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 4, 1.3, -1, -3.641592653589793, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 5, 1.05, -1.2, 1.5707963267948966, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 6, 1.4, -1.4, -0, 3.2, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 7, 1.85, -1.4, -1.7707963267948965, 3.2, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 8, 2.1, -1.65, -3.141592653589793, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 9, 2, -1.85, 1.5707963267948966, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 10, 2.25, -2.05, -0, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 11, 2.4, -2, -1.5707963267948966, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 12, 2.4, -2.2, -3.141592653589793, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 13, 2.2, -2.3, 1.5707963267948966, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 14, 2.55, -2.4, 0.2, 3.4, 0.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 15, 3, -2.4, -0, 4, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 16, 3.5, -2.4, -0, 4.5, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 17, 3.85, -2.15, -1.7707963267948965, 3.4, 0.6) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 18, 3.6, -1.8, -3.141592653589793, 3, 0.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 19, 3.25, -1.85, -3.741592653589793, 3, 0.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 20, 3, -1.6, 1.9707963267948967, 3.2, 0.6) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 21, 2.6, -1.4, 3.141592653589793, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 22, 2, -1.2, -2.641592653589793, 3.6, 0.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 23, 1.4, -0.8, -2.9415926535897934, 3.8, 0.3) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('austria-redbull-ring', 24, 0.8, -0.5, -0, 4.2, 0.2) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'f1') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'short') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'fast') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('austria-redbull-ring', 'austria') ON CONFLICT DO NOTHING;
-- ---------------------------------------------------------------------------
-- Great Britain (Silverstone)
-- ---------------------------------------------------------------------------
INSERT INTO tracks (id, name, description, author_id, visibility, scale,
length_m, width_m, lane_width_m, surface, best_lap_ms, best_lap_holder,
bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y,
created_ms, updated_ms)
VALUES ('great-britain-silverstone', 'Great Britain (Silverstone)', 'Silverstone Circuit. Fast, flowing corners; home of British motorsport.', 'system', 'system', 27,
3.55, 2.4, 0.18, 'carpet', 0, 'Hamilton',
0.3, -2.65, 3.85, -0.25,
1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 0, 0.3, -0.3, -0, 5, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 1, 2.4, -0.3, -0, 5, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 2, 2.8, -0.45, -1.5707963267948966, 3, 0.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 3, 2.6, -0.8, -3.141592653589793, 3.4, 0.6) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 4, 2.95, -1.05, -2.0707963267948966, 3.2, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 5, 3.3, -1.3, -0, 2.2, 1.6) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 6, 3.6, -1.15, 1.5707963267948966, 2.2, 1.6) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 7, 3.6, -0.8, 3.141592653589793, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 8, 3.85, -0.55, 1.5707963267948966, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 9, 3.55, -0.25, -0, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 10, 3.25, -0.25, -1.5707963267948966, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 11, 3.05, -0.55, -1.8707963267948966, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 12, 2.85, -0.45, -3.141592653589793, 4, 0.3) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 13, 1.8, -0.3, -0, 4.6, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 14, 1.2, -0.45, 1.7707963267948965, 3.8, 0.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 15, 1, -0.85, 3.541592653589793, 3.6, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 16, 0.7, -1.15, -1.8707963267948966, 2.4, 1.2) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 17, 0.5, -0.9, -0, 2.6, 0.9) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 18, 0.7, -0.65, 1.5707963267948966, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 19, 0.45, -0.45, 3.541592653589793, 2.6, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 20, 0.75, -1, -1.1707963267948966, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 21, 0.5, -1.5, 3.541592653589793, 2.6, 0.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 22, 0.3, -1.15, 1.5707963267948966, 2.4, 1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 23, 0.7, -1.6, -0.7853981633974483, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 24, 2, -1.95, -1.9707963267948967, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 25, 3, -2.4, -3.3415926535897933, 3.4, 0.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 26, 2.4, -2.65, 1.8707963267948966, 4, 0.3) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 27, 1.4, -2.6, 3.141592653589793, 4.4, 0.2) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 28, 0.5, -2.3, 2.8415926535897933, 4.8, 0.2) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('great-britain-silverstone', 29, 0.3, -1.7, 1.2707963267948965, 5, 0.1) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'f1') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'fast') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'flowing') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('great-britain-silverstone', 'silverstone') ON CONFLICT DO NOTHING;
-- ---------------------------------------------------------------------------
-- Belgium (Spa-Francorchamps)
-- ---------------------------------------------------------------------------
INSERT INTO tracks (id, name, description, author_id, visibility, scale,
length_m, width_m, lane_width_m, surface, best_lap_ms, best_lap_holder,
bounds_min_x, bounds_min_y, bounds_max_x, bounds_max_y,
created_ms, updated_ms)
VALUES ('belgium-spa', 'Belgium (Spa-Francorchamps)', 'Spa-Francorchamps. Long, fast, legendary Eau Rouge / Raidillon.', 'system', 'system', 27,
3.85, 2.25, 0.18, 'carpet', 0, 'Verstappen',
0.4, -2.75, 4.25, -0.5,
1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 0, 0.4, -0.5, -0, 4.8, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 1, 1.6, -0.5, -0, 4.8, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 2, 1.95, -0.6, -0.3, 4.2, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 3, 2.2, -0.95, -0.9, 4, 0.6) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 4, 2.1, -1.3, -3.541592653589793, 4, 0.6) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 5, 2.7, -1.5, -0, 5, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 6, 3.5, -1.5, -0, 5, 0) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 7, 3.85, -1.3, 1.5707963267948966, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 8, 3.6, -1.1, -3.4415926535897934, 2.6, 1.2) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 9, 3.3, -1.25, -4.0415926535897935, 2.6, 1.2) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 10, 3.5, -1.55, -1.5707963267948966, 3.6, 0.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 11, 3.25, -1.85, -0, 2.2, 1.6) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 12, 3.05, -1.7, 1.5707963267948966, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 13, 3.25, -1.5, -0, 3, 0.7) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 14, 3.7, -1.65, -1.8707963267948966, 3.4, 0.5) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 15, 4.1, -2, -2.1707963267948966, 4, 0.3) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 16, 4.25, -2.4, -3.541592653589793, 4.6, 0.3) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 17, 4, -2.7, 1.8707963267948966, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 18, 3.7, -2.55, -3.141592653589793, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 19, 3.55, -2.75, -3.8415926535897933, 2.4, 1.4) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 20, 3.05, -2.7, 1.5707963267948966, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 21, 2.85, -2.45, -0, 2, 1.8) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 22, 2, -2.2, -2.8415926535897933, 3.6, 0.3) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 23, 1.2, -1.6, -2.541592653589793, 4, 0.2) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_waypoints (track_id, seq, x, y, heading_rad, speed_ms, curvature) VALUES ('belgium-spa', 24, 0.6, -1.05, -2.8415926535897933, 4.4, 0.2) ON CONFLICT (track_id, seq) DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'f1') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'long') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'fast') ON CONFLICT DO NOTHING;
INSERT INTO track_tags (track_id, tag) VALUES ('belgium-spa', 'spa') ON CONFLICT DO NOTHING;
-- ---------------------------------------------------------------------------
-- Cars Catalog
-- ---------------------------------------------------------------------------
INSERT INTO cars (id, name, owner_id, visibility, scale,
length_mm, width_mm, height_mm, weight_g,
chassis_model, chassis_material, chassis_printed, chassis_wheelbase_mm, chassis_track_mm,
motor_kind, motor_class, motor_kv, motor_power_w,
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
drive, top_speed_ms, color_hex, avatar_url, active,
created_ms, updated_ms)
VALUES ('f1-2024-redbull-rb20', 'Oracle Red Bull Racing RB20', 'system', 'system', 24,
238, 83, 40, 33,
'RB20 carbon monocoque', 'carbon-fibre', FALSE, 150, 75,
'turbo-hybrid', 'F1 PU', 0, 746000,
0, 0, 0, 'li-ion MGU-K',
'2WD', 95, '#1e3a8a', '', TRUE,
1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING;
INSERT INTO cars (id, name, owner_id, visibility, scale,
length_mm, width_mm, height_mm, weight_g,
chassis_model, chassis_material, chassis_printed, chassis_wheelbase_mm, chassis_track_mm,
motor_kind, motor_class, motor_kv, motor_power_w,
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
drive, top_speed_ms, color_hex, avatar_url, active,
created_ms, updated_ms)
VALUES ('f1-2024-ferrari-sf24', 'Scuderia Ferrari SF-24', 'system', 'system', 24,
238, 83, 40, 33,
'SF-24 carbon monocoque', 'carbon-fibre', FALSE, 150, 75,
'turbo-hybrid', 'F1 PU 066/12', 0, 746000,
0, 0, 0, 'li-ion MGU-K',
'2WD', 94, '#dc2626', '', TRUE,
1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING;
INSERT INTO cars (id, name, owner_id, visibility, scale,
length_mm, width_mm, height_mm, weight_g,
chassis_model, chassis_material, chassis_printed, chassis_wheelbase_mm, chassis_track_mm,
motor_kind, motor_class, motor_kv, motor_power_w,
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
drive, top_speed_ms, color_hex, avatar_url, active,
created_ms, updated_ms)
VALUES ('f1-2024-mclaren-mcl38', 'McLaren MCL38', 'system', 'system', 24,
238, 83, 40, 33,
'MCL38 carbon monocoque', 'carbon-fibre', FALSE, 150, 75,
'turbo-hybrid', 'F1 PU Mercedes-derived', 0, 746000,
0, 0, 0, 'li-ion MGU-K',
'2WD', 94, '#ea580c', '', TRUE,
1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING;
INSERT INTO cars (id, name, owner_id, visibility, scale,
length_mm, width_mm, height_mm, weight_g,
chassis_model, chassis_material, chassis_printed, chassis_wheelbase_mm, chassis_track_mm,
motor_kind, motor_class, motor_kv, motor_power_w,
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
drive, top_speed_ms, color_hex, avatar_url, active,
created_ms, updated_ms)
VALUES ('f1-2024-mercedes-w15', 'Mercedes-AMG W15', 'system', 'system', 24,
238, 83, 40, 33,
'W15 carbon monocoque', 'carbon-fibre', FALSE, 150, 75,
'turbo-hybrid', 'F1 PU M15', 0, 746000,
0, 0, 0, 'li-ion MGU-K',
'2WD', 94, '#06b6d4', '', TRUE,
1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING;
INSERT INTO cars (id, name, owner_id, visibility, scale,
length_mm, width_mm, height_mm, weight_g,
chassis_model, chassis_material, chassis_printed, chassis_wheelbase_mm, chassis_track_mm,
motor_kind, motor_class, motor_kv, motor_power_w,
battery_voltage_v, battery_capacity_mah, battery_cells, battery_chemistry,
drive, top_speed_ms, color_hex, avatar_url, active,
created_ms, updated_ms)
VALUES ('f1-2024-astonmartin-amr24', 'Aston Martin Aramco AMR24', 'system', 'system', 24,
238, 83, 40, 33,
'AMR24 carbon monocoque', 'carbon-fibre', FALSE, 150, 75,
'turbo-hybrid', 'F1 PU Mercedes-derived', 0, 746000,
0, 0, 0, 'li-ion MGU-K',
'2WD', 94, '#16a34a', '', TRUE,
1700000000000, 1700000000000)
ON CONFLICT (id) DO NOTHING;