-- 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 = {}, {} -- fromX/fromZ: forrige waypoint på ruten ind til markøren (giver ankomstretningen) local function addMarker(index, name, group, x, z, fromX, fromZ) wayPoints[index] = {x = x, z = z, incoming = {}} if fromX ~= nil then wayPoints[1000 + index] = {x = fromX, z = fromZ, incoming = {}} wayPoints[index].incoming = {1000 + index} end 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, 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) 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 -- 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} 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 = withDrivePath(makeDriver(markers[1], {SUGARBEET})) manager:getNextPickup(loader, false) 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) 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) -- G1: tæt silo-række, kørsel mod -x. Næsen stopper ved markøren, vognen holder et -- vogntog BAGVED -> markørens silo er den bag næsen, ikke den (nærmere) foran. resetWorld() addMarker(1, "1 Silo", "Siloer", 1243, 0, 1260, 0) addMarker(2, "2 Silo", "Siloer", 1228, 0, 1243, 0) table.insert(stations, makeStation(101, 1255, 1, {[HAY] = 9000}, false, {[HAY] = 9000})) -- bag markør 1 table.insert(stations, makeStation(102, 1239, 1, {[SUGARBEET] = 5000}, false, {[SUGARBEET] = 5000})) -- foran markør 1, bag markør 2 table.insert(stations, makeStation(103, 1224, 1, {}, false, {})) -- foran markør 2 check("G1 roer ligger i siloen bag markør 2", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 2) check("G1 hø ligger i siloen bag markør 1", manager:getNextPickup(makeDriver(markers[1], {HAY}), false), 1) -- G2: ingen trigger bag næsen (eller ukendt retning) -> nærmeste trigger som før resetWorld() 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 = withDrivePath(makeSupplyDriver(markers[1], markers[2], {HAY, GRASS})) manager:getNextPickup(stopper, false) 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 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)