From 28d2f2c086ae115045b401fdbad16108a8a103b0 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Sun, 27 Sep 2026 23:55:10 +0200 Subject: [PATCH] =?UTF-8?q?feat(genstart):=20husk=20ved=20hvert=20gem=20hv?= =?UTF-8?q?ilke=20maskiner=20Courseplay=20k=C3=B8rer=20(uden=20AD)=20og=20?= =?UTF-8?q?start=20netop=20dem=20igen=20efter=20indl=C3=A6sning=20fra=20n?= =?UTF-8?q?=C3=A6rmeste=20waypoint=20via=20CP's=20HUD-start=20=E2=80=94=20?= =?UTF-8?q?ogs=C3=A5=20CP-jobs=20uden=20markrolle=20(v1.45.0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adCpResume.lua | 117 ++++++++++++++++++++++++ FS25_ADSmartPickup/adFieldWork.lua | 35 ++++++- FS25_ADSmartPickup/adRunsController.lua | 3 + FS25_ADSmartPickup/modDesc.xml | 3 +- tests/test_adCpResume.lua | 26 ++++++ 5 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 FS25_ADSmartPickup/adCpResume.lua create mode 100644 tests/test_adCpResume.lua diff --git a/FS25_ADSmartPickup/adCpResume.lua b/FS25_ADSmartPickup/adCpResume.lua new file mode 100644 index 0000000..4d78937 --- /dev/null +++ b/FS25_ADSmartPickup/adCpResume.lua @@ -0,0 +1,117 @@ +-- AD CP Resume +-- Spillet stopper alle arbejdere ved gem, og Courseplay genstarter dem ikke (AutoDrive gør selv). Ved hvert gem +-- skrives de maskiner hvor CP kører (uden AD) til ADSmartPickup_cpresume.xml; efter indlæsning startes netop +-- dem igen fra nærmeste waypoint med CP's egen HUD-start (samme job som spilleren sidst valgte i HUD'en). + +ADCpResume = {} +ADCpResume.FILE_NAME = "ADSmartPickup_cpresume.xml" +ADCpResume.ROOT = "ADSmartPickupCpResume" +ADCpResume.LOG_PREFIX = "ADSmartPickup: CP-genstart: " +ADCpResume.pending = nil -- {[vehicleId] = true} fra seneste gem; nil = ingen fil (gammel regel bruges) + +-- ---- ren logik ---- + +-- vehicles = {{id, cpActive, adActive}} -> id'er der skal huskes +function ADCpResume.activeIds(vehicles) + local ids = {} + for _, vehicle in ipairs(vehicles or {}) do + if vehicle.cpActive and not vehicle.adActive then table.insert(ids, vehicle.id) end + end + return ids +end + +function ADCpResume.fromRecords(ids) + local set = {} + for _, id in ipairs(ids or {}) do + if type(id) == "string" and id ~= "" then set[id] = true end + end + return set +end + +-- vehicles = {{id, aiActive, controlled}} i fast rækkefølge -> id'er der skal startes (nil uden gemt liste) +function ADCpResume.toStart(pending, vehicles) + if pending == nil then return nil end + local ids = {} + for _, vehicle in ipairs(vehicles or {}) do + if pending[vehicle.id] and not vehicle.aiActive and not vehicle.controlled then table.insert(ids, vehicle.id) end + end + return ids +end + +-- ---- spil ---- + +local function vehicleState(adEnv, vehicle) + local ad = vehicle.ad + return { + id = ADRunsController.getVehicleId(vehicle), + cpActive = vehicle.getIsCpActive ~= nil and vehicle:getIsCpActive() == true, + adActive = ad ~= nil and ad.stateModule ~= nil and ad.stateModule:isActive(), + aiActive = vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive() == true, + controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() == true, + } +end + +local function farmVehicles(adEnv) + local list, byId = {}, {} + for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do + local ok, state = pcall(vehicleState, adEnv, vehicle) + if ok and state.id ~= nil then + table.insert(list, state) + byId[state.id] = vehicle + end + end + return list, byId +end + +-- Starter CP fra nærmeste waypoint (HUD-startpunktet sættes som spillerens "start ved nærmeste"-tast). +function ADCpResume.startNearest(vehicle) + if vehicle.cpStartStopDriver == nil or (vehicle.getIsAIActive ~= nil and vehicle:getIsAIActive()) then return false end + if vehicle.getCpStartingPointSetting ~= nil and CpFieldWorkJobParameters ~= nil and vehicle.hasCpCourse ~= nil and vehicle:hasCpCourse() then + local setting = vehicle:getCpStartingPointSetting() + if setting ~= nil then setting:setValue(CpFieldWorkJobParameters.START_AT_NEAREST_POINT) end + end + vehicle:cpStartStopDriver(true) + return true +end + +-- Id'er der skal startes nu (nil = ingen gemt liste). Listen bruges kun én gang. +function ADCpResume.takeToStart(adEnv) + local list, byId = farmVehicles(adEnv) + local ids = ADCpResume.toStart(ADCpResume.pending, list) + ADCpResume.pending = nil + return ids, byId +end + +local function getPath() + local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil + if path == nil then return nil end + return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADCpResume.FILE_NAME)) +end + +function ADCpResume.save(adEnv) + local path = getPath() + if path == nil or adEnv == nil then return false end + local ids = ADCpResume.activeIds((farmVehicles(adEnv))) + local xmlFile = XMLFile.create("ADSmartPickupCpResume", path, ADCpResume.ROOT) + if xmlFile == nil then return false end + for index, id in ipairs(ids) do + xmlFile:setString(string.format("%s.vehicle(%d)#id", ADCpResume.ROOT, index - 1), id) + end + xmlFile:save() + xmlFile:delete() + Logging.info(ADCpResume.LOG_PREFIX .. "gemt: %d maskine(r) kører Courseplay", #ids) + return true +end + +function ADCpResume.load() + ADCpResume.pending = nil + local path = getPath() + if path == nil or not fileExists(path) then return end + local xmlFile = XMLFile.load("ADSmartPickupCpResume", path) + if xmlFile == nil then return end + local ids = {} + xmlFile:iterate(ADCpResume.ROOT .. ".vehicle", function(_, key) table.insert(ids, xmlFile:getString(key .. "#id")) end) + xmlFile:delete() + ADCpResume.pending = ADCpResume.fromRecords(ids) + Logging.info(ADCpResume.LOG_PREFIX .. "%d maskine(r) kørte Courseplay ved sidste gem", #ids) +end diff --git a/FS25_ADSmartPickup/adFieldWork.lua b/FS25_ADSmartPickup/adFieldWork.lua index 17c0c70..9f42c8f 100644 --- a/FS25_ADSmartPickup/adFieldWork.lua +++ b/FS25_ADSmartPickup/adFieldWork.lua @@ -933,9 +933,15 @@ end -- Efter genindlæsning: spillet stopper alle arbejdere ved gem, og Courseplay genstarter dem ikke (in-game 27/9). -- En maskine med CP-kursus og et husket waypoint (CP nulstiller det når kurset er kørt færdigt) var midt i et job: --- start den fra sidste waypoint med CP's eget "fortsæt"-job (HUD'ens eget startpunkt røres ikke). Står den på en +-- start den fra sidste waypoint med CP's eget "fortsæt"-job (med gemt liste fra ADCpResume: netop de maskiner +-- der kørte CP ved gem, fra nærmeste waypoint) (HUD'ens eget startpunkt røres ikke). Står den på en -- mark hvis aktuelle trin er maskinens rolle, meldes den ind på marken igen, så den følges og trinnet afsluttes. function ADFieldWork.resumeAfterLoad(adEnv) + -- gemt liste over maskiner hvor CP kørte (ADCpResume): den afgør alene hvem der startes, fra nærmeste waypoint + local toStart, cpVehicles = nil, {} + if ADCpResume ~= nil then toStart, cpVehicles = ADCpResume.takeToStart(adEnv) end + local startSet, handled = {}, {} + for _, id in ipairs(toStart or {}) do startSet[id] = true end local fields, stageById = {}, {} for _, info in ipairs(ADFields.list()) do local field = ADFieldJobs.getField(info.id) @@ -948,18 +954,24 @@ function ADFieldWork.resumeAfterLoad(adEnv) local vehicle = rig.vehicle local wasWorking = vehicle.startCpAtLastWp ~= nil and vehicle.hasCpCourse ~= nil and vehicle:hasCpCourse() and vehicle.getCpLastRememberedWaypointIx ~= nil and vehicle:getCpLastRememberedWaypointIx() ~= nil + if toStart ~= nil then wasWorking = startSet[rig.id] == true end + handled[rig.id] = true local fieldId, distance = nil, nil - if wasWorking then + if wasWorking and toStart ~= nil then + fieldId = ADFieldJobs.resumeField(rig.x, rig.z, rig.role, fields) + elseif wasWorking then fieldId = ADFieldJobs.resumeField(rig.x, rig.z, rig.role, fields) local course = ADCpBridge.getCourse(vehicle) local okWp, wp = pcall(function() return course ~= nil and course:getWaypoint(vehicle:getCpLastRememberedWaypointIx()) or nil end) if okWp and wp ~= nil and wp.x ~= nil then distance = math.sqrt((wp.x - rig.x) ^ 2 + (wp.z - rig.z) ^ 2) end end - if wasWorking and not rig.busy and not rig.controlled and not ADFieldJobs.shouldResume({fieldId = fieldId, distance = distance}) then + if wasWorking and toStart == nil and not rig.busy and not rig.controlled and not ADFieldJobs.shouldResume({fieldId = fieldId, distance = distance}) then Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' genstartes ikke: husket waypoint fra et gammelt job (%s væk, ikke på en mark i gang)", tostring(rig.label), distance ~= nil and string.format("%.0f m", distance) or "ukendt afstand") elseif wasWorking and not rig.busy and not rig.controlled then - local ok, started = pcall(vehicle.startCpAtLastWp, vehicle) + local ok, started + if toStart ~= nil then ok, started = pcall(ADCpResume.startNearest, vehicle) + else ok, started = pcall(vehicle.startCpAtLastWp, vehicle) end if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "'%s' kunne ikke genstartes: %s", tostring(rig.label), tostring(started)) elseif started then @@ -980,11 +992,24 @@ function ADFieldWork.resumeAfterLoad(adEnv) log(fieldId, "'%s' fortsætter efter genindlæsning (Courseplay fra sidste waypoint)%s", tostring(rig.label), assignment.storeId ~= nil and "" or (fillType ~= nil and " — intet lager til resten fundet" or "")) else - Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' genstartet fra sidste waypoint efter genindlæsning", tostring(rig.label)) + Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' genstartet fra %s waypoint efter genindlæsning", tostring(rig.label), + toStart ~= nil and "nærmeste" or "sidste") end end end end + -- CP-maskiner uden markrolle (fx balleopsamler, tømmevogn i CP-job): start dem også + for _, id in ipairs(toStart or {}) do + local vehicle = cpVehicles[id] + if not handled[id] and vehicle ~= nil then + local ok, started = pcall(ADCpResume.startNearest, vehicle) + if ok and started then + Logging.info(ADFieldJobs.LOG_PREFIX .. "'%s' genstartet fra nærmeste waypoint efter genindlæsning", tostring(ADRuns.vehicleLabel(vehicle))) + elseif not ok then + Logging.warning(ADFieldJobs.LOG_PREFIX .. "'%s' kunne ikke genstartes: %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(started)) + end + end + end end -- Markmaskiner der stadig hedder modelnavnet ("Unrealistic Engine 3") får et navn efter rollen ("Roeoptager 2"). diff --git a/FS25_ADSmartPickup/adRunsController.lua b/FS25_ADSmartPickup/adRunsController.lua index a1d7efc..5d98bde 100644 --- a/FS25_ADSmartPickup/adRunsController.lua +++ b/FS25_ADSmartPickup/adRunsController.lua @@ -1436,6 +1436,8 @@ function ADRunsController.save() -- navnefilen skal skrives under spillets gem: FS25 genskriver savegame-mappen, og filer ingen skriver -- under gemningen forsvinder (in-game 27/9: ADSmartPickup_names.xml blev slettet ved første gem) if ADNames ~= nil and ADNames.byId ~= nil then pcall(ADNames.save) end + -- maskiner hvor CP kører nu, så de kan genstartes efter indlæsning (samme gem-regel som navnefilen) + if ADCpResume ~= nil and ADFieldSilos ~= nil and ADFieldSilos.adEnv ~= nil then pcall(ADCpResume.save, ADFieldSilos.adEnv) end return saved end @@ -1450,6 +1452,7 @@ function ADRunsController.load(adEnv) if ADNames ~= nil then pcall(ADNames.load) end pcall(ADRunsController.migrateBuildings, adEnv) if ADFieldSilos ~= nil then pcall(ADFieldSilos.load) end + if ADCpResume ~= nil then pcall(ADCpResume.load) end local vehiclesById = getFarmVehiclesById(adEnv) for _, run in ipairs(store.runs) do for _, vehicleId in ipairs(run.vehicleIds) do diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index d4bbede..c55cec6 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.44.0.0 + 1.45.0.0 <en>AD Smart Pickup</en> @@ -45,6 +45,7 @@ + diff --git a/tests/test_adCpResume.lua b/tests/test_adCpResume.lua new file mode 100644 index 0000000..e7768ab --- /dev/null +++ b/tests/test_adCpResume.lua @@ -0,0 +1,26 @@ +-- Kør: luajit tests/test_adCpResume.lua (fra repo-roden) +dofile("FS25_ADSmartPickup/adCpResume.lua") +local R = ADCpResume +local failures = 0 +local function check(name, actual, expected) + if actual == expected then print("OK " .. name) else + failures = failures + 1 + print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) + end +end +-- hvem skrives ved gem: CP kører, AD styrer ikke (AD genstarter selv sine) +local ids = R.activeIds({{id = "a", cpActive = true}, {id = "b", cpActive = true, adActive = true}, {id = "c"}, {id = "d", cpActive = true}}) +check("gem: CP-aktive uden AD", table.concat(ids, " "), "a d") +check("gem: tom liste", #R.activeIds({}), 0) +-- records frem og tilbage +local set = R.fromRecords({"a", "", "d", "a"}) +check("indlæs: a", set.a, true) +check("indlæs: tomt id springes over", set[""], nil) +-- hvem startes ved indlæsning +local pending = {a = true, b = true, c = true, d = true} +local vehicles = {{id = "a"}, {id = "b", aiActive = true}, {id = "c", controlled = true}, {id = "d"}, {id = "e"}} +local start = R.toStart(pending, vehicles) +check("start: kun gemte, ikke allerede kørende/spiller i", table.concat(start, " "), "a d") +check("start: ingen gemt liste -> nil (gammel regel bruges)", R.toStart(nil, vehicles), nil) +print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) +os.exit(failures == 0 and 0 or 1)