fix(wait): fysisk optagede Wait-punkter springes over; feat(udkørsel): fodring — med 'Lager til' køres ved >= 10.000 l i kilden (v1.18.3.0)

This commit is contained in:
2026-09-24 12:51:12 +02:00
parent 889689cfce
commit 925163758a
9 changed files with 88 additions and 7 deletions
+2
View File
@@ -306,7 +306,9 @@ function ADOutbound.buildPlanInput(adEnv, run, vehicle)
product.store = findStoreMarker(adEnv, vehicle, product, capacity, sources, nil, run) product.store = findStoreMarker(adEnv, vehicle, product, capacity, sources, nil, run)
end end
end end
local feedMode = ADRunsController ~= nil and ADRunsController.getStoreMarkers ~= nil and #ADRunsController.getStoreMarkers(adEnv, run) > 0
local input = { local input = {
feedMode = feedMode,
capacity = capacity, capacity = capacity,
hasAutoload = hasAutoload, hasAutoload = hasAutoload,
sellAtPercent = run.sellAtPercent, sellAtPercent = run.sellAtPercent,
+5 -1
View File
@@ -44,6 +44,10 @@ local function sumSources(product)
return total, percent, pallets return total, percent, pallets
end 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) function ADOutboundPlanner.evaluate(product, input)
local available, storagePercent, palletCount = sumSources(product) local available, storagePercent, palletCount = sumSources(product)
local percent = product.quote ~= nil and product.quote.percent or nil 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 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}) table.insert(sell, {product = product, e = e, kind = "sell", targetWayPointId = product.quote.wayPointId, targetName = product.quote.stationName, percent = e.percent})
added = true 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}) table.insert(store, {product = product, e = e, kind = "store", targetWayPointId = product.store.wayPointId, targetName = product.store.name})
added = true added = true
end end
+21 -2
View File
@@ -135,11 +135,30 @@ end
-- ledigt (eller allerede reserveret af dette køretøj) og som isReachable(candidate) -- 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). -- 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. -- 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 = {} local free = {}
for _, candidate in ipairs(candidates) do for _, candidate in ipairs(candidates) do
local owner = claims[candidate.marker.id] 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 local dx, dz = candidate.x - x, candidate.z - z
table.insert(free, {candidate = candidate, distance = dx * dx + dz * dz}) table.insert(free, {candidate = candidate, distance = dx * dx + dz * dz})
end end
+25 -1
View File
@@ -53,12 +53,36 @@ end
-- Nærmeste ledige (eller eget reserverede) Wait-punkt fra vognens position; isReachable(candidate) -- Nærmeste ledige (eller eget reserverede) Wait-punkt fra vognens position; isReachable(candidate)
-- valgfri. nil uden position eller uden brugbart punkt. Reserverer IKKE selv. -- 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) function ADWaitPool.pickFree(adEnv, vehicle, poolGroup, isReachable)
local x, z = getVehiclePosition(vehicle) local x, z = getVehiclePosition(vehicle)
if x == nil then if x == nil then
return nil return nil
end 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 end
-- Ender ruten i målet? (AutoDrive returnerer tom/afkortet liste når der ingen vej er.) -- Ender ruten i målet? (AutoDrive returnerer tom/afkortet liste når der ingen vej er.)
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107"> <modDesc descVersion="107">
<author>masterdraco</author> <author>masterdraco</author>
<version>1.18.2.0</version> <version>1.18.3.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
+7 -1
View File
@@ -1,6 +1,6 @@
# AD Smart Pickup — brugermanual # 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)* *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. 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. 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 Markører i markernes mapper, fx *F91 Wait*, bliver ikke rørt, fordi det er **mappens** navn, der
tæller. 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. 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. 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 ### Sådan vælges hver tur
Før hver pålæsning, og hvert femte sekund mens traktoren holder, vurderer moden: Før hver pålæsning, og hvert femte sekund mens traktoren holder, vurderer moden:
+7 -1
View File
@@ -1,6 +1,6 @@
# AD Smart Pickup — user manual # 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)* *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. 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. 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 Markers in field folders, such as *F91 Wait*, are left alone, because it is the **folder** name
that counts. 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 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. 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 ### How each trip is chosen
Before each loading, and every five seconds while the tractor waits, the mod checks: Before each loading, and every five seconds while the tractor waits, the mod checks:
+11
View File
@@ -156,5 +156,16 @@ check("P15 kilde uden lagre beholdes", kept[3].wayPointId, 5)
local none = P.dedupeSharedSources({}, function() return nil end) local none = P.dedupeSharedSources({}, function() return nil end)
check("P15 tom liste", #none, 0) 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")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+9
View File
@@ -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("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("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) 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) -- 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 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) check("unåeligt nærmeste springes over", ADSupplyPlanner.pickNearestFreeWaitPoint(waitPoints, {}, 10, 0, V1, unreachableA).marker.id, 2)