fix: stå og læs ved den valgte silo — AutoDrive kørte tom videre (v1.2.0.0)

Med rotateTargets + mapper afslutter AutoDrives LoadAtDestinationTask pålæsningen
i det øjeblik markøren nås; traktoren kørte derfor lige igennem silo-markøren og
tom til aflæsning. Når moden har valgt siloen, ses rotationen som slået fra for
netop det køretøj under opgavens update, så AutoDrive står og læsser. Grebet
slippes når kilden er tom eller AD stopper.

Desuden: samme valg logges kun én gang, og loggen viser eget/nabo-lager.
This commit is contained in:
2026-09-21 15:55:07 +02:00
parent 95615253b0
commit bcc245863e
4 changed files with 150 additions and 5 deletions
+78 -4
View File
@@ -24,6 +24,10 @@ local isHooked = false
-- køretøj -> {ingredients, narrowedTo}. AutoDrive roterer selv vare ved triggeren -- køretøj -> {ingredients, narrowedTo}. AutoDrive roterer selv vare ved triggeren
-- når flere er valgt, så valget indsnævres til én pr. tur; listen huskes her. -- når flere er valgt, så valget indsnævres til én pr. tur; listen huskes her.
local supplyState = setmetatable({}, {__mode = "k"}) local supplyState = setmetatable({}, {__mode = "k"})
-- køretøj -> {station, fillTypes}: siloen moden sendte køretøjet til på denne tur.
local activePickups = setmetatable({}, {__mode = "k"})
-- køretøj -> senest loggede valg (AutoDrive spørger flere gange pr. start).
local lastLogged = setmetatable({}, {__mode = "k"})
-- Cross-mod adgang: FS25-mods er sandboxede; AutoDrives globals ligger -- Cross-mod adgang: FS25-mods er sandboxede; AutoDrives globals ligger
-- i den globale tabel FS25_AutoDrive (zip-navnet). -- i den globale tabel FS25_AutoDrive (zip-navnet).
@@ -155,7 +159,7 @@ local function findLeastFilled(markerStations, fillTypes, farmId)
for _, entry in ipairs(markerStations) do for _, entry in ipairs(markerStations) do
local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId) local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId)
if level >= ADSmartPickup.MIN_USEFUL_LITERS and level < best[isOwn].level then if level >= ADSmartPickup.MIN_USEFUL_LITERS and level < best[isOwn].level then
best[isOwn] = {level = level, marker = entry.marker} best[isOwn] = {level = level, marker = entry.marker, station = entry.station, isOwn = isOwn}
end end
end end
@@ -168,6 +172,9 @@ local function findLeastFilled(markerStations, fillTypes, farmId)
markerName = choice.marker.name, markerName = choice.marker.name,
group = choice.marker.group, group = choice.marker.group,
level = choice.level, level = choice.level,
station = choice.station,
isOwn = choice.isOwn,
fillTypes = fillTypes,
} }
end end
@@ -243,6 +250,9 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients)
markerName = choice.markerName, markerName = choice.markerName,
group = choice.group, group = choice.group,
level = choice.level, level = choice.level,
station = choice.station,
isOwn = choice.isOwn,
fillTypes = choice.fillTypes,
fillType = need.fillType, fillType = need.fillType,
need = need, need = need,
} }
@@ -277,11 +287,63 @@ function ADSmartPickup.choosePickup(adEnv, driver)
end end
local function describeChoice(choice) local function describeChoice(choice)
local source = choice.isOwn and "eget lager" or "nabo-lager"
if choice.fillType == nil then if choice.fillType == nil then
return string.format("%.0f l", choice.level) return string.format("%.0f l, %s", choice.level, source)
end end
local fillTypeName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(choice.fillType) or tostring(choice.fillType) local fillTypeName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(choice.fillType) or tostring(choice.fillType)
return string.format("%s: kilde %.0f l, målet har %.0f/%.0f l", tostring(fillTypeName), choice.level, choice.need.level, choice.need.capacity) return string.format("%s: kilde %.0f l, %s, målet har %.0f/%.0f l", tostring(fillTypeName), choice.level, source, choice.need.level, choice.need.capacity)
end
local function logChoiceOnce(driver, choice)
local message = string.format("'%s' -> %s (%s)", tostring(driver:getName()), tostring(choice.markerName), describeChoice(choice))
if lastLogged[driver] ~= message then
lastLogged[driver] = message
Logging.info(ADSmartPickup.LOG_PREFIX .. "%s", message)
end
end
-- Sendte moden køretøjet til en silo der stadig har varen? Så skal AutoDrive
-- stå og læsse dér; løber kilden tør, slippes grebet og turen fortsætter.
function ADSmartPickup.shouldWaitForLoading(vehicle)
local pickup = activePickups[vehicle]
if pickup == nil then
return false
end
local level = getStationFillLevel(pickup.station, pickup.fillTypes, getFarmId(vehicle))
return level >= ADSmartPickup.MIN_USEFUL_LITERS
end
-- Med "Rotér mål" + mapper afslutter AutoDrive pålæsningen i det øjeblik markøren
-- nås (LoadAtDestinationTask: rotation = "prøv næste silo", aldrig vente). Moden
-- har valgt en silo der HAR varen, så under opgavens update ser AutoDrive
-- rotationen som slået fra for netop det køretøj -> den står og læsser som normalt.
local function installLoadWaitHook(adEnv)
local task, autoDrive = adEnv.LoadAtDestinationTask, adEnv.AutoDrive
if task == nil or task.update == nil or autoDrive == nil or autoDrive.getSetting == nil then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "kunne ikke koble på pålæsningsopgaven; traktoren venter ikke ved siloen")
return
end
local originalGetSetting, originalUpdate = autoDrive.getSetting, task.update
local waitingVehicle = nil
autoDrive.getSetting = function(settingName, vehicle)
if waitingVehicle ~= nil and vehicle == waitingVehicle and settingName == "rotateTargets" then
return autoDrive.RT_NONE
end
return originalGetSetting(settingName, vehicle)
end
task.update = function(self, dt)
local ok, shouldWait = pcall(ADSmartPickup.shouldWaitForLoading, self.vehicle)
waitingVehicle = ok and shouldWait and self.vehicle or nil
local updateOk, err = pcall(originalUpdate, self, dt)
waitingVehicle = nil
if not updateOk then
error(err, 0)
end
end
end end
local function installHook(adEnv) local function installHook(adEnv)
@@ -292,27 +354,39 @@ local function installHook(adEnv)
local ok, choice = pcall(ADSmartPickup.choosePickup, adEnv, driver) local ok, choice = pcall(ADSmartPickup.choosePickup, adEnv, driver)
if not ok then if not ok then
Logging.warning(ADSmartPickup.LOG_PREFIX .. "fejl i silo-opslag, bruger AutoDrives rotation: %s", tostring(choice)) Logging.warning(ADSmartPickup.LOG_PREFIX .. "fejl i silo-opslag, bruger AutoDrives rotation: %s", tostring(choice))
activePickups[driver] = nil
return originalGetNextPickup(self, driver, forcedSkip) return originalGetNextPickup(self, driver, forcedSkip)
end end
if choice == nil then if choice == nil then
activePickups[driver] = nil
return originalGetNextPickup(self, driver, forcedSkip) return originalGetNextPickup(self, driver, forcedSkip)
end end
activePickups[driver] = {station = choice.station, fillTypes = choice.fillTypes}
-- hold AutoDrives egen rotations-tilstand konsistent med vores valg -- hold AutoDrives egen rotations-tilstand konsistent med vores valg
self.pickups[choice.group] = self.pickups[choice.group] or {} self.pickups[choice.group] = self.pickups[choice.group] or {}
self.pickups[choice.group].lastTarget = choice.markerIndex self.pickups[choice.group].lastTarget = choice.markerIndex
self.pickups[choice.group].lastVehicle = driver self.pickups[choice.group].lastVehicle = driver
Logging.info(ADSmartPickup.LOG_PREFIX .. "'%s' -> %s (%s)", tostring(driver:getName()), tostring(choice.markerName), describeChoice(choice)) logChoiceOnce(driver, choice)
return choice.markerIndex return choice.markerIndex
end end
installLoadWaitHook(adEnv)
Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning)") Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning)")
end end
-- Når AutoDrive stopper, får køretøjet hele ingredienslisten tilbage i varevalget -- Når AutoDrive stopper, får køretøjet hele ingredienslisten tilbage i varevalget
-- (AutoDrive gemmer valget i savegame, så listen overlever også en genindlæsning). -- (AutoDrive gemmer valget i savegame, så listen overlever også en genindlæsning).
local function restoreStoppedVehicles() local function restoreStoppedVehicles()
-- en standset traktor må ikke tage "stå og læs"-grebet med over i næste opgave
for vehicle in pairs(activePickups) do
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if stateModule == nil or stateModule.isActive == nil or not stateModule:isActive() then
activePickups[vehicle] = nil
end
end
for vehicle, state in pairs(supplyState) do for vehicle, state in pairs(supplyState) do
local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if stateModule == nil or not stateModule:isActive() then if stateModule == nil or not stateModule:isActive() 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.1.0.0</version> <version>1.2.0.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
+5
View File
@@ -30,6 +30,11 @@ Slå fra ved at vælge en anden enkelt vare.
- Siloens eget lager rangeres før nabo-lån (udvidelses-siloer deler lager inden for `storageRadius`). - Siloens eget lager rangeres før nabo-lån (udvidelses-siloer deler lager inden for `storageRadius`).
- Intet fund, intet behov eller fejl i et spil-API → AutoDrives originale rotation. Valg logges som `ADSmartPickup:` i log.txt. - Intet fund, intet behov eller fejl i et spil-API → AutoDrives originale rotation. Valg logges som `ADSmartPickup:` i log.txt.
- Kræver i AutoDrive: "Brug mapper" + "Rotér mål: pålæsning" på køretøjet. - Kræver i AutoDrive: "Brug mapper" + "Rotér mål: pålæsning" på køretøjet.
- **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
køretøj, så den holder ved markøren og læsser som uden rotation. Løber kilden tør, slippes grebet og
turen fortsætter med det der er i vognen.
Begrænsninger: AD læsser fuld vogn — er der mindre plads i målet end ét læs, venter traktoren ved Begrænsninger: AD læsser fuld vogn — er der mindre plads i målet end ét læs, venter traktoren ved
aflæsning. Målet skal være en placeable med aflæsningstrigger (ikke en blandevogn). aflæsning. Målet skal være en placeable med aflæsningstrigger (ikke en blandevogn).
+66
View File
@@ -71,7 +71,15 @@ local function resetWorld()
FS25_AutoDrive.ADMultipleTargetsManager.pickups = {} FS25_AutoDrive.ADMultipleTargetsManager.pickups = {}
end end
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},
-- opgaven noterer hvad AutoDrive "ser" af indstillinger mens den opdaterer
LoadAtDestinationTask = {update = function(self)
self.seenRotate = FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", self.vehicle)
self.seenOther = FS25_AutoDrive.AutoDrive.getSetting("useFolders", self.vehicle)
if self.shouldFail then error("task boom") end
end},
ADMultipleTargetsManager = {pickups = {}, getNextPickup = function() return "ORIGINAL" end}, ADMultipleTargetsManager = {pickups = {}, getNextPickup = function() return "ORIGINAL" end},
ADGraphManager = { ADGraphManager = {
getMapMarkersInGroup = function(_, group) getMapMarkersInGroup = function(_, group)
@@ -263,5 +271,63 @@ bareStation.owningPlaceable = {spec_productionPoint = {productionPoint = {storag
feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS, SILAGE}) feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS, SILAGE})
check("S10 lager læses fra produktionspunktet", manager:getNextPickup(feeder, false), 3) check("S10 lager læses fra produktionspunktet", manager:getNextPickup(feeder, false), 3)
-- L1: moden valgte siloen -> AutoDrive skal stå og læsse (rotation skjules under opgaven)
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})
manager:getNextPickup(loader, false)
local task = {vehicle = loader}
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)
-- 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)
check("L2 fremmed køretøj uberørt", strangerTask.seenRotate, RT_ONLYPICKUP)
-- L3: kilden løber tør -> grebet slippes, AutoDrive kører videre med det den har
loadLevels[SUGARBEET] = 0
FS25_AutoDrive.LoadAtDestinationTask.update(task, 16)
check("L3 tom kilde -> rotation synlig igen", task.seenRotate, RT_ONLYPICKUP)
-- L4: fejl i AutoDrives opgave må ikke efterlade rotationen skjult
loadLevels[SUGARBEET] = 5000
task.shouldFail = true
check("L4 fejl i opgaven sendes videre", pcall(FS25_AutoDrive.LoadAtDestinationTask.update, task, 16), false)
check("L4 rotation synlig efter fejl", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", loader), RT_ONLYPICKUP)
-- L5: AutoDrives original valgte (intet fund) -> ingen ventetid
task.shouldFail = false
loader.ad.stateModule.getSelectedFillTypes = function() return {WHEAT} end
manager:getNextPickup(loader, false)
FS25_AutoDrive.LoadAtDestinationTask.update(task, 16)
check("L5 AutoDrives eget valg -> rotation uberørt", task.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})
manager:getNextPickup(stopper, false)
local stopperTask = {vehicle = stopper}
FS25_AutoDrive.LoadAtDestinationTask.update(stopperTask, 16)
check("L7 venter mens AD kører", stopperTask.seenRotate, RT_NONE)
stopper.ad.stateModule.active = false
listener:update(16)
FS25_AutoDrive.LoadAtDestinationTask.update(stopperTask, 16)
check("L7 greb ryddet når AD stopper", stopperTask.seenRotate, RT_ONLYPICKUP)
-- L6: samme valg logges kun én gang
resetWorld()
addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[SUGARBEET] = 5000}, false, {[SUGARBEET] = 5000}))
local chatty = makeDriver(markers[1], {SUGARBEET})
manager:getNextPickup(chatty, false); manager:getNextPickup(chatty, false)
check("L6 gentaget valg logges én gang", #logLines, 1)
check("L6 loggen viser eget lager", logLines[1]:find("eget lager", 1, true) ~= nil, true)
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)