feat(genstart): husk ved hvert gem hvilke maskiner Courseplay kører (uden AD) og start netop dem igen efter indlæsning fra nærmeste waypoint via CP's HUD-start — også CP-jobs uden markrolle (v1.45.0.0)

This commit is contained in:
2026-09-27 23:55:10 +02:00
parent 6aeb8e4179
commit 28d2f2c086
5 changed files with 178 additions and 6 deletions
+117
View File
@@ -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
+30 -5
View File
@@ -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").
+3
View File
@@ -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
+2 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.44.0.0</version>
<version>1.45.0.0</version>
<title>
<en>AD Smart Pickup</en>
</title>
@@ -45,6 +45,7 @@
<sourceFile filename="adFieldSiloPlanner.lua"/>
<sourceFile filename="adFieldSiloConfig.lua"/>
<sourceFile filename="adFieldSilos.lua"/>
<sourceFile filename="adCpResume.lua"/>
<sourceFile filename="adFieldJobs.lua"/>
<sourceFile filename="adMonthView.lua"/>
<sourceFile filename="adProfiler.lua"/>
+26
View File
@@ -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)