diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua index 86d3cdc..91586ae 100644 --- a/FS25_ADSmartPickup/adSmartPickup.lua +++ b/FS25_ADSmartPickup/adSmartPickup.lua @@ -24,6 +24,10 @@ local isHooked = false -- 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. 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 -- i den globale tabel FS25_AutoDrive (zip-navnet). @@ -155,7 +159,7 @@ local function findLeastFilled(markerStations, fillTypes, farmId) for _, entry in ipairs(markerStations) do local level, isOwn = getStationFillLevel(entry.station, fillTypes, farmId) 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 @@ -168,6 +172,9 @@ local function findLeastFilled(markerStations, fillTypes, farmId) markerName = choice.marker.name, group = choice.marker.group, level = choice.level, + station = choice.station, + isOwn = choice.isOwn, + fillTypes = fillTypes, } end @@ -243,6 +250,9 @@ function ADSmartPickup.findSupplyPickup(adEnv, driver, ingredients) markerName = choice.markerName, group = choice.group, level = choice.level, + station = choice.station, + isOwn = choice.isOwn, + fillTypes = choice.fillTypes, fillType = need.fillType, need = need, } @@ -277,11 +287,63 @@ function ADSmartPickup.choosePickup(adEnv, driver) end local function describeChoice(choice) + local source = choice.isOwn and "eget lager" or "nabo-lager" if choice.fillType == nil then - return string.format("%.0f l", choice.level) + return string.format("%.0f l, %s", choice.level, source) end 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 local function installHook(adEnv) @@ -292,27 +354,39 @@ local function installHook(adEnv) local ok, choice = pcall(ADSmartPickup.choosePickup, adEnv, driver) if not ok then Logging.warning(ADSmartPickup.LOG_PREFIX .. "fejl i silo-opslag, bruger AutoDrives rotation: %s", tostring(choice)) + activePickups[driver] = nil return originalGetNextPickup(self, driver, forcedSkip) end if choice == nil then + activePickups[driver] = nil return originalGetNextPickup(self, driver, forcedSkip) end + activePickups[driver] = {station = choice.station, fillTypes = choice.fillTypes} -- hold AutoDrives egen rotations-tilstand konsistent med vores valg self.pickups[choice.group] = self.pickups[choice.group] or {} self.pickups[choice.group].lastTarget = choice.markerIndex 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 end + installLoadWaitHook(adEnv) + Logging.info(ADSmartPickup.LOG_PREFIX .. "aktiv (mindst fyldte silo med varen; flere valgte varer = behovsstyret forsyning)") end -- 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). 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 local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil if stateModule == nil or not stateModule:isActive() then diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index a2e5c61..09b8b9a 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -1,7 +1,7 @@ masterdraco - 1.1.0.0 + 1.2.0.0 <en>AD Smart Pickup</en> diff --git a/README.md b/README.md index d86fad0..8521979 100644 --- a/README.md +++ b/README.md @@ -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`). - 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. +- **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 aflæsning. Målet skal være en placeable med aflæsningstrigger (ikke en blandevogn). diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua index 4eab065..a0c6abc 100644 --- a/tests/test_adSmartPickup.lua +++ b/tests/test_adSmartPickup.lua @@ -71,7 +71,15 @@ local function resetWorld() FS25_AutoDrive.ADMultipleTargetsManager.pickups = {} 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}, + -- 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}, ADGraphManager = { getMapMarkersInGroup = function(_, group) @@ -263,5 +271,63 @@ bareStation.owningPlaceable = {spec_productionPoint = {productionPoint = {storag feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS, SILAGE}) 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")) os.exit(failures == 0 and 0 or 1)