Files
fs25-adsmartpickup/tests/test_adSmartPickup.lua
T
masterdraco e414c99b58 fix: markørens silo er den bag traktorens næse, ikke nærmeste trigger (v1.3.0.0)
I tætte silo-rækker står markøren ved NÆSTE silos rist (næsen stopper dér, vognen
holder et vogntog bagved). Nærmeste-trigger-koblingen ramte derfor nabosiloen og
forskød alle 40 markører én silo: roer i silo 2 blev til '1 Silo Storage'.
Koblingen bruger nu ankomstretningen fra forrige waypoint og vælger nærmeste
trigger bag næsen (højst 2 m foran); ellers nærmeste trigger som før.
Verificeret mod savegame: 40 markører -> 40 forskellige siloer, TMR Unload -> TMR.
2026-09-21 16:00:53 +02:00

356 lines
18 KiB
Lua

-- 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},
-- 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)
-- 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)
-- 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)