diff --git a/FS25_ADSmartPickup/adLoadSwap.lua b/FS25_ADSmartPickup/adLoadSwap.lua new file mode 100644 index 0000000..3ccd5db --- /dev/null +++ b/FS25_ADSmartPickup/adLoadSwap.lua @@ -0,0 +1,107 @@ +-- AD Load Swap +-- En traktor der venter ved et Wait-punkt med en vare målet er fuldt af, mens målet +-- MANGLER en anden vare i traktorens liste, låser forsyningen (alle venter med halm, +-- ingen henter TMR). Her afleveres resten i en silo der tager varen, så vognen er tom +-- og næste tur henter det målet mangler. + +ADLoadSwap = {} + +ADLoadSwap.LOG_PREFIX = "ADSmartPickup: " +-- Opslaget går over alle markører; ventende traktorer tjekker derfor kun så tit. +ADLoadSwap.CHECK_INTERVAL_MS = 5000 + +local warnedNoSilo = setmetatable({}, {__mode = "k"}) + +-- Er en anden vare i listen så tom i målet, at et FULDT læs passer, og findes den i en silo? +function ADLoadSwap.findMissingIngredient(adEnv, vehicle, restFillType) + local ingredients = ADSmartPickup.getIngredientsFor(adEnv, vehicle) + if ingredients == nil or #ingredients < 2 then + return nil + end + local choice = ADSmartPickup.findSupplyPickup(adEnv, vehicle, ingredients) + if choice == nil or choice.fillType == nil or choice.fillType == restFillType or choice.need.fits ~= true then + return nil + end + return choice +end + +-- Kun rene lager-siloer: stalde og fabrikker har egne regler, og målet selv er fuldt. +local function isSilo(station, homeStation) + local placeable = station.owningPlaceable + return placeable ~= nil and placeable.spec_silo ~= nil and placeable.getHusbandryFillLevel == nil + and placeable.spec_productionPoint == nil and (homeStation == nil or placeable ~= homeStation.owningPlaceable) +end + +local function acceptsRest(station, fillType, rest, farmId) + if station.getIsFillTypeSupported ~= nil and not station:getIsFillTypeSupported(fillType) then + return false + end + local free = station.getFreeCapacity ~= nil and station:getFreeCapacity(fillType, farmId) or 0 + return free >= rest +end + +-- Nærmeste silo med plads til resten og vejnet til/fra; siloer der allerede har varen foretrækkes. +function ADLoadSwap.findDumpMarker(adEnv, vehicle, fillType, rest, homeWayPointId, isReachable) + local farmId = ADSmartPickup.getFarmId(vehicle) + local homeStation = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, homeWayPointId, farmId) + local x, _, z = getWorldTranslation(vehicle.rootNode) + local candidates = {} + for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do + local station = ADSmartPickup.getUnloadStationAtWayPointId(adEnv, marker.id, farmId) + if station ~= nil and isSilo(station, homeStation) and acceptsRest(station, fillType, rest, farmId) then + local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id) + local dx, dz = wayPoint.x - x, wayPoint.z - z + table.insert(candidates, { + marker = marker, + hasFillType = ADSmartPickup.getOwnStorageLevel(station, {fillType}) > 0, + distance = dx * dx + dz * dz, + }) + end + end + table.sort(candidates, function(a, b) + if a.hasFillType ~= b.hasFillType then + return a.hasFillType + end + return a.distance < b.distance + end) + for _, candidate in ipairs(candidates) do + if isReachable(candidate.marker.id) then + return candidate.marker + end + end + return nil +end + +-- Kaldes fra Wait-logikken hvert tick; redirect(task, id) og release(vehicle) er dens egne. +-- true = traktoren er sendt til en silo (Wait-punktet er frigivet). +function ADLoadSwap.tryFromWait(adEnv, task, wait, dt, redirect, release, isReachable) + wait.swapTimerMs = (wait.swapTimerMs or 0) + (dt or 0) + if wait.swapTimerMs < ADLoadSwap.CHECK_INTERVAL_MS then + return false + end + wait.swapTimerMs = 0 + local vehicle = task.vehicle + local missing = ADLoadSwap.findMissingIngredient(adEnv, vehicle, wait.fillType) + if missing == nil then + return false + end + local marker = ADLoadSwap.findDumpMarker(adEnv, vehicle, wait.fillType, wait.rest, wait.homeDestinationID, isReachable) + local restName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(wait.fillType) or tostring(wait.fillType) + local missingName = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(missing.fillType) or tostring(missing.fillType) + if marker == nil then + if not warnedNoSilo[vehicle] then + warnedNoSilo[vehicle] = true + Logging.warning(ADLoadSwap.LOG_PREFIX .. "'%s' venter med %.0f l %s, og målet mangler %s — ingen silo med plads og vejnet til/fra tager resten, så den bliver ved Wait-punktet", tostring(vehicle:getName()), wait.rest, tostring(restName), tostring(missingName)) + end + return false + end + warnedNoSilo[vehicle] = nil + release(vehicle) + redirect(task, marker.id) + Logging.info(ADLoadSwap.LOG_PREFIX .. "'%s' bytter læs: %.0f l %s tilbage i %s (markør %s), målet mangler %s", tostring(vehicle:getName()), wait.rest, tostring(restName), tostring(marker.name), tostring(marker.markerIndex or marker.id), tostring(missingName)) + return true +end + +function ADLoadSwap.clearWarning(vehicle) + warnedNoSilo[vehicle] = nil +end diff --git a/FS25_ADSmartPickup/adUnloadWait.lua b/FS25_ADSmartPickup/adUnloadWait.lua index 62ba880..cb54087 100644 --- a/FS25_ADSmartPickup/adUnloadWait.lua +++ b/FS25_ADSmartPickup/adUnloadWait.lua @@ -96,6 +96,9 @@ local function release(vehicle) claims[wait.waitMarker.id] = nil end waits[vehicle] = nil + if ADLoadSwap ~= nil then + ADLoadSwap.clearWarning(vehicle) + end end -- Står køretøjet ved målet med rest-læs, som målet ikke kan tage? Så find et Wait-punkt. @@ -136,7 +139,7 @@ local function startWaiting(adEnv, task) end -- Ventende køretøj: er der nu plads til hele resten, køres tilbage til målet. -local function updateWaiting(adEnv, task, wait) +local function updateWaiting(adEnv, task, wait, dt) local vehicle = task.vehicle if wait.task ~= task then release(vehicle) @@ -149,6 +152,12 @@ local function updateWaiting(adEnv, task, wait) end local free, capacity = ADSmartPickup.getTargetFreeCapacity(adEnv, vehicle, fillType) if free == nil or not ADSupplyPlanner.canUnloadRest(free, rest, capacity) then + if ADLoadSwap ~= nil then + local isReachable = function(markerId) + return hasRouteBothWays(adEnv, vehicle, markerId, wait.homeDestinationID) + end + ADLoadSwap.tryFromWait(adEnv, task, wait, dt, redirect, release, isReachable) + end return end local home = wait.homeDestinationID @@ -157,10 +166,10 @@ local function updateWaiting(adEnv, task, wait) Logging.info(ADUnloadWait.LOG_PREFIX .. "'%s' kører tilbage fra %s og læsser %.0f l af (målet har %.0f l fri)", tostring(vehicle:getName()), tostring(wait.waitMarker.name), rest, free) end -function ADUnloadWait.beforeUpdate(adEnv, task) +function ADUnloadWait.beforeUpdate(adEnv, task, dt) local wait = waits[task.vehicle] if wait ~= nil then - updateWaiting(adEnv, task, wait) + updateWaiting(adEnv, task, wait, dt) else startWaiting(adEnv, task) end @@ -204,7 +213,7 @@ function ADUnloadWait.install(adEnv) local originalUpdate, originalFinished, originalGetSetting = task.update, task.finished, autoDrive.getSetting task.update = function(self, dt) - local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self) + local ok, err = pcall(ADUnloadWait.beforeUpdate, adEnv, self, dt) if not ok then Logging.warning(ADUnloadWait.LOG_PREFIX .. "fejl i Wait-punkt-logik, AutoDrive fortsætter selv: %s", tostring(err)) release(self.vehicle) diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index 681394c..d054b10 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -16,6 +16,7 @@ + diff --git a/tests/test_adSmartPickup.lua b/tests/test_adSmartPickup.lua index 9b61381..6d26360 100644 --- a/tests/test_adSmartPickup.lua +++ b/tests/test_adSmartPickup.lua @@ -31,10 +31,13 @@ local function makeStation(node, x, z, levels, isBroken, ownLevels) return { owningPlaceable = placeable, loadTriggers = {{triggerNode = node}}, + unloadTriggers = {{exactFillRootNode = node}}, getFillLevel = function(_, fillType) if isBroken then error("boom") end return levels[fillType] or 0 end, + getIsFillTypeSupported = function(_, fillType) return levels[fillType] ~= nil or (ownLevels ~= nil and ownLevels[fillType] ~= nil) end, + getFreeCapacity = function(_, fillType) return (levels.capacity or 200000) - (levels[fillType] or 0) end, } end @@ -163,6 +166,7 @@ end dofile("FS25_ADSmartPickup/adSupplyPlanner.lua") dofile("FS25_ADSmartPickup/adRuns.lua") dofile("FS25_ADSmartPickup/adRunsController.lua") +dofile("FS25_ADSmartPickup/adLoadSwap.lua") dofile("FS25_ADSmartPickup/adUnloadWait.lua") dofile("FS25_ADSmartPickup/adSmartPickup.lua") listener:update(16) -- installerer hook'en @@ -758,6 +762,63 @@ UnloadTask.update(tp, 16) check("I4 kun run'ets pulje", tp.destinationID, 40) ADRunsController.store = ADRuns.newStore() +-- ==== S: læs-bytte fra Wait — halm-rest ved Wait, målet mangler TMR -> aflæs halm i silo ==== +local function buildSwapWorld() + buildWaitWorld() -- stald: halm FULD (50000/50000), TMR 10000/100000 + -- halmladen (markør 21) er også et aflæsningssted med plads + table.insert(unloadStations, stations[2]) + stations[2].unloadTriggers = {{exactFillRootNode = 121}} + stations[2].getIsFillTypeSupported = function(_, fillType) return fillType == STRAW end + stations[2].getFreeCapacity = function() return 100000 end + stations[2].owningPlaceable = {spec_silo = {storages = {{getFillLevel = function(_, fillType) return fillType == STRAW and 50000 or 0 end}}}} +end + +-- S1: ventende traktor med halm-rest -> TMR mangler et fuldt læs (40000 l vogn, 90000 l fri) -> til halmladen +buildSwapWorld() +local sv, st = makeUnloadingVehicle("Fastrac Bytte", 521, 5000, 35000) +UnloadTask.update(st, 16) +check("S1 venter først ved Wait", st.destinationID, 40) +UnloadTask.update(st, ADLoadSwap.CHECK_INTERVAL_MS + 1) +check("S1 omdirigeret til halmladen", st.destinationID, 21) +check("S1 logget bytte", countLogsSince(1, "bytter læs") >= 1, true) +check("S1 Wait-punkt frigivet", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", sv), RT_ONLYPICKUP) + +-- S2: målet mangler IKKE et fuldt læs af TMR -> bliv ved Wait +buildSwapWorld() +waitBarnLevels[TMR] = 75000 -- 25000 fri < 40000 vogn +local sv2, st2 = makeUnloadingVehicle("Fastrac Bliv", 522, 5000, 35000) +UnloadTask.update(st2, 16) +UnloadTask.update(st2, ADLoadSwap.CHECK_INTERVAL_MS + 1) +check("S2 bliver ved Wait", st2.destinationID, 40) + +-- S3: ingen silo med plads/vare -> bliv ved Wait, én WARN +buildSwapWorld() +stations[2].getFreeCapacity = function() return 1000 end +local sv3, st3 = makeUnloadingVehicle("Fastrac Ingen", 523, 5000, 35000) +UnloadTask.update(st3, 16) +local mark3 = #logLines +UnloadTask.update(st3, ADLoadSwap.CHECK_INTERVAL_MS + 1) +UnloadTask.update(st3, ADLoadSwap.CHECK_INTERVAL_MS + 1) +check("S3 bliver ved Wait", st3.destinationID, 40) +check("S3 én advarsel", countLogsSince(mark3, "ingen silo"), 1) + +-- S4: silo uden vejnet hjem springes over +buildSwapWorld() +FS25_AutoDrive.ADGraphManager.unreachable[21] = true +local sv4, st4 = makeUnloadingVehicle("Fastrac Vej", 524, 5000, 35000) +UnloadTask.update(st4, 16) +UnloadTask.update(st4, ADLoadSwap.CHECK_INTERVAL_MS + 1) +check("S4 unåelig silo -> bliv", st4.destinationID, 40) +FS25_AutoDrive.ADGraphManager.unreachable = {} + +-- S5: traktor med kun én vare i listen bytter aldrig +buildSwapWorld() +local sv5, st5 = makeUnloadingVehicle("Fastrac Solo", 525, 5000, 35000) +sv5.ad.stateModule.selectedFillTypes = {STRAW} +UnloadTask.update(st5, 16) +UnloadTask.update(st5, ADLoadSwap.CHECK_INTERVAL_MS + 1) +check("S5 én vare -> bliver", st5.destinationID, 40) + -- ==== E: eksponerede opslag (bruges af runs-controller og læs-bytte) ==== local eDriver = buildCowWorld() check("E1 aflæsningsstation ved waypoint", ADSmartPickup.getUnloadStationAtWayPointId(FS25_AutoDrive, 30, 1), unloadStations[1])