-- 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 FS25_AutoDrive = { 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) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL")) os.exit(failures == 0 and 0 or 1)