From 5c912c524e31fb15d9fedc819ac8c7d30b6b7407 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Fri, 25 Sep 2026 21:50:30 +0200 Subject: [PATCH] =?UTF-8?q?feat(marker):=20job-l=C3=B8kke=20=E2=80=94=20Au?= =?UTF-8?q?toDrive=20til=20F-mark=C3=B8r,=20Courseplay=20p=C3=A5=20marken,?= =?UTF-8?q?=20n=C3=A6ste=20trin,=20gem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Afvigelser fra planen: starting-timeout 60 s og pause efter 'nåede ikke markøren' (undgår genudsendelse hvert sekund); J2-testen bruger en slået mark (readyRatio 0). --- FS25_ADSmartPickup/adFieldJobs.lua | 396 ++++++++++++++++++ FS25_ADSmartPickup/adFieldStorage.lua | 64 +++ FS25_ADSmartPickup/adRunsController.lua | 3 + FS25_ADSmartPickup/adSmartPickup.lua | 6 + FS25_ADSmartPickup/modDesc.xml | 7 + .../plans/2026-09-25-markarbejde-etape1.md | 3 +- tests/test_adFieldJobs.lua | 37 ++ 7 files changed, 515 insertions(+), 1 deletion(-) create mode 100644 FS25_ADSmartPickup/adFieldJobs.lua create mode 100644 FS25_ADSmartPickup/adFieldStorage.lua create mode 100644 tests/test_adFieldJobs.lua 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)