Nice and stable before liveview
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
// lib/serverConfig.ts
|
||||
// Parse AC server config to get available cars
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
|
||||
const SERVER_CONFIG_PATH = '/mnt/combined/servers/acserver_EVENTS/utils/cfg/server_cfg.ini';
|
||||
|
||||
export async function getAvailableCars(): Promise<string[]> {
|
||||
try {
|
||||
const content = await fs.readFile(SERVER_CONFIG_PATH, 'utf-8');
|
||||
|
||||
// Find the CARS= line
|
||||
const carsLine = content
|
||||
.split('\n')
|
||||
.find(line => line.trim().startsWith('CARS='));
|
||||
|
||||
if (!carsLine) {
|
||||
console.error('CARS line not found in server config');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Extract cars list after "CARS="
|
||||
const carsString = carsLine.split('CARS=')[1].trim();
|
||||
|
||||
// Split by semicolon and remove duplicates
|
||||
const cars = carsString
|
||||
.split(';')
|
||||
.map(car => car.trim())
|
||||
.filter(car => car.length > 0);
|
||||
|
||||
// Remove duplicates using Set
|
||||
const uniqueCars = Array.from(new Set(cars));
|
||||
|
||||
return uniqueCars.sort();
|
||||
} catch (error) {
|
||||
console.error('Error reading server config:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Format car name for display (optional - makes it prettier)
|
||||
export function formatCarName(carId: string): string {
|
||||
return carId
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, char => char.toUpperCase());
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// lib/telemetryBridge.ts
|
||||
// Bridge between C++ Unix socket and Next.js SSE
|
||||
|
||||
import { Socket } from 'net';
|
||||
|
||||
const TELEMETRY_SOCKET_PATH = '/tmp/ACtelemetry_socket';
|
||||
|
||||
interface CarTelemetry {
|
||||
carID: number;
|
||||
driver_name: string;
|
||||
driver_guid: string;
|
||||
car_model: string;
|
||||
normalizedSplinePos: number;
|
||||
speed_kmh: number;
|
||||
gear: number;
|
||||
rpm: number;
|
||||
last_lap_time: number;
|
||||
best_lap_time: number;
|
||||
current_lap: number;
|
||||
position: number;
|
||||
}
|
||||
|
||||
interface TelemetryPacket {
|
||||
server_id: number;
|
||||
car_count: number;
|
||||
cars: CarTelemetry[];
|
||||
}
|
||||
|
||||
type TelemetryCallback = (data: TelemetryPacket) => void;
|
||||
|
||||
class TelemetryBridge {
|
||||
private socket: Socket | null = null;
|
||||
private connected: boolean = false;
|
||||
private callbacks: Set<TelemetryCallback> = new Set();
|
||||
private reconnectTimeout: NodeJS.Timeout | null = null;
|
||||
private buffer: Buffer = Buffer.alloc(0);
|
||||
|
||||
constructor() {
|
||||
this.connect();
|
||||
}
|
||||
|
||||
private connect() {
|
||||
console.log('[Telemetry] Connecting to', TELEMETRY_SOCKET_PATH);
|
||||
|
||||
this.socket = new Socket();
|
||||
|
||||
this.socket.connect(TELEMETRY_SOCKET_PATH, () => {
|
||||
console.log('[Telemetry] Connected to C++ socket');
|
||||
this.connected = true;
|
||||
this.buffer = Buffer.alloc(0); // Reset buffer on new connection
|
||||
});
|
||||
|
||||
this.socket.on('data', (data: Buffer) => {
|
||||
// Append new data to buffer
|
||||
this.buffer = Buffer.concat([this.buffer, data]);
|
||||
|
||||
// Parse complete packets from buffer
|
||||
this.parsePackets();
|
||||
});
|
||||
|
||||
this.socket.on('error', (err) => {
|
||||
console.error('[Telemetry] Socket error:', err.message);
|
||||
this.connected = false;
|
||||
});
|
||||
|
||||
this.socket.on('close', () => {
|
||||
console.log('[Telemetry] Connection closed, reconnecting in 2s...');
|
||||
this.connected = false;
|
||||
this.socket = null;
|
||||
|
||||
// Reconnect after 2 seconds
|
||||
if (this.reconnectTimeout) clearTimeout(this.reconnectTimeout);
|
||||
this.reconnectTimeout = setTimeout(() => this.connect(), 2000);
|
||||
});
|
||||
}
|
||||
|
||||
private parsePackets() {
|
||||
// Telemetry packet structure from C++:
|
||||
// uint8_t server_id (1 byte)
|
||||
// uint8_t car_count (1 byte)
|
||||
// car_telemetry cars[64] (each car = 158 bytes)
|
||||
|
||||
const HEADER_SIZE = 2;
|
||||
const CAR_SIZE = 158; // Size of car_telemetry struct
|
||||
|
||||
while (this.buffer.length >= HEADER_SIZE) {
|
||||
const server_id = this.buffer.readUInt8(0);
|
||||
const car_count = this.buffer.readUInt8(1);
|
||||
|
||||
const expected_size = HEADER_SIZE + (car_count * CAR_SIZE);
|
||||
|
||||
if (this.buffer.length < expected_size) {
|
||||
// Not enough data yet, wait for more
|
||||
break;
|
||||
}
|
||||
|
||||
// Parse the packet
|
||||
const packet: TelemetryPacket = {
|
||||
server_id,
|
||||
car_count,
|
||||
cars: [],
|
||||
};
|
||||
|
||||
let offset = HEADER_SIZE;
|
||||
|
||||
for (let i = 0; i < car_count; i++) {
|
||||
const carID = this.buffer.readUInt8(offset);
|
||||
offset += 1;
|
||||
|
||||
const driver_name = this.buffer.toString('utf8', offset, offset + 64).replace(/\0.*$/g, '');
|
||||
offset += 64;
|
||||
|
||||
const driver_guid = this.buffer.toString('utf8', offset, offset + 64).replace(/\0.*$/g, '');
|
||||
offset += 64;
|
||||
|
||||
const car_model = this.buffer.toString('utf8', offset, offset + 64).replace(/\0.*$/g, '');
|
||||
offset += 64;
|
||||
|
||||
const normalizedSplinePos = this.buffer.readFloatLE(offset);
|
||||
offset += 4;
|
||||
|
||||
const speed_kmh = this.buffer.readFloatLE(offset);
|
||||
offset += 4;
|
||||
|
||||
const gear = this.buffer.readUInt8(offset);
|
||||
offset += 1;
|
||||
|
||||
const rpm = this.buffer.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
|
||||
const last_lap_time = this.buffer.readUInt32LE(offset);
|
||||
offset += 4;
|
||||
|
||||
const best_lap_time = this.buffer.readUInt32LE(offset);
|
||||
offset += 4;
|
||||
|
||||
const current_lap = this.buffer.readUInt16LE(offset);
|
||||
offset += 2;
|
||||
|
||||
const position = this.buffer.readUInt8(offset);
|
||||
offset += 1;
|
||||
|
||||
packet.cars.push({
|
||||
carID,
|
||||
driver_name,
|
||||
driver_guid,
|
||||
car_model,
|
||||
normalizedSplinePos,
|
||||
speed_kmh,
|
||||
gear,
|
||||
rpm,
|
||||
last_lap_time,
|
||||
best_lap_time,
|
||||
current_lap,
|
||||
position,
|
||||
});
|
||||
}
|
||||
|
||||
// Emit packet to all callbacks
|
||||
this.callbacks.forEach(cb => cb(packet));
|
||||
|
||||
// Remove processed packet from buffer
|
||||
this.buffer = this.buffer.subarray(expected_size);
|
||||
}
|
||||
}
|
||||
|
||||
public subscribe(callback: TelemetryCallback): () => void {
|
||||
this.callbacks.add(callback);
|
||||
console.log('[Telemetry] Subscriber added, total:', this.callbacks.size);
|
||||
|
||||
// Return unsubscribe function
|
||||
return () => {
|
||||
this.callbacks.delete(callback);
|
||||
console.log('[Telemetry] Subscriber removed, total:', this.callbacks.size);
|
||||
};
|
||||
}
|
||||
|
||||
public isConnected(): boolean {
|
||||
return this.connected;
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (this.reconnectTimeout) {
|
||||
clearTimeout(this.reconnectTimeout);
|
||||
}
|
||||
if (this.socket) {
|
||||
this.socket.destroy();
|
||||
this.socket = null;
|
||||
}
|
||||
this.connected = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
let bridge: TelemetryBridge | null = null;
|
||||
|
||||
export function getTelemetryBridge(): TelemetryBridge {
|
||||
if (!bridge) {
|
||||
bridge = new TelemetryBridge();
|
||||
}
|
||||
return bridge;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// lib/trackUtils.ts
|
||||
// Utility functions for track name handling
|
||||
|
||||
/**
|
||||
* Clean track name from database format
|
||||
* Example: "csp/2000/../jr_road_atlanta_2022" -> "jr_road_atlanta_2022"
|
||||
*/
|
||||
export function cleanTrackName(trackName: string): string {
|
||||
if (!trackName) return '';
|
||||
|
||||
// Split by '/' and get the last segment
|
||||
const segments = trackName.split('/');
|
||||
const lastSegment = segments[segments.length - 1];
|
||||
|
||||
return lastSegment.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean track config name
|
||||
* Example: "csp/2000/../full" -> "full"
|
||||
*/
|
||||
export function cleanTrackConfig(configName: string): string {
|
||||
if (!configName) return '';
|
||||
|
||||
// Split by '/' and get the last segment
|
||||
const segments = configName.split('/');
|
||||
const lastSegment = segments[segments.length - 1];
|
||||
|
||||
return lastSegment.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format track name for display
|
||||
* Example: "jr_road_atlanta_2022" -> "JR Road Atlanta 2022"
|
||||
*/
|
||||
export function formatTrackName(trackName: string): string {
|
||||
return cleanTrackName(trackName)
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, char => char.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get track map URL
|
||||
* Handles the track name cleaning and URL construction
|
||||
*/
|
||||
export function getTrackMapUrl(trackName: string, trackConfig?: string): string {
|
||||
const baseUrl = 'https://openwheels.racing/files/img/tracks';
|
||||
const cleanedTrack = cleanTrackName(trackName);
|
||||
const cleanedConfig = trackConfig ? cleanTrackConfig(trackConfig) : '';
|
||||
|
||||
// Try with config first, fallback to map.png if no config or config is default
|
||||
if (cleanedConfig && cleanedConfig !== 'default' && cleanedConfig !== '') {
|
||||
return `${baseUrl}/${cleanedTrack}/${cleanedConfig}/map.png`;
|
||||
}
|
||||
|
||||
return `${baseUrl}/${cleanedTrack}/map.png`;
|
||||
}
|
||||
Reference in New Issue
Block a user