// EM:RP Launcher Runtime Map Writer - Step 6C // Normal 411/Infernus must stay stock unless that exact SQL vehicle has custom metadata. // This helper is safe to import from Electron code, but the current launcher C# agent also // performs the same normalization in Core/CustomVehicleAssets.cs. const fs = require('fs'); const path = require('path'); function n(...values) { for (const value of values) { const parsed = Number(value || 0); if (Number.isFinite(parsed) && parsed > 0) return parsed; } return 0; } function s(...values) { for (const value of values) { const text = String(value || '').trim(); if (text) return text; } return ''; } function pickLiveSampVehicleId(raw, dbVehicleId, serverVehicleSlot, ordinal) { const candidates = [ raw.samp_vehicle_id, raw.samp_id, raw.vrid, raw.vrID, raw.live_vehicle_id, raw.runtime_vehicle_id, raw.vehicleid, raw.vehicle_id ]; for (const value of candidates) { const parsed = Number(value || 0); if (!Number.isFinite(parsed) || parsed <= 0) continue; // Reject array index fallback, e.g. SQL 758 row becomes samp_vehicle_id=1. if (parsed === ordinal && dbVehicleId !== ordinal && serverVehicleSlot !== ordinal) continue; return parsed; } return 0; } function normalizeRawBinding(raw, ordinal) { const dbVehicleId = n(raw.db_vehicle_id, raw.sql_vehicle_id, raw.vehicle_sql_id, raw.dbid, raw.VID); const serverVehicleSlot = n(raw.server_vehicle_slot, raw.vehicle_slot, raw.server_slot, raw.slot, dbVehicleId); const sampVehicleId = pickLiveSampVehicleId(raw, dbVehicleId, serverVehicleSlot, ordinal); const fallbackModel = n(raw.fallback_model, raw.base_model, raw.fallback, raw.model, raw.cartype, raw.CarType); const customModelId = n(raw.custom_model_id, raw.model_id, raw.launcher_model_id, raw.custom, raw.custom_model, raw.modeltype, raw.Modeltype); const assetKey = s(raw.asset_key, raw.asset, raw.custom_asset_key, raw.custom_vehicle_asset); return { db_vehicle_id: dbVehicleId, server_vehicle_slot: serverVehicleSlot, samp_vehicle_id: sampVehicleId, fallback_model: fallbackModel, custom_model_id: customModelId, asset_key: assetKey, display_name: s(raw.display_name, raw.name, raw.vehicle_name, assetKey), is_custom: 1, normal_411_policy: 'stock_unless_custom_metadata' }; } function isValidCustomVehicleBinding(v, ordinal) { if (v.db_vehicle_id <= 0) return false; if (v.server_vehicle_slot <= 0) return false; if (v.samp_vehicle_id <= 0) return false; if (v.samp_vehicle_id === ordinal && v.db_vehicle_id !== ordinal && v.server_vehicle_slot !== ordinal) return false; if (v.fallback_model < 400 || v.fallback_model > 611) return false; if (!v.asset_key) return false; return true; } function normalizeRuntimeMap(apiJson) { const input = Array.isArray(apiJson?.vehicles) ? apiJson.vehicles : (Array.isArray(apiJson?.bindings) ? apiJson.bindings : (Array.isArray(apiJson?.vehicle_bindings) ? apiJson.vehicle_bindings : (Array.isArray(apiJson?.data) ? apiJson.data : []))); const vehicles = []; const skipped = []; input.forEach((raw, index) => { const ordinal = index + 1; const v = normalizeRawBinding(raw || {}, ordinal); if (!isValidCustomVehicleBinding(v, ordinal)) { skipped.push({ ...v, ordinal, reason: 'not_active_custom_vehicle_or_missing_real_live_samp_id' }); return; } vehicles.push(v); }); return { step: '6C', generated_at: new Date().toISOString(), generated_by: 'electron_runtime_map_preserve_live_samp_id', normal_411_policy: 'stock_unless_custom_metadata', bindings_total: vehicles.length, skipped_total: skipped.length, vehicles, bindings: vehicles, skipped }; } function writeRuntimeMapAtomic(targetFile, apiJson) { const runtimeMap = normalizeRuntimeMap(apiJson); fs.mkdirSync(path.dirname(targetFile), { recursive: true }); const tmp = `${targetFile}.tmp`; fs.writeFileSync(tmp, JSON.stringify(runtimeMap, null, 2), 'utf8'); fs.renameSync(tmp, targetFile); return runtimeMap; } module.exports = { normalizeRuntimeMap, writeRuntimeMapAtomic, isValidCustomVehicleBinding, pickLiveSampVehicleId };