From 8ec0853faedcac3d9b26e6526ec9321fc8920bdf Mon Sep 17 00:00:00 2001 From: masterdraco Date: Thu, 24 Sep 2026 01:36:29 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20l=C3=A6s=20p=C3=A5=20vej=20t=C3=A6lles?= =?UTF-8?q?=20med=20=E2=80=94=20flere=20traktorer=20til=20samme=20stald=20?= =?UTF-8?q?fordeler=20sig=20p=C3=A5=20varer=20og=20stalde=20(v1.14.1.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adRunsController.lua | 4 +- FS25_ADSmartPickup/adSmartPickup.lua | 58 ++++++++++++++++++++++--- FS25_ADSmartPickup/adSupplyPlanner.lua | 31 +++++++++++++ FS25_ADSmartPickup/adUnloadWait.lua | 4 ++ FS25_ADSmartPickup/modDesc.xml | 2 +- tests/test_adSmartPickup.lua | 15 +++++++ tests/test_adSupplyPlanner.lua | 17 ++++++++ 7 files changed, 124 insertions(+), 7 deletions(-) diff --git a/FS25_ADSmartPickup/adRunsController.lua b/FS25_ADSmartPickup/adRunsController.lua index c58d77d..b03d0d8 100644 --- a/FS25_ADSmartPickup/adRunsController.lua +++ b/FS25_ADSmartPickup/adRunsController.lua @@ -252,7 +252,9 @@ function ADRunsController.chooseTarget(adEnv, run, vehicle) local ingredients = ADRunsController.getRunIngredients(adEnv, run, vehicle) local bestId, bestNeed = nil, nil for _, entry in ipairs(getTargetStations(adEnv, run, farmId)) do - local ranked = ADSmartPickup.getTargetNeedsRanked(entry.station, ingredients, farmId) + -- læs andre traktorer er på vej med til stalden, tæller med — så fordeler de sig på staldene + local pending = ADSmartPickup.getPending ~= nil and ADSmartPickup.getPending(entry.wayPointId, vehicle) or nil + local ranked = ADSmartPickup.getTargetNeedsRanked(entry.station, ingredients, farmId, pending) local need = ranked[1] if need ~= nil and need.freeSpace >= (ADSupplyPlanner.MIN_FREE_LITERS or 1000) then local isBetter = bestNeed == nil diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index 7d12abc..aa70b13 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -49,6 +49,9 @@ local supplyState = setmetatable({}, {__mode = "k"}) -- køretøj -> {station, fillTypes}: siloen moden sendte køretøjet til på denne tur. local activePickups = setmetatable({}, {__mode = "k"}) -- køretøj -> senest loggede valg (AutoDrive spørger flere gange pr. start). +-- køretøj -> {wayPointId, fillType, liters}: læs traktoren er på vej med (fordeler flere traktorer til +-- samme mål; ryddes ved aflæsning, stop eller ny tur) +local enRoute = setmetatable({}, {__mode = "k"}) local lastLogged = setmetatable({}, {__mode = "k"}) -- Cross-mod adgang: FS25-mods er sandboxede; AutoDrives globals ligger @@ -436,7 +439,7 @@ local function getFoodPriority(station, fillType) end local capacity = foodSpec.capacity or 0 local troughScore = capacity > 0 and math.max(0, capacity - total) / capacity or 0 - return troughScore, foodFillTypes[fillType] + return troughScore, foodFillTypes[fillType], capacity end -- En stald kan MODTAGE foder, halm og vand — ikke sine egne outputs (mælk, gødning, gylle), @@ -508,16 +511,49 @@ local function getTargetNeeds(station, ingredients, farmId, demand) for _, fillType in ipairs(ingredients) do local level, capacity = getTargetLevelAndCapacity(station, fillType, farmId) if level ~= nil then - local troughScore, priority = getFoodPriority(station, fillType) - table.insert(needs, {fillType = fillType, level = level, capacity = capacity, litersPerMinute = demand[fillType], scoreOverride = troughScore, priority = priority}) + local troughScore, priority, troughCapacity = getFoodPriority(station, fillType) + table.insert(needs, {fillType = fillType, level = level, capacity = capacity, litersPerMinute = demand[fillType], scoreOverride = troughScore, priority = priority, troughCapacity = troughCapacity}) end end return needs end -- Til fanen: samme rangering som valget bruger (uden vogn-størrelse). Fulde varer sidst. -function ADSmartPickup.getTargetNeedsRanked(station, fillTypes, farmId) +-- ---- læs på vej ---- + +function ADSmartPickup.setEnRoute(vehicle, wayPointId, fillType, liters) + if wayPointId == nil or fillType == nil or liters == nil or liters <= 0 then + enRoute[vehicle] = nil + return + end + enRoute[vehicle] = {wayPointId = wayPointId, fillType = fillType, liters = liters} +end + +function ADSmartPickup.clearEnRoute(vehicle) + enRoute[vehicle] = nil +end + +function ADSmartPickup.resetEnRoute() + enRoute = setmetatable({}, {__mode = "k"}) +end + +-- {[fillType] = liter} som ANDRE (aktive) traktorer er på vej med til målet. +function ADSmartPickup.getPending(wayPointId, excludeVehicle) + local pending = {} + for vehicle, entry in pairs(enRoute) do + if vehicle ~= excludeVehicle and entry.wayPointId == wayPointId then + pending[entry.fillType] = (pending[entry.fillType] or 0) + entry.liters + end + end + return pending +end + +-- pending (valgfri): læs på vej til målet, lægges oven i (fanen og mål-valget mellem stalde). +function ADSmartPickup.getTargetNeedsRanked(station, fillTypes, farmId, pending) local needs = getTargetNeeds(station, fillTypes, farmId, {}) + if pending ~= nil and next(pending) ~= nil then + needs = ADSupplyPlanner.applyPending(needs, pending) + end local ranked = ADSupplyPlanner.rankNeeds(needs) local seen = {} for _, need in ipairs(ranked) do seen[need.fillType] = true end @@ -593,7 +629,8 @@ 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) - local ranked = ADSupplyPlanner.rankNeeds(getTargetNeeds(target, ingredients, farmId, demand), loadCapacity) + local needs = ADSupplyPlanner.applyPending(getTargetNeeds(target, ingredients, farmId, demand), ADSmartPickup.getPending(wayPointId, driver)) + local ranked = ADSupplyPlanner.rankNeeds(needs, 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 @@ -602,6 +639,9 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients, targetWayPoi return { markerIndex = choice.markerIndex, markerId = choice.markerId, + targetWayPointId = wayPointId, + -- det der reelt kommer af: vognen, kildens beholdning og målets fri plads sætter grænsen + pendingLiters = math.min(loadCapacity or math.huge, choice.level or math.huge, need.freeSpace or math.huge), markerName = choice.markerName, group = choice.group, level = choice.level, @@ -619,6 +659,7 @@ end local function applySupplyChoice(driver, ingredients, choice) local stateModule = driver.ad.stateModule + ADSmartPickup.setEnRoute(driver, choice.targetWayPointId, choice.fillType, choice.pendingLiters) stateModule:setFillType(choice.fillType) stateModule.selectedFillTypes = {choice.fillType} stateModule:raiseDirtyFlag() @@ -687,6 +728,7 @@ end -- Samlet valg: behovsstyret med ingrediensliste, ellers mappe-logik. nil = AutoDrives original. function ADSmartPickup.choosePickup(adEnv, driver) + enRoute[driver] = nil -- ny tur: forrige læs er leveret (eller opgivet) chooseRunTarget(adEnv, driver) local ingredients = ADSmartPickup.getIngredientsFor(adEnv, driver) if ingredients == nil then @@ -913,6 +955,12 @@ local function restoreStoppedVehicles() activePickups[vehicle] = nil end end + for vehicle in pairs(enRoute) do + local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil + if stateModule == nil or stateModule.isActive == nil or not stateModule:isActive() then + enRoute[vehicle] = nil + end + end for vehicle, state in pairs(supplyState) do local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil if stateModule == nil or not stateModule:isActive() then diff --git a/FS25_ADSmartPickup/adSupplyPlanner.lua b/FS25_ADSmartPickup/adSupplyPlanner.lua index 8830ce9..4bf49ab 100644 --- a/FS25_ADSmartPickup/adSupplyPlanner.lua +++ b/FS25_ADSmartPickup/adSupplyPlanner.lua @@ -210,6 +210,37 @@ function ADSupplyPlanner.rankNeeds(needs, loadCapacity) return ranked end +-- ---- læs på vej ---- +-- pending = {[fillType] = liter} som ANDRE traktorer allerede er på vej med til samme mål. +-- Almindelige lagre: niveau + på vej (højst kapaciteten). Foder i et fælles trug (scoreOverride + +-- troughCapacity): varens niveau stiger, og trugets fri plads falder med ALT foder på vej. +-- Returnerer NY liste; input røres ikke. +function ADSupplyPlanner.applyPending(needs, pending) + local troughPending = 0 + for _, need in ipairs(needs) do + if need.scoreOverride ~= nil and pending[need.fillType] ~= nil then + troughPending = troughPending + pending[need.fillType] + end + end + local result = {} + for index, need in ipairs(needs) do + local copy = {} + for key, value in pairs(need) do copy[key] = value end + local extra = pending[need.fillType] or 0 + if need.scoreOverride ~= nil and troughPending > 0 then + local free = math.max(0, (need.capacity - need.level) - troughPending) + copy.level = need.level + extra + copy.capacity = copy.level + free + local troughCapacity = need.troughCapacity or need.capacity + copy.scoreOverride = troughCapacity > 0 and math.max(0, need.scoreOverride - troughPending / troughCapacity) or 0 + elseif extra > 0 then + copy.level = math.min(need.capacity, need.level + extra) + end + result[index] = copy + end + return result +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. diff --git a/FS25_ADSmartPickup/adUnloadWait.lua b/FS25_ADSmartPickup/adUnloadWait.lua index dfa3bec..6f72ac6 100644 --- a/FS25_ADSmartPickup/adUnloadWait.lua +++ b/FS25_ADSmartPickup/adUnloadWait.lua @@ -392,6 +392,10 @@ function ADUnloadWait.install(adEnv) task.finished = function(self) release(self.vehicle) restoreTarget(self.vehicle) + -- læsset er afleveret: tæller ikke længere som "på vej" for de andre traktorer + if ADSmartPickup ~= nil and ADSmartPickup.clearEnRoute ~= nil then + ADSmartPickup.clearEnRoute(self.vehicle) + end return originalFinished(self) end end diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 72b056e..5f947eb 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.14.0.0 + 1.14.1.0 <en>AD Smart Pickup</en> diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua index 13999d7..b0d4e39 100644 --- a/tests/test_adSmartPickup.lua +++ b/tests/test_adSmartPickup.lua @@ -82,6 +82,7 @@ local function addMarker(index, name, group, x, z, fromX, fromZ) end local function resetWorld() + if ADSmartPickup ~= nil and ADSmartPickup.resetEnRoute ~= nil then ADSmartPickup.resetEnRoute() end stations, unloadStations, markers, wayPoints, nodePositions, logLines = {}, {}, {}, {}, {}, {} FS25_AutoDrive.ADMultipleTargetsManager.pickups = {} end @@ -1140,6 +1141,20 @@ check("N1 strengt opslag finder intet", ADSmartPickup.getLoadStationAtWayPointId check("N1 løst opslag finder tanken", ADSmartPickup.getLoadStationNearWayPointId(FS25_AutoDrive, 1, 1, 40), stations[1]) check("N1 uden for afstanden -> nil", ADSmartPickup.getLoadStationNearWayPointId(FS25_AutoDrive, 1, 1, 20), nil) +-- V: flere traktorer til samme mål fordeler sig — læs på vej tælles med (in-game 24/9: alle 4 Fastrac'er +-- hentede det samme). Græs mangler mest (20000/100000), ensilage næst (50000/100000). +buildFeedWorld() +local feederA = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS, SILAGE}) +feederA.trailers = {level = 0, capacity = 70000} +local feederB = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS, SILAGE}) +feederB.trailers = {level = 0, capacity = 70000} +check("V1 A henter græs (mangler mest)", manager:getNextPickup(feederA, false), 3) +check("V1 B henter ensilage (græs er på vej med A)", manager:getNextPickup(feederB, false), 4) +check("V1 A's læs registreret på vej", ADSmartPickup.getPending(10, feederB)[GRASS], 40000) +ADSmartPickup.clearEnRoute(feederA) +check("V2 A læsset af -> intet på vej", ADSmartPickup.getPending(10, feederB)[GRASS], nil) +check("V2 egne læs tæller ikke", ADSmartPickup.getPending(10, feederB)[SILAGE], nil) + -- X: udkørsel koblet på getNextPickup-wrap'en og Wait-logikken resetWorld() addMarker(1, "Suger Load", "Pickup points", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[WHEAT] = 1000})) diff --git a/tests/test_adSupplyPlanner.lua b/tests/test_adSupplyPlanner.lua index 01325cb..889e841 100644 --- a/tests/test_adSupplyPlanner.lua +++ b/tests/test_adSupplyPlanner.lua @@ -206,5 +206,22 @@ check("NF1 ikke-foder (halm) påvirkes ikke", ADSupplyPlanner.isFoodAllowed(stra 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) +-- Læs på vej (andre traktorer): lægges oven i målets niveau; foder deler trug -> hele trugets fri plads falder +local pendingNeeds = { + {fillType = 31, level = 5000, capacity = 20000}, -- halm + {fillType = 116, level = 0, capacity = 30000, scoreOverride = 1.0, troughCapacity = 30000}, -- TMR, tomt trug + {fillType = 7, level = 0, capacity = 30000, scoreOverride = 1.0, troughCapacity = 30000}, -- ensilage +} +local adjusted = ADSupplyPlanner.applyPending(pendingNeeds, {[116] = 20000, [31] = 20000}) +check("AP halm: niveau + på vej, højst kapacitet", adjusted[1].level, 20000) +check("AP TMR: niveau + på vej", adjusted[2].level, 20000) +check("AP TMR: kapacitet = niveau + resterende fri", adjusted[2].capacity, 30000) +check("AP ensilage: truget har kun 10000 fri tilbage", adjusted[3].capacity - adjusted[3].level, 10000) +check("AP trug-score falder", math.floor(adjusted[3].scoreOverride * 100 + 0.5), 33) +check("AP input urørt", pendingNeeds[2].level, 0) +check("AP intet på vej -> samme værdier", ADSupplyPlanner.applyPending(pendingNeeds, {})[1].level, 5000) +local ranked = ADSupplyPlanner.rankNeeds(adjusted, 8000) +check("AP halm fuld (på vej) -> kun trug-varer tilbage", #ranked, 2) + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)