-- Copyright (C) 2025 snoutie -- Authors: snoutie (copyright@achtarmig.org) -- This program is free software: you can redistribute it and/or modify -- it under the terms of the GNU Affero General Public License as published -- by the Free Software Foundation, either version 3 of the License, or -- (at your option) any later version. -- This program is distributed in the hope that it will be useful, -- but WITHOUT ANY WARRANTY; without even the implied warranty of -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -- GNU Affero General Public License for more details. -- You should have received a copy of the GNU Affero General Public License -- along with this program. If not, see . local M = {} M.dependencies = { 'freeroam_facilities', 'ui_fadeScreen', 'career_career', 'career_saveSystem', 'career_modules_inventory' } local extensionName = "career_vehicleSaveSystem" local vehicleSaves_temp = "/temp/career/vehicle_saves" local function CreateDirectories(dir) local p = path.dirname(dir) if not FS:directoryExists(p) then CreateDirectories(string.sub(p, 1, #p - 1)) end FS:directoryCreate(dir) end local function ClearDirectory(dir) local files = FS:findFiles(dir .. "/", '*', 0, false, true) for i = 1, tableSize(files) do FS:remove(files[i]) end end local function CopySaves(from, to) local save_folders = FS:directoryList(from, false, true) for i = 1, tableSize(save_folders) do local folder_dir, folder_name, _ = path.split(save_folders[i]) FS:directoryCreate(to .. "/" .. folder_name) local save_files = FS:findFiles(folder_dir .. folder_name .. "/", '*.json', 0, false, false) for n = 1, tableSize(save_files) do local file_dir, file_name, file_ext = path.split(save_files[n]) FS:remove(to .. "/" .. folder_name .. "/" .. file_name) FS:copyFile(file_dir .. file_name, to .. "/" .. folder_name .. "/" .. file_name) end end end local function LoadFiles(from) ClearDirectory(vehicleSaves_temp) CopySaves(from, vehicleSaves_temp) end local function SaveFiles(to) CopySaves(vehicleSaves_temp, to) end local function GetVehicleSaveFile(root, inventoryId) local path = root .. "/" .. inventoryId CreateDirectories(path) return path .. "/save.json" end local function EnqueueVehicleToSave(inventoryId) log("I", "saving", "enqueued vehicle " .. inventoryId) M.queuedVehicleSaves[inventoryId] = true end local function DequeueVehicleToSave(inventoryId) log("I", "saving", "dequeued vehicle " .. inventoryId) M.queuedVehicleSaves[inventoryId] = nil end local function TeleportVehicle(inventoryId, delayed, callback) log("I", "loading", "teleporting vehicle " .. inventoryId) local veh = be:getObjectByID(career_modules_inventory.getVehicleIdFromInventoryId(inventoryId)) local location = { pos = veh:getPosition(), rot = quat(0, 0, 1, 0) * quat(veh:getRefNodeRotation()) } local garage = career_modules_inventory.getClosestGarage(location.pos) extensions.core_jobsystem.create(function(job) if delayed then job.sleep(1) end freeroam_facilities.teleportToGarage(garage.id, veh, false) if callback then callback() end end) end local fadeInProgress = false local function SetFade(fade) if fadeInProgress and fade then return end if not fadeInProgress and not fade then return end if fade then ui_fadeScreen.start(0.5) fadeInProgress = true else ui_fadeScreen.stop(0.5) fadeInProgress = false end end local function VehiclesSaved() if next(M.queuedVehicleSaves) == nil then log('I', 'saving', 'all vehicles saved') return true else log('I', 'saving', 'still saving vehicles') end return false end local function CheckSavedAsync(callback) extensions.core_jobsystem.create( function(job) while true do if VehiclesSaved() then break end job.sleep(0.1) end if callback then callback() end end) end local function SaveVehicle(inventoryId) local saveFile = GetVehicleSaveFile(vehicleSaves_temp, inventoryId) local vehicleId = career_modules_inventory.getVehicleIdFromInventoryId(inventoryId) if vehicleId then log('I', 'saving', 'saving vehicle ' .. inventoryId .. " to " .. saveFile) local object = be:getObjectByID(vehicleId) object:queueLuaCommand("beamstate.save(\"" .. saveFile .. "\"); obj:queueGameEngineLua('career_vehicleSaveSystem.DequeueVehicleToSave(" .. inventoryId .. ")');") return true else log('I', 'saving', 'vehicle ' .. inventoryId .. " not spawned") return false end end local function FinishedLoading(inventoryId) M.SetFade(false) local veh = be:getObjectByID(career_modules_inventory.getVehicleIdFromInventoryId(inventoryId)) local pos, _ = freeroam_facilities.getGaragePosRot(career_modules_inventory.getClosestGarage(), veh) career_modules_playerDriving.showPosition(pos) end local function LoadVehicle(inventoryId, loadDamaged) local saveFile = GetVehicleSaveFile(vehicleSaves_temp, inventoryId) local vehicleId = career_modules_inventory.getVehicleIdFromInventoryId(inventoryId) if vehicleId then log("I", "loading", "loading vehicle from " .. saveFile) if loadDamaged then local object = be:getObjectByID(vehicleId) object:queueLuaCommand( "beamstate.load(\"" .. saveFile .. "\"); " .. "for key, value in pairs(v.data.controller) do " .. " if value['fileName'] == 'advancedCouplerControl' then " .. " local c = controller.getController(value['name']) " .. " if c['reset'] then " .. " c['reset']() " .. " end " .. " end " .. " end; " .. " obj:queueGameEngineLua(' " .. " career_vehicleSaveSystem.TeleportVehicle( " .. " " .. inventoryId .. ", " .. " true, " .. " function() " .. " career_vehicleSaveSystem.FinishedLoading(" .. inventoryId .. ")" .. " end) " .. " ');" ) else FinishedLoading(inventoryId) end end end local function onSaveCurrentSaveSlotAsyncStart() career_saveSystem.registerAsyncSaveExtension(extensionName) end local function onSaveCurrentSaveSlot(currentSavePath, oldSaveDate, vehiclesThumbnailUpdate) local vehicles = career_modules_inventory.getVehicles() for id, _ in pairs(vehicles) do EnqueueVehicleToSave(id) end for id, _ in pairs(M.queuedVehicleSaves) do if not SaveVehicle(id) then DequeueVehicleToSave(id) end end CheckSavedAsync(function() SaveFiles(currentSavePath .. "/career/vehicle_saves") career_saveSystem.asyncSaveExtensionFinished(extensionName) end) end local function onCareerActive() local _, saveSlot = career_saveSystem.getCurrentSaveSlot() local vehicleSaves_saveSlot = saveSlot .. "/career/vehicle_saves" CreateDirectories(vehicleSaves_saveSlot) CreateDirectories(vehicleSaves_temp) LoadFiles(vehicleSaves_saveSlot) end M.queuedVehicleSaves = {} M.SaveVehicle = SaveVehicle M.LoadVehicle = LoadVehicle M.FinishedLoading = FinishedLoading M.TeleportVehicle = TeleportVehicle M.SetFade = SetFade M.CheckSavedAsync = CheckSavedAsync M.EnqueueVehicleToSave = EnqueueVehicleToSave M.DequeueVehicleToSave = DequeueVehicleToSave M.onSaveCurrentSaveSlotAsyncStart = onSaveCurrentSaveSlotAsyncStart M.onSaveCurrentSaveSlot = onSaveCurrentSaveSlot M.onCareerActive = onCareerActive return M