diff --git a/FS25_ADSmartPickup/adOutbound.lua b/FS25_ADSmartPickup/adOutbound.lua index a2f864c..4134668 100644 --- a/FS25_ADSmartPickup/adOutbound.lua +++ b/FS25_ADSmartPickup/adOutbound.lua @@ -306,7 +306,9 @@ function ADOutbound.buildPlanInput(adEnv, run, vehicle) product.store = findStoreMarker(adEnv, vehicle, product, capacity, sources, nil, run) end end + local feedMode = ADRunsController ~= nil and ADRunsController.getStoreMarkers ~= nil and #ADRunsController.getStoreMarkers(adEnv, run) > 0 local input = { + feedMode = feedMode, capacity = capacity, hasAutoload = hasAutoload, sellAtPercent = run.sellAtPercent, diff --git a/FS25_ADSmartPickup/adOutboundPlanner.lua b/FS25_ADSmartPickup/adOutboundPlanner.lua index d2a71f0..1ae8452 100644 --- a/FS25_ADSmartPickup/adOutboundPlanner.lua +++ b/FS25_ADSmartPickup/adOutboundPlanner.lua @@ -44,6 +44,10 @@ local function sumSources(product) return total, percent, pallets end +-- Fodring: flowet har "Lager til" (et bestemt anlæg der bruger varen, fx gylle til en biogas-fermentor). +-- Så køres der så snart kilden har så meget — at vente på fuldt læs/80 % sulter anlægget. +ADOutboundPlanner.FEED_MIN_LITERS = 10000 + function ADOutboundPlanner.evaluate(product, input) local available, storagePercent, palletCount = sumSources(product) local percent = product.quote ~= nil and product.quote.percent or nil @@ -119,7 +123,7 @@ local function collectCandidates(input) if wantsSell(product.policy) and e.priceOk and product.quote ~= nil and canSellWithoutReserve(e, input) then table.insert(sell, {product = product, e = e, kind = "sell", targetWayPointId = product.quote.wayPointId, targetName = product.quote.stationName, percent = e.percent}) added = true - elseif wantsStore(product.policy) and (e.fullLoad or e.storageHigh) and product.store ~= nil then + elseif wantsStore(product.policy) and (e.fullLoad or e.storageHigh or (input.feedMode and e.available >= ADOutboundPlanner.FEED_MIN_LITERS)) and product.store ~= nil then table.insert(store, {product = product, e = e, kind = "store", targetWayPointId = product.store.wayPointId, targetName = product.store.name}) added = true end diff --git a/FS25_ADSmartPickup/adSupplyPlanner.lua b/FS25_ADSmartPickup/adSupplyPlanner.lua index 4bf49ab..d303486 100644 --- a/FS25_ADSmartPickup/adSupplyPlanner.lua +++ b/FS25_ADSmartPickup/adSupplyPlanner.lua @@ -135,11 +135,30 @@ end -- ledigt (eller allerede reserveret af dette køretøj) og som isReachable(candidate) -- godkender (valgfri; AutoDrive slukker sig selv, hvis den sendes mod et punkt uden rute). -- nil hvis intet punkt duer. Nåelighed tjekkes kun for de punkter der er i spil, nærmeste først. -function ADSupplyPlanner.pickNearestFreeWaitPoint(candidates, claims, x, z, vehicle, isReachable) +-- Wait-punkter hvor et ANDET køretøj fysisk holder (positions = {{x, z}}): hvert køretøj optager +-- højst ét punkt — det nærmeste inden for radius — så et nabopunkt 6 m væk ikke blokeres ved en fejl. +function ADSupplyPlanner.markOccupiedWaitPoints(candidates, positions, radius) + local occupied = {} + local limit = (radius or 6) * (radius or 6) + for _, position in ipairs(positions or {}) do + local best, bestDistance = nil, limit + for _, candidate in ipairs(candidates or {}) do + local dx, dz = candidate.x - position.x, candidate.z - position.z + local distance = dx * dx + dz * dz + if distance <= bestDistance then best, bestDistance = candidate, distance end + end + if best ~= nil then occupied[best.marker.id] = true end + end + return occupied +end + +-- occupied (valgfri) = {[markerId] = true} fysisk optaget af andre; springes over medmindre man selv har reservationen. +function ADSupplyPlanner.pickNearestFreeWaitPoint(candidates, claims, x, z, vehicle, isReachable, occupied) local free = {} for _, candidate in ipairs(candidates) do local owner = claims[candidate.marker.id] - if owner == nil or owner == vehicle then + local isTaken = occupied ~= nil and occupied[candidate.marker.id] == true and owner ~= vehicle + if (owner == nil or owner == vehicle) and not isTaken then local dx, dz = candidate.x - x, candidate.z - z table.insert(free, {candidate = candidate, distance = dx * dx + dz * dz}) end diff --git a/FS25_ADSmartPickup/adWaitPool.lua b/FS25_ADSmartPickup/adWaitPool.lua index 8a6c1a4..d4986db 100644 --- a/FS25_ADSmartPickup/adWaitPool.lua +++ b/FS25_ADSmartPickup/adWaitPool.lua @@ -53,12 +53,36 @@ end -- Nærmeste ledige (eller eget reserverede) Wait-punkt fra vognens position; isReachable(candidate) -- valgfri. nil uden position eller uden brugbart punkt. Reserverer IKKE selv. +ADWaitPool.OCCUPIED_RADIUS = 6 + +-- Positioner for ANDRE motoriserede køretøjer (ikke vognen selv eller dens redskaber) — til at se om et +-- Wait-punkt fysisk er optaget, også uden reservation (fx lige efter en omplanlægning). +local function otherVehiclePositions(vehicle) + local positions = {} + local system = g_currentMission ~= nil and g_currentMission.vehicleSystem or nil + local own = {} + if vehicle.getChildVehicles ~= nil then + local ok, children = pcall(vehicle.getChildVehicles, vehicle) + for _, child in pairs(ok and children or {}) do own[child] = true end + end + own[vehicle] = true + for _, other in pairs(system ~= nil and system.vehicles or {}) do + if not own[other] and other.spec_motorized ~= nil and other.rootNode ~= nil then + local ok, x, _, z = pcall(getWorldTranslation, other.rootNode) + if ok and x ~= nil then table.insert(positions, {x = x, z = z}) end + end + end + return positions +end + function ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable) local x, z = getVehiclePosition(vehicle) if x == nil then return nil end - return ADSupplyPlanner.pickNearestFreeWaitPoint(ADWaitPool.getCandidates(adEnv, poolGroup), ADWaitPool.claims, x, z, vehicle, isReachable) + local candidates = ADWaitPool.getCandidates(adEnv, poolGroup) + local occupied = ADSupplyPlanner.markOccupiedWaitPoints(candidates, otherVehiclePositions(vehicle), ADWaitPool.OCCUPIED_RADIUS) + return ADSupplyPlanner.pickNearestFreeWaitPoint(candidates, ADWaitPool.claims, x, z, vehicle, isReachable, occupied) end -- Ender ruten i målet? (AutoDrive returnerer tom/afkortet liste når der ingen vej er.) diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 38d92e6..cbb07b3 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.18.2.0 + 1.18.3.0 <en>AD Smart Pickup</en> diff --git a/docs/BRUGERMANUAL.md b/docs/BRUGERMANUAL.md index 0286f14..37a478d 100644 --- a/docs/BRUGERMANUAL.md +++ b/docs/BRUGERMANUAL.md @@ -1,6 +1,6 @@ # AD Smart Pickup — brugermanual -Version 1.18.2 · Farming Simulator 25 · tillæg til AutoDrive +Version 1.18.3 · Farming Simulator 25 · tillæg til AutoDrive *English version: [USER_MANUAL.md](USER_MANUAL.md)* @@ -102,6 +102,8 @@ Markøren skal højst være **40 m** fra bygningens trigger. 3. Forbind hvert Wait-punkt til vejnettet **i begge retninger**, både fra bygningerne og tilbage. Wait-markører bliver aldrig brugt som kilde eller mål, heller ikke når de står tæt på en bygning. +Et Wait-punkt regnes som optaget, når det er reserveret, eller når et andet køretøj fysisk holder +højst 6 m fra det. Markører i markernes mapper, fx *F91 Wait*, bliver ikke rørt, fordi det er **mappens** navn, der tæller. @@ -309,6 +311,10 @@ de valgte tage læsset, venter traktoren ved Wait, og loggen skriver hvorfor, fx fuld*. Vælger du ingen, finder moden selv den nærmeste silo eller egne fabrik, der bruger varen. Paller følger stadig reglen om nærmeste palleskur. +Med **Lager til** valgt regnes flowet som **fodring**: traktoren kører, så snart kilden har mindst +10.000 l af varen. Den venter ikke på fuldt læs eller *Kør til lager over*, for et anlæg som en +biogas-fermentor går i stå, hvis det kun får læs en gang imellem. + ### Sådan vælges hver tur Før hver pålæsning, og hvert femte sekund mens traktoren holder, vurderer moden: diff --git a/docs/USER_MANUAL.md b/docs/USER_MANUAL.md index c8c7314..a8b650b 100644 --- a/docs/USER_MANUAL.md +++ b/docs/USER_MANUAL.md @@ -1,6 +1,6 @@ # AD Smart Pickup — user manual -Version 1.18.2 · Farming Simulator 25 · add-on for AutoDrive +Version 1.18.3 · Farming Simulator 25 · add-on for AutoDrive *Dansk version: [BRUGERMANUAL.md](BRUGERMANUAL.md)* @@ -103,6 +103,8 @@ The marker must be at most **40 m** from the building's trigger. 3. Connect every Wait point to the road network **in both directions**, from the buildings and back. Wait markers are never used as a source or target, even when they stand close to a building. +A Wait point counts as taken when it is reserved, or when another vehicle physically stands within +6 m of it. Markers in field folders, such as *F91 Wait*, are left alone, because it is the **folder** name that counts. @@ -313,6 +315,10 @@ never to the AIO factory. If none of the chosen buildings can take the load, the Wait, and the log says why, such as *valgt lager: BGA500kw: fuld*. If you pick none, the mod finds the nearest silo or own factory that uses the good by itself. Pallets still follow the nearest pallet shed rule. +With **Store to** chosen, the flow counts as **feeding**: the tractor drives as soon as the source has +at least 10,000 l of the good. It does not wait for a full load or *Drive to storage above*, because a +plant such as a biogas fermenter stalls when it only gets a load now and then. + ### How each trip is chosen Before each loading, and every five seconds while the tractor waits, the mod checks: diff --git a/tests/test_adOutboundPlanner.lua b/tests/test_adOutboundPlanner.lua index 8e93c2b..de3f99a 100644 --- a/tests/test_adOutboundPlanner.lua +++ b/tests/test_adOutboundPlanner.lua @@ -156,5 +156,16 @@ check("P15 kilde uden lagre beholdes", kept[3].wayPointId, 5) local none = P.dedupeSharedSources({}, function() return nil end) check("P15 tom liste", #none, 0) +-- P16: fodring ("Lager til" valgt = feedMode): kør så snart kilden har FEED_MIN_LITERS — ikke fuldt læs / 80 % +local feedStore = {wayPointId = 50, name = "Slurry Drop 1", free = 100000} +local feedProduct = product("LIQUIDMANURE", FORAGE, "store", {{wayPointId = 2, available = 20000, level = 20000, capacity = 500000}}, nil, feedStore) +local tripNoFeed = P.plan(input({feedProduct}, {capacity = 100000})) +check("P16 uden fodring: 4 % og intet fuldt læs -> ingen tur", tripNoFeed, nil) +local tripFeed = P.plan(input({feedProduct}, {capacity = 100000, feedMode = true})) +check("P16 fodring: 20k >= minimum -> lagertur", tripFeed and tripFeed.kind, "store") +check("P16 fodring: mål = valgt drop", tripFeed and tripFeed.targetWayPointId, 50) +local tinyProduct = product("LIQUIDMANURE", FORAGE, "store", {{wayPointId = 2, available = 3000, level = 3000, capacity = 500000}}, nil, feedStore) +check("P16 fodring: under minimum -> ingen tur", P.plan(input({tinyProduct}, {capacity = 100000, feedMode = true})), nil) + print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1) diff --git a/tests/test_adSupplyPlanner.lua b/tests/test_adSupplyPlanner.lua index 889e841..ee3cd08 100644 --- a/tests/test_adSupplyPlanner.lua +++ b/tests/test_adSupplyPlanner.lua @@ -159,6 +159,15 @@ check("reserveret springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitP check("egen reservation tæller som ledig", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1).marker.id, 1) check("alle optaget -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V2, [2] = V2, [3] = V2}, 10, 0, V1), nil) check("ingen punkter -> nil", ADSupplyPlanner.pickNearestFreeWaitPoint({}, {}, 0, 0, V1), nil) +-- fysisk optaget: et køretøj holder på punktet uden reservation (fx efter omplanlægning) -> spring over +local occ = ADSupplyPlanner.markOccupiedWaitPoints(waitPoints, {{x = waitPoints[1].x + 2, z = waitPoints[1].z}}, 6) +check("optaget punkt markeres", occ[waitPoints[1].marker.id], true) +check("optaget springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, nil, occ).marker.id, 2) +check("egen reservation vinder over optaget (det er en selv)", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {[1] = V1}, 10, 0, V1, nil, occ).marker.id, 1) +check("køretøj langt væk optager intet", next(ADSupplyPlanner.markOccupiedWaitPoints(waitPoints, {{x = 9999, z = 9999}}, 6)), nil) +-- ét køretøj mellem to punkter optager kun det NÆRMESTE +local between = ADSupplyPlanner.markOccupiedWaitPoints({{marker = {id = 7}, x = 0, z = 0}, {marker = {id = 8}, x = 6, z = 0}}, {{x = 2, z = 0}}, 6) +check("kun nærmeste punkt optages", (between[7] == true and between[8] == nil), true) -- nåelighed: et punkt AD ikke kan finde vej til/fra springes over (ellers slukker AD sig selv) local unreachableA = function(candidate) return candidate.marker.id ~= 1 end check("unåeligt nærmeste springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, unreachableA).marker.id, 2)