Files
career_retrievedamaged/lua/ge/extensions/career/modules/inventory.lua

1467 lines
54 KiB
Lua
Raw Normal View History

2025-12-11 12:16:55 +01:00
-- 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