From de36abe1faff2d5d6ff614904923da0cdad6ee11 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Wed, 23 Sep 2026 09:57:59 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20controller=20=E2=80=94=20udk=C3=B8rsels?= =?UTF-8?q?-runs:=20start,=20validering,=20produkter,=20kilder,=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adRunsController.lua | 182 ++++++++++++++++++++++-- tests/test_adRunsController.lua | 86 +++++++++++ 2 files changed, 256 insertions(+), 12 deletions(-) diff --git a/FS25_ADSmartPickup/adRunsController.lua b/FS25_ADSmartPickup/adRunsController.lua index 3385029..5e765fe 100644 --- a/FS25_ADSmartPickup/adRunsController.lua +++ b/FS25_ADSmartPickup/adRunsController.lua @@ -73,8 +73,8 @@ end -- ---- store-wrappers (erstatter altid hele store'en; modellen muterer aldrig) ---- -function ADRunsController.createRun(name) - local store, run = ADRuns.createRun(ADRunsController.store, name) +function ADRunsController.createRun(name, kind) + local store, run = ADRuns.createRun(ADRunsController.store, name, kind) ADRunsController.store = store return run end @@ -129,6 +129,9 @@ end -- findSupplyPickup selv til de ønskede produktioners inputs bagefter. -- Run'ets mål som liste (flere stalde pr. run); gamle runs har kun targetWayPointId. function ADRunsController.getRunTargets(run) + if run.kind == "outbound" then + return {} + end if run.targetWayPointIds ~= nil and #run.targetWayPointIds > 0 then return run.targetWayPointIds end @@ -272,10 +275,144 @@ local function intersects(listA, listB) return false end --- reason: "noTarget" | "noTrailer" | "cannotCarry" | "farFromNetwork" (l10n-nøgler i GUI'et). +local function hasAutoload(adEnv, vehicle) + local trailers = adEnv.AutoDrive.getAllUnits ~= nil and adEnv.AutoDrive.getAllUnits(vehicle) or nil + for _, trailer in ipairs(trailers or {}) do + if adEnv.AutoDrive.hasAL ~= nil and adEnv.AutoDrive:hasAL(trailer) then + return true + end + end + return false +end + +-- Er langt fra vejnettet? (AD's getDistanceFromNetwork kalder vehicle:getClosestWayPoint(), +-- som ikke alle AD-køretøjer har — set in-game på en høster — så uden metoden springes tjekket over.) +local function isFarFromNetwork(adEnv, vehicle) + local distance = 0 + if vehicle.getClosestWayPoint ~= nil and adEnv.ADGraphManager.getDistanceFromNetwork ~= nil then + local ok, value = pcall(adEnv.ADGraphManager.getDistanceFromNetwork, adEnv.ADGraphManager, vehicle) + distance = ok and value or 0 + end + return distance == nil or distance > ADRunsController.MAX_NETWORK_DISTANCE +end + +-- ---- udkørsel ---- + +-- Kilder ved markørerne (fabrik/silo/objektlager) — {source} i run'ets rækkefølge; ukendte udelades. +local function getRunSources(adEnv, run, farmId) + local sources = {} + if ADSources == nil then + return sources + end + for _, wayPointId in ipairs(run.sourceWayPointIds or {}) do + local source = ADSources.identify(adEnv, wayPointId, farmId) + if source ~= nil then + table.insert(sources, source) + end + end + return sources +end + +-- Alle varer kilderne rummer/producerer med run'ets politik (uanset vogn) — til fanen Udkørsel. +-- {fillType, name, title, policy, isPallet, available, level, capacity}, sorteret på titel. +function ADRunsController.getRunProducts(adEnv, run) + local farmId = g_currentMission:getFarmId() + local byName, result = {}, {} + for _, source in ipairs(getRunSources(adEnv, run, farmId)) do + for _, item in ipairs(ADSources.getProducts(adEnv, source, farmId)) do + local product = byName[item.name] + if product == nil then + product = {fillType = item.fillType, name = item.name, title = item.title or item.name, policy = ADRuns.getProductPolicy(run, item.name), isPallet = item.isPallet, available = 0, level = 0, capacity = 0} + byName[item.name] = product + table.insert(result, product) + end + product.available = product.available + (item.available or 0) + product.level = product.level + (item.level or 0) + product.capacity = product.capacity + (item.capacity or 0) + end + end + table.sort(result, function(a, b) return tostring(a.title) < tostring(b.title) end) + return result +end + +-- Run'ets varer (fillType-indeks) som denne vogn kan bære: bulk via AD's liste, paller via UAL. +local function getOutboundFillTypes(adEnv, run, vehicle) + local carriable = {} + for _, fillType in ipairs(ADRunsController.getCarriableFillTypes(adEnv, vehicle)) do + carriable[fillType] = true + end + local canLoadPallets = hasAutoload(adEnv, vehicle) + local result, palletCount, chosen = {}, 0, 0 + for _, product in ipairs(ADRunsController.getRunProducts(adEnv, run)) do + if product.policy ~= "off" then + chosen = chosen + 1 + if product.isPallet then + palletCount = palletCount + 1 + if canLoadPallets then table.insert(result, product.fillType) end + elseif carriable[product.fillType] then + table.insert(result, product.fillType) + end + end + end + return result, chosen, palletCount, canLoadPallets +end + +local function validateOutboundVehicle(adEnv, run, vehicle, result) + if #getRunSources(adEnv, run, ADSmartPickup.getFarmId(vehicle)) == 0 then + result.reason = "noSource" + return result + end + if result.capacity == nil then + result.reason = "noTrailer" + return result + end + local fillTypes, chosen, palletCount, canLoadPallets = getOutboundFillTypes(adEnv, run, vehicle) + if chosen == 0 then + result.reason = "noProducts" + return result + end + if #fillTypes == 0 then + result.reason = (palletCount == chosen and not canLoadPallets) and "needsAutoload" or "cannotCarry" + return result + end + if isFarFromNetwork(adEnv, vehicle) then + result.reason = "farFromNetwork" + return result + end + result.ok = true + return result +end + +-- Markører med en genkendelig kilde: {marker, kind} sorteret på navn — dropdown/afkrydsning i fanen. +function ADRunsController.getSourceMarkers(adEnv, farmId) + local result = {} + if ADSources == nil then + return result + end + for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do + local source = ADSources.identify(adEnv, marker.id, farmId) + if source ~= nil then + table.insert(result, {marker = marker, kind = source.kind, name = source.name}) + end + end + table.sort(result, function(a, b) return tostring(a.marker.name) < tostring(b.marker.name) end) + return result +end + +-- Seneste planlægger-begrundelser og advarsel for run'et (fra ADOutbound) + produkter. +function ADRunsController.getOutboundStatus(adEnv, run) + local status = ADOutbound ~= nil and ADOutbound.getRunStatus ~= nil and ADOutbound.getRunStatus(run.id) or {reasons = {}} + return {reasons = status.reasons or {}, warning = status.warning, products = ADRunsController.getRunProducts(adEnv, run)} +end + +-- reason: "noTarget" | "noTrailer" | "cannotCarry" | "farFromNetwork" (l10n-nøgler i GUI'et); +-- udkørsel desuden "noSource" | "noProducts" | "needsAutoload". function ADRunsController.validateVehicle(adEnv, run, vehicle) local carriable = ADRunsController.getCarriableFillTypes(adEnv, vehicle) local result = {ok = false, carriable = carriable, capacity = getLoadCapacity(adEnv, vehicle)} + if run.kind == "outbound" then + return validateOutboundVehicle(adEnv, run, vehicle, result) + end local hasTarget = false for _, wayPointId in ipairs(ADRunsController.getRunTargets(run)) do if adEnv.ADGraphManager:getMapMarkerByWayPointId(wayPointId) ~= nil then @@ -294,14 +431,7 @@ function ADRunsController.validateVehicle(adEnv, run, vehicle) result.reason = "cannotCarry" return result end - -- AD's getDistanceFromNetwork kalder vehicle:getClosestWayPoint(), som ikke alle - -- AD-køretøjer har (set in-game på en høster) — uden metoden springes tjekket over. - local distance = 0 - if vehicle.getClosestWayPoint ~= nil and adEnv.ADGraphManager.getDistanceFromNetwork ~= nil then - local ok, value = pcall(adEnv.ADGraphManager.getDistanceFromNetwork, adEnv.ADGraphManager, vehicle) - distance = ok and value or 0 - end - if distance == nil or distance > ADRunsController.MAX_NETWORK_DISTANCE then + if isFarFromNetwork(adEnv, vehicle) then result.reason = "farFromNetwork" return result end @@ -386,7 +516,30 @@ local function findNearestLoadMarker(adEnv, vehicle, farmId) return best end +-- Udkørsel: 1. og 2. markør = første kilde (pladsholder — kilde og mål vælges pr. tur af +-- ADOutbound), varevalg = run'ets varer vognen kan bære, rotation "kun pålæsning" (hook'en). +local function applyOutboundRunToVehicle(adEnv, run, vehicle) + local stateModule = vehicle.ad.stateModule + stateModule:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER) + local firstId = run.sourceWayPointIds[1] + stateModule:setFirstMarkerByWayPointId(firstId) + stateModule:setSecondMarkerByWayPointId(firstId) + local fillTypes = getOutboundFillTypes(adEnv, run, vehicle) + stateModule.selectedFillTypes = copyList(fillTypes) + if fillTypes[1] ~= nil then + stateModule:setFillType(fillTypes[1]) + end + stateModule.loopCounter = 0 + stateModule:setLoopsDone(0) + setSetting(adEnv, vehicle, "useFolders", true) + setSetting(adEnv, vehicle, "rotateTargets", adEnv.AutoDrive.RT_ONLYPICKUP) + stateModule:raiseDirtyFlag() +end + local function applyRunToVehicle(adEnv, run, vehicle, ingredients) + if run.kind == "outbound" then + return applyOutboundRunToVehicle(adEnv, run, vehicle) + end local stateModule = vehicle.ad.stateModule local farmId = ADSmartPickup.getFarmId(vehicle) stateModule:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER) @@ -465,7 +618,8 @@ function ADRunsController.startRun(adEnv, runId) local ingredients = ADRunsController.getRunIngredients(adEnv, run, vehicle) local ok, err = pcall(applyAndStart, adEnv, run, vehicle, ingredients) if ok then - Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' startet mod markør %s", tostring(run.name), tostring(vehicle:getName()), tostring(run.targetWayPointId)) + local target = run.kind == "outbound" and ("kilde " .. tostring(run.sourceWayPointIds[1])) or ("markør " .. tostring(run.targetWayPointId)) + Logging.info(ADRunsController.LOG_PREFIX .. "run '%s': '%s' startet mod %s", tostring(run.name), tostring(vehicle:getName()), target) else result.ok, result.reason = false, "error" Logging.warning(ADRunsController.LOG_PREFIX .. "run '%s': kunne ikke starte '%s': %s", tostring(run.name), tostring(vehicle:getName()), tostring(err)) @@ -539,6 +693,10 @@ function ADRunsController.getVehicleActivity(adEnv, vehicle) if wait ~= nil then return {kind = "waiting", fillType = wait.fillType, liters = wait.rest, place = wait.markerName} end + local hold = ADOutbound ~= nil and ADOutbound.getHoldFor ~= nil and ADOutbound.getHoldFor(vehicle) or nil + if hold ~= nil then + return {kind = "holding", place = hold.markerName, reason = hold.reasons ~= nil and hold.reasons[1] or nil} + end local fillType, liters = nil, 0 if ADUnloadWait ~= nil and ADUnloadWait.getRestLoad ~= nil then fillType, liters = ADUnloadWait.getRestLoad(adEnv, vehicle) diff --git a/tests/test_adRunsController.lua b/tests/test_adRunsController.lua index 845a83c..c87cdc3 100644 --- a/tests/test_adRunsController.lua +++ b/tests/test_adRunsController.lua @@ -412,5 +412,91 @@ ADRunsController.startRun(adEnv, sugar.id) check("C18 rigtig last -> målet", sugarVehicle.ad.stateModule.secondMarkerId, 77) ADUnloadWait = nil; ADLoadSwap = nil +-- C-out: udkørsels-runs — oprettelse, kilder, produkter, validering, start, status +local BASKET = 7 +FILL_TYPE_NAMES[BASKET] = "STRAW_BASKET"; FILL_TYPE_INDEX["STRAW_BASKET"] = BASKET +local sourcesAt, productsAt = {}, {} +ADSources = { + identify = function(_, id) return sourcesAt[id] end, + getProducts = function(_, source) return productsAt[source.wayPointId] or {} end, + getSellMarkers = function() return {} end, + stationAccepts = function() return false end, + getSourcePlaceables = function() return {} end, +} +local outboundStatus = {reasons = {"SUGAR 78 % af top (kræver 90), lager 41 %"}, warning = nil} +local holdFor = {} +ADOutbound = { + isOutboundVehicle = function(v) local r = ADRunsController.getRunForVehicle(v) return r ~= nil and r.kind == "outbound" end, + getRunStatus = function() return outboundStatus end, + getHoldFor = function(v) return holdFor[v] end, + getTripFor = function() return nil end, + releaseInactive = function() end, +} +ADUnloadWait = {getRestLoad = function() return nil, 0 end, getWaitFor = function() return nil end} +buildWorld() +addMarker(1, "Suger Load", "Pickup points", 100, 0); loadAt[1] = {} +addMarker(3, "Objektlager", "Pickup points", 300, 0) +addMarker(10, "selling station", "selling", 500, 0) +sourcesAt[1] = {kind = "factory", wayPointId = 1, station = loadAt[1], name = "Sugar mill"} +sourcesAt[3] = {kind = "objectStorage", wayPointId = 3, name = "Objektlager"} +productsAt[1] = {{fillType = TMR, name = "FORAGE", title = "TMR", available = 150000, level = 150000, capacity = 180000, isPallet = false}} +productsAt[3] = {{fillType = BASKET, name = "STRAW_BASKET", title = "Halmkurve", available = 8000, level = 8, capacity = 4000, isPallet = true}} +local bulkTractor = makeVehicle("vehicleBulk", "MT635", {STRAW, TMR}, 20000, 100, 0) +local ualTractor = makeVehicle("vehicleUal", "Fendt", {}, 8000, 300, 0) +ualTractor.trailers[1] = {ual = true} +adEnv.AutoDrive.hasAL = function(_, trailer) return trailer.ual == true end + +local out = ADRunsController.createRun("Sugar out", "outbound") +check("Cout1 kind", ADRuns.getRun(ADRunsController.store, out.id).kind, "outbound") +check("Cout1 supply-run har kind supply", ADRunsController.createRun("Cows").kind, "supply") +check("Cout1 kilde-markører", #ADRunsController.getSourceMarkers(adEnv, 1), 2) +check("Cout1 første kilde-markør", ADRunsController.getSourceMarkers(adEnv, 1)[1].marker.name, "Objektlager") + +-- Cout2: validering +local outRun = ADRuns.getRun(ADRunsController.store, out.id) +check("Cout2 ingen kilder -> noSource", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "noSource") +ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, out.id, 1) +ADRunsController.store = ADRuns.toggleSource(ADRunsController.store, out.id, 3) +outRun = ADRuns.getRun(ADRunsController.store, out.id) +check("Cout2 ingen vare valgt -> noProducts", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "noProducts") +ADRunsController.store = ADRuns.setProductPolicy(ADRunsController.store, out.id, "STRAW_BASKET", "sell") +outRun = ADRuns.getRun(ADRunsController.store, out.id) +check("Cout2 kun paller uden UAL -> needsAutoload", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).reason, "needsAutoload") +check("Cout2 UAL-vogn ok", ADRunsController.validateVehicle(adEnv, outRun, ualTractor).ok, true) +ADRunsController.store = ADRuns.setProductPolicy(ADRunsController.store, out.id, "FORAGE", "store") +outRun = ADRuns.getRun(ADRunsController.store, out.id) +check("Cout2 bulk-vogn ok med TMR", ADRunsController.validateVehicle(adEnv, outRun, bulkTractor).ok, true) +local strawOnly = makeVehicle("vehicleStraw", "Halm", {STRAW}, 20000, 100, 0) +check("Cout2 kan ikke bære -> cannotCarry", ADRunsController.validateVehicle(adEnv, outRun, strawOnly).reason, "cannotCarry") + +-- Cout3: start sætter mode, 1./2. markør = første kilde, varevalg = det vognen kan bære af run'ets varer +ADRunsController.assignVehicle(out.id, bulkTractor) +ADRunsController.assignVehicle(out.id, ualTractor) +local results = ADRunsController.startRun(adEnv, out.id) +check("Cout3 to startet", #results, 2) +check("Cout3 mode P&D", bulkTractor.ad.stateModule.mode, MODE_PICKUPANDDELIVER) +check("Cout3 1. markør = første kilde", bulkTractor.ad.stateModule.firstMarkerId, 1) +check("Cout3 2. markør = første kilde (pladsholder)", bulkTractor.ad.stateModule.secondMarkerId, 1) +check("Cout3 bulk-vogn: kun TMR", table.concat(bulkTractor.ad.stateModule.selectedFillTypes, ","), tostring(TMR)) +check("Cout3 UAL-vogn: kurve", table.concat(ualTractor.ad.stateModule.selectedFillTypes, ","), tostring(BASKET)) +check("Cout3 rotation kun pålæsning", bulkTractor.ad.settings.rotateTargets.current, 2) +check("Cout3 startet", bulkTractor.started, 1) + +-- Cout4: produkter til fanen (politik slået op, sorteret på titel) og status +local list = ADRunsController.getRunProducts(adEnv, outRun) +check("Cout4 to produkter", #list, 2) +check("Cout4 første = Halmkurve", list[1].title, "Halmkurve") +check("Cout4 politik", list[1].policy, "sell") +check("Cout4 TMR lager", list[2].policy, "store") +check("Cout4 available", list[2].available, 150000) +local st = ADRunsController.getOutboundStatus(adEnv, outRun) +check("Cout4 status-begrundelse", st.reasons[1], "SUGAR 78 % af top (kræver 90), lager 41 %") +holdFor[bulkTractor] = {markerName = "Wait 3", reasons = {"SUGAR 78 % af top (kræver 90), lager 41 %"}} +local activity = ADRunsController.getVehicleActivity(adEnv, bulkTractor) +check("Cout4 aktivitet = holder", activity.kind, "holding") +check("Cout4 aktivitet sted", activity.place, "Wait 3") +check("Cout4 udkørsel har ingen mål", #ADRunsController.getRunTargets(outRun), 0) +ADUnloadWait = nil; ADOutbound = nil; ADSources = nil + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)