// lib/trackMapConfig.ts // Parse and use AC track map.ini configuration files export interface TrackMapConfig { width: number; height: number; margin: number; scaleFactor: number; xOffset: number; zOffset: number; drawingSize: number; } // Cache for parsed configs const configCache = new Map(); /** * Clean track name from database format (removes CSP prefix) * Example: "csp/2100/../spa" -> "spa" */ export function cleanTrackPath(track: string): string { // Remove CSP prefix pattern: csp/XXXX/../ const cleaned = track.replace(/^csp\/\d+\/\.\.\//, ''); return cleaned; } /** * Fetch and parse a track's map.ini file */ export async function getTrackMapConfig( track: string, trackConfig: string = '' ): Promise { // Clean track name first const cleanTrack = cleanTrackPath(track); const cleanConfig = trackConfig || ''; const cacheKey = `${cleanTrack}|${cleanConfig}`; // Check cache first if (configCache.has(cacheKey)) { return configCache.get(cacheKey)!; } try { // Try to fetch from openwheels.racing first let configUrl = cleanConfig && cleanConfig !== 'default' ? `https://openwheels.racing/files/img/tracks/${cleanTrack}/${cleanConfig}/map.ini` : `https://openwheels.racing/files/img/tracks/${cleanTrack}/map.ini`; let response = await fetch(configUrl); // Fallback to local public directory if (!response.ok) { const localPath = cleanConfig && cleanConfig !== 'default' ? `/tracks/${cleanTrack}/${cleanConfig}/map.ini` : `/tracks/${cleanTrack}/map.ini`; response = await fetch(localPath); } if (!response.ok) { console.log(`[TrackMap] No map.ini found for ${cleanTrack}/${cleanConfig}`); configCache.set(cacheKey, null); return null; } const iniText = await response.text(); const config = parseMapIni(iniText); console.log(`[TrackMap] Loaded config for ${cleanTrack}/${cleanConfig}:`, config); configCache.set(cacheKey, config); return config; } catch (error) { console.warn(`[TrackMap] Failed to load map.ini for ${cleanTrack}:`, error); configCache.set(cacheKey, null); return null; } } /** * Parse map.ini text format */ function parseMapIni(iniText: string): TrackMapConfig { const lines = iniText.split('\n'); const config: Partial = {}; for (const line of lines) { const trimmed = line.trim(); // Skip comments and empty lines if (!trimmed || trimmed.startsWith(';') || trimmed.startsWith('[')) { continue; } // Parse KEY=VALUE const [key, value] = trimmed.split('=').map(s => s.trim()); if (!key || !value) continue; const numValue = parseFloat(value); switch (key.toUpperCase()) { case 'WIDTH': config.width = numValue; break; case 'HEIGHT': config.height = numValue; break; case 'MARGIN': config.margin = numValue; break; case 'SCALE_FACTOR': config.scaleFactor = numValue; break; case 'X_OFFSET': config.xOffset = numValue; break; case 'Z_OFFSET': config.zOffset = numValue; break; case 'DRAWING_SIZE': config.drawingSize = numValue; break; } } // Return with defaults if any values are missing return { width: config.width || 1000, height: config.height || 1000, margin: config.margin || 20, scaleFactor: config.scaleFactor || 1.0, xOffset: config.xOffset || 0, zOffset: config.zOffset || 0, drawingSize: config.drawingSize || 10, }; } export function worldToMapCoords( worldX: number, worldY: number, worldZ: number, config: TrackMapConfig ): { x: number; y: number } { var aspectRatio = config.width / config.height; // Add offsets to player position console.log("config" , config); console.log("aspectRatio:", aspectRatio); if (aspectRatio < 1) { worldX = worldX * aspectRatio; } else { worldZ = worldZ / aspectRatio; } var x = (worldX) + config.xOffset; var y = (worldZ) + config.zOffset; y /= config.scaleFactor; x /= config.scaleFactor; // Percentages x = (x * 100) / (config.width); y = (y * 100) / (config.height); return { x, y }; }