-- Kør: luajit tests/test_adSmartPickup.lua (fra repo-roden) -- Mock'er de få FS25-/AutoDrive-API'er moden rører og tester valg-logikken. local SUGARBEET, WHEAT = 10, 11 local HAY, GRASS, SILAGE = 20, 21, 22 local logLines = {} -- ---- FS25-mocks ---- MathUtil = {vector2Length = function(x, z) return math.sqrt(x * x + z * z) end} Logging = { info = function(fmt, ...) table.insert(logLines, string.format(fmt, ...)) end, warning = function(fmt, ...) table.insert(logLines, "WARN " .. string.format(fmt, ...)) end, } local nodePositions = {} function entityExists(node) return nodePositions[node] ~= nil end function getWorldTranslation(node) local p = nodePositions[node]; return p.x, 0, p.z end local listener function addModEventListener(l) listener = l end local function makeStation(node, x, z, levels, isBroken, ownLevels) nodePositions[node] = {x = x, z = z} local placeable = ownLevels ~= nil and {spec_silo = {storages = { {getFillLevel = function(_, fillType) return ownLevels[fillType] or 0 end}, }}} or nil return { owningPlaceable = placeable, loadTriggers = {{triggerNode = node}}, getFillLevel = function(_, fillType) if isBroken then error("boom") end return levels[fillType] or 0 end, } end -- Aflæsningsmål (fx TMR-blander): ét lager med niveau + kapacitet pr. vare. local function makeUnloadStation(node, x, z, levels, capacities, placeable, isBroken) nodePositions[node] = {x = x, z = z} local storage = { getFillLevel = function(_, fillType) if isBroken then error("boom") end return levels[fillType] or 0 end, getFreeCapacity = function(_, fillType) return (capacities[fillType] or 0) - (levels[fillType] or 0) end, } return { owningPlaceable = placeable, unloadTriggers = {{exactFillRootNode = node}}, targetStorages = {[storage] = storage}, getIsFillTypeSupported = function(_, fillType) return capacities[fillType] ~= nil end, } end local stations, unloadStations = {}, {} g_currentMission = { storageSystem = { getLoadingStations = function() return stations end, getUnloadingStations = function() return unloadStations end, }, accessHandler = {canFarmAccess = function() return true end}, } -- ---- AutoDrive-mocks ---- local markers, wayPoints = {}, {} local function addMarker(index, name, group, x, z) wayPoints[index] = {x = x, z = z} table.insert(markers, {id = index, markerIndex = index, name = name, group = group}) end local function resetWorld() stations, unloadStations, markers, wayPoints, nodePositions, logLines = {}, {}, {}, {}, {}, {} 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) local result = {} for _, m in ipairs(markers) do if m.group == group then table.insert(result, m) end end return result end, getWayPointById = function(_, id) return wayPoints[id] end, getMapMarkers = function() return markers end, }, } local function makeDriver(firstMarker, fillTypes) return { getName = function() return "Testtraktor" end, getOwnerFarmId = function() return 1 end, ad = {stateModule = { getFirstMarker = function() return firstMarker end, getSecondMarker = function() return nil end, getSelectedFillTypes = function() return fillTypes end, getFillType = function() return fillTypes[1] end, }}, } 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} stateModule.getFirstMarker = function() return firstMarker end stateModule.getSecondMarker = function() return secondMarker end stateModule.getSelectedFillTypes = function(self) return self.selectedFillTypes end stateModule.getFillType = function(self) return self.fillType end stateModule.setFillType = function(self, fillType) self.fillType = fillType end stateModule.raiseDirtyFlag = function(self) self.dirtyCount = self.dirtyCount + 1 end stateModule.isActive = function(self) return self.active end return { getName = function() return "Fodertraktor" end, getOwnerFarmId = function() return 1 end, ad = {stateModule = stateModule}, } end dofile("FS25_ADSmartPickup/adSupplyPlanner.lua") dofile("FS25_ADSmartPickup/adSmartPickup.lua") listener:update(16) -- installerer hook'en local manager = FS25_AutoDrive.ADMultipleTargetsManager local failures = 0 local function check(name, actual, expected) if actual == expected then print("OK " .. name) else failures = failures + 1 print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) end end -- 1: vælger mindst fyldte silo med varen; springer tomme og forkert vare over resetWorld() addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[WHEAT] = 900000})) addMarker(2, "Silo 2", "Siloer", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[SUGARBEET] = 800000})) addMarker(3, "Silo 3", "Siloer", 1000, 0); table.insert(stations, makeStation(103, 1000, 15, {[SUGARBEET] = 12000})) addMarker(4, "Silo 4", "Siloer", 1500, 0); table.insert(stations, makeStation(104, 1500, 15, {[SUGARBEET] = 0})) addMarker(5, "Fabrik", "Andet", 1000, 5); -- anden mappe, skal ignoreres check("mindst fyldte med varen vælges", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 3) check("AD-rotationstilstand opdateret", manager.pickups["Siloer"].lastTarget, 3) -- 2: ingen silo har varen -> AutoDrives originale rotation check("fallback når varen ikke findes", manager:getNextPickup(makeDriver(markers[1], {99}), false), "ORIGINAL") -- 3: markør uden silo i nærheden ignoreres resetWorld() addMarker(1, "Langt væk", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 200, {[SUGARBEET] = 5})) addMarker(2, "Silo 2", "Siloer", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[SUGARBEET] = 700})) check("markør uden nær silo ignoreres", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 2) -- 4: flere valgte varer lægges sammen resetWorld() addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[SUGARBEET] = 100, [WHEAT] = 5000})) addMarker(2, "Silo 2", "Siloer", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[SUGARBEET] = 3000})) check("flere varetyper summeres", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET, WHEAT}), false), 2) -- 4b: klynge med delt lager — eget indhold slår nabo-sum, også når nabo-summen er mindre resetWorld() addMarker(1, "Nabo", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[SUGARBEET] = 800}, false, {})) addMarker(2, "Ejer stor", "Siloer", 14, 0); table.insert(stations, makeStation(102, 14, 15, {[SUGARBEET] = 900000}, false, {[SUGARBEET] = 500000})) addMarker(3, "Ejer lille", "Siloer", 28, 0);table.insert(stations, makeStation(103, 28, 15, {[SUGARBEET] = 900000}, false, {[SUGARBEET] = 20000})) check("eget lager foran nabo-lån, mindst først", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 3) -- 4c: kun nabo-lån muligt -> bruges stadig resetWorld() addMarker(1, "Nabo", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[SUGARBEET] = 800}, false, {})) check("nabo-lån bruges når ingen ejer varen", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 1) -- 5: fejl i spil-API -> fallback + advarsel, aldrig crash resetWorld() addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {}, true)) check("fejl giver fallback", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), "ORIGINAL") check("fejl logges som advarsel", logLines[#logLines]:sub(1, 4), "WARN") -- 6: gruppen "All" og manglende første markør røres ikke check("gruppe All -> fallback", manager:getNextPickup(makeDriver({group = "All", markerIndex = 1}, {SUGARBEET}), false), "ORIGINAL") check("ingen første markør -> fallback", manager:getNextPickup(makeDriver(nil, {SUGARBEET}), false), "ORIGINAL") -- ==== Behovsstyret forsyning (flere varer valgt + aflæsningsmål med lager) ==== local BLANDER = {name = "TMR-blander placeable"} local FULL = {[HAY] = 100000, [GRASS] = 100000, [SILAGE] = 100000} local blanderLevels local function buildFeedWorld() resetWorld() blanderLevels = {[HAY] = 80000, [GRASS] = 20000, [SILAGE] = 50000} addMarker(1, "Hølade", "Lader", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[HAY] = 50000})) addMarker(2, "Græssilo stor", "Siloer", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[GRASS] = 300000})) addMarker(3, "Græssilo lille", "Andet", 1000, 0);table.insert(stations, makeStation(103, 1000, 15, {[GRASS] = 40000})) addMarker(4, "Ensilagesilo", "Siloer", 1500, 0); table.insert(stations, makeStation(104, 1500, 15, {[SILAGE] = 200000})) addMarker(10, "TMR-blander", "Produktion", 5000, 0) table.insert(unloadStations, makeUnloadStation(110, 5000, 10, blanderLevels, FULL, BLANDER)) end local function markerByIndex(index) for _, m in ipairs(markers) do if m.markerIndex == index then return m end end end -- S1: mest trængende ingrediens vælges, kilde findes på tværs af mapper, AD's vare skiftes buildFeedWorld() local feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS, SILAGE}) check("S1 mest trængende vare, mindst fyldte kilde", manager:getNextPickup(feeder, false), 3) check("S1 AD's vare skiftet til græs", feeder.ad.stateModule.fillType, GRASS) check("S1 AD's valg indsnævret til én vare", #feeder.ad.stateModule.selectedFillTypes, 1) check("S1 ændring synkroniseres (dirty-flag)", feeder.ad.stateModule.dirtyCount > 0, true) check("S1 rotationstilstand følger valgt markørs mappe", manager.pickups["Andet"].lastTarget, 3) -- S2: næste tur — gemt liste genbruges; græs er nu fuldt og springes over -> ensilage blanderLevels[GRASS] = 99500 check("S2 fuld ingrediens springes over", manager:getNextPickup(feeder, false), 4) check("S2 vare skiftet til ensilage", feeder.ad.stateModule.fillType, SILAGE) -- S3: ingrediens uden kilde springes over -> næste i behovs-rækkefølgen blanderLevels[SILAGE] = 10000 stations[4] = makeStation(104, 1500, 15, {[SILAGE] = 0}) check("S3 ingen kilde -> næste ingrediens", manager:getNextPickup(feeder, false), 1) check("S3 vare skiftet til hø", feeder.ad.stateModule.fillType, HAY) -- S4: AD stopper -> hele ingredienslisten gendannes i AD's varevalg feeder.ad.stateModule.active = false listener:update(16) check("S4 valg gendannet ved stop", #feeder.ad.stateModule.selectedFillTypes, 3) -- S5: aflæsningsmålets egen læssestation bruges ikke som kilde buildFeedWorld() addMarker(11, "Blander udtag", "Produktion", 5030, 0) table.insert(stations, makeStation(111, 5030, 15, {[GRASS] = 10}, false, nil)) stations[#stations].owningPlaceable = BLANDER feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS}) check("S5 målets egen station udelukkes som kilde", manager:getNextPickup(feeder, false), 3) -- S6: brugeren vælger selv en anden enkelt vare -> behovsstyring fra (mappe-logik) feeder.ad.stateModule.selectedFillTypes = {HAY} feeder.ad.stateModule.fillType = HAY check("S6 brugerens enkelt-valg -> mappe-logik", manager:getNextPickup(feeder, false), 1) check("S6 vare røres ikke", feeder.ad.stateModule.fillType, HAY) -- S7: intet behov (alt fuldt) -> mappe-logik, valg indsnævres ikke buildFeedWorld() blanderLevels[HAY], blanderLevels[GRASS], blanderLevels[SILAGE] = 99900, 99900, 99900 feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS, SILAGE}) check("S7 intet behov -> mappe-logik", manager:getNextPickup(feeder, false), 1) check("S7 valg ikke indsnævret", #feeder.ad.stateModule.selectedFillTypes, 3) -- S8: ingen aflæsningsstation ved målet -> mappe-logik buildFeedWorld() unloadStations = {} feeder = makeSupplyDriver(markerByIndex(2), markerByIndex(10), {GRASS, SILAGE}) check("S8 ingen station ved mål -> mappe-logik", manager:getNextPickup(feeder, false), 4) -- S9: fejl i målets lager-API -> AutoDrives original + advarsel buildFeedWorld() unloadStations = {makeUnloadStation(110, 5000, 10, blanderLevels, FULL, BLANDER, true)} feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS}) check("S9 fejl giver fallback", manager:getNextPickup(feeder, false), "ORIGINAL") check("S9 fejl logges som advarsel", logLines[#logLines]:sub(1, 4), "WARN") -- S10: stationen eksponerer ikke targetStorages -> lageret læses fra produktionspunktet buildFeedWorld() local bareStation = unloadStations[1] local productionStorage = next(bareStation.targetStorages) bareStation.targetStorages = nil bareStation.owningPlaceable = {spec_productionPoint = {productionPoint = {storage = productionStorage}}} 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)