From 1b7b74ad2c529635e0f0544772cc0df932f56abb Mon Sep 17 00:00:00 2001 From: masterdraco Date: Thu, 24 Sep 2026 00:56:01 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20bedste=20foder=20f=C3=B8rst=20=E2=80=94?= =?UTF-8?q?=20lavere=20foder=20kun=20n=C3=A5r=20truget=20er=20n=C3=B8dlide?= =?UTF-8?q?nde=20(=E2=89=A575=20%=20fri);=20forsynings-traktorer=20uden=20?= =?UTF-8?q?noget=20at=20hente=20holder=20ved=20Wait=20(v1.14.0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adOutbound.lua | 21 ++++++++++++++++ FS25_ADSmartPickup/adSmartPickup.lua | 34 +++++++++++++++++++++++--- FS25_ADSmartPickup/adSupplyPlanner.lua | 22 +++++++++++++++++ FS25_ADSmartPickup/modDesc.xml | 2 +- tests/test_adSmartPickup.lua | 25 +++++++++++++++++++ tests/test_adSupplyPlanner.lua | 13 ++++++++++ 6 files changed, 112 insertions(+), 5 deletions(-) diff --git a/FS25_ADSmartPickup/adOutbound.lua b/FS25_ADSmartPickup/adOutbound.lua index 41bc612..2261eb2 100644 --- a/FS25_ADSmartPickup/adOutbound.lua +++ b/FS25_ADSmartPickup/adOutbound.lua @@ -421,6 +421,16 @@ local function startHold(adEnv, vehicle, run, reasons, sourcesById) return choiceFor(marker, source, nil) end +-- Forsynings-traktor uden noget at hente: hold ved et Wait-punkt i run'ets pulje; replan() returnerer +-- waypoint-id for næste kilde (eller nil) og kaldes hvert CHECK_INTERVAL_MS. +function ADOutbound.holdSupplyVehicle(adEnv, vehicle, run, reasons, replan) + local choice = startHold(adEnv, vehicle, run, reasons, {}) + if choice ~= nil and holds[vehicle] ~= nil then + holds[vehicle].replan = replan + end + return choice +end + -- Hook 1: AutoDrive spørger om næste pålæsning. Tur -> kilden; ingen tur -> Wait-punkt. function ADOutbound.choosePickup(adEnv, vehicle) local run = getRun(vehicle) @@ -494,6 +504,17 @@ local function updateHold(adEnv, task, hold, dt) return true end hold.timerMs = 0 + -- forsynings-traktor: egen genplanlægning (ADSmartPickup) giver en kilde-markør at køre til + if hold.replan ~= nil then + local ok, targetId = pcall(hold.replan) + if ok and targetId ~= nil then + releaseHold(vehicle) + ADWaitPool.redirect(task, targetId) + task.activatedUALLoading = false + Logging.info(ADOutbound.LOG_PREFIX .. "'%s' kører fra Wait: der er noget at hente igen", tostring(vehicle:getName())) + end + return true + end ADOutbound.choosePickup(adEnv, vehicle) local state = trips[vehicle] if state ~= nil then diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index dfa7562..7d12abc 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -292,6 +292,7 @@ local function findBestSource(markerStations, fillTypes, farmId) end return { markerIndex = choice.marker.markerIndex, + markerId = choice.marker.id, markerName = choice.marker.name, group = choice.marker.group, level = choice.level, @@ -592,11 +593,15 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients, targetWayPoi local allMarkers = adEnv.ADGraphManager:getMapMarkers() local markerStations = collectMarkerStations(adEnv, allMarkers, farmId, target.owningPlaceable) local loadCapacity = getLoadCapacity(adEnv, driver) - for _, need in ipairs(ADSupplyPlanner.rankNeeds(getTargetNeeds(target, ingredients, farmId, demand), loadCapacity)) do - local choice = findBestSource(markerStations, {need.fillType}, farmId) + local ranked = ADSupplyPlanner.rankNeeds(getTargetNeeds(target, ingredients, farmId, demand), loadCapacity) + -- bedste foder først: lavere foder kun når truget er nødlidende (ADSupplyPlanner.isFoodAllowed) + local bestFood = ADSupplyPlanner.bestFoodPriority ~= nil and ADSupplyPlanner.bestFoodPriority(ranked) or nil + for _, need in ipairs(ranked) do + local choice = (ADSupplyPlanner.isFoodAllowed == nil or ADSupplyPlanner.isFoodAllowed(need, bestFood)) and findBestSource(markerStations, {need.fillType}, farmId) or nil if choice ~= nil then return { markerIndex = choice.markerIndex, + markerId = choice.markerId, markerName = choice.markerName, group = choice.group, level = choice.level, @@ -691,6 +696,25 @@ function ADSmartPickup.choosePickup(adEnv, driver) local choice = ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients) if choice == nil then + -- Traktor i et forsynings-run uden noget (tilladt) at hente: hold ved Wait og prøv igen, i stedet + -- for at falde tilbage til mappe-logikken (den henter hvad som helst, fx ensilage i et halvfuldt trug). + local run = ADRunsController ~= nil and ADRunsController.getRunForVehicle(driver) or nil + if run ~= nil and (run.kind or "supply") == "supply" and ADOutbound ~= nil and ADOutbound.holdSupplyVehicle ~= nil then + local replan = function() + local list = ADSmartPickup.getIngredientsFor(adEnv, driver) + local next = list ~= nil and ADSmartPickup.findSupplyPickup(adEnv, driver, list) or nil + if next == nil then + return nil + end + applySupplyChoice(driver, list, next) + activePickups[driver] = {station = next.station, fillTypes = next.fillTypes, markerName = next.markerName, fillType = next.fillType} + return next.markerId + end + local held = ADOutbound.holdSupplyVehicle(adEnv, driver, run, {"intet at hente (bedste foder mangler, truget ikke nødlidende)"}, replan) + if held ~= nil then + return held + end + end return ADSmartPickup.findLeastFilledPickup(adEnv, driver) end applySupplyChoice(driver, ingredients, choice) @@ -855,14 +879,16 @@ local function installHook(adEnv) activePickups[driver] = nil return originalGetNextPickup(self, driver, forcedSkip) end - activePickups[driver] = {station = choice.station, fillTypes = choice.fillTypes, markerName = choice.markerName, fillType = choice.fillType or choice.fillTypes[1]} + activePickups[driver] = choice.station ~= nil and {station = choice.station, fillTypes = choice.fillTypes, markerName = choice.markerName, fillType = choice.fillType or choice.fillTypes[1]} or nil -- hold AutoDrives egen rotations-tilstand konsistent med vores valg self.pickups[choice.group] = self.pickups[choice.group] or {} self.pickups[choice.group].lastTarget = choice.markerIndex self.pickups[choice.group].lastVehicle = driver - logChoiceOnce(driver, choice) + if choice.level ~= nil then + logChoiceOnce(driver, choice) -- et hold ved Wait logges af ADOutbound + end return choice.markerIndex end diff --git a/FS25_ADSmartPickup/adSupplyPlanner.lua b/FS25_ADSmartPickup/adSupplyPlanner.lua index a14bf78..8830ce9 100644 --- a/FS25_ADSmartPickup/adSupplyPlanner.lua +++ b/FS25_ADSmartPickup/adSupplyPlanner.lua @@ -191,6 +191,7 @@ function ADSupplyPlanner.rankNeeds(needs, loadCapacity) fits = ADSupplyPlanner.fullLoadFits(freeSpace, loadCapacity), score = need.scoreOverride or ADSupplyPlanner.scoreNeed(need.level, need.capacity, freeSpace, minutesToEmpty), priority = need.priority or 0, + trough = need.scoreOverride ~= nil, }) end end @@ -209,6 +210,27 @@ function ADSupplyPlanner.rankNeeds(needs, loadCapacity) return ranked end +-- ---- bedste foder først ---- +-- Foder i et stald-trug deler plads. Lavere foder (ensilage/hø, vægt 0,8) må kun hentes når der er +-- bedre foder (TMR, 1,0) på listen, hvis truget er nødlidende: mindst så stor en andel fri. +ADSupplyPlanner.EMERGENCY_TROUGH_FREE = 0.75 + +-- need.trough = true for foder i et fælles trug; need.score = trugets fri andel; need.priority = fodervægt. +function ADSupplyPlanner.bestFoodPriority(needs) + local best = nil + for _, need in ipairs(needs or {}) do + if need.trough and (best == nil or (need.priority or 0) > best) then best = need.priority or 0 end + end + return best +end + +function ADSupplyPlanner.isFoodAllowed(need, bestPriority) + if not need.trough or bestPriority == nil or (need.priority or 0) >= bestPriority then + return true + end + return (need.score or 0) >= ADSupplyPlanner.EMERGENCY_TROUGH_FREE +end + -- state: nil eller {ingredients, narrowedTo} fra forrige tur. selected: AD's aktuelle varevalg. -- >1 valgt = (ny) ingrediensliste. Præcis den vare vi selv indsnævrede til = gemt liste. -- Alt andet = brugeren har valgt noget andet -> nil (behovsstyring fra). diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 24cb6ed..72b056e 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.13.7.0 + 1.14.0.0 <en>AD Smart Pickup</en> diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua index b0e0151..13999d7 100644 --- a/tests/test_adSmartPickup.lua +++ b/tests/test_adSmartPickup.lua @@ -770,6 +770,31 @@ check("E5 uden TMR-kilde: hø før græs", manager:getNextPickup(e5Driver, false -- trug 70 % fuldt (30 % trang) vs. halm 80 % tomt -> halm e5Food[HAY] = 21000 check("E5 halmlager tommere end truget -> halm", manager:getNextPickup(e5Driver, false), 21) +-- E6 (bedste foder først): TMR-kilden er tom, truget halvfuldt, halm fuld -> INGEN ensilage/hø; en traktor +-- i et forsynings-run holder ved Wait i stedet (in-game 24/9: Fastrac'erne fyldte truget med ensilage) +e5Straw.level = 100000 +e5Food[TMR], e5Food[HAY], e5Food[GRASS] = 0, 15000, 0 -- 50 % fri +e5Driver.uniqueId = "vehicleE6" +testVehicles = {e5Driver} +ADRunsController.store = ADRuns.newStore() +local e6Run = ADRunsController.createRun("Cow Food") +ADRunsController.updateRun(e6Run.id, {targetWayPointId = 30, ingredientMode = "manual", ingredients = {"FORAGE", "DRYGRASS_WINDROW", "GRASS_WINDROW", "STRAW"}}) +ADRunsController.assignVehicle(e6Run.id, e5Driver) +local holdCalls = {} +ADOutbound = { + isOutboundVehicle = function() return false end, + holdSupplyVehicle = function(adEnv, vehicle, run, reasons, replan) table.insert(holdCalls, {reasons = reasons, replan = replan}) return {markerIndex = 99, markerName = "Cow Wait", group = "Cow Wait", fillTypes = {}} end, + releaseInactive = function() end, install = function() end, +} +check("E6 halvfuldt trug uden TMR: hold ved Wait", manager:getNextPickup(e5Driver, false), 99) +check("E6 hold kaldt én gang", #holdCalls, 1) +check("E6 replan finder intet endnu", holdCalls[1].replan(), nil) +e5Food[HAY] = 3000 -- 90 % fri: nødlidende -> hø tilladt +check("E6 replan: nødlidende trug -> hø", holdCalls[1].replan(), 23) +check("E6 nødlidende trug: hø direkte", manager:getNextPickup(e5Driver, false), 23) +ADOutbound = nil +ADRunsController.store = ADRuns.newStore() +testVehicles = {} g_currentMission.animalFoodSystem = nil -- W13: AD stopper vognen i triggeren FØR markøren (isTargetReached=false) — Wait udløses alligevel diff --git a/tests/test_adSupplyPlanner.lua b/tests/test_adSupplyPlanner.lua index 4f6da2b..01325cb 100644 --- a/tests/test_adSupplyPlanner.lua +++ b/tests/test_adSupplyPlanner.lua @@ -193,5 +193,18 @@ check("så hø (0.8)", trough[3].fillType, HAYX) check("så græs (0.4)", trough[4].fillType, GRASS) check("scoreOverride bruges som score", trough[2].score, 0.3) +-- Bedste foder først: lavere foder (ensilage 0,8) må kun hentes når truget er nødlidende +-- (fri ≥ EMERGENCY_TROUGH_FREE af kapaciteten), hvis der er bedre foder (TMR 1,0) i samme trug +local tmrNeed = {fillType = 116, priority = 1.0, score = 0.6, trough = true} +local silageNeed = {fillType = 7, priority = 0.8, score = 0.6, trough = true} +local strawNeed = {fillType = 31, priority = 0, score = 0.9} +check("NF1 bedste foder tillades altid", ADSupplyPlanner.isFoodAllowed(tmrNeed, 1.0), true) +check("NF1 ensilage afvist ved halvfuldt trug", ADSupplyPlanner.isFoodAllowed(silageNeed, 1.0), false) +silageNeed.score = 0.8 +check("NF1 ensilage tilladt når truget er næsten tomt", ADSupplyPlanner.isFoodAllowed(silageNeed, 1.0), true) +check("NF1 ikke-foder (halm) påvirkes ikke", ADSupplyPlanner.isFoodAllowed(strawNeed, 1.0), true) +check("NF1 intet bedre foder på listen -> tilladt", ADSupplyPlanner.isFoodAllowed({fillType = 7, priority = 0.8, score = 0.1, trough = true}, 0.8), true) +check("NF1 bedste foderprioritet", ADSupplyPlanner.bestFoodPriority({tmrNeed, silageNeed, strawNeed}), 1.0) + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)