fix: spor-regel, vent kun ved målet, sikkerhedsventil (v1.4.0.0)

In-game-fund 2026-09-21:
- 'Dropof'-markøren vandt over 'Pickup' ved ensilage-tankene (samme tank, modsat
  side): triggeren skal nu ligge i trækkerens spor (<= 6 m til siden); nødløsning
  uden spor-træf kun inden for 12 m når retningen kendes.
- Rotationen blev skjult under hele pålæsningsopgaven, så traktoren ventede ved
  nabosiloens tomme trigger på vej til målet: skjules nu først når målet er nået.
- Sikkerhedsventil: 30 s ved markøren uden læsning -> grebet slippes.
Verificeret mod savegame: 40/40 silo-markører, 7 pickup koblet, 0 dropof.
This commit is contained in:
2026-09-21 16:15:55 +02:00
parent e414c99b58
commit e6412d78e4
4 changed files with 114 additions and 14 deletions
+47 -7
View File
@@ -16,8 +16,17 @@ ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE = 40
-- I tætte silo-rækker ligger nabosiloens trigger FORAN næsen og er ofte nærmere -- 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. -- end markørens egen. Triggere mere end dette foran næsen hører til naboen.
ADSmartPickup.MAX_TRIGGER_AHEAD_OF_MARKER = 2 ADSmartPickup.MAX_TRIGGER_AHEAD_OF_MARKER = 2
-- 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
-- Kendes køreretningen, men ligger ingen trigger i sporet bag næsen (kurve ind til
-- markøren), accepteres kun en trigger helt tæt på.
ADSmartPickup.MAX_FALLBACK_TRIGGER_DISTANCE = 12
-- Under dette niveau regnes siloen som tom (undgår ture efter støv-rester). -- Under dette niveau regnes siloen som tom (undgår ture efter støv-rester).
ADSmartPickup.MIN_USEFUL_LITERS = 1 ADSmartPickup.MIN_USEFUL_LITERS = 1
-- Sikkerhedsventil: står køretøjet ved markøren så længe uden at vognen fyldes
-- (forkert markør↔silo-kobling, røret når ikke vognen), slippes grebet.
ADSmartPickup.MAX_WAIT_WITHOUT_LOADING_MS = 30000
ADSmartPickup.LOG_PREFIX = "ADSmartPickup: " ADSmartPickup.LOG_PREFIX = "ADSmartPickup: "
local LOAD_TRIGGER_NODE_FIELDS = {"triggerNode"} local LOAD_TRIGGER_NODE_FIELDS = {"triggerNode"}
@@ -96,22 +105,26 @@ local function getDistancesToTriggers(triggers, nodeFields, wayPoint, direction)
local tx, _, tz = getWorldTranslation(node) local tx, _, tz = getWorldTranslation(node)
local distance = MathUtil.vector2Length(wayPoint.x - tx, wayPoint.z - tz) local distance = MathUtil.vector2Length(wayPoint.x - tx, wayPoint.z - tz)
closest = math.min(closest, distance) closest = math.min(closest, distance)
-- > 0: triggeren ligger bag næsen set i køreretningen if direction ~= nil then
local behind = direction ~= nil and ((wayPoint.x - tx) * direction.x + (wayPoint.z - tz) * direction.z) or nil -- behind > 0: bag næsen set i køreretningen; lateral: afstand fra sporet
if behind ~= nil and behind >= -ADSmartPickup.MAX_TRIGGER_AHEAD_OF_MARKER then local behind = (wayPoint.x - tx) * direction.x + (wayPoint.z - tz) * direction.z
local lateral = math.abs((wayPoint.x - tx) * direction.z - (wayPoint.z - tz) * direction.x)
if behind >= -ADSmartPickup.MAX_TRIGGER_AHEAD_OF_MARKER and lateral <= ADSmartPickup.MAX_TRIGGER_LATERAL_OFFSET then
closestBehind = math.min(closestBehind, distance) closestBehind = math.min(closestBehind, distance)
end end
end end
end end
end end
end
return closest, closestBehind return closest, closestBehind
end end
-- Markørens station: nærmeste trigger bag næsen; findes ingen (eller er -- Markørens station: nærmeste trigger i sporet bag næsen. Findes ingen, bruges
-- retningen ukendt), nærmeste trigger overhovedet. -- nærmeste trigger overhovedet — helt tæt på hvis retningen kendes, ellers som før.
local function getClosestStation(adEnv, candidateStations, triggersField, nodeFields, wayPoint, farmId) local function getClosestStation(adEnv, candidateStations, triggersField, nodeFields, wayPoint, farmId)
local direction = getArrivalDirection(adEnv, wayPoint) local direction = getArrivalDirection(adEnv, wayPoint)
local best = {any = {distance = ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE}, behind = {distance = ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE}} local maxFallbackDistance = direction ~= nil and ADSmartPickup.MAX_FALLBACK_TRIGGER_DISTANCE or ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE
local best = {any = {distance = maxFallbackDistance}, behind = {distance = ADSmartPickup.MAX_MARKER_TO_TRIGGER_DISTANCE}}
for _, station in pairs(candidateStations) do for _, station in pairs(candidateStations) do
if g_currentMission.accessHandler:canFarmAccess(farmId, station) then if g_currentMission.accessHandler:canFarmAccess(farmId, station) then
local closest, closestBehind = getDistancesToTriggers(station[triggersField], nodeFields, wayPoint, direction) local closest, closestBehind = getDistancesToTriggers(station[triggersField], nodeFields, wayPoint, direction)
@@ -339,6 +352,12 @@ function ADSmartPickup.shouldWaitForLoading(vehicle)
if pickup == nil then if pickup == nil then
return false return false
end end
-- kun FREMME ved markøren: undervejs ruller vognen gennem nabosiloers triggere,
-- og dér skal AutoDrives rotation gælde (kør videre ved tom trigger).
local drivePathModule = vehicle.ad ~= nil and vehicle.ad.drivePathModule or nil
if drivePathModule == nil or not drivePathModule:isTargetReached() then
return false
end
local level = getStationFillLevel(pickup.station, pickup.fillTypes, getFarmId(vehicle)) local level = getStationFillLevel(pickup.station, pickup.fillTypes, getFarmId(vehicle))
return level >= ADSmartPickup.MIN_USEFUL_LITERS return level >= ADSmartPickup.MIN_USEFUL_LITERS
end end
@@ -364,9 +383,30 @@ local function installLoadWaitHook(adEnv)
return originalGetSetting(settingName, vehicle) return originalGetSetting(settingName, vehicle)
end end
-- true når vognen ikke er blevet fyldt i MAX_WAIT_WITHOUT_LOADING_MS ved markøren
local function hasLoadingStalled(loadTask, dt)
local pickup = activePickups[loadTask.vehicle]
local fillLevel = autoDrive.getAllFillLevels ~= nil and autoDrive.getAllFillLevels(loadTask.trailers) or 0
if pickup.lastFillLevel == nil or fillLevel > pickup.lastFillLevel then
pickup.lastFillLevel, pickup.stalledMs = fillLevel, 0
return false
end
pickup.stalledMs = pickup.stalledMs + dt
return pickup.stalledMs > ADSmartPickup.MAX_WAIT_WITHOUT_LOADING_MS
end
task.update = function(self, dt) task.update = function(self, dt)
local ok, shouldWait = pcall(ADSmartPickup.shouldWaitForLoading, self.vehicle) local ok, shouldWait = pcall(ADSmartPickup.shouldWaitForLoading, self.vehicle)
waitingVehicle = ok and shouldWait and self.vehicle or nil shouldWait = ok and shouldWait
if shouldWait then
local stalledOk, isStalled = pcall(hasLoadingStalled, self, dt)
if not stalledOk or isStalled then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "'%s' læsser ikke ved markøren; slipper grebet så AutoDrive kører videre", tostring(self.vehicle:getName()))
activePickups[self.vehicle] = nil
shouldWait = false
end
end
waitingVehicle = shouldWait and self.vehicle or nil
local updateOk, err = pcall(originalUpdate, self, dt) local updateOk, err = pcall(originalUpdate, self, dt)
waitingVehicle = nil waitingVehicle = nil
if not updateOk then if not updateOk then
+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.3.0.0</version> <version>1.4.0.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
+8
View File
@@ -34,6 +34,14 @@ 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 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 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. nærmere end markørens egen. Ingen trigger bag næsen/ukendt retning → nærmeste trigger som før.
- **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
markøren), accepteres kun en trigger inden for 12 m.
- **Kun fremme ved markøren (v1.4):** rotationen skjules først når målet er nået — undervejs ruller
vognen gennem nabosiloers triggere, og dér skal AutoDrive køre videre ved tom trigger.
- **Sikkerhedsventil (v1.4):** 30 s ved markøren uden at vognen fyldes → grebet slippes (advarsel i
log), og AutoDrive kører videre som med almindelig rotation.
- **Står og læsser (v1.2):** med rotation + mapper afslutter AutoDrive normalt pålæsningen i det øjeblik - **Står og læsser (v1.2):** med rotation + mapper afslutter AutoDrive normalt pålæsningen i det øjeblik
markøren nås (læsning skal være startet på vej ind, ellers kører den tom videre). Når moden har valgt markøren nås (læsning skal være startet på vej ind, ellers kører den tom videre). Når moden har valgt
siloen, ser AutoDrive under `LoadAtDestinationTask.update` rotationen som slået fra for netop det siloen, ser AutoDrive under `LoadAtDestinationTask.update` rotationen som slået fra for netop det
+57 -5
View File
@@ -78,7 +78,11 @@ end
local RT_NONE, RT_ONLYPICKUP = 1, 2 local RT_NONE, RT_ONLYPICKUP = 1, 2
FS25_AutoDrive = { FS25_AutoDrive = {
AutoDrive = {RT_NONE = RT_NONE, getSetting = function(settingName) return settingName == "rotateTargets" and RT_ONLYPICKUP or "andet" end}, AutoDrive = {
RT_NONE = RT_NONE,
getSetting = function(settingName) return settingName == "rotateTargets" and RT_ONLYPICKUP or "andet" end,
getAllFillLevels = function(trailers) return trailers.level end,
},
-- opgaven noterer hvad AutoDrive "ser" af indstillinger mens den opdaterer -- opgaven noterer hvad AutoDrive "ser" af indstillinger mens den opdaterer
LoadAtDestinationTask = {update = function(self) LoadAtDestinationTask = {update = function(self)
self.seenRotate = FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", self.vehicle) self.seenRotate = FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", self.vehicle)
@@ -110,6 +114,13 @@ local function makeDriver(firstMarker, fillTypes)
} }
end end
-- Køretøjet er "fremme ved markøren" medmindre testen siger andet.
local function withDrivePath(driver)
driver.targetReached = true
driver.ad.drivePathModule = {isTargetReached = function() return driver.targetReached end}
return driver
end
-- Muterbar stateModule-mock: behovsstyringen skifter vare og indsnævrer AD's valg. -- Muterbar stateModule-mock: behovsstyringen skifter vare og indsnævrer AD's valg.
local function makeSupplyDriver(firstMarker, secondMarker, fillTypes) local function makeSupplyDriver(firstMarker, secondMarker, fillTypes)
local stateModule = {selectedFillTypes = fillTypes, fillType = fillTypes[1], active = true, dirtyCount = 0} local stateModule = {selectedFillTypes = fillTypes, fillType = fillTypes[1], active = true, dirtyCount = 0}
@@ -280,14 +291,23 @@ check("S10 lager læses fra produktionspunktet", manager:getNextPickup(feeder, f
resetWorld() resetWorld()
local loadLevels = {[SUGARBEET] = 5000} local loadLevels = {[SUGARBEET] = 5000}
addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, loadLevels)) addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, loadLevels))
local loader = makeDriver(markers[1], {SUGARBEET}) local loader = withDrivePath(makeDriver(markers[1], {SUGARBEET}))
manager:getNextPickup(loader, false) manager:getNextPickup(loader, false)
local task = {vehicle = loader} local task = {vehicle = loader, trailers = {level = 0}}
FS25_AutoDrive.LoadAtDestinationTask.update(task, 16) FS25_AutoDrive.LoadAtDestinationTask.update(task, 16)
check("L1 rotation skjult mens kilden har varen", task.seenRotate, RT_NONE) check("L1 rotation skjult mens kilden har varen", task.seenRotate, RT_NONE)
check("L1 andre indstillinger uberørt", task.seenOther, "andet") check("L1 andre indstillinger uberørt", task.seenOther, "andet")
check("L1 rotation synlig igen efter opgaven", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", loader), RT_ONLYPICKUP) check("L1 rotation synlig igen efter opgaven", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", loader), RT_ONLYPICKUP)
-- L8: PÅ VEJ til siloen ruller vognen gennem nabosiloers triggere -> dér skal
-- AutoDrives rotation gælde (kør videre ved tom trigger), ikke "stå og vent"
loader.targetReached = false
FS25_AutoDrive.LoadAtDestinationTask.update(task, 16)
check("L8 undervejs: rotation uberørt", task.seenRotate, RT_ONLYPICKUP)
loader.targetReached = true
FS25_AutoDrive.LoadAtDestinationTask.update(task, 16)
check("L8 fremme ved markøren: står og læsser", task.seenRotate, RT_NONE)
-- L2: andre køretøjer (ikke sendt af moden) beholder AutoDrives rotation -- L2: andre køretøjer (ikke sendt af moden) beholder AutoDrives rotation
local strangerTask = {vehicle = makeDriver(markers[1], {SUGARBEET})} local strangerTask = {vehicle = makeDriver(markers[1], {SUGARBEET})}
FS25_AutoDrive.LoadAtDestinationTask.update(strangerTask, 16) FS25_AutoDrive.LoadAtDestinationTask.update(strangerTask, 16)
@@ -328,14 +348,46 @@ addMarker(1, "Ensom silo", "Siloer", 100, 0, 120, 0)
table.insert(stations, makeStation(101, 95, 1, {[HAY] = 9000})) table.insert(stations, makeStation(101, 95, 1, {[HAY] = 9000}))
check("G2 kun trigger foran -> nærmeste bruges", manager:getNextPickup(makeDriver(markers[1], {HAY}), false), 1) check("G2 kun trigger foran -> nærmeste bruges", manager:getNextPickup(makeDriver(markers[1], {HAY}), false), 1)
-- G3: tank med aflæsningsbane på den ene side og pålæsningsbane på den anden.
-- Dropoff-markøren ligger 18 m ved siden af læsserøret -> ikke en pålæsningsplads,
-- selv om den står først i listen og "ser" samme tank.
resetWorld()
addMarker(1, "1 Silage Dropof", "Ensilage", 1265, -2278, 1265, -2290) -- kører +z, røret 18 m til siden
addMarker(2, "1 Silage Pickup", "Ensilage", 1247, -2278, 1247, -2266) -- kører -z, røret 7 m bag næsen
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)
-- 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
table.insert(stations, makeStation(101, 103, -8, {[SILAGE] = 4000})) -- 8 m foran, 3 m til siden
check("G4 tæt trigger foran næsen bruges som nødløsning", manager:getNextPickup(makeDriver(markers[1], {SILAGE}), false), 1)
-- L9: sikkerhedsventil — står den ved markøren uden at vognen fyldes, slippes grebet
resetWorld()
addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[SUGARBEET] = 5000}))
local stuck = withDrivePath(makeDriver(markers[1], {SUGARBEET}))
manager:getNextPickup(stuck, false)
local stuckTask = {vehicle = stuck, trailers = {level = 0}}
FS25_AutoDrive.LoadAtDestinationTask.update(stuckTask, 10000)
stuckTask.trailers.level = 800 -- der læsses: uret nulstilles
FS25_AutoDrive.LoadAtDestinationTask.update(stuckTask, 25000)
FS25_AutoDrive.LoadAtDestinationTask.update(stuckTask, 25000)
check("L9 venter stadig mens der er læsset for nylig", stuckTask.seenRotate, RT_NONE)
FS25_AutoDrive.LoadAtDestinationTask.update(stuckTask, 10000) -- 35 s uden fremgang
check("L9 ingen læsning i 30 s -> greb sluppet", stuckTask.seenRotate, RT_ONLYPICKUP)
check("L9 sluppet greb logges som advarsel", logLines[#logLines]:sub(1, 4), "WARN")
FS25_AutoDrive.LoadAtDestinationTask.update(stuckTask, 16)
check("L9 grebet tages ikke igen på samme tur", stuckTask.seenRotate, RT_ONLYPICKUP)
-- L7: AD stopper -> grebet ryddes, næste opgave venter ikke på en gammel silo -- L7: AD stopper -> grebet ryddes, næste opgave venter ikke på en gammel silo
resetWorld() resetWorld()
addMarker(1, "Silo 1", "Andet", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[HAY] = 5000})) addMarker(1, "Silo 1", "Andet", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[HAY] = 5000}))
addMarker(2, "Blander", "Fabrik", 5000, 0) addMarker(2, "Blander", "Fabrik", 5000, 0)
table.insert(unloadStations, makeUnloadStation(201, 5000, 15, {[HAY] = 0}, {[HAY] = 1000000, [GRASS] = 1000000}, {})) table.insert(unloadStations, makeUnloadStation(201, 5000, 15, {[HAY] = 0}, {[HAY] = 1000000, [GRASS] = 1000000}, {}))
local stopper = makeSupplyDriver(markers[1], markers[2], {HAY, GRASS}) local stopper = withDrivePath(makeSupplyDriver(markers[1], markers[2], {HAY, GRASS}))
manager:getNextPickup(stopper, false) manager:getNextPickup(stopper, false)
local stopperTask = {vehicle = stopper} local stopperTask = {vehicle = stopper, trailers = {level = 0}}
FS25_AutoDrive.LoadAtDestinationTask.update(stopperTask, 16) FS25_AutoDrive.LoadAtDestinationTask.update(stopperTask, 16)
check("L7 venter mens AD kører", stopperTask.seenRotate, RT_NONE) check("L7 venter mens AD kører", stopperTask.seenRotate, RT_NONE)
stopper.ad.stateModule.active = false stopper.ad.stateModule.active = false