diff --git a/FS25_ADSmartPickup/adFieldJobs.lua b/FS25_ADSmartPickup/adFieldJobs.lua
new file mode 100644
index 0000000..334f946
--- /dev/null
+++ b/FS25_ADSmartPickup/adFieldJobs.lua
@@ -0,0 +1,396 @@
+-- AD Field Jobs
+-- Markarbejdets job-løkke (hvert sekund): opdater klar-andel, start marker, send sæt ud, følg dem
+-- gennem AutoDrive-kørsel -> Courseplay-arbejde -> hjem, og gå videre til næste trin. decide() er ren
+-- og testet; resten oversætter til AutoDrive/Courseplay og er pcall'et pr. mark og pr. sæt.
+
+ADFieldJobs = {}
+
+ADFieldJobs.LOG_PREFIX = "ADSmartPickup: "
+ADFieldJobs.TICK_MS = 1000
+ADFieldJobs.ARRIVE_DISTANCE = 30
+ADFieldJobs.IDLE_CONFIRM_MS = 5000
+ADFieldJobs.START_TIMEOUT_MS = 60000
+ADFieldJobs.state = {fields = {}, fleet = {}, assignments = {}}
+ADFieldJobs.timer = 0
+ADFieldJobs.now = 0
+ADFieldJobs.loaded = false
+
+local ACTIVE_PHASES = {driving = true, starting = true, working = true}
+
+-- ---- ren beslutning ----
+
+function ADFieldJobs.decide(field, info, assignments, now)
+ if field.stage == "ready" then
+ if not ADFieldModel.shouldStart(field) then return {action = "none"} end
+ if info == nil or info.markerId == nil then return {action = "wait"} end
+ return {action = "start"}
+ end
+ if not ADFieldModel.isWorking(field) then return {action = "none"} end
+ local active, finished = 0, 0
+ for _, assignment in ipairs(assignments) do
+ if ACTIVE_PHASES[assignment.phase] then active = active + 1 end
+ if assignment.phase == "finished" or assignment.phase == "returning" then finished = finished + 1 end
+ end
+ if active == 0 and finished > 0 then
+ local stillStanding = field.stage == "mowing" and info ~= nil and (info.readyRatio or 0) >= ADFieldModel.CUT_RATIO
+ if not stillStanding then return {action = "advance"} end
+ end
+ if info == nil or info.markerId == nil then return {action = "wait"} end
+ local role = ADFieldModel.wantsRig(field, active, now)
+ if role ~= nil then return {action = "dispatch", role = role} end
+ return {action = "none"}
+end
+
+-- ---- tilstand ----
+
+local function log(fieldId, fmt, ...)
+ Logging.info(ADFieldJobs.LOG_PREFIX .. "F%s " .. fmt, tostring(fieldId), ...)
+end
+
+function ADFieldJobs.getField(id)
+ return ADFieldJobs.state.fields[id] or ADFieldModel.newField(id)
+end
+
+local function setField(field)
+ ADFieldJobs.state.fields[field.id] = field
+end
+
+function ADFieldJobs.assignmentsFor(fieldId)
+ local list = {}
+ for _, assignment in pairs(ADFieldJobs.state.assignments) do
+ if assignment.fieldId == fieldId then table.insert(list, assignment) end
+ end
+ return list
+end
+
+function ADFieldJobs.save()
+ local ok, err = pcall(ADFieldStorage.save, ADFieldJobs.state)
+ if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke gemmes: %s", tostring(err)) end
+end
+
+function ADFieldJobs.load()
+ local ok, loaded = pcall(ADFieldStorage.load)
+ if ok then
+ ADFieldJobs.state.fields, ADFieldJobs.state.fleet = loaded.fields, loaded.fleet
+ else
+ Logging.warning(ADFieldJobs.LOG_PREFIX .. "marker kunne ikke indlæses: %s", tostring(loaded))
+ end
+ ADFieldJobs.loaded = true
+end
+
+function ADFieldJobs.setPlan(id, changes)
+ setField(ADFieldModel.with(ADFieldJobs.getField(id), {plan = changes}))
+ ADFieldJobs.save()
+end
+
+function ADFieldJobs.requestStart(id)
+ setField(ADFieldModel.with(ADFieldJobs.getField(id), {startRequested = true}))
+end
+
+local function note(fieldId, changes)
+ setField(ADFieldModel.with(ADFieldJobs.getField(fieldId), changes))
+end
+
+function ADFieldJobs.toggleFleet(vehicleId)
+ ADFieldJobs.state.fleet = ADFieldFleet.toggle(ADFieldJobs.state.fleet, vehicleId)
+ ADFieldJobs.save()
+end
+
+-- ---- AutoDrive ----
+
+local function stateModule(vehicle)
+ return vehicle.ad ~= nil and vehicle.ad.stateModule or nil
+end
+
+local function isAdActive(vehicle)
+ local module = stateModule(vehicle)
+ return module ~= nil and module:isActive()
+end
+
+local function stopAd(vehicle)
+ if isAdActive(vehicle) then vehicle:stopAutoDrive() end
+end
+
+local function startAd(adEnv, vehicle, mode, firstId, secondId, withCp)
+ local module = stateModule(vehicle)
+ stopAd(vehicle)
+ module:setMode(mode)
+ module:setFirstMarkerByWayPointId(firstId)
+ if secondId ~= nil then module:setSecondMarkerByWayPointId(secondId) end
+ module:setStartHelper(withCp == true)
+ if withCp then
+ module.usedHelper = adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
+ if vehicle.ad.settings ~= nil and vehicle.ad.settings.enableParkAtJobFinished ~= nil then
+ vehicle.ad.settings.enableParkAtJobFinished.current = 2
+ vehicle.ad.settings.enableParkAtJobFinished.new = 2
+ end
+ end
+ local farmId = vehicle:getOwnerFarmId()
+ if module.setActualFarmId ~= nil then module:setActualFarmId(farmId) end
+ module:raiseDirtyFlag()
+ local current = module:getCurrentMode()
+ if current ~= nil and current.start ~= nil then
+ current:start(adEnv.AutoDrive.USER_PLAYER or 1)
+ else
+ vehicle:startAutoDrive()
+ end
+end
+
+local function parkMarkerId(adEnv, vehicle)
+ local module = stateModule(vehicle)
+ local index = module ~= nil and module.getParkDestinationAtJobFinished ~= nil and module:getParkDestinationAtJobFinished() or nil
+ if index == nil or index < 1 then return nil end
+ local marker = adEnv.ADGraphManager:getMapMarkerById(index)
+ return marker ~= nil and marker.id or nil
+end
+
+local function restLoad(adEnv, vehicle)
+ local ok, level = pcall(function()
+ return (adEnv.AutoDrive.getAllFillLevels(adEnv.AutoDrive.getAllUnits(vehicle)))
+ end)
+ return ok and tonumber(level) or 0
+end
+
+local function vehiclePosition(vehicle)
+ local x, _, z = getWorldTranslation(vehicle.rootNode)
+ return x, z
+end
+
+local function isControlledByPlayer(vehicle)
+ return (vehicle.getIsControlled ~= nil and vehicle:getIsControlled()) or false
+end
+
+-- ---- flåde ----
+
+function ADFieldJobs.getRigs(adEnv)
+ local rigs = {}
+ for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
+ local role = ADFieldFleet.roleFromSpecs(ADCpBridge.getSpecs(vehicle))
+ local id = ADRunsController.getVehicleId(vehicle)
+ if role ~= nil and id ~= nil then
+ local x, z = vehiclePosition(vehicle)
+ table.insert(rigs, {
+ id = id, vehicle = vehicle, role = role, label = ADRuns.vehicleLabel(vehicle),
+ enabled = ADFieldJobs.state.fleet[id] == true,
+ busy = ADFieldJobs.state.assignments[id] ~= nil or ADRunsController.getRunForVehicle(vehicle) ~= nil,
+ controlled = isControlledByPlayer(vehicle),
+ width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
+ })
+ end
+ end
+ return rigs
+end
+
+-- ---- silo ----
+
+local function chooseStore(adEnv, field, info, vehicle)
+ local fillType = field.plan.product == "hay" and info.hay or info.windrow
+ local isReachable = function(markerId) return ADUnloadWait.hasRouteTo(adEnv, vehicle, markerId) end
+ if #field.plan.storeBuildingIds > 0 then
+ local markerIds = {}
+ for _, buildingId in ipairs(field.plan.storeBuildingIds) do
+ local building = ADBuildings.find(adEnv, vehicle:getOwnerFarmId(), buildingId)
+ for _, marker in ipairs(building ~= nil and ADBuildings.getMarkers(building, "unload") or {}) do
+ table.insert(markerIds, marker.id)
+ end
+ end
+ return ADLoadSwap.findForcedDumpMarker(adEnv, vehicle, fillType, 1, markerIds, isReachable)
+ end
+ return ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, 1, info.markerId, isReachable, nil, nil, false)
+end
+
+-- ---- udsendelse og opfølgning ----
+
+local function release(assignment, phase)
+ assignment.phase = phase or "released"
+ ADFieldJobs.state.assignments[assignment.vehicleId] = nil
+end
+
+local function dispatch(adEnv, field, info, role)
+ local candidates = ADFieldJobs.getRigs(adEnv)
+ local rig, reason, args = ADFieldFleet.pick(candidates, role, info.x, info.z, field.mowWidth)
+ if rig == nil then
+ note(field.id, {reason = {key = "spu_field_reason_" .. reason, role = role, args = args}})
+ return
+ end
+ local assignment = {vehicleId = rig.id, vehicle = rig.vehicle, fieldId = field.id, role = role, phase = "driving", since = ADFieldJobs.now, width = rig.width}
+ ADFieldJobs.state.assignments[rig.id] = assignment
+ startAd(adEnv, rig.vehicle, adEnv.AutoDrive.MODE_DRIVETO, info.markerId, nil, false)
+ log(field.id, "%s '%s' sendt til %s (%.1f m)", role, rig.label, tostring(info.markerName), rig.width)
+ note(field.id, {reason = false})
+end
+
+local function beginWork(adEnv, field, info, assignment)
+ local vehicle = assignment.vehicle
+ if assignment.role == "collector" then
+ local store, why = chooseStore(adEnv, field, info, vehicle)
+ if store == nil then
+ release(assignment)
+ note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(why)}})
+ log(field.id, "intet lager: %s", tostring(why))
+ return
+ end
+ assignment.storeId = store.id
+ -- AD overtager ved "fuld": Hent og aflever F -> silo, hjælper = Courseplay
+ local module = stateModule(vehicle)
+ module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
+ module:setFirstMarkerByWayPointId(info.markerId)
+ module:setSecondMarkerByWayPointId(store.id)
+ module:setStartHelper(true)
+ module.usedHelper = adEnv.ADStateModule ~= nil and adEnv.ADStateModule.HELPER_CP or 1
+ if vehicle.ad.settings ~= nil and vehicle.ad.settings.enableParkAtJobFinished ~= nil then
+ vehicle.ad.settings.enableParkAtJobFinished.current = 2
+ vehicle.ad.settings.enableParkAtJobFinished.new = 2
+ end
+ module:raiseDirtyFlag()
+ log(field.id, "læssevogn '%s' aflæsser i %s", ADRuns.vehicleLabel(vehicle), tostring(store.name))
+ end
+ local point = info.points[1] or {x = info.x, z = info.z}
+ assignment.phase = "starting"
+ assignment.since = ADFieldJobs.now
+ local handle, err = ADCpBridge.start(vehicle, point.x, point.z, function(ok, reason)
+ if ok then
+ assignment.phase = "working"
+ assignment.since = ADFieldJobs.now
+ if assignment.role == "mower" then
+ note(field.id, {mowWidth = assignment.width})
+ end
+ log(field.id, "Courseplay startet (%s)", assignment.role)
+ else
+ release(assignment)
+ note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(reason)}})
+ log(field.id, "Courseplay fejlede: %s", tostring(reason))
+ end
+ end)
+ if handle == nil then
+ release(assignment)
+ log(field.id, "Courseplay kunne ikke startes: %s", tostring(err))
+ note(field.id, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = tostring(err)}})
+ return
+ end
+ assignment.handle = handle
+end
+
+local function follow(adEnv, assignment)
+ local vehicle = assignment.vehicle
+ if isControlledByPlayer(vehicle) then
+ if assignment.handle ~= nil then assignment.handle.cancelled = true end
+ log(assignment.fieldId, "'%s' frigivet — spilleren kører det", ADRuns.vehicleLabel(vehicle))
+ return release(assignment)
+ end
+ local field = ADFieldJobs.getField(assignment.fieldId)
+ local info = ADFields.getInfo(assignment.fieldId)
+ if assignment.phase == "driving" then
+ if not isAdActive(vehicle) and ADFieldJobs.now - assignment.since > 2000 then
+ local x, z = vehiclePosition(vehicle)
+ local wayPoint = adEnv.ADGraphManager:getWayPointById(info.markerId)
+ local distance = wayPoint ~= nil and math.sqrt((wayPoint.x - x) ^ 2 + (wayPoint.z - z) ^ 2) or math.huge
+ if distance <= ADFieldJobs.ARRIVE_DISTANCE then
+ beginWork(adEnv, field, info, assignment)
+ else
+ log(assignment.fieldId, "'%s' nåede ikke %s (%.0f m væk)", ADRuns.vehicleLabel(vehicle), tostring(info.markerName), distance)
+ release(assignment)
+ note(assignment.fieldId, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS,
+ reason = {text = string.format("%s nåede ikke %s", ADRuns.vehicleLabel(vehicle), tostring(info.markerName))}})
+ end
+ end
+ elseif assignment.phase == "starting" then
+ if ADFieldJobs.now - assignment.since > ADFieldJobs.START_TIMEOUT_MS then
+ if assignment.handle ~= nil then assignment.handle.cancelled = true end
+ release(assignment)
+ note(assignment.fieldId, {retryAt = ADFieldJobs.now + ADFieldModel.RETRY_MS, reason = {text = "Courseplay svarede ikke (60 s)"}})
+ log(assignment.fieldId, "Courseplay svarede ikke inden 60 s")
+ end
+ elseif assignment.phase == "working" then
+ local busy = ADCpBridge.isWorking(vehicle) or isAdActive(vehicle)
+ if busy then
+ assignment.idleSince = nil
+ else
+ assignment.idleSince = assignment.idleSince or ADFieldJobs.now
+ local rest = assignment.role == "collector" and assignment.storeId ~= nil and not assignment.deliveredRest and restLoad(adEnv, vehicle) or 0
+ if rest > 0 and ADFieldJobs.now - assignment.idleSince >= ADFieldJobs.IDLE_CONFIRM_MS then
+ -- CP færdig med læs på: AD tømmer kun selv med parkering sat, så vi sender resten til silo
+ assignment.deliveredRest = true
+ assignment.idleSince = nil
+ startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, assignment.storeId, nil, false)
+ log(assignment.fieldId, "'%s' kører rest-læs (%.0f l) til silo", ADRuns.vehicleLabel(vehicle), rest)
+ elseif ADFieldJobs.now - assignment.idleSince >= ADFieldJobs.IDLE_CONFIRM_MS then
+ log(assignment.fieldId, "%s '%s' færdig", assignment.role, ADRuns.vehicleLabel(vehicle))
+ local park = assignment.role ~= "collector" and parkMarkerId(adEnv, vehicle) or nil
+ if park ~= nil then startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil, false) end
+ assignment.phase = "finished"
+ ADFieldJobs.finished = ADFieldJobs.finished or {}
+ table.insert(ADFieldJobs.finished, assignment)
+ ADFieldJobs.state.assignments[assignment.vehicleId] = nil
+ end
+ end
+ end
+end
+
+function ADFieldJobs.stopField(adEnv, id)
+ for _, assignment in ipairs(ADFieldJobs.assignmentsFor(id)) do
+ if assignment.handle ~= nil then assignment.handle.cancelled = true end
+ pcall(stopAd, assignment.vehicle)
+ pcall(function() if assignment.vehicle.stopCurrentAIJob ~= nil and ADCpBridge.isWorking(assignment.vehicle) then assignment.vehicle:stopCurrentAIJob(AIMessageSuccessStoppedByUser.new()) end end)
+ release(assignment)
+ end
+ setField(ADFieldModel.stop(ADFieldJobs.getField(id)))
+ ADFieldJobs.save()
+ log(id, "stoppet fra menuen")
+end
+
+local function finishedFor(fieldId)
+ local list = {}
+ for _, assignment in ipairs(ADFieldJobs.finished or {}) do
+ if assignment.fieldId == fieldId then table.insert(list, assignment) end
+ end
+ return list
+end
+
+local function stepField(adEnv, info)
+ local field = ADFieldModel.onReadiness(ADFieldJobs.getField(info.id), info.readyRatio or 0)
+ setField(field)
+ local assignments = ADFieldJobs.assignmentsFor(info.id)
+ for _, done in ipairs(finishedFor(info.id)) do table.insert(assignments, done) end
+ local decision = ADFieldJobs.decide(field, info, assignments, ADFieldJobs.now)
+ if decision.action == "start" then
+ setField(ADFieldModel.start(field))
+ log(info.id, "starter (%s)", field.plan.product)
+ ADFieldJobs.save()
+ elseif decision.action == "advance" then
+ local nextField = ADFieldModel.advance(field)
+ setField(nextField)
+ local remaining = {}
+ for _, done in ipairs(ADFieldJobs.finished or {}) do
+ if done.fieldId ~= info.id then table.insert(remaining, done) end
+ end
+ ADFieldJobs.finished = remaining
+ log(info.id, "%s -> %s", field.stage, nextField.stage)
+ ADFieldJobs.save()
+ elseif decision.action == "dispatch" then
+ dispatch(adEnv, field, info, decision.role)
+ end
+end
+
+function ADFieldJobs.tick(adEnv, dt)
+ ADFieldJobs.now = ADFieldJobs.now + dt
+ ADFieldJobs.timer = ADFieldJobs.timer + dt
+ if ADFieldJobs.timer < ADFieldJobs.TICK_MS then return end
+ ADFieldJobs.timer = 0
+ if not ADFieldJobs.loaded then ADFieldJobs.load() end
+ local farmId = g_currentMission:getFarmId()
+ ADFields.scanNext(adEnv, farmId)
+ for _, assignment in pairs(ADFieldJobs.state.assignments) do
+ local ok, err = pcall(follow, adEnv, assignment)
+ if not ok then
+ Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s opfølgning fejlede: %s", tostring(assignment.fieldId), tostring(err))
+ release(assignment)
+ end
+ end
+ for _, info in ipairs(ADFields.list()) do
+ if info.mowable or ADFieldModel.isWorking(ADFieldJobs.getField(info.id)) then
+ local ok, err = pcall(stepField, adEnv, info)
+ if not ok then Logging.warning(ADFieldJobs.LOG_PREFIX .. "F%s fejl: %s", tostring(info.id), tostring(err)) end
+ end
+ end
+end
diff --git a/FS25_ADSmartPickup/adFieldStorage.lua b/FS25_ADSmartPickup/adFieldStorage.lua
new file mode 100644
index 0000000..16f0d6e
--- /dev/null
+++ b/FS25_ADSmartPickup/adFieldStorage.lua
@@ -0,0 +1,64 @@
+-- AD Field Storage
+-- Markernes trin/plan og markflåden gemmes i savegame-mappen ved siden af runs.
+
+ADFieldStorage = {}
+
+ADFieldStorage.FILE_NAME = "ADSmartPickup_fields.xml"
+ADFieldStorage.ROOT = "ADSmartPickupFields"
+
+function ADFieldStorage.getPath()
+ local runsPath = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
+ if runsPath == nil then return nil end
+ return (string.gsub(runsPath, ADRunsStorage.FILE_NAME .. "$", ADFieldStorage.FILE_NAME))
+end
+
+function ADFieldStorage.save(state)
+ local path = ADFieldStorage.getPath()
+ if path == nil then return false end
+ local xmlFile = XMLFile.create("ADSmartPickupFields", path, ADFieldStorage.ROOT)
+ if xmlFile == nil then return false end
+ local index = 0
+ for _, field in pairs(state.fields) do
+ local record = ADFieldModel.toRecord(field)
+ local key = string.format("%s.field(%d)", ADFieldStorage.ROOT, index)
+ xmlFile:setInt(key .. "#id", record.id)
+ xmlFile:setString(key .. "#stage", record.stage)
+ xmlFile:setString(key .. "#mode", record.mode)
+ xmlFile:setString(key .. "#product", record.product)
+ xmlFile:setInt(key .. "#rake", record.rake)
+ xmlFile:setString(key .. "#store", record.store)
+ xmlFile:setFloat(key .. "#mowWidth", record.mowWidth)
+ index = index + 1
+ end
+ local vehicleIndex = 0
+ for vehicleId in pairs(state.fleet) do
+ xmlFile:setString(string.format("%s.fleet.vehicle(%d)#id", ADFieldStorage.ROOT, vehicleIndex), vehicleId)
+ vehicleIndex = vehicleIndex + 1
+ end
+ xmlFile:save()
+ xmlFile:delete()
+ return true
+end
+
+function ADFieldStorage.load()
+ local result = {fields = {}, fleet = {}}
+ local path = ADFieldStorage.getPath()
+ if path == nil or not fileExists(path) then return result end
+ local xmlFile = XMLFile.load("ADSmartPickupFields", path)
+ if xmlFile == nil then return result end
+ xmlFile:iterate(ADFieldStorage.ROOT .. ".field", function(_, key)
+ local field = ADFieldModel.fromRecord({
+ id = xmlFile:getInt(key .. "#id"), stage = xmlFile:getString(key .. "#stage"),
+ mode = xmlFile:getString(key .. "#mode"), product = xmlFile:getString(key .. "#product"),
+ rake = xmlFile:getInt(key .. "#rake"), store = xmlFile:getString(key .. "#store"),
+ mowWidth = xmlFile:getFloat(key .. "#mowWidth"),
+ })
+ if field.id ~= nil then result.fields[field.id] = field end
+ end)
+ xmlFile:iterate(ADFieldStorage.ROOT .. ".fleet.vehicle", function(_, key)
+ local id = xmlFile:getString(key .. "#id")
+ if id ~= nil and id ~= "" then result.fleet[id] = true end
+ end)
+ xmlFile:delete()
+ return result
+end
diff --git a/FS25_ADSmartPickup/adRunsController.lua b/FS25_ADSmartPickup/adRunsController.lua
index aa906ec..db0630e 100644
--- a/FS25_ADSmartPickup/adRunsController.lua
+++ b/FS25_ADSmartPickup/adRunsController.lua
@@ -1284,6 +1284,9 @@ function ADRunsController.save()
Logging.warning(ADRunsController.LOG_PREFIX .. "kunne ikke gemme runs: %s", tostring(saved))
return false
end
+ if ADFieldJobs ~= nil and ADFieldJobs.loaded then
+ pcall(ADFieldJobs.save)
+ end
return saved
end
diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua
index 717a9dc..c9e1108 100644
--- a/FS25_ADSmartPickup/adSmartPickup.lua
+++ b/FS25_ADSmartPickup/adSmartPickup.lua
@@ -1208,6 +1208,12 @@ function ADSmartPickup:update(dt)
if ADRunsController ~= nil and ADRunsController.applyVehicleNamesThrottled ~= nil then
pcall(ADRunsController.applyVehicleNamesThrottled, getAutoDriveEnv(), dt)
end
+ if ADFieldJobs ~= nil then
+ local okFields, errFields = pcall(ADFieldJobs.tick, getAutoDriveEnv(), dt)
+ if not okFields then
+ Logging.warning(ADSmartPickup.LOG_PREFIX .. "markarbejde fejlede: %s", tostring(errFields))
+ end
+ end
local ok, err = pcall(restoreStoppedVehicles)
if not ok then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke gendanne varevalg: %s", tostring(err))
diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml
index 8206710..dcb1442 100644
--- a/FS25_ADSmartPickup/modDesc.xml
+++ b/FS25_ADSmartPickup/modDesc.xml
@@ -26,6 +26,13 @@
+
+
+
+
+
+
+
diff --git a/docs/superpowers/plans/2026-09-25-markarbejde-etape1.md b/docs/superpowers/plans/2026-09-25-markarbejde-etape1.md
index 9abf19a..3148152 100644
--- a/docs/superpowers/plans/2026-09-25-markarbejde-etape1.md
+++ b/docs/superpowers/plans/2026-09-25-markarbejde-etape1.md
@@ -1088,7 +1088,8 @@ local mowing = M.start(auto)
local wanted = J.decide(mowing, info, {}, 0)
check("J2 send slåmaskine", wanted.action, "dispatch"); check("J2 rolle", wanted.role, "mower")
check("J2 sæt i gang -> ingenting", J.decide(mowing, info, {{phase = "working"}}, 0).action, "none")
-check("J2 færdig -> videre", J.decide(mowing, info, {{phase = "finished"}}, 0).action, "advance")
+local mowedInfo = {id = 109, markerId = 5, mowable = true, readyRatio = 0}
+check("J2 færdig -> videre", J.decide(mowing, mowedInfo, {{phase = "finished"}}, 0).action, "advance")
-- J3: sæt frigivet (spilleren tog det) tæller ikke som færdigt
check("J3 frigivet -> send nyt", J.decide(mowing, info, {{phase = "released"}}, 0).action, "dispatch")
-- J4: slåning færdig men marken stadig klar -> ikke videre (afbrudt)
diff --git a/tests/test_adFieldJobs.lua b/tests/test_adFieldJobs.lua
new file mode 100644
index 0000000..d4b18e5
--- /dev/null
+++ b/tests/test_adFieldJobs.lua
@@ -0,0 +1,37 @@
+-- Kør: luajit tests/test_adFieldJobs.lua (fra repo-roden)
+dofile("FS25_ADSmartPickup/adFieldModel.lua")
+dofile("FS25_ADSmartPickup/adFieldJobs.lua")
+local J = ADFieldJobs
+local M = ADFieldModel
+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
+local info = {id = 109, markerId = 5, mowable = true, readyRatio = 1}
+
+-- J1: klar auto-mark med markør -> start; uden markør -> vent
+local auto = M.onReadiness(M.newField(109), 1); auto.plan.mode = "auto"
+check("J1 start", J.decide(auto, info, {}, 0).action, "start")
+check("J1 uden markør", J.decide(auto, {id = 109, mowable = true}, {}, 0).action, "wait")
+
+-- J2: arbejdende trin uden sæt -> send rolle; alle sæt færdige -> næste trin
+local mowing = M.start(auto)
+local wanted = J.decide(mowing, info, {}, 0)
+check("J2 send slåmaskine", wanted.action, "dispatch"); check("J2 rolle", wanted.role, "mower")
+check("J2 sæt i gang -> ingenting", J.decide(mowing, info, {{phase = "working"}}, 0).action, "none")
+local mowedInfo = {id = 109, markerId = 5, mowable = true, readyRatio = 0}
+check("J2 færdig -> videre", J.decide(mowing, mowedInfo, {{phase = "finished"}}, 0).action, "advance")
+-- J3: sæt frigivet (spilleren tog det) tæller ikke som færdigt
+check("J3 frigivet -> send nyt", J.decide(mowing, info, {{phase = "released"}}, 0).action, "dispatch")
+-- J4: slåning færdig men marken stadig klar -> ikke videre (afbrudt)
+check("J4 afbrudt slåning", J.decide(mowing, {id = 109, markerId = 5, mowable = true, readyRatio = 0.8}, {{phase = "finished"}}, 0).action, "dispatch")
+-- J5: opsamling efter genindlæsning uden sæt -> send læssevogn
+local collecting = M.advance(mowing)
+check("J5 genoptag opsamling", J.decide(collecting, info, {}, 0).role, "collector")
+check("J5 to vogne maks", J.decide(collecting, info, {{phase = "working"}, {phase = "driving"}}, 0).action, "none")
+
+print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
+os.exit(failures == 0 and 0 or 1)