-- This Source Code Form is subject to the terms of the bCDDL, v. 1.1. -- If a copy of the bCDDL was not distributed with this -- file, You can obtain one at http://beamng.com/bCDDL-1.1.txt local M = {} M.dependencies = { 'career_career', "career_modules_log", "render_renderViews", "util_screenshotCreator" } local dateUtils = require('utils/dateUtils') local minimumVersion = 42 local defaultVehicle = { model = "covet", config = "DXi_M" } local xVec, yVec, zVec = vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1) local saveAnyVehiclePosDEBUG = false local slotAmount = 20 local vehicles = {} local dirtiedVehicles = {} local vehIdToInventoryId = {} local inventoryIdToVehId = {} local currentVehicle local lastVehicle local favoriteVehicle local sellAllVehicles local loadedVehiclesLocations local unicycleSavedPosition local vehicleToEnterId local vehiclesMovedToStorage local loanedVehicleReturned local function getClosestGarage(pos) local facilities = freeroam_facilities.getFacilities(getCurrentLevelIdentifier()) local playerPos = pos or getPlayerVehicle(0):getPosition() local closestGarage local minDist = math.huge for _, garage in ipairs(facilities.garages) do local zones = freeroam_facilities.getZonesForFacility(garage) if zones and not tableIsEmpty(zones) then local dist = zones[1].center:distance(playerPos) if dist < minDist then closestGarage = garage minDist = dist end end end return closestGarage end local function onExtensionLoaded() if not career_career.isActive() then return false end -- load from saveslot local saveSlot, savePath = career_saveSystem.getCurrentSaveSlot() if not saveSlot or not savePath then return end table.clear(vehicles) local saveInfo = jsonReadFile(savePath .. "/info.json") if not saveInfo or saveInfo.version < minimumVersion then return end -- load the vehicles local files = FS:findFiles(savePath .. "/career/vehicles/", '*.json', 0, false, false) for i = 1, tableSize(files) do local vehicleData = jsonReadFile(files[i]) vehicleData.partConditions = deserialize(vehicleData.partConditions) if vehicleData.timeToAccess then vehicleData.timeToAccess = vehicleData.timeToAccess - dateUtils.timeSince(saveInfo.date) if vehicleData.timeToAccess <= 0 then vehicleData.timeToAccess = nil vehicleData.delayReason = nil end end vehicles[vehicleData.id] = vehicleData if tableIsEmpty(core_vehicles.getModel(vehicleData.model)) or not FS:fileExists(vehicleData.config.partConfigFilename) then vehicleData.missingFile = true end end local inventoryData = jsonReadFile(savePath .. "/career/inventory.json") -- Sell all vehicles when the save version is not the newest one if saveInfo.version < career_saveSystem.getSaveSystemVersion() then sellAllVehicles = true if inventoryData then inventoryData.currentVehicle = nil inventoryData.lastVehicle = nil inventoryData.spawnedPlayerVehicles = nil end end if inventoryData then vehicleToEnterId = tonumber(inventoryData.currentVehicle) lastVehicle = tonumber(inventoryData.lastVehicle) favoriteVehicle = tonumber(inventoryData.favoriteVehicle) if inventoryData.spawnedPlayerVehicles then loadedVehiclesLocations = {} for inventoryId, transform in pairs(inventoryData.spawnedPlayerVehicles) do inventoryId = tonumber(inventoryId) if not saveAnyVehiclePosDEBUG then transform.option = "garage" end loadedVehiclesLocations[inventoryId] = transform end else loadedVehiclesLocations = nil -- will force spawning at garage end -- if the last currentVehicle is not spawned, then dont enter it if not (loadedVehiclesLocations and loadedVehiclesLocations[vehicleToEnterId]) then vehicleToEnterId = nil end unicycleSavedPosition = inventoryData.unicyclePos end end local function updateVehicleThumbnail(inventoryId, filename, callback) local vehId = M.getVehicleIdFromInventoryId(inventoryId) if not vehId then return end local vehObj = getObjectByID(vehId) local bb = vehObj:getSpawnWorldOOBB() local bbCenter = bb:getCenter() local resolution = vec3(500, 281, 0) local fov = 50 local nearPlane = 0.1 local camPos = util_screenshotCreator.frameVehicle(vehObj, fov, nearPlane, resolution.x / resolution.y) local options = { pos = camPos, rot = quatFromDir(bbCenter - camPos), filename = filename, renderViewName = "careerVehicleRenderView" .. inventoryId, resolution = resolution, fov = fov, nearPlane = nearPlane, screenshotDelay = 0.5 } render_renderViews.takeScreenshot(options, callback) end local function setVehicleDirty(inventoryId) dirtiedVehicles[inventoryId] = true end local function updatePartConditionsOfSpawnedVehicles(callback) local callbackCounter = 0 for vehId, inventoryId in pairs(vehIdToInventoryId) do setVehicleDirty(inventoryId) -- update part conditions and call the callback when all vehicles have been processed M.updatePartConditions(vehId, inventoryId, callback and function() callbackCounter = callbackCounter + 1 if callbackCounter >= tableSize(vehIdToInventoryId) then callback() end end) end if callback and tableIsEmpty(vehIdToInventoryId) then callback() end end local extensionName = "inventory" local function inventorySaveFinished(currentSavePath, oldSaveDate) -- if there are more async saving steps waiting for the vehicle save to finish, we need to call registerAsyncSaveExtension inside their onVehicleSaveFinished function first extensions.hook("onVehicleSaveFinished", currentSavePath, oldSaveDate) career_saveSystem.asyncSaveExtensionFinished(extensionName) guihooks.trigger("saveFinished") end local function onSaveCurrentSaveSlotAsyncStart() career_saveSystem.registerAsyncSaveExtension(extensionName) end local finishedSaveTasks = {} local function checkSaveFinished(currentSavePath, oldSaveDate) for _, fin in pairs(finishedSaveTasks) do if not fin then return --not finished end end inventorySaveFinished(currentSavePath, oldSaveDate) end local function saveVehiclesData(currentSavePath, oldSaveDate, vehiclesThumbnailUpdate) local vehiclesCopy = deepcopy(vehicles) local currentDate = os.date("!%Y-%m-%dT%H:%M:%SZ") for id, vehicle in pairs(vehiclesCopy) do if dirtiedVehicles[id] or not vehicle.dirtyDate then vehicles[id].dirtyDate = currentDate vehicle.dirtyDate = currentDate dirtiedVehicles[id] = nil end if (vehicle.dirtyDate > oldSaveDate) then vehicle.partConditions = serialize(vehicle.partConditions) local thumbnailFilename = currentSavePath .. "/career/vehicles/" .. id .. ".png" if vehiclesThumbnailUpdate and tableContains(vehiclesThumbnailUpdate, id) and inventoryIdToVehId[id] then finishedSaveTasks["thumbnail" .. id] = false updateVehicleThumbnail(id, thumbnailFilename, function() finishedSaveTasks["thumbnail" .. id] = true checkSaveFinished(currentSavePath, oldSaveDate) end) vehicle.defaultThumbnail = nil vehicles[id].defaultThumbnail = nil elseif not vehicle.defaultThumbnail then local _, oldSavePath = career_saveSystem.getCurrentSaveSlot() FS:copyFile(oldSavePath .. "/career/vehicles/" .. id .. ".png", thumbnailFilename) end career_saveSystem.jsonWriteFileSafe(currentSavePath .. "/career/vehicles/" .. id .. ".json", vehicle, true) end end if currentVehicle then dirtiedVehicles[currentVehicle] = true end -- Remove vehicle files for vehicles that have been deleted local files = FS:findFiles(currentSavePath .. "/career/vehicles/", '*.json', 0, false, false) for i = 1, tableSize(files) do local dir, filename, ext = path.split(files[i]) local fileNameNoExt = string.sub(filename, 1, -6) local inventoryId = tonumber(fileNameNoExt) if not vehicles[inventoryId] then FS:removeFile(dir .. filename) FS:removeFile(dir .. inventoryId .. ".png") end end end -- TODO update a vehicles part conditions in the table when you exit a vehicle local function onSaveCurrentSaveSlot(currentSavePath, oldSaveDate, vehiclesThumbnailUpdate) local data = {} data.currentVehicle = currentVehicle data.lastVehicle = lastVehicle data.favoriteVehicle = favoriteVehicle data.spawnedPlayerVehicles = {} for inventoryId, vehId in pairs(inventoryIdToVehId) do local veh = getObjectByID(vehId) if veh then data.spawnedPlayerVehicles[inventoryId] = { pos = veh:getPosition(), rot = quat(0, 0, 1, 0) * quat(veh:getRefNodeRotation()) } end end if gameplay_walk.isWalking() then local playerVeh = getPlayerVehicle(0) data.unicyclePos = playerVeh:getPosition() end table.clear(finishedSaveTasks) finishedSaveTasks.updatePartConditions = false updatePartConditionsOfSpawnedVehicles(function() saveVehiclesData(currentSavePath, oldSaveDate, vehiclesThumbnailUpdate) finishedSaveTasks.updatePartConditions = true checkSaveFinished(currentSavePath, oldSaveDate) end) career_saveSystem.jsonWriteFileSafe(currentSavePath .. "/career/inventory.json", data, true) end local function assignInventoryIdToVehId(inventoryId, vehId) if vehIdToInventoryId[vehId] then inventoryIdToVehId[vehIdToInventoryId[vehId]] = nil end vehIdToInventoryId[vehId] = inventoryId inventoryIdToVehId[inventoryId] = vehId end local function getNumberOfFreeSlots() local ownedVehiclesAmount = 0 for inventoryId, vehicle in pairs(vehicles) do if vehicle.owned and not vehicle.takesNoInventorySpace then ownedVehiclesAmount = ownedVehiclesAmount + 1 end end return slotAmount - ownedVehiclesAmount end local function hasFreeSlot() return getNumberOfFreeSlots() > 0 end local inventoryIdAfterUpdatingPartConditions local function addVehicle(vehId, inventoryId, options) options = options or {} if options.owned == nil then options.owned = true end local vehicle = scenetree.findObjectById(vehId) local vehicleData = core_vehicle_manager.getVehicleData(vehId) if vehicle and vehicleData then if not inventoryId then inventoryId = 1 while vehicles[inventoryId] do inventoryId = inventoryId + 1 end end local niceName if vehicleData.vdata and vehicleData.vdata.information then niceName = vehicleData.vdata.information.name end vehicles[inventoryId] = vehicles[inventoryId] or {} vehicles[inventoryId].model = vehicle.JBeam or "" vehicles[inventoryId].config = vehicleData.config vehicles[inventoryId].id = inventoryId vehicles[inventoryId].niceName = niceName vehicles[inventoryId].config.licenseName = core_vehicles.getVehicleLicenseText(vehicle) vehicles[inventoryId].owned = options.owned vehicles[inventoryId].defaultThumbnail = true if vehicle.JBeam and vehicleData.config and vehicleData.config.partConfigFilename then local dir, configName, ext = path.splitWithoutExt(vehicleData.config.partConfigFilename) local baseConfig = core_vehicles.getConfig(vehicle.JBeam, configName) vehicles[inventoryId].configBaseValue = baseConfig.Value vehicles[inventoryId].takesNoInventorySpace = baseConfig.takesNoInventorySpace else log("D", "", "Couldnt find base value for added vehicle, so using default value") vehicles[inventoryId].configBaseValue = 1000 end assignInventoryIdToVehId(inventoryId, vehId) inventoryIdAfterUpdatingPartConditions = inventoryId vehicle:queueLuaCommand(string.format( "if not partCondition.getConditions() then partCondition.initConditions() end obj:queueGameEngineLua('career_modules_inventory.updatePartConditions(%d, %d)')", vehId, inventoryId)) if tableSize(vehicles) == 1 then M.setFavoriteVehicle(inventoryId) end return inventoryId end end local skipPartConditionsBeforeWalking local function removeVehicleObject(inventoryId, skipPartConditions) if currentVehicle == inventoryId then skipPartConditionsBeforeWalking = true gameplay_walk.setWalkingMode(true, nil, nil, true) end extensions.hook("onInventoryPreRemoveVehicleObject", inventoryId, M.getVehicleIdFromInventoryId(inventoryId)) -- TODO save part conditions local vehId = inventoryIdToVehId[inventoryId] if vehId then local obj = getObjectByID(vehId) if obj then obj:delete() end vehIdToInventoryId[vehId] = nil end inventoryIdToVehId[inventoryId] = nil end local function removeVehicle(inventoryId) removeVehicleObject(inventoryId) vehicles[inventoryId] = nil extensions.hook("onVehicleRemoved", inventoryId) if favoriteVehicle == inventoryId then M.setFavoriteVehicle(next(vehicles)) end end local function onPartConditionsUpdateFinished() if inventoryIdAfterUpdatingPartConditions then extensions.hook("onVehicleAdded", inventoryIdAfterUpdatingPartConditions) inventoryIdAfterUpdatingPartConditions = nil end end local function getPartConditionsCallback(partConditions, inventoryId) vehicles[inventoryId].partConditions = partConditions onPartConditionsUpdateFinished() career_modules_partInventory.updatePartConditionsInInventory() end local function updatePartConditions(vehId, inventoryId, callback) local veh if vehId then veh = getObjectByID(vehId) else veh = getObjectByID(inventoryIdToVehId[inventoryId]) end if not veh then log("E", "", "Couldnt find vehicle object to get part conditions") return end core_vehicleBridge.requestValue( veh, function(res) getPartConditionsCallback(res.result, inventoryId) if callback then callback() end end, 'getPartConditions' ) end local function applyPartConditions(inventoryId, vehId) local veh = scenetree.findObjectById(vehId or inventoryIdToVehId[inventoryId]) if not veh then return end core_vehicleBridge.executeAction(veh, 'initPartConditions', vehicles[inventoryId].partConditions) end -- replaceOption 1: replace the current vehicle object -- replaceOption 2: replace the vehicle object with the same inventoryId local function spawnVehicle(inventoryId, replaceOption, callback) local vehInfo = vehicles[inventoryId] local carConfigToLoad = vehInfo.config local carModelToLoad = vehInfo.model if carConfigToLoad then -- if the vehicle doesnt exist (deleted mod) then dont spawn if tableIsEmpty(core_vehicles.getModel(carModelToLoad)) or not FS:fileExists(vehInfo.config.partConfigFilename) then return end local vehObj local vehicleData = {} vehicleData.config = carConfigToLoad vehicleData.keepOtherVehRotation = true core_vehicle_manager.queueAdditionalVehicleData({ spawnWithEngineRunning = false }) if replaceOption == 1 then vehObj = core_vehicles.replaceVehicle(carModelToLoad, vehicleData) elseif replaceOption == 2 then -- check if the vehicle object with the same inventoryId exists and then replace it specifically local oldVehId = inventoryIdToVehId[inventoryId] local oldVehObj if oldVehId then oldVehObj = getObjectByID(oldVehId) end vehObj = core_vehicles.replaceVehicle(carModelToLoad, vehicleData, oldVehObj) else vehicleData.autoEnterVehicle = false vehObj = core_vehicles.spawnNewVehicle(carModelToLoad, vehicleData) end assignInventoryIdToVehId(inventoryId, vehObj:getID()) local numberOfBrokenParts = career_modules_valueCalculator.getNumberOfBrokenParts(vehInfo.partConditions) if numberOfBrokenParts > 0 and numberOfBrokenParts < career_modules_valueCalculator.getBrokenPartsThreshold() then career_modules_insurance_insurance.repairPartConditions({ partConditions = vehInfo.partConditions }) end if vehInfo.partConditions then core_vehicleBridge.executeAction(vehObj, 'initPartConditions', vehInfo.partConditions, 0, 1, 1) if callback then core_vehicleBridge.requestValue(vehObj, callback, 'ping') end else core_vehicleBridge.executeAction(vehObj, 'initPartConditions', {}, 0, 1, 1) core_vehicleBridge.requestValue(vehObj, function(res) career_modules_inventory.updatePartConditions(nil, inventoryId, callback) end, 'ping') end gameplay_walk.removeVehicleFromBlacklist(vehObj:getId()) return vehObj end end -- loadOption 1: dont reload the vehicle -- loadOption 2: force reload the vehicle local enterCallbackFunction local function enterVehicleActual(id, loadOption) if not id or loadOption == 1 then currentVehicle = id elseif inventoryIdToVehId[id] and loadOption ~= 2 then -- vehicle is already spawned. enter it gameplay_walk.setWalkingMode(false, nil, nil, true) be:enterVehicle(0, getObjectByID(inventoryIdToVehId[id])) currentVehicle = id else if spawnVehicle(id, 1, enterCallbackFunction) then currentVehicle = id end enterCallbackFunction = nil end if currentVehicle then dirtiedVehicles[currentVehicle] = true end extensions.hook("onEnterVehicleFinished", currentVehicle) if enterCallbackFunction then enterCallbackFunction() end end -- loadOption 1: dont reload the vehicle -- loadOption 2: force reload the vehicle local function enterVehicle(newInventoryId, loadOption, callback) local vehInfo = vehicles[newInventoryId] if vehInfo and vehInfo.timeToAccess then return end career_modules_log.addLog(string.format("Enter vehicle %s", newInventoryId or "no vehicle"), "inventory") enterCallbackFunction = callback if loadOption == 1 then enterVehicleActual(newInventoryId, loadOption) return end if currentVehicle then updatePartConditionsOfSpawnedVehicles(function() enterVehicleActual(newInventoryId, loadOption) end) else enterVehicleActual(newInventoryId, loadOption) end end local saveCareer local function setupInventory() if career_modules_linearTutorial.getLinearStep() == -1 then if loadedVehiclesLocations then local vehiclesToTeleportToGarage = {} for inventoryId, location in pairs(loadedVehiclesLocations) do local vehInfo = vehicles[inventoryId] if vehInfo.loanType == "work" then career_modules_loanerVehicles.returnVehicle(inventoryId) loanedVehicleReturned = true else if career_modules_insurance_insurance.inventoryVehNeedsRepair(inventoryId) then vehiclesMovedToStorage = true else local veh = spawnVehicle(inventoryId) if veh then if location.option == "garage" then location.vehId = veh:getID() vehiclesToTeleportToGarage[inventoryId] = location end spawn.safeTeleport(veh, location.pos, location.rot) end end end end loadedVehiclesLocations = nil -- The teleport to garage needs to happen with one frame delay because that's when the OOBBs get updated extensions.core_jobsystem.create( function(job) for inventoryId, location in pairs(vehiclesToTeleportToGarage) do local veh = getObjectByID(location.vehId) local garage = getClosestGarage(location.pos) freeroam_facilities.teleportToGarage(garage.id, veh) job.sleep(0.1) end end ) end if vehicleToEnterId and inventoryIdToVehId[vehicleToEnterId] then enterVehicle(vehicleToEnterId) else gameplay_walk.setWalkingMode(true) end end local saveSlot, savePath = career_saveSystem.getCurrentSaveSlot() local data = jsonReadFile(savePath .. "/info.json") if not data then -- this means this is a new career save saveCareer = 0 if career_modules_linearTutorial.getLinearStep() == -1 then -- default placement is in front of the dealership, facing it spawn.safeTeleport(getPlayerVehicle(0), vec3(838.51, -522.42, 165.75)) gameplay_walk.setRot(vec3(-1, -1, 0), vec3(0, 0, 1)) else -- spawn the tutorial vehicle local model, config = "covet", "vehicles/covet/covet_tutorial.pc" local pos, rot = vec3(-24.026, 609.157, 75.112), quatFromDir(vec3(1, 0, 0)) local options = { config = config, licenseText = "TUTORIAL", vehicleName = "TutorialVehicle", pos = pos, rot = rot } local spawningOptions = sanitizeVehicleSpawnOptions(model, options) spawningOptions.autoEnterVehicle = false local veh = core_vehicles.spawnNewVehicle(model, spawningOptions) core_vehicleBridge.executeAction(veh, 'setIgnitionLevel', 0) gameplay_walk.setWalkingMode(true) -- move walking character into position spawn.safeTeleport(getPlayerVehicle(0), vec3(-20.746, 598.736, 75.112)) gameplay_walk.setRot(vec3(0, 1, 0), vec3(0, 0, 1)) end else if gameplay_walk.isWalking() then if unicycleSavedPosition then spawn.safeTeleport(getPlayerVehicle(0), unicycleSavedPosition) else freeroam_facilities.teleportToGarage("servicestationGarage", getPlayerVehicle(0)) end end end commands.setGameCamera() end local function onCareerModulesActivated(alreadyInLevel) if sellAllVehicles then for inventoryId, vehicle in pairs(vehicles) do if vehicle.owned then M.sellVehicle(inventoryId) elseif vehicle.loanType == "work" then career_modules_loanerVehicles.returnVehicle(inventoryId) loanedVehicleReturned = true else M.removeVehicle(inventoryId) end end sellAllVehicles = nil end if alreadyInLevel then setupInventory() end end local function onClientStartMission(levelPath) setupInventory() end local function setPartConditionResetSnapshot(veh, callback) core_vehicleBridge.executeAction(veh, 'createPartConditionSnapshot', "beforeReset") core_vehicleBridge.executeAction(veh, 'setPartConditionResetSnapshotKey', "beforeReset") if callback then core_vehicleBridge.requestValue(veh, callback, "ping") end end local function onBigMapActivated() if currentVehicle then setPartConditionResetSnapshot(getPlayerVehicle(0)) end end local function teleportedFromBigmap() if currentVehicle and career_career.isAutosaveEnabled() then career_saveSystem.saveCurrent() end end local function getCurrentVehicle() return currentVehicle end local function getLastVehicle() return lastVehicle end local function getVehicleIdFromInventoryId(inventoryId) if inventoryId then return inventoryIdToVehId[inventoryId] end end local function getInventoryIdFromVehicleId(vehId) if vehId then return vehIdToInventoryId[vehId] end end local function getMapInventoryIdToVehId() return inventoryIdToVehId end local function getCurrentVehicleId() return getVehicleIdFromInventoryId(currentVehicle) end local function isSeatedInsideOwnedVehicle() return currentVehicle and true or false end local function getVehiclesInGarage(garage, intersecting) local zones = freeroam_facilities.getZonesForFacility(garage) local spawnedVehicles = {} local res = {} for inventoryId, vehId in pairs(inventoryIdToVehId) do spawnedVehicles[inventoryId] = getObjectByID(vehId) end for _, zone in ipairs(zones) do for inventoryId, veh in pairs(spawnedVehicles) do if intersecting then local vehBB = veh:getWorldBox() local vehBBExtents = vehBB:getExtents() * 0.5 local vehPos = veh:getPosition() local zoneExtents = vec3(zone.aabb.xMax - zone.aabb.xMin, zone.aabb.yMax - zone.aabb.yMin, zone.aabb.zMax - zone.aabb.zMin) zoneExtents.z = math.min(zoneExtents.z, 10000) if overlapsOBB_OBB(vehBB:getCenter(), xVec * vehBBExtents.x, yVec * vehBBExtents.y, zVec * vehBBExtents.z, zone.center, xVec * zoneExtents.x / 2, yVec * zoneExtents.y / 2, zVec * zoneExtents.z / 2) then for nodeId = 0, veh:getNodeCount() - 1 do if zone:containsPoint2D(veh:getNodePosition(nodeId) + vehPos) then res[inventoryId] = true break end end end elseif zone:containsVehicle(veh) then res[inventoryId] = true end end end return res end local function removeVehiclesFromGarageExcept(inventoryId) local garage = getClosestGarage() local inventoryIdsInGarage = getVehiclesInGarage(garage, true) for otherInventoryId, _ in pairs(inventoryIdsInGarage) do if otherInventoryId ~= inventoryId then local vehInfo = vehicles[otherInventoryId] if vehInfo.owned then M.removeVehicleObject(otherInventoryId) end end end end local function getDefaultVehicleThumb(vehInfo) local model = core_vehicles.getModel(vehInfo.model) if not model then return nil end local _, configKey = path.splitWithoutExt(vehInfo.config.partConfigFilename) local config = model.configs[configKey] if not config then return nil end return config.preview end local function getVehicleThumbnail(inventoryId) if not inventoryId then return end local vehicle = vehicles[inventoryId] if not vehicle then return end local _, savePath = career_saveSystem.getCurrentSaveSlot() local thumbnailPath = savePath .. "/career/vehicles/" .. inventoryId .. ".png" if not vehicle.defaultThumbnail and FS:fileExists(thumbnailPath) then return thumbnailPath else return getDefaultVehicleThumb(vehicle) end end local originComputerId local menuIsOpen local buttonsActive = {} local chooseButtonsData = {} local menuHeader local function processPerformanceData(performanceData) if not performanceData then return end career_modules_vehiclePerformance.addScoresToPerformanceData(performanceData) -- Process drivetrain information if performanceData.powertrainLayout then local frontWheelDrive = performanceData.powertrainLayout.poweredWheelsFront > 0 local rearWheelDrive = performanceData.powertrainLayout.poweredWheelsRear > 0 performanceData.drivetrain = frontWheelDrive and rearWheelDrive and "AWD" or frontWheelDrive and "FWD" or rearWheelDrive and "RWD" or "Unknown" end -- Process fuel type information if performanceData.fuelType then if performanceData.fuelType["fuelTank:gasoline"] then performanceData.fuelType = "Gasoline" elseif performanceData.fuelType["fuelTank:diesel"] then performanceData.fuelType = "Diesel" elseif performanceData.fuelType["fuelTank:electric"] then performanceData.fuelType = "Electric" elseif next(performanceData.fuelType) then performanceData.fuelType = next(performanceData.fuelType) else performanceData.fuelType = "Unknown" end end -- Process induction type information if performanceData.inductionType then if performanceData.inductionType.naturalAspiration then performanceData.inductionType = "NA" elseif performanceData.inductionType.turbocharger then performanceData.inductionType = "Turbocharger" elseif next(performanceData.inductionType) then performanceData.inductionType = next(performanceData.inductionType) else performanceData.inductionType = "Unknown" end end if performanceData.lateralAcceleration then performanceData.lateralGForce = performanceData.lateralAcceleration.maxAcceleration / 9.81 end if performanceData.power and performanceData.power.propulsionPowerCombined and performanceData.weight then local powerInHP = performanceData.power.propulsionPowerCombined / 735.5 performanceData.powerPerTon = powerInHP * 1000 / performanceData.weight performanceData.power = powerInHP end end local function getVehicleUiData(inventoryId, inventoryIdsInGarage) local vehicleData = deepcopy(vehicles[inventoryId]) if not vehicleData then return end if not inventoryIdsInGarage then inventoryIdsInGarage = getVehiclesInGarage(getClosestGarage()) end vehicleData.value = career_modules_valueCalculator.getInventoryVehicleValue(inventoryId) vehicleData.valueRepaired = career_modules_valueCalculator.getInventoryVehicleValue(inventoryId, true) vehicleData.quickRepairExtraPrice = career_modules_insurance_insurance.getQuickRepairExtraPrice() vehicleData.initialRepairTime = career_modules_insurance_insurance.getInvVehRepairTime(inventoryId) if inventoryIdToVehId[inventoryId] then local vehObj = getObjectByID(inventoryIdToVehId[inventoryId]) if vehObj then vehicleData.distance = vehObj:getPosition():distance(getPlayerVehicle(0):getPosition()) vehicleData.inGarage = inventoryIdsInGarage[inventoryId] end vehicleData.inStorage = false else vehicleData.inStorage = true end for otherInventoryId, _ in pairs(inventoryIdsInGarage) do if otherInventoryId ~= inventoryId then vehicleData.otherVehicleInGarage = true break end end vehicleData.needsRepair = career_modules_insurance_insurance.inventoryVehNeedsRepair(vehicleData.id) if inventoryId == favoriteVehicle then vehicleData.favorite = true end local vehInsuranceInfo = career_modules_insurance_insurance.getVehInsuranceInfo(inventoryId) if vehInsuranceInfo then vehicleData.insuranceInfo = vehInsuranceInfo.insuranceInfo vehicleData.isInsured = vehInsuranceInfo.isInsured vehicleData.insuranceClass = vehInsuranceInfo.insuranceClass vehicleData.thumbnail = getVehicleThumbnail(inventoryId) end vehicleData.repairPermission = career_modules_permissions.getStatusForTag("vehicleRepair", { inventoryId = inventoryId }) vehicleData.sellPermission = career_modules_permissions.getStatusForTag("vehicleSelling", { inventoryId = inventoryId }) vehicleData.favoritePermission = career_modules_permissions.getStatusForTag("vehicleFavorite", { inventoryId = inventoryId }) vehicleData.storePermission = career_modules_permissions.getStatusForTag("vehicleStoring", { inventoryId = inventoryId }) vehicleData.licensePlateChangePermission = career_modules_permissions.getStatusForTag( { "vehicleLicensePlate", "vehicleModification" }, { inventoryId = inventoryId }) vehicleData.returnLoanerPermission = career_modules_permissions.getStatusForTag("returnLoanedVehicle", { inventoryId = inventoryId }) vehicleData.listedForSale = career_modules_marketplace.findVehicleListing(inventoryId) ~= nil for _, performanceData in ipairs(vehicleData.performanceHistory or {}) do processPerformanceData(performanceData) end if vehicleData.certificationData then processPerformanceData(vehicleData.certificationData) end return vehicleData end local function sendDataToUi() menuIsOpen = true local data = { vehicles = {} } data.menuHeader = menuHeader data.chooseButtonsData = chooseButtonsData data.buttonsActive = buttonsActive local inventoryIdsInGarage = getVehiclesInGarage(getClosestGarage()) for inventoryId, vehicle in pairs(vehicles) do data.vehicles[tostring(inventoryId)] = getVehicleUiData(inventoryId, inventoryIdsInGarage) end data.numberOfFreeSlots = getNumberOfFreeSlots() data.originComputerId = originComputerId if not career_modules_linearTutorial.getTutorialFlag("purchasedFirstCar") then data.tutorialActive = true end data.playerMoney = career_modules_playerAttributes.getAttributeValue("money") guihooks.trigger("vehicleInventoryData", data) end local function onUpdate(dtReal, dtSim, dtRaw) if saveCareer then -- we delay the save here so that the part condition initialization is definitely finished beforehand if saveCareer >= 10 then career_saveSystem.saveCurrent() -- this is the save just after starting a new career saveCareer = nil else saveCareer = saveCareer + 1 end end if vehiclesMovedToStorage then guihooks.trigger("toastrMsg", { type = "warning", label = "vehStored", title = "Vehicle stored", msg = "One or more of your vehicles were damaged at the end of your last session. They have been moved to your storage and have to be repaired." }) vehiclesMovedToStorage = nil end if loanedVehicleReturned then guihooks.trigger("toastrMsg", { type = "warning", label = "loanReturned", title = "Loaner returned", msg = "Your loaned vehicles have been returned to their respective owners." }) loanedVehicleReturned = nil end for inventoryId, vehInfo in pairs(vehicles) do if vehInfo.timeToAccess then vehInfo.timeToAccess = vehInfo.timeToAccess - dtReal setVehicleDirty(inventoryId) if vehInfo.timeToAccess < 0 then if vehInfo.delayReason == "bought" then ui_message(string.format("The %s has been delivered to your vehicle storage.", vehInfo.niceName), nil, "vehicleInventory") elseif vehInfo.delayReason == "repair" then ui_message( string.format("Your %s has been repaired and returned to your vehicle storage.", vehInfo .niceName), nil, "vehicleInventory") end vehInfo.timeToAccess = nil vehInfo.delayReason = nil if menuIsOpen then sendDataToUi() end end end end end local function onBeforeWalkingModeToggled(enabled, vehicleInFrontVehId) if enabled then enterVehicle(nil, skipPartConditionsBeforeWalking and 1 or nil) elseif vehIdToInventoryId[vehicleInFrontVehId] then enterVehicle(vehIdToInventoryId[vehicleInFrontVehId], 1) end skipPartConditionsBeforeWalking = nil end local function getInventoryIdsInClosestGarage(onlyFirst) -- get closest garage local closestGarage = getClosestGarage() -- check if a vehicle is in the zone of the closest garage local inventoryIdsInGarage = getVehiclesInGarage(closestGarage, true) local inventoryIdsList = {} for inventoryId, _ in pairs(inventoryIdsInGarage) do table.insert(inventoryIdsList, inventoryId) end if getPlayerVehicle(0) then local playerPos = getPlayerVehicle(0):getPosition() table.sort(inventoryIdsList, function(id1, id2) local veh1 = getObjectByID(inventoryIdToVehId[id1]) local veh2 = getObjectByID(inventoryIdToVehId[id2]) return veh1:getPosition():distance(playerPos) < veh2:getPosition():distance(playerPos) end) end if onlyFirst then return next(inventoryIdsInGarage) else return inventoryIdsList end end local callbackAfterFade local function onScreenFadeState(state) if callbackAfterFade and state == 1 then career_modules_vehicleDeletionService.deleteFlaggedVehicles() callbackAfterFade() callbackAfterFade = nil end end local function tprint(tbl, indent) if not indent then indent = 0 end local toprint = string.rep(" ", indent) .. "{\r\n" indent = indent + 2 for k, v in pairs(tbl) do toprint = toprint .. string.rep(" ", indent) if (type(k) == "number") then toprint = toprint .. "[" .. k .. "] = " elseif (type(k) == "string") then toprint = toprint .. k .. "= " end if (type(v) == "number") then toprint = toprint .. v .. ",\r\n" elseif (type(v) == "string") then toprint = toprint .. "\"" .. v .. "\",\r\n" elseif (type(v) == "table") then toprint = toprint .. tprint(v, indent + 2) .. ",\r\n" else toprint = toprint .. "\"" .. tostring(v) .. "\",\r\n" end end toprint = toprint .. string.rep(" ", indent - 2) .. "}" return toprint end local function openMenu(_chooseButtonsData, header, _buttonsActive) print(tprint(_chooseButtonsData), header, _buttonsActive) buttonsActive = _buttonsActive or {} if buttonsActive.repairEnabled == nil then buttonsActive.repairEnabled = true end if buttonsActive.sellEnabled == nil then buttonsActive.sellEnabled = true end if buttonsActive.favoriteEnabled == nil then buttonsActive.favoriteEnabled = true end if buttonsActive.storingEnabled == nil then buttonsActive.storingEnabled = true end if buttonsActive.returnLoanerEnabled == nil then buttonsActive.returnLoanerEnabled = true end menuHeader = header or "Vehicle Inventory" chooseButtonsData = _chooseButtonsData or { {} } for _, buttonData in ipairs(chooseButtonsData) do buttonData.buttonText = buttonData.buttonText or "Choose Vehicle" if buttonData.repairRequired == nil then buttonData.repairRequired = true end if buttonData.insuranceRequired == nil then buttonData.insuranceRequired = false end if buttonData.ownedRequired == nil then buttonData.ownedRequired = false end buttonData.callback = buttonData.callback or function() end end guihooks.trigger('ChangeState', { state = 'vehicleInventory' }) updatePartConditionsOfSpawnedVehicles() end local function closeMenu() career_career.closeAllMenus() end local function spawnVehicleAfterFade(enterAfterSpawn, inventoryId, callback) ui_fadeScreen.start(0.5) callbackAfterFade = function() if enterAfterSpawn then enterVehicle(inventoryId, nil, callback) else -- if the vehicle is already spawned, call the callback directly if inventoryIdToVehId[inventoryId] then callback() else spawnVehicle(inventoryId, nil, callback) end end end end local function spawnVehicleAndTeleportToGarage(enterAfterSpawn, inventoryId, replaceOthers) if inventoryId == currentVehicle then return end spawnVehicleAfterFade(enterAfterSpawn, inventoryId, function() if replaceOthers then removeVehiclesFromGarageExcept(inventoryId) end local vehObj = getObjectByID(inventoryIdToVehId[inventoryId]) setPartConditionResetSnapshot(vehObj, function() local closestGarage = getClosestGarage() freeroam_facilities.teleportToGarage(closestGarage.id, vehObj, false) career_modules_fuel.minimumRefuelingCheck(vehObj:getId()) setVehicleDirty(inventoryId) guihooks.trigger('ChangeState', { state = 'play' }) ui_fadeScreen.stop(0.5) local pos, _ = freeroam_facilities.getGaragePosRot(closestGarage, vehObj) career_modules_playerDriving.showPosition(pos) career_modules_log.addLog( string.format("Spawned vehicle %d in garage %s. replaceOthers == %s", inventoryId, closestGarage.id, replaceOthers), "inventory") end) end) end local function openMenuFromComputer(_originComputerId) originComputerId = _originComputerId M.openMenu( { { callback = function(inventoryId) spawnVehicleAndTeleportToGarage(false, inventoryId) end, buttonText = "Retrieve", insuranceRequired = true, requiredVehicleNotInGarage = true }, { callback = function(inventoryId) spawnVehicleAndTeleportToGarage(false, inventoryId, true) end, buttonText = "Replace current vehicle", insuranceRequired = true, requiredOtherVehicleInGarage = true }, { callback = function(inventoryId) career_modules_vehiclePerformance.openMenu({ inventoryId = inventoryId, computerId = originComputerId }) end, buttonText = "Performance Index", repairRequired = false } }, "Spawn Vehicle", nil ) career_modules_log.addLog(string.format("Opened vehicle inventory from computer %s", originComputerId), "inventory") end local function chooseVehicleFromMenu(inventoryId, buttonIndex, repairPrevVeh) chooseButtonsData[buttonIndex].callback(inventoryId, repairPrevVeh) end local function openInventoryMenuForChoosingListing() M.openMenu( { { callback = function(inventoryId) guihooks.trigger('addListing', { inventoryId = inventoryId }) end, buttonText = "List for Sale", repairRequired = true, ownedRequired = true, notForSaleRequired = true, } }, "List for Sale", { repairEnabled = false, sellEnabled = false, favoriteEnabled = false, storingEnabled = false, returnLoanerEnabled = false } ) end local function onExitVehicleInventory() menuIsOpen = false menuHeader = nil end local function onEnterVehicleFinished(inventoryId) if inventoryId then lastVehicle = inventoryId end end local function getVehicles() return vehicles end local function getVehicle(inventoryId) return vehicles[inventoryId] end local function sellVehicle(inventoryId, price) local vehicle = vehicles[inventoryId] if not vehicle then return end local value = price or career_modules_valueCalculator.getInventoryVehicleValue(inventoryId) extensions.hook("onBeforeVehicleSell", { inventoryId = inventoryId, price = value }) career_modules_playerAttributes.addAttributes({ money = value }, { tags = { "vehicleSold", "selling" }, label = "Sold a vehicle: " .. (vehicle.niceName or "(Unnamed Vehicle)") }) removeVehicle(inventoryId) Engine.Audio.playOnce('AudioGui', 'event:>UI>Career>Buy_01') career_modules_log.addLog(string.format("Sold vehicle %d for %f", inventoryId, value), "inventory") return true end local function sellVehicleFromInventory(inventoryId) if sellVehicle(inventoryId) then career_saveSystem.saveCurrent() sendDataToUi() end end local function returnLoanedVehicleFromInventory(inventoryId) career_modules_loanerVehicles.returnVehicle(inventoryId, function() career_saveSystem.saveCurrent() sendDataToUi() end) end local function expediteRepairFromInventory(inventoryId, price) career_modules_insurance_insurance.expediteRepair(inventoryId, price) career_saveSystem.saveCurrent() sendDataToUi() end local function delayVehicleAccess(inventoryId, delay, reason) local vehInfo = vehicles[inventoryId] if not vehInfo or delay <= 0 then return end vehInfo.timeToAccess = delay vehInfo.delayReason = reason end local function onAvailableMissionsSentToUi() if not currentVehicle then return end updatePartConditions(inventoryIdToVehId[currentVehicle], currentVehicle, function() guihooks.trigger('gameContextPlayerVehicleDamageInfo', { needsRepair = career_modules_insurance_insurance.inventoryVehNeedsRepair(currentVehicle) }) end) end local function setFavoriteVehicle(inventoryId) if vehicles[inventoryId] then favoriteVehicle = inventoryId end end local function getFavoriteVehicle() return favoriteVehicle end local function onComputerAddFunctions(menuData, computerFunctions) if not menuData.computerFacility.functions["vehicleInventory"] then return end local computerFunctionData = { id = "vehicleInventory", label = "My Vehicles", callback = function() openMenuFromComputer(menuData.computerFacility.id) end, order = 1 } if menuData.tutorialPartShoppingActive or menuData.tutorialTuningActive then computerFunctionData.disabled = true computerFunctionData.reason = career_modules_computer.reasons.tutorialActive end computerFunctions.general[computerFunctionData.id] = computerFunctionData end local function setLicensePlateText(inventoryId, text) local vehId = getVehicleIdFromInventoryId(inventoryId) if inventoryId then core_vehicles.setPlateText(text, vehId) end vehicles[inventoryId].config.licenseName = text end local function purchaseLicensePlateText(inventoryId, text, money) local price = { money = { amount = money } } if not career_modules_payment.canPay(price) then return end career_modules_payment.pay(price, { label = string.format("Change the license plate text"), tags = { "licensePlate", "buying" } }) setLicensePlateText(inventoryId, text) Engine.Audio.playOnce('AudioGui', 'event:>UI>Career>Buy_01') setVehicleDirty(inventoryId) end local permissionTags = { notOwned = { vehicleSelling = "forbidden", --selling a vehicle vehicleRepair = "forbidden", interactMission = "forbidden", --use the mission POI to start a mission painting = "forbidden", partBuying = "forbidden", vehicleLicensePlate = "forbidden", tuning = "forbidden", vehicleStoring = "forbidden", partSwapping = "forbidden", recoveryTowToGarage = "forbidden", returnLoanedVehicle = "allowed", vehicleFavorite = "forbidden" } } local function onCheckPermission(tags, permissions, additionalData) if not additionalData or not additionalData.inventoryId then return end local vehData = vehicles[additionalData.inventoryId] if not vehData then return end for _, tag in ipairs(tags) do if not vehData.owned then if permissionTags.notOwned[tag] then table.insert(permissions, { permission = permissionTags.notOwned[tag] }) end elseif tag == "returnLoanedVehicle" then table.insert(permissions, { permission = "hidden" }) end if tag == "vehicleRepair" and (vehData.timeToAccess or vehData.missingFile) then table.insert(permissions, { permission = "forbidden" }) end if tag == "vehicleSelling" and vehData.timeToAccess then table.insert(permissions, { permission = "forbidden" }) end if tag == "vehicleFavorite" and (vehData.favorite or vehData.missingFile) then table.insert(permissions, { permission = "forbidden" }) end if tag == "vehicleStoring" and not inventoryIdToVehId[additionalData.inventoryId] then table.insert(permissions, { permission = "forbidden" }) end end end local function onGetRawPoiListForLevel(levelIdentifier, elements) if next(inventoryIdToVehId) then for invId, vehId in pairs(inventoryIdToVehId) do if be:getPlayerVehicleID(0) ~= vehId then -- don't display the current player's vehicle if map.objects[vehId] then local desc = "Player's vehicle" if vehicles[invId].loanType then desc = "Loaned vehicle" end local id = "plVeh" .. vehId local dist, distUnit = translateDistance( map.objects[vehId].pos:distance(getPlayerVehicle(0):getPosition()), true) local plate = vehicles[invId].config.licenseName local odometer, odoUnit = translateDistance( career_modules_valueCalculator.getVehicleMileageById(invId), true) desc = string.format("%s | Distance: %0.2f %s | Licence plate: %s | Odometer: %i %s", desc, dist, distUnit, plate, odometer, odoUnit) table.insert(elements, { id = id, data = { type = "playerVehicle", id = id }, markerInfo = { bigmapMarker = { pos = map.objects[vehId].pos, icon = "vehicle_marker_outlined", name = vehicles[invId].niceName, description = desc, thumbnail = getVehicleThumbnail(invId), previews = getVehicleThumbnail(invId), cluster = false } } }) end end end end end local function getDirtiedVehicles() return dirtiedVehicles end local function isEmpty() return tableIsEmpty(vehicles) end local function isLicensePlateValid(text) return core_vehicles.isLicensePlateValid(text) end local function isVehicleNameValid(text) if not text or text == "" then return false end return not text:find('["\\\b\f\n\r\t]') end local function renameVehicle(inventoryId, name) if not isVehicleNameValid(name) then log("E", "inventory", "Invalid characters in vehicle name: " .. name) return false end vehicles[inventoryId].niceName = name setVehicleDirty(inventoryId) return true end local function debugRespawnCurrentVehicle() local inventoryId = getCurrentVehicle() if not inventoryId then return end career_modules_inventory.updatePartConditions(nil, inventoryId, function() spawnVehicle(inventoryId, 2) end) end M.addVehicle = addVehicle M.removeVehicle = removeVehicle M.enterVehicle = enterVehicle M.sellVehicle = sellVehicle M.sellVehicleFromInventory = sellVehicleFromInventory M.returnLoanedVehicleFromInventory = returnLoanedVehicleFromInventory M.expediteRepairFromInventory = expediteRepairFromInventory M.updatePartConditions = updatePartConditions M.updatePartConditionsOfSpawnedVehicles = updatePartConditionsOfSpawnedVehicles M.removeVehicleObject = removeVehicleObject M.openMenu = openMenu M.closeMenu = closeMenu M.openMenuFromComputer = openMenuFromComputer M.chooseVehicleFromMenu = chooseVehicleFromMenu M.delayVehicleAccess = delayVehicleAccess M.hasFreeSlot = hasFreeSlot M.getNumberOfFreeSlots = getNumberOfFreeSlots M.setFavoriteVehicle = setFavoriteVehicle M.getFavoriteVehicle = getFavoriteVehicle M.sendDataToUi = sendDataToUi M.setLicensePlateText = setLicensePlateText M.purchaseLicensePlateText = purchaseLicensePlateText M.getVehicleThumbnail = getVehicleThumbnail M.renameVehicle = renameVehicle M.isLicensePlateValid = isLicensePlateValid M.isVehicleNameValid = isVehicleNameValid M.onExtensionLoaded = onExtensionLoaded M.onSaveCurrentSaveSlot = onSaveCurrentSaveSlot M.onClientStartMission = onClientStartMission M.onBigMapActivated = onBigMapActivated M.onUpdate = onUpdate M.onBeforeWalkingModeToggled = onBeforeWalkingModeToggled M.onCareerModulesActivated = onCareerModulesActivated M.onEnterVehicleFinished = onEnterVehicleFinished M.onExitVehicleInventory = onExitVehicleInventory M.onScreenFadeState = onScreenFadeState M.onAvailableMissionsSentToUi = onAvailableMissionsSentToUi M.onComputerAddFunctions = onComputerAddFunctions M.onSaveCurrentSaveSlotAsyncStart = onSaveCurrentSaveSlotAsyncStart M.onCheckPermission = onCheckPermission M.onGetRawPoiListForLevel = onGetRawPoiListForLevel M.openInventoryMenuForChoosingListing = openInventoryMenuForChoosingListing M.getPartConditionsCallback = getPartConditionsCallback M.applyPartConditions = applyPartConditions M.teleportedFromBigmap = teleportedFromBigmap M.setVehicleDirty = setVehicleDirty M.getDirtiedVehicles = getDirtiedVehicles M.getVehicles = getVehicles M.getVehicle = getVehicle M.isEmpty = isEmpty M.spawnVehicle = spawnVehicle M.getInventoryIdsInClosestGarage = getInventoryIdsInClosestGarage M.getClosestGarage = getClosestGarage M.isSeatedInsideOwnedVehicle = isSeatedInsideOwnedVehicle -- Debug M.getCurrentVehicle = getCurrentVehicle M.getCurrentVehicleId = getCurrentVehicleId M.getLastVehicle = getLastVehicle M.getVehicleIdFromInventoryId = getVehicleIdFromInventoryId M.getInventoryIdFromVehicleId = getInventoryIdFromVehicleId M.getMapInventoryIdToVehId = getMapInventoryIdToVehId M.debugRespawnCurrentVehicle = debugRespawnCurrentVehicle M.getVehicleUiData = getVehicleUiData return M