bcc245863e
Med rotateTargets + mapper afslutter AutoDrives LoadAtDestinationTask pålæsningen i det øjeblik markøren nås; traktoren kørte derfor lige igennem silo-markøren og tom til aflæsning. Når moden har valgt siloen, ses rotationen som slået fra for netop det køretøj under opgavens update, så AutoDrive står og læsser. Grebet slippes når kilden er tom eller AD stopper. Desuden: samme valg logges kun én gang, og loggen viser eget/nabo-lager.
334 lines
17 KiB
Lua
334 lines
17 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 = {}, {}
|
|
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)
|