diff --git a/lua/ge/extensions/career/modules/inventory.lua b/lua/ge/extensions/career/modules/inventory.lua new file mode 100644 index 0000000..3734abf --- /dev/null +++ b/lua/ge/extensions/career/modules/inventory.lua @@ -0,0 +1,1466 @@ +-- 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 diff --git a/lua/ge/extensions/career/vehicleRetrieval.lua b/lua/ge/extensions/career/vehicleRetrieval.lua index 5cd3878..1fce8a8 100644 --- a/lua/ge/extensions/career/vehicleRetrieval.lua +++ b/lua/ge/extensions/career/vehicleRetrieval.lua @@ -19,6 +19,7 @@ M.dependencies = { 'career_modules_inventory', 'freeroam_facilities', "career_ve "career_modules_permissions", 'career_modules_valueCalculator' } local career_modules_inventory_removeVehicleObject +local career_modules_inventory_openMenu local vehicleObjectsToRemove = {} local function spawnVehicle(inventoryId, callback) @@ -56,9 +57,16 @@ local function onComputerAddFunctions(menuData, computerFunctions) if menuData.computerFacility.functions["vehicleInventory"] then local favouriteVehicleId = career_modules_inventory.getFavoriteVehicle() + local vehicleValue = career_modules_valueCalculator.getInventoryVehicleValue(favouriteVehicleId, true) + local currentVehicleValue = career_modules_valueCalculator.getInventoryVehicleValue(favouriteVehicleId) + local function_label = "Retrieve Favourite Vehicle" + if currentVehicleValue < vehicleValue then + function_label = function_label .. " (Damaged)" + end + local computerFunctionData = { id = "retrieve_damaged", - label = "Retrieve Favourite Damaged", + label = function_label, callback = function() Retrieve(favouriteVehicleId) end, order = 1 } @@ -90,9 +98,24 @@ local function RemoveVehicleObject(inventoryId) end end +local function OpenMenu(_chooseButtonsData, header, _buttonsActive) + if _chooseButtonsData then + for _, buttonData in ipairs(_chooseButtonsData) do + if buttonData.buttonText == "Retrieve" then + buttonData.buttonText = "Retrieve" + buttonData.callback = function(inventoryId) Retrieve(inventoryId) end + buttonData.repairRequired = false + end + end + end + career_modules_inventory_openMenu(_chooseButtonsData, header, _buttonsActive) +end + local function onCareerActive() career_modules_inventory_removeVehicleObject = career_modules_inventory.removeVehicleObject career_modules_inventory.removeVehicleObject = RemoveVehicleObject + career_modules_inventory_openMenu = career_modules_inventory.openMenu + career_modules_inventory.openMenu = OpenMenu end M.onComputerAddFunctions = onComputerAddFunctions diff --git a/scripts/retrievedamagedcareer/modScript.lua b/scripts/retrievedamagedcareer/modScript.lua index 1fe623b..39c9bd3 100644 --- a/scripts/retrievedamagedcareer/modScript.lua +++ b/scripts/retrievedamagedcareer/modScript.lua @@ -19,6 +19,7 @@ M.dependencies = { 'career_career' } extensions.load("career_vehicleSaveSystem") extensions.load("career_vehicleRetrieval") +extensions.load("career_modules_inventory") M.onInit = function() setExtensionUnloadMode(M, "manual")