diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index b051088..890e894 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -14,8 +14,9 @@ ADSmartPickup = {} -- vogntog bagved. Siloer tættere end dette regnes som "markørens silo". ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE = 40 -- I tætte silo-rækker ligger nabosiloens trigger FORAN næsen og er ofte nærmere --- end markørens egen. Triggere mere end dette foran næsen hører til naboen. -ADSmartPickup.MAX_TRIGGER_AHEAD_OF_MARKER = 2 +-- end markørens egen. Vognen holder altid BAG næsen, så en trigger foran næsen +-- hører til naboen (en ensom silo med markøren ved røret fanges af nødløsningen). +ADSmartPickup.MAX_TRIGGER_AHEAD_OF_MARKER = 0 -- Vognen følger trækkerens spor: en trigger længere til siden end dette er en -- anden bane (fx aflæsningssiden af samme tank), ikke markørens læsseplads. ADSmartPickup.MAX_TRIGGER_LATERAL_OFFSET = 6 @@ -194,13 +195,26 @@ local function collectMarkerStations(adEnv, candidateMarkers, farmId, excludedPl return markerStations end --- Mindst fyldte silo der har varen; siloer med varen i EGET lager går altid --- forud for dem der kun kan låne fra naboer. Returnerer et valg eller nil. -local function findLeastFilled(markerStations, fillTypes, farmId) - local best = {[true] = {level = math.huge}, [false] = {level = math.huge}} +local function getFillTypeNames(fillTypes) + local names = {} + for _, fillType in ipairs(fillTypes) do + local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or nil + if name ~= nil then + table.insert(names, name) + end + end + return names +end + +-- Bedste kilde der har varen: mindst fyldte først for lagervarer, fyldeste først for +-- varer hvis tanke løber fulde (ADSupplyPlanner). Siloer med varen i EGET lager går +-- altid forud for dem der kun kan låne fra naboer. Returnerer et valg eller nil. +local function findBestSource(markerStations, fillTypes, farmId) + local prefersFullest = ADSupplyPlanner.prefersFullestSource(getFillTypeNames(fillTypes)) + local best = {[true] = {}, [false] = {}} for _, entry in ipairs(markerStations) do local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId) - if level >= ADSmartPickup.MIN_USEFUL_LITERS and level < best[isOwn].level then + if level >= ADSmartPickup.MIN_USEFUL_LITERS and ADSupplyPlanner.isBetterSource(level, best[isOwn].level, prefersFullest) then best[isOwn] = {level = level, marker = entry.marker, station = entry.station, isOwn = isOwn} end end @@ -217,6 +231,7 @@ local function findLeastFilled(markerStations, fillTypes, farmId) station = choice.station, isOwn = choice.isOwn, fillTypes = fillTypes, + prefersFullest = prefersFullest, } end @@ -232,7 +247,7 @@ function ADSmartPickup.findLeastFilledPickup(adEnv, driver) return nil end local groupMarkers = adEnv.ADGraphManager:getMapMarkersInGroup(firstMarker.group) - return findLeastFilled(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId) + return findBestSource(collectMarkerStations(adEnv, groupMarkers, farmId, nil), fillTypes, farmId) end -- Lagrene bag aflæsningsstationen; produktionspunkter uden targetStorages læses direkte. @@ -285,7 +300,7 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients) local allMarkers = adEnv.ADGraphManager:getMapMarkers() local markerStations = collectMarkerStations(adEnv, allMarkers, farmId, target.owningPlaceable) for _, need in ipairs(ADSupplyPlanner.rankNeeds(getTargetNeeds(target, ingredients, farmId))) do - local choice = findLeastFilled(markerStations, {need.fillType}, farmId) + local choice = findBestSource(markerStations, {need.fillType}, farmId) if choice ~= nil then return { markerIndex = choice.markerIndex, @@ -295,6 +310,7 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients) station = choice.station, isOwn = choice.isOwn, fillTypes = choice.fillTypes, + prefersFullest = choice.prefersFullest, fillType = need.fillType, need = need, } @@ -330,6 +346,7 @@ end local function describeChoice(choice) local source = choice.isOwn and "eget lager" or "nabo-lager" + source = source .. (choice.prefersFullest and ", fyldeste først" or ", mindst fyldte først") if choice.fillType == nil then return string.format("%.0f l, %s", choice.level, source) end diff --git a/FS25_ADSmartPickup/adSupplyPlanner.lua b/FS25_ADSmartPickup/adSupplyPlanner.lua index 5bc8411..e3552e9 100644 --- a/FS25_ADSmartPickup/adSupplyPlanner.lua +++ b/FS25_ADSmartPickup/adSupplyPlanner.lua @@ -7,6 +7,39 @@ ADSupplyPlanner = {} -- Mindre fri plads end dette i målet regnes som "fuldt" (undgår ture med læs der ikke kan komme af). ADSupplyPlanner.MIN_FREE_LITERS = 1000 +-- Varer hvis kilder LØBER FULDE (dyr/produktion fylder tanken løbende og går i stå +-- når den er fuld): her hentes den fyldeste kilde først. Alt andet er lagervarer, +-- hvor den mindst fyldte silo tømmes først så rester ryddes og siloer frigives. +ADSupplyPlanner.FULLEST_FIRST_FILL_TYPES = { + MILK = true, GOATMILK = true, BUFFALOMILK = true, SHEEPMILK = true, + LIQUIDMANURE = true, DIGESTATE = true, +} + +-- fillTypeNames: navnene på de varer der hentes på turen. Kun når ALLE er +-- "løber fuld"-varer vendes strategien; blandet eller ukendt valg = mindst først. +function ADSupplyPlanner.prefersFullestSource(fillTypeNames) + if #fillTypeNames == 0 then + return false + end + for _, name in ipairs(fillTypeNames) do + if not ADSupplyPlanner.FULLEST_FIRST_FILL_TYPES[name] then + return false + end + end + return true +end + +-- Er en kilde med dette niveau bedre end den hidtil bedste (nil = ingen endnu)? +function ADSupplyPlanner.isBetterSource(level, bestLevel, prefersFullest) + if bestLevel == nil then + return true + end + if prefersFullest then + return level > bestLevel + end + return level < bestLevel +end + function ADSupplyPlanner.copyList(list) local copy = {} for index, value in ipairs(list) do diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 675cf98..deebe28 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.4.0.0 + 1.5.0.0 <en>AD Smart Pickup</en> diff --git a/README.md b/README.md index cdafc8d..bf0f833 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ AutoDrive-tillæg til Farming Simulator 25 med to funktioner oven på Pickup&Del ## 1. Vare-bevidst mappe-rotation (én vare valgt) Når pålæsningsmål roteres i en AD-mappe, køres der direkte til den silo i mappen, der har den -valgte vare — **mindst fyldte først** — i stedet for at prøve markørerne én efter én. +valgte vare — mindst fyldte først (mælk/gylle: fyldeste først) — i stedet for at prøve markørerne én efter én. ## 2. Behovsstyret forsyning (flere varer valgt) @@ -34,6 +34,11 @@ Slå fra ved at vælge en anden enkelt vare. bagved. Markørens silo er derfor nærmeste trigger **bag næsen** set i ankomstretningen (forrige waypoint), højst 2 m foran. I tætte rækker (15 m mellem siloerne) er nabosiloens trigger ellers nærmere end markørens egen. Ingen trigger bag næsen/ukendt retning → nærmeste trigger som før. +- **Kildestrategi (v1.5):** lagervarer hentes fra den **mindst fyldte** silo først (rester ryddes, + siloer frigives). Varer hvis tanke *løber fulde* — mælk, gylle, digestat — hentes fra den **fyldeste** + kilde først, så produktionen ikke går i stå. Listen er `ADSupplyPlanner.FULLEST_FIRST_FILL_TYPES` + (`adSupplyPlanner.lua`); tilføj fx `MANURE = true` hvis møddinger også skal tømmes fyldest-først. + Loggen viser den brugte strategi. En trigger foran næsen tæller aldrig som markørens egen (v1.5). - **Spor-regel (v1.4):** triggeren skal ligge i trækkerens spor (højst 6 m til siden). En tank med aflæsningsbane på den ene side og pålæsningsbane på den anden giver ellers to markører til samme tank, og "Dropof"-markøren kunne vinde. Kendes retningen, men ligger intet i sporet (kurve ind til diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua index be6a227..b6d2250 100644 --- a/tests/test_adSmartPickup.lua +++ b/tests/test_adSmartPickup.lua @@ -3,6 +3,9 @@ local SUGARBEET, WHEAT = 10, 11 local HAY, GRASS, SILAGE = 20, 21, 22 +local MILK = 30 +local FILL_TYPE_NAMES = {[SUGARBEET] = "SUGARBEET", [WHEAT] = "WHEAT", [HAY] = "DRYGRASS_WINDROW", [GRASS] = "GRASS_WINDROW", [SILAGE] = "SILAGE", [MILK] = "MILK"} +g_fillTypeManager = {getFillTypeNameByIndex = function(_, index) return FILL_TYPE_NAMES[index] end} local logLines = {} -- ---- FS25-mocks ---- @@ -357,6 +360,15 @@ addMarker(2, "1 Silage Pickup", "Ensilage", 1247, -2278, 1247, -2266) -- køre table.insert(stations, makeStation(101, 1247, -2271, {[SILAGE] = 4000})) check("G3 pickup-markøren vælges, ikke dropoff", manager:getNextPickup(makeDriver(markers[1], {SILAGE}), false), 2) +-- G5: markør-drift i en tank-række — næste tanks rør ligger 2 m FORAN næsen og er +-- nærmest, men vognen holder under røret 11 m bagved +resetWorld() +addMarker(1, "5 Silage Pickup", "Ensilage", 1246, -2218, 1246, -2230) -- kører +z +table.insert(stations, makeStation(101, 1246, -2216, {[SILAGE] = 9000})) -- 2 m foran næsen +table.insert(stations, makeStation(102, 1246, -2229, {[HAY] = 4000})) -- 11 m bag næsen: kun den har hø +addMarker(2, "6 Silage Pickup", "Ensilage", 1246, -2208, 1246, -2218) +check("G5 markør 5 hører til tanken bag næsen", manager:getNextPickup(makeDriver(markers[1], {HAY}), false), 1) + -- G4: kurve ind til markøren -> retningen snyder og røret ligger "foran"; tæt på = ok resetWorld() addMarker(1, "7 Silage Pickup", "Ensilage", 100, 0, 100, 20) -- kører -z @@ -380,6 +392,14 @@ check("L9 sluppet greb logges som advarsel", logLines[#logLines]:sub(1, 4), "WAR FS25_AutoDrive.LoadAtDestinationTask.update(stuckTask, 16) check("L9 grebet tages ikke igen på samme tur", stuckTask.seenRotate, RT_ONLYPICKUP) +-- M1: mælketanke løber fulde -> den FYLDESTE hentes først (siloer: mindst fyldte) +resetWorld() +addMarker(1, "Kostald 1", "Mælk", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[MILK] = 2000})) +addMarker(2, "Kostald 2", "Mælk", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[MILK] = 48000})) +addMarker(3, "Kostald 3", "Mælk", 1000, 0); table.insert(stations, makeStation(103, 1000, 15, {[MILK] = 30000})) +check("M1 fyldeste mælketank først", manager:getNextPickup(makeDriver(markers[1], {MILK}), false), 2) +check("M1 loggen nævner strategien", logLines[#logLines]:find("fyldeste først", 1, true) ~= nil, true) + -- L7: AD stopper -> grebet ryddes, næste opgave venter ikke på en gammel silo resetWorld() addMarker(1, "Silo 1", "Andet", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[HAY] = 5000})) diff --git a/tests/test_adSupplyPlanner.lua b/tests/test_adSupplyPlanner.lua index 863e70c..b4dd5ed 100644 --- a/tests/test_adSupplyPlanner.lua +++ b/tests/test_adSupplyPlanner.lua @@ -51,5 +51,16 @@ check("bruger valgte andet -> nil", ADSupplyPlanner.resolveIngredients(state, {H check("nyt multi-valg erstatter gemt liste", #ADSupplyPlanner.resolveIngredients(state, {HAY, SILAGE}), 2) check("ugyldigt valg -> nil", ADSupplyPlanner.resolveIngredients(state, nil), nil) +-- Kildestrategi: lager-siloer tømmes mindst-først; tanke der løber fulde (mælk, gylle) fyldest-først +check("mælk hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"MILK"}), true) +check("gylle hentes fyldest-først", ADSupplyPlanner.prefersFullestSource({"LIQUIDMANURE"}), true) +check("roer hentes mindst-først", ADSupplyPlanner.prefersFullestSource({"SUGARBEET"}), false) +check("blandet valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({"MILK", "WHEAT"}), false) +check("ukendt/tomt valg -> mindst-først", ADSupplyPlanner.prefersFullestSource({}), false) +check("mindst-først: lavere niveau er bedre", ADSupplyPlanner.isBetterSource(100, 500, false), true) +check("mindst-først: højere niveau er ikke bedre", ADSupplyPlanner.isBetterSource(900, 500, false), false) +check("fyldest-først: højere niveau er bedre", ADSupplyPlanner.isBetterSource(900, 500, true), true) +check("første kandidat er altid bedre end ingen", ADSupplyPlanner.isBetterSource(900, nil, false), true) + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)