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
+48 -8
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
-- end markørens egen. Triggere mere end dette foran næsen hører til naboen.
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).
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: "
local LOAD_TRIGGER_NODE_FIELDS = {"triggerNode"}
@@ -96,10 +105,13 @@ local function getDistancesToTriggers(triggers, nodeFields, wayPoint, direction)
local tx, _, tz = getWorldTranslation(node)
local distance = MathUtil.vector2Length(wayPoint.x - tx, wayPoint.z - tz)
closest = math.min(closest, distance)
-- > 0: triggeren ligger bag næsen set i køreretningen
local behind = direction ~= nil and ((wayPoint.x - tx) * direction.x + (wayPoint.z - tz) * direction.z) or nil
if behind ~= nil and behind >= -ADSmartPickup.MAX_TRIGGER_AHEAD_OF_MARKER then
closestBehind = math.min(closestBehind, distance)
if direction ~= nil then
-- behind > 0: bag næsen set i køreretningen; lateral: afstand fra sporet
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)
end
end
end
end
@@ -107,11 +119,12 @@ local function getDistancesToTriggers(triggers, nodeFields, wayPoint, direction)
return closest, closestBehind
end
-- Markørens station: nærmeste trigger bag næsen; findes ingen (eller er
-- retningen ukendt), nærmeste trigger overhovedet.
-- Markørens station: nærmeste trigger i sporet bag næsen. Findes ingen, bruges
-- 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 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
if g_currentMission.accessHandler:canFarmAccess(farmId, station) then
local closest, closestBehind = getDistancesToTriggers(station[triggersField], nodeFields, wayPoint, direction)
@@ -339,6 +352,12 @@ function ADSmartPickup.shouldWaitForLoading(vehicle)
if pickup == nil then
return false
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))
return level >= ADSmartPickup.MIN_USEFUL_LITERS
end
@@ -364,9 +383,30 @@ local function installLoadWaitHook(adEnv)
return originalGetSetting(settingName, vehicle)
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)
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)
waitingVehicle = nil
if not updateOk then
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.3.0.0</version>
<version>1.4.0.0</version>
<title>
<en>AD Smart Pickup</en>
</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
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.
- **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
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
+57 -5
View File
@@ -78,7 +78,11 @@ end
local RT_NONE, RT_ONLYPICKUP = 1, 2
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
LoadAtDestinationTask = {update = function(self)
self.seenRotate = FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", self.vehicle)
@@ -110,6 +114,13 @@ local function makeDriver(firstMarker, fillTypes)
}
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.
local function makeSupplyDriver(firstMarker, secondMarker, fillTypes)
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()
local loadLevels = {[SUGARBEET] = 5000}
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)
local task = {vehicle = loader}
local task = {vehicle = loader, trailers = {level = 0}}
FS25_AutoDrive.LoadAtDestinationTask.update(task, 16)
check("L1 rotation skjult mens kilden har varen", task.seenRotate, RT_NONE)
check("L1 andre indstillinger uberørt", task.seenOther, "andet")
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
local strangerTask = {vehicle = makeDriver(markers[1], {SUGARBEET})}
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}))
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
resetWorld()
addMarker(1, "Silo 1", "Andet", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[HAY] = 5000}))
addMarker(2, "Blander", "Fabrik", 5000, 0)
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)
local stopperTask = {vehicle = stopper}
local stopperTask = {vehicle = stopper, trailers = {level = 0}}
FS25_AutoDrive.LoadAtDestinationTask.update(stopperTask, 16)
check("L7 venter mens AD kører", stopperTask.seenRotate, RT_NONE)
stopper.ad.stateModule.active = false