From e6412d78e4c63dc9af3f036293d1fd73ed3019d6 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Mon, 21 Sep 2026 16:15:55 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20spor-regel,=20vent=20kun=20ved=20m=C3=A5?= =?UTF-8?q?let,=20sikkerhedsventil=20(v1.4.0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- FS25_ADSmartPickup/adSmartPickup.lua | 56 +++++++++++++++++++++---- FS25_ADSmartPickup/modDesc.xml | 2 +- README.md | 8 ++++ tests/test_adSmartPickup.lua | 62 +++++++++++++++++++++++++--- 4 files changed, 114 insertions(+), 14 deletions(-) diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index 723e05d..b051088 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -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 diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index cd4060a..675cf98 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.3.0.0 + 1.4.0.0 <en>AD Smart Pickup</en> diff --git a/README.md b/README.md index 8d3255d..cdafc8d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua index 33ed85a..be6a227 100644 --- a/tests/test_adSmartPickup.lua +++ b/tests/test_adSmartPickup.lua @@ -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