Files
fs25-adsmartpickup/tests/test_adSmartPickup.lua
T

1071 lines
59 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 MILK = 30
local FILL_TYPE_NAMES = {[SUGARBEET] = "SUGARBEET", [WHEAT] = "WHEAT", [HAY] = "DRYGRASS_WINDROW", [GRASS] = "GRASS_WINDROW", [SILAGE] = "SILAGE", [MILK] = "MILK"}
g_fillTypeManager = {
getFillTypeNameByIndex = function(_, index) return FILL_TYPE_NAMES[index] end,
getFillTypeIndexByName = function(_, name) for index, candidate in pairs(FILL_TYPE_NAMES) do if candidate == name then return index end end return nil end,
}
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}},
unloadTriggers = {{exactFillRootNode = node}},
getFillLevel = function(_, fillType)
if isBroken then error("boom") end
return levels[fillType] or 0
end,
getIsFillTypeSupported = function(_, fillType) return levels[fillType] ~= nil or (ownLevels ~= nil and ownLevels[fillType] ~= nil) end,
getFreeCapacity = function(_, fillType) return (levels.capacity or 200000) - (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},
getFarmId = function() return 1 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
local testVehicles = {}
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, trailers.capacity or 0, false, 0 end,
getAllUnits = function(vehicle) return vehicle.trailers or {level = 0} end,
getAllDischargeableUnits = function(vehicle) return vehicle.units or {} end,
getSupportedFillTypesOfAllUnitsAlphabetically = function(vehicle) return vehicle.carriable or {} end,
getAllVehicles = function() return testVehicles 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},
-- aflæsningsopgaven: original update tæller bare kald; finished markerer
UnloadAtDestinationTask = {
STATE_PATHPLANNING = 1, STATE_DRIVING = 2,
update = function(self) self.updates = (self.updates or 0) + 1 end,
finished = function(self) self.isFinished = true 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,
getMapMarkerByWayPointId = function(_, id) for _, m in ipairs(markers) do if m.id == id then return m end end end,
-- ruter: alt nåeligt medmindre testen sætter unreachable[id] (til eller fra)
unreachable = {},
getPathTo = function(self, vehicle, id) if self.unreachable[id] then return {} end; return {{id = id}} end,
pathFromTo = function(self, fromId, toId) if self.unreachable[fromId] or self.unreachable[toId] then return {} end; return {{id = fromId}, {id = toId}} 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/adRuns.lua")
dofile("FS25_ADSmartPickup/adRunsController.lua")
dofile("FS25_ADSmartPickup/adLoadSwap.lua")
dofile("FS25_ADSmartPickup/adUnloadWait.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)
-- ==== Fabriksstyret forsyning (aflæsningsmålet er et produktionspunkt) ====
ProductionPoint = {PROD_STATUS = {INACTIVE = 0, RUNNING = 1, MISSING_INPUTS = 2, NO_OUTPUT_SPACE = 3}}
local WATER = 40
FILL_TYPE_NAMES[WATER] = "WATER"
-- Fabrik med TMR-opskrift: sluger ensilage, nipper til hø. Vand kan vognen ikke bære.
local function buildFactoryWorld(tmrEnabled, tmrStatus)
buildFeedWorld()
local pp = {
enabled = {tmr = tmrEnabled},
productions = {
{id = "tmr", cyclesPerMinute = 1, status = tmrStatus, inputs = {{type = SILAGE, amount = 250}, {type = HAY, amount = 10}, {type = WATER, amount = 1000}}},
{id = "off", cyclesPerMinute = 1, status = ProductionPoint.PROD_STATUS.INACTIVE, inputs = {{type = GRASS, amount = 500}}},
},
getIsProductionEnabled = function(self, id) return self.enabled[id] == true end,
}
local factory = {spec_productionPoint = {productionPoint = pp}, getName = function() return "AIO" end}
unloadStations = {makeUnloadStation(110, 5000, 10, blanderLevels, {[HAY] = 100000, [GRASS] = 100000, [SILAGE] = 100000, [WATER] = 100000}, factory)}
local driver = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS})
driver.carriable = {HAY, GRASS, SILAGE}
return driver, pp
end
-- F1: tændt produktion styrer listen: ensilage (200 min til tom) vinder over hø (8000 min)
-- og over græs, som er relativt tommest men ikke et input i fabrikken
local factoryDriver = buildFactoryWorld(true, ProductionPoint.PROD_STATUS.RUNNING)
check("F1 fabrikkens input med kortest tid til tom", manager:getNextPickup(factoryDriver, false), 4)
check("F1 AD's vare skiftet til ensilage (ikke i brugerens valg)", factoryDriver.ad.stateModule.fillType, SILAGE)
check("F1 log nævner minutter til tom", logLines[#logLines]:find("min til tom", 1, true) ~= nil, true)
-- F2: næste tur — listen genudledes fra fabrikken, ikke fra brugerens valg
blanderLevels[SILAGE] = 99500
check("F2 fuld ensilage -> hø (næste fabriks-input)", manager:getNextPickup(factoryDriver, false), 1)
-- F3: AD stopper -> brugerens OPRINDELIGE valg gendannes, ikke fabrikkens liste
factoryDriver.ad.stateModule.active = false
listener:update(16)
check("F3 brugerens valg gendannet ved stop", #factoryDriver.ad.stateModule.selectedFillTypes, 2)
-- F4: manuelt slukket produktion -> fallback til brugerens valg (græs er tommest)
factoryDriver = buildFactoryWorld(false, ProductionPoint.PROD_STATUS.INACTIVE)
local function countLogsSince(startIndex, needle)
local count = 0
for i = startIndex + 1, #logLines do
if logLines[i]:find(needle, 1, true) then count = count + 1 end
end
return count
end
local logMark = #logLines
check("F4 slukket fabrik -> brugerens valg", manager:getNextPickup(factoryDriver, false), 3)
check("F4 loggen forklarer fallback", countLogsSince(logMark, "ingen ønskede produktioner"), 1)
manager:getNextPickup(factoryDriver, false)
check("F4 forklaringen logges kun én gang", countLogsSince(logMark, "ingen ønskede produktioner"), 1)
-- F5: auto-slukket af kortet pga. manglende input -> stadig fabrikkens liste
factoryDriver = buildFactoryWorld(false, ProductionPoint.PROD_STATUS.MISSING_INPUTS)
check("F5 auto-slukket fabrik fodres stadig", manager:getNextPickup(factoryDriver, false), 4)
-- F6: produktionspunkt uden getIsProductionEnabled -> activeProductions-listen bruges
local fallbackDriver, fallbackPp = buildFactoryWorld(false, ProductionPoint.PROD_STATUS.RUNNING)
fallbackPp.getIsProductionEnabled = nil
fallbackPp.activeProductions = {fallbackPp.productions[1]}
check("F6 activeProductions som fallback", manager:getNextPickup(fallbackDriver, false), 4)
-- ==== En fabriks inputs stjæles aldrig (halm+TMR til kostald; TMR-fabrikken har halm som input) ====
local STRAW, TMR = 41, 42
FILL_TYPE_NAMES[STRAW], FILL_TYPE_NAMES[TMR] = "STRAW", "FORAGE"
local cowLevels
local function buildCowWorld()
resetWorld()
local tmrFactory = {
getName = function() return "TMR-fabrik" end,
spec_productionPoint = {productionPoint = {productions = {
{id = "tmr", cyclesPerMinute = 1, inputs = {{type = STRAW, amount = 10}, {type = SILAGE, amount = 100}}, outputs = {{type = TMR, amount = 50}}},
}}},
}
-- fabrikkens læssestation har BÅDE halm (input, mindst fyldt) og TMR (output)
addMarker(20, "TMR Load", "Fabrication", 0, 0)
table.insert(stations, makeStation(120, 0, 15, {[STRAW] = 5000, [TMR] = 30000}))
stations[#stations].owningPlaceable = tmrFactory
addMarker(21, "Halmlade", "Lader", 500, 0)
table.insert(stations, makeStation(121, 500, 15, {[STRAW] = 50000}))
addMarker(30, "Cow 1 Food", "Cow Food", 5000, 0)
cowLevels = {[STRAW] = 0, [TMR] = 40000}
table.insert(unloadStations, makeUnloadStation(130, 5000, 10, cowLevels, {[STRAW] = 100000, [TMR] = 100000}, {name = "kostald"}))
local driver = makeSupplyDriver(markerByIndex(21), markerByIndex(30), {TMR, STRAW})
driver.carriable = {TMR, STRAW}
return driver
end
-- P1: halm mangler mest -> hentes fra halmladen, IKKE fra fabrikken (selv om den er mindst fyldt)
local cowDriver = buildCowWorld()
check("P1 halm hentes fra siloen, ikke fabrikkens input", manager:getNextPickup(cowDriver, false), 21)
-- P2: halm fuld -> TMR hentes gerne fra fabrikken (det er dens output)
cowLevels[STRAW] = 99500
check("P2 fabrikkens output må hentes", manager:getNextPickup(cowDriver, false), 20)
-- P3: mappe-logik (én vare) beskytter også fabrikkens input
cowDriver = buildCowWorld()
addMarker(22, "Halmlade 2", "Fabrication", 1000, 0)
table.insert(stations, makeStation(122, 1000, 15, {[STRAW] = 60000}))
local strawDriver = makeDriver(markerByIndex(20), {STRAW})
check("P3 mappe-logik springer fabrikkens input over", manager:getNextPickup(strawDriver, false), 22)
-- P4: stald som mål — niveau/kapacitet læses fra stalden selv (getHusbandry*), ikke fra
-- stationens lagre; stationen selv "understøtter" kun foder, men stalden tager også halm
cowDriver = buildCowWorld()
local barnLevels = {[STRAW] = 45000, [TMR] = 10000}
local barnCapacity = {[STRAW] = 50000, [TMR] = 100000}
local barn = {
getName = function() return "Kostald" end,
getHusbandryFillLevel = function(_, fillType) return barnLevels[fillType] or 0 end,
getHusbandryCapacity = function(_, fillType) return barnCapacity[fillType] or 0 end,
getHusbandryFreeCapacity = function(_, fillType) return (barnCapacity[fillType] or 0) - (barnLevels[fillType] or 0) end,
getHusbandryIsFillTypeSupported = function(_, fillType) return barnCapacity[fillType] ~= nil end,
}
local barnStation = unloadStations[1]
barnStation.targetStorages = nil
barnStation.owningPlaceable = barn
barnStation.getIsFillTypeSupported = function(_, fillType) return fillType == TMR end
barnStation.getFreeCapacity = function(_, fillType) return 999999 end
check("P4 stald: TMR (90 % tom) hentes før halm (10 % tom)", manager:getNextPickup(cowDriver, false), 20)
barnLevels[TMR] = 95000
check("P4 stald: halm hentes når TMR er fuld", manager:getNextPickup(cowDriver, false), 21)
-- ==== Wait-punkter: rest-læs + fuld stald -> vent ved nærmeste ledige Wait, tilbage når resten passer ====
local UnloadTask = FS25_AutoDrive.UnloadAtDestinationTask
local waitBarnLevels, waitBarnCapacity = {}, {[STRAW] = 50000, [TMR] = 100000}
local waitVehicles = {}
local function buildWaitWorld()
-- tidligere testkøretøjer slipper deres Wait-punkter (AD "stoppes" på dem)
for _, vehicle in ipairs(waitVehicles) do vehicle.ad.stateModule.active = false end
listener:update(16)
waitVehicles = {}
buildCowWorld()
waitBarnLevels = {[STRAW] = 50000, [TMR] = 10000} -- halm FULD
local barn2 = {
getName = function() return "Kostald" end,
getHusbandryFillLevel = function(_, fillType) return waitBarnLevels[fillType] or 0 end,
getHusbandryCapacity = function(_, fillType) return waitBarnCapacity[fillType] or 0 end,
getHusbandryFreeCapacity = function(_, fillType) return (waitBarnCapacity[fillType] or 0) - (waitBarnLevels[fillType] or 0) end,
getHusbandryIsFillTypeSupported = function(_, fillType) return waitBarnCapacity[fillType] ~= nil end,
}
unloadStations[1].targetStorages = nil
unloadStations[1].owningPlaceable = barn2
addMarker(40, "Wait 1", "Cow Wait", 5100, 0)
addMarker(41, "Wait 2", "Cow Wait", 5300, 0)
end
-- traktor der holder ved stalden (markør 30) med rest-læs halm på vognen
local function makeUnloadingVehicle(name, node, x, restStraw)
local vehicle = makeSupplyDriver(markerByIndex(21), markerByIndex(30), {TMR, STRAW})
vehicle.ad.stateModule.secondMarkerId = 30
vehicle.ad.stateModule.setSecondMarkerByWayPointId = function(self, id) self.secondMarkerId = id end
vehicle.ad.stateModule.getSecondMarker = function(self) return markerByIndex(self.secondMarkerId) end
vehicle.getName = function() return name end
vehicle.rootNode = node
nodePositions[node] = {x = x, z = 10}
vehicle.units = {{object = {
getFillUnitFillLevel = function() return restStraw end,
getFillUnitFillType = function() return STRAW end,
}, fillUnitIndex = 1}}
vehicle.trailers = {level = restStraw, capacity = 40000}
vehicle.targetReached = true
vehicle.ad.drivePathModule = {
isTargetReached = function() return vehicle.targetReached end,
setPathTo = function(self, id) self.pathTo = id end,
}
vehicle.ad.trailerModule = {isActiveAtTrigger = function() return false end}
local task = {vehicle = vehicle, destinationID = 30, state = UnloadTask.STATE_DRIVING, trailers = vehicle.trailers}
table.insert(waitVehicles, vehicle)
return vehicle, task
end
-- W1: fuld stald + rest-læs -> omdirigeres til nærmeste ledige Wait; AD's original kaldes stadig
buildWaitWorld()
local v1, t1 = makeUnloadingVehicle("Fastrac A", 501, 5000, 35000)
local waitLogMark = #logLines
UnloadTask.update(t1, 16)
check("W1 omdirigeret til nærmeste Wait", t1.destinationID, 40)
check("W1 rute lagt til Wait", v1.ad.drivePathModule.pathTo, 40)
check("W1 original update kaldt", t1.updates, 1)
check("W1 log fortæller om ventning", countLogsSince(waitLogMark, "Wait 1"), 1)
-- W2: anden traktor i samme situation -> Wait 1 er optaget -> Wait 2
local v2, t2 = makeUnloadingVehicle("Fastrac B", 502, 5010, 20000)
UnloadTask.update(t2, 16)
check("W2 optaget Wait springes over", t2.destinationID, 41)
-- W3: mens A venter, skjules rotationen (ellers kører AD videre fra Wait-punktet ved "rotér aflæsning")
check("W3 rotation skjult for ventende", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", v1), RT_NONE)
check("W3 andre indstillinger urørt", FS25_AutoDrive.AutoDrive.getSetting("useFolders", v1), "andet")
-- W4: stalden får plads til HELE resten -> A sendes hjem, Wait 1 frigives, rotation tilbage
waitBarnLevels[STRAW] = 10000 -- 40000 fri >= 35000
v1.targetReached = true -- står ved Wait 1
UnloadTask.update(t1, 16)
check("W4 sendt tilbage til stalden", t1.destinationID, 30)
check("W4 rute lagt hjem", v1.ad.drivePathModule.pathTo, 30)
check("W4 rotation gendannet", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", v1), RT_ONLYPICKUP)
local v3, t3 = makeUnloadingVehicle("Fastrac C", 503, 5020, 30000)
waitBarnLevels[STRAW] = 50000
UnloadTask.update(t3, 16)
check("W4 Wait 1 er ledig igen", t3.destinationID, 40)
-- W5: plads til noget, men ikke hele resten -> B bliver ved Wait (kun hele læs kommer af)
waitBarnLevels[STRAW] = 45000 -- 5000 fri < 20000
UnloadTask.update(t2, 16)
check("W5 delvis plads -> venter stadig", t2.destinationID, 41)
-- W6: stalden har plads ved ankomst -> ingen omdirigering (AD læsser af som normalt)
buildWaitWorld()
waitBarnLevels[STRAW] = 10000
local v6, t6 = makeUnloadingVehicle("Fastrac D", 506, 5000, 35000)
UnloadTask.update(t6, 16)
check("W6 plads ved ankomst -> uændret mål", t6.destinationID, 30)
-- W7: ingen Wait-punkter -> uændret (AD venter som i dag) + én advarsel
buildCowWorld()
unloadStations[1].targetStorages = nil
unloadStations[1].owningPlaceable = { getHusbandryFillLevel = function() return 50000 end, getHusbandryCapacity = function() return 50000 end,
getHusbandryFreeCapacity = function() return 0 end, getHusbandryIsFillTypeSupported = function() return true end }
local v7, t7 = makeUnloadingVehicle("Fastrac E", 507, 5000, 35000)
local warnMark = #logLines
UnloadTask.update(t7, 16); UnloadTask.update(t7, 16)
check("W7 uden Wait-punkter -> uændret mål", t7.destinationID, 30)
check("W7 én advarsel", countLogsSince(warnMark, "WARN"), 1)
-- W8: to andre holder begge punkter; stoppes de i AD, frigives punkterne
buildWaitWorld()
local vA, tA = makeUnloadingVehicle("Fastrac A2", 521, 5000, 35000)
local vB, tB = makeUnloadingVehicle("Fastrac B2", 522, 5000, 35000)
UnloadTask.update(tA, 16); UnloadTask.update(tB, 16)
local v8, t8 = makeUnloadingVehicle("Fastrac F", 508, 5000, 35000)
UnloadTask.update(t8, 16)
check("W8 alle Wait-punkter optaget -> uændret", t8.destinationID, 30)
vA.ad.stateModule.active, vB.ad.stateModule.active = false, false
listener:update(16)
UnloadTask.update(t8, 16)
check("W8 venter ved Wait 1 efter frigivelse", t8.destinationID, 40)
UnloadTask.finished(t8)
check("W8 original finished kaldt", t8.isFinished, true)
local v9, t9 = makeUnloadingVehicle("Fastrac G", 509, 5000, 35000)
UnloadTask.update(t9, 16)
check("W8 Wait 1 frigivet efter finished", t9.destinationID, 40)
check("W8 rotation gendannet efter finished", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", v8), RT_ONLYPICKUP)
-- W9: intet vejnet til nærmeste Wait -> næste; intet net til nogen -> uændret (AD slukker ellers sig selv)
buildWaitWorld()
FS25_AutoDrive.ADGraphManager.unreachable = {[40] = true}
local v10, t10 = makeUnloadingVehicle("Fastrac H", 510, 5000, 35000)
UnloadTask.update(t10, 16)
check("W9 unåeligt Wait 1 -> Wait 2", t10.destinationID, 41)
FS25_AutoDrive.ADGraphManager.unreachable = {[40] = true, [41] = true}
local v11, t11 = makeUnloadingVehicle("Fastrac I", 511, 5000, 35000)
local unreachableMark = #logLines
UnloadTask.update(t11, 16)
check("W9 intet nåeligt -> uændret mål", t11.destinationID, 30)
check("W9 advarsel om manglende vejnet", countLogsSince(unreachableMark, "WARN"), 1)
FS25_AutoDrive.ADGraphManager.unreachable = {}
-- W10: kæmpe vogn (700.000 l rest) i 50.000 l-stald -> hjem når halvdelen af stalden er fri
buildWaitWorld()
local v12, t12 = makeUnloadingVehicle("Fastrac J", 512, 5000, 700000)
UnloadTask.update(t12, 16)
check("W10 venter", t12.destinationID, 40)
waitBarnLevels[STRAW] = 30000 -- 20000 fri < 25000
UnloadTask.update(t12, 16)
check("W10 under halv stald fri -> venter stadig", t12.destinationID, 40)
waitBarnLevels[STRAW] = 20000 -- 30000 fri >= 25000
UnloadTask.update(t12, 16)
check("W10 halv stald fri -> hjem", t12.destinationID, 30)
-- 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)
-- G5: markør-drift i en tank-række — næste tanks rør ligger 2 m FORAN næsen og er
-- nærmest, men vognen holder under røret 11 m bagved
resetWorld()
addMarker(1, "5 Silage Pickup", "Ensilage", 1246, -2218, 1246, -2230) -- kører +z
table.insert(stations, makeStation(101, 1246, -2216, {[SILAGE] = 9000})) -- 2 m foran næsen
table.insert(stations, makeStation(102, 1246, -2229, {[HAY] = 4000})) -- 11 m bag næsen: kun den har hø
addMarker(2, "6 Silage Pickup", "Ensilage", 1246, -2208, 1246, -2218)
check("G5 markør 5 hører til tanken bag næsen", manager:getNextPickup(makeDriver(markers[1], {HAY}), false), 1)
-- 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)
-- M1: mælketanke løber fulde -> den FYLDESTE hentes først (siloer: mindst fyldte)
resetWorld()
addMarker(1, "Kostald 1", "Mælk", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[MILK] = 2000}))
addMarker(2, "Kostald 2", "Mælk", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[MILK] = 48000}))
addMarker(3, "Kostald 3", "Mælk", 1000, 0); table.insert(stations, makeStation(103, 1000, 15, {[MILK] = 30000}))
check("M1 fyldeste mælketank først", manager:getNextPickup(makeDriver(markers[1], {MILK}), false), 2)
check("M1 loggen nævner strategien", logLines[#logLines]:find("fyldeste først", 1, true) ~= nil, true)
-- 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)
-- W11: reservationer kan aflæses (GUI'ets Wait-puljer-visning)
buildWaitWorld()
local vc1, tc1 = makeUnloadingVehicle("Fastrac Claim", 531, 5000, 35000)
UnloadTask.update(tc1, 16)
local claims = ADUnloadWait.getClaims()
check("W11 én reservation", #claims, 1)
check("W11 markør", claims[1].marker.id, 40)
check("W11 køretøj", claims[1].vehicle, vc1)
-- W12: AD regner vognen som "aflæssende" ved et fuldt trug (isUnloading bliver hængende) —
-- omdirigeres alligevel, og AD's aflæsning stoppes først
buildWaitWorld()
local vs, ts = makeUnloadingVehicle("Fastrac Tippet", 541, 5000, 35000)
local stopped = false
vs.ad.trailerModule = {isActiveAtTrigger = function() return true end, stopUnloading = function() stopped = true end, stopLoading = function() end}
UnloadTask.update(ts, 16)
check("W12 omdirigeret trods isActiveAtTrigger", ts.destinationID, 40)
check("W12 AD's aflæsning stoppet før kørsel", stopped, true)
-- E5: fodertrug = én trang; inden for truget vælges efter spillets fodervægt (TMR 1,0 > hø 0,8 > græs 0,4);
-- et tommere halmlager vinder over truget
buildCowWorld()
addMarker(23, "Høsilo", "Lader", 700, 0); table.insert(stations, makeStation(123, 700, 15, {[HAY] = 80000}, false, {[HAY] = 80000}))
addMarker(24, "Græssilo", "Lader", 800, 0); table.insert(stations, makeStation(124, 800, 15, {[GRASS] = 80000}, false, {[GRASS] = 80000}))
local e5Food = {[TMR] = 0, [HAY] = 0, [GRASS] = 0}
local e5Straw = {level = 20000}
local e5Barn = {
getName = function() return "Prioritets-stald" end,
spec_husbandryFood = {fillLevels = e5Food, capacity = 30000, animalTypeIndex = 7},
spec_husbandryStraw = {},
getHusbandryIsFillTypeSupported = function(_, fillType) return fillType == STRAW end,
getHusbandryFillLevel = function(_, fillType) if fillType == STRAW then return e5Straw.level end return 0 end,
getHusbandryCapacity = function(_, fillType) if fillType == STRAW then return 100000 end return 0 end,
}
g_currentMission.animalFoodSystem = {
getAnimalFood = function(_, animalTypeIndex) if animalTypeIndex == 7 then return {groups = {
{fillTypes = {TMR}, productionWeight = 1.0}, {fillTypes = {HAY}, productionWeight = 0.8}, {fillTypes = {GRASS}, productionWeight = 0.4}}} end end,
getMixturesByAnimalTypeIndex = function() return {} end,
}
unloadStations[1].targetStorages = nil
unloadStations[1].owningPlaceable = e5Barn
local e5Driver = makeSupplyDriver(markerByIndex(21), markerByIndex(30), {TMR, HAY, GRASS, STRAW})
e5Driver.carriable = {TMR, HAY, GRASS, STRAW}
e5Driver.trailers = {level = 0, capacity = 8000}
-- trug tomt (100 % trang) vs. halm 80 % tomt -> truget først, og TMR før hø/græs
check("E5 tomt trug: TMR først", manager:getNextPickup(e5Driver, false), 20)
-- ingen TMR-kilde -> hø (0,8) før græs (0,4)
stations[1].getFillLevel = function() return 0 end
stations[1].owningPlaceable = nil
check("E5 uden TMR-kilde: hø før græs", manager:getNextPickup(e5Driver, false), 23)
-- trug 70 % fuldt (30 % trang) vs. halm 80 % tomt -> halm
e5Food[HAY] = 21000
check("E5 halmlager tommere end truget -> halm", manager:getNextPickup(e5Driver, false), 21)
g_currentMission.animalFoodSystem = nil
-- W13: AD stopper vognen i triggeren FØR markøren (isTargetReached=false) — Wait udløses alligevel
-- når vognen er inden for AD's trigger-afstand til aflæsningsstedet
buildWaitWorld()
local vn, tn = makeUnloadingVehicle("Fastrac Før markør", 581, 5000, 35000)
vn.targetReached = false
FS25_AutoDrive.AutoDrive.getDistanceToUnloadPosition = function() return 8.6 end
FS25_AutoDrive.AutoDrive.getMaxTriggerDistance = function() return 15 end
UnloadTask.update(tn, 16)
check("W13 Wait selv om markøren ikke er nået", tn.destinationID, 40)
FS25_AutoDrive.AutoDrive.getDistanceToUnloadPosition = nil
FS25_AutoDrive.AutoDrive.getMaxTriggerDistance = nil
-- W14: mens der ventes, peger AD's mål på Wait-punktet (så AD ikke tipper i småportioner);
-- behovet måles stadig mod hjem-målet, og AD's mål gendannes når den kører hjem
buildWaitWorld()
local vm, tm = makeUnloadingVehicle("Fastrac Mål", 591, 5000, 35000)
UnloadTask.update(tm, 16)
check("W14 AD's mål = Wait-punkt under ventetid", vm.ad.stateModule.secondMarkerId, 40)
waitBarnLevels[STRAW] = 10000 -- plads hjemme
UnloadTask.update(tm, 16)
check("W14 kører hjem (behov målt mod stalden, ikke Wait-punktet)", tm.destinationID, 30)
check("W14 AD's mål gendannet", vm.ad.stateModule.secondMarkerId, 30)
-- ==== I: run-integration — ingredienser fra run i stedet for multi-valg; Wait-pulje pr. run ====
-- I1: traktor i et run med ÉN valgt vare i AD får alligevel behovsstyring (run'ets liste)
local runDriver = buildCowWorld()
runDriver.uniqueId = "vehicleRun"
runDriver.ad.stateModule.selectedFillTypes = {STRAW}
runDriver.ad.stateModule.fillType = STRAW
testVehicles = {runDriver}
ADRunsController.store = ADRuns.newStore()
local feedRun = ADRunsController.createRun("Cow Feed")
ADRunsController.updateRun(feedRun.id, {targetWayPointId = 30, ingredientMode = "manual", ingredients = {"FORAGE", "STRAW"}})
ADRunsController.assignVehicle(feedRun.id, runDriver)
check("I1 run-liste styrer: halm mangler -> halmladen", manager:getNextPickup(runDriver, false), 21)
cowLevels[STRAW] = 99500
check("I1 run-liste styrer: TMR når halm er fuld", manager:getNextPickup(runDriver, false), 20)
check("I1 getIngredientsFor = run'ets liste", table.concat(ADSmartPickup.getIngredientsFor(FS25_AutoDrive, runDriver), ","), TMR .. "," .. STRAW)
-- I2: samme traktor uden run -> som før (én vare = mappe-logik i mappen "Lader")
ADRunsController.store = ADRuns.newStore()
runDriver.ad.stateModule.selectedFillTypes = {STRAW}
cowLevels[STRAW] = 0
check("I2 uden run: mappe-logik", manager:getNextPickup(runDriver, false), 21)
check("I2 uden run og én vare: ingen ingrediensliste", ADSmartPickup.getIngredientsFor(FS25_AutoDrive, runDriver), nil)
-- I3: run med tom auto-liste (intet mål) -> falder tilbage til AD's valg
local emptyRun = ADRunsController.createRun("Tomt")
ADRunsController.assignVehicle(emptyRun.id, runDriver)
check("I3 tom run-liste -> som uden run", manager:getNextPickup(runDriver, false), 21)
-- I4: Wait-kandidater begrænses til run'ets pulje
buildWaitWorld()
addMarker(42, "Pig Wait 1", "Pig Wait", 5050, 0) -- nærmere end Cow Wait, men forkert pulje
local vp, tp = makeUnloadingVehicle("Fastrac Pulje", 511, 5000, 35000)
vp.uniqueId = "vehiclePool"
testVehicles = {vp}
ADRunsController.store = ADRuns.newStore()
local poolRun = ADRunsController.createRun("Cow Feed")
ADRunsController.updateRun(poolRun.id, {targetWayPointId = 30, waitPoolGroup = "Cow Wait"})
ADRunsController.assignVehicle(poolRun.id, vp)
UnloadTask.update(tp, 16)
check("I4 kun run'ets pulje", tp.destinationID, 40)
ADRunsController.store = ADRuns.newStore()
-- ==== S: læs-bytte fra Wait — halm-rest ved Wait, målet mangler TMR -> aflæs halm i silo ====
local function buildSwapWorld()
buildWaitWorld() -- stald: halm FULD (50000/50000), TMR 10000/100000
-- halmladen (markør 21) er også et aflæsningssted med plads
table.insert(unloadStations, stations[2])
stations[2].unloadTriggers = {{exactFillRootNode = 121}}
stations[2].getIsFillTypeSupported = function(_, fillType) return fillType == STRAW end
stations[2].getFreeCapacity = function() return 100000 end
stations[2].owningPlaceable = {spec_silo = {storages = {{getFillLevel = function(_, fillType) return fillType == STRAW and 50000 or 0 end}}}}
end
-- S1: ventende traktor med halm-rest -> TMR mangler et fuldt læs (40000 l vogn, 90000 l fri) -> til halmladen
buildSwapWorld()
local sv, st = makeUnloadingVehicle("Fastrac Bytte", 521, 5000, 35000)
UnloadTask.update(st, 16)
check("S1 venter først ved Wait", st.destinationID, 40)
UnloadTask.update(st, ADLoadSwap.CHECK_INTERVAL_MS + 1)
check("S1 omdirigeret til halmladen", st.destinationID, 21)
check("S1 logget bytte", countLogsSince(1, "bytter læs") >= 1, true)
check("S1 Wait-punkt frigivet", FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", sv), RT_ONLYPICKUP)
check("S1 AD's mål peger midlertidigt på siloen (ellers tipper AD ikke)", sv.ad.stateModule.secondMarkerId, 21)
UnloadTask.finished(st)
check("S1 mål gendannet efter aflæsning", sv.ad.stateModule.secondMarkerId, 30)
-- S2: målet mangler IKKE et fuldt læs af TMR -> bliv ved Wait
buildSwapWorld()
waitBarnLevels[TMR] = 75000 -- 25000 fri < 40000 vogn
local sv2, st2 = makeUnloadingVehicle("Fastrac Bliv", 522, 5000, 35000)
UnloadTask.update(st2, 16)
UnloadTask.update(st2, ADLoadSwap.CHECK_INTERVAL_MS + 1)
check("S2 bliver ved Wait", st2.destinationID, 40)
-- S3: ingen silo med plads/vare -> bliv ved Wait, én WARN
buildSwapWorld()
stations[2].getFreeCapacity = function() return 1000 end
local sv3, st3 = makeUnloadingVehicle("Fastrac Ingen", 523, 5000, 35000)
UnloadTask.update(st3, 16)
local mark3 = #logLines
UnloadTask.update(st3, ADLoadSwap.CHECK_INTERVAL_MS + 1)
UnloadTask.update(st3, ADLoadSwap.CHECK_INTERVAL_MS + 1)
check("S3 bliver ved Wait", st3.destinationID, 40)
check("S3 én advarsel", countLogsSince(mark3, "bliver ved Wait-punktet"), 1)
-- S4: silo uden vejnet hjem springes over
buildSwapWorld()
FS25_AutoDrive.ADGraphManager.unreachable[21] = true
local sv4, st4 = makeUnloadingVehicle("Fastrac Vej", 524, 5000, 35000)
UnloadTask.update(st4, 16)
UnloadTask.update(st4, ADLoadSwap.CHECK_INTERVAL_MS + 1)
check("S4 unåelig silo -> bliv", st4.destinationID, 40)
FS25_AutoDrive.ADGraphManager.unreachable = {}
-- S5: traktor med kun én vare i listen bytter aldrig
buildSwapWorld()
local sv5, st5 = makeUnloadingVehicle("Fastrac Solo", 525, 5000, 35000)
sv5.ad.stateModule.selectedFillTypes = {STRAW}
UnloadTask.update(st5, 16)
UnloadTask.update(st5, ADLoadSwap.CHECK_INTERVAL_MS + 1)
check("S5 én vare -> bliver", st5.destinationID, 40)
-- S6: efter bytte til siloen skal traktoren blive der (ikke sendes til et nyt Wait-punkt,
-- fordi siloen fejlagtigt tolkes som "målet er fuldt")
buildSwapWorld()
local sv6, st6 = makeUnloadingVehicle("Fastrac Silo", 526, 5000, 35000)
UnloadTask.update(st6, 16)
UnloadTask.update(st6, ADLoadSwap.CHECK_INTERVAL_MS + 1)
local mark6 = #logLines
UnloadTask.update(st6, 16)
UnloadTask.update(st6, 16)
check("S6 bliver ved siloen efter bytte", st6.destinationID, 21)
check("S6 ingen ny Wait-omdirigering", countLogsSince(mark6, "venter ved"), 0)
-- I5: run'ets "kør hjem under X %" styrer hvornår den ventende kører hjem
buildWaitWorld()
local vt, tt = makeUnloadingVehicle("Fastrac Tærskel", 551, 5000, 60000) -- rest > stald: kun tærsklen kan sende hjem
vt.uniqueId = "vehicleThreshold"
testVehicles = {vt}
ADRunsController.store = ADRuns.newStore()
local thrRun = ADRunsController.createRun("Tærskel")
ADRunsController.updateRun(thrRun.id, {targetWayPointId = 30, returnBelowPercent = 20})
ADRunsController.assignVehicle(thrRun.id, vt)
UnloadTask.update(tt, 16)
check("I5 venter først", tt.destinationID, 40)
waitBarnLevels[STRAW] = 15000 -- 70 % fri: 50 %-reglen ville sende hjem, men tærsklen er "under 20 %" (= 80 % fri)
UnloadTask.update(tt, 16)
check("I5 70 % fri er ikke nok ved 'under 20 %'", tt.destinationID, 40)
waitBarnLevels[STRAW] = 9000 -- 82 % fri >= 80 %
UnloadTask.update(tt, 16)
check("I5 hjem når målet er under 20 %", tt.destinationID, 30)
ADRunsController.store = ADRuns.newStore()
-- I6: run med to stalde — ved turstart vælges den stald der mangler mest, AD's mål skiftes, og
-- varen hentes efter DEN stalds behov
buildCowWorld()
addMarker(31, "Cow 2 Food", "Cow Food", 6000, 0)
local cow2Levels = {[STRAW] = 95000, [TMR] = 95000}
table.insert(unloadStations, makeUnloadStation(131, 6000, 10, cow2Levels, {[STRAW] = 100000, [TMR] = 100000}, {name = "kostald 2"}))
cowLevels[STRAW], cowLevels[TMR] = 90000, 20000 -- stald 1: TMR mangler
local i6 = makeSupplyDriver(markerByIndex(21), markerByIndex(31), {TMR, STRAW}) -- AD peger på stald 2 (fuld)
i6.uniqueId = "vehicleI6"; i6.carriable = {TMR, STRAW}; i6.trailers = {level = 0, capacity = 8000}
i6.ad.stateModule.setSecondMarkerByWayPointId = function(self, id) self.secondMarkerId = id end
i6.ad.stateModule.getSecondMarker = function(self) return markerByIndex(self.secondMarkerId or 31) end
testVehicles = {i6}
ADRunsController.store = ADRuns.newStore()
local twoBarns = ADRunsController.createRun("Kvæg")
ADRunsController.updateRun(twoBarns.id, {targetWayPointIds = {30, 31}})
ADRunsController.assignVehicle(twoBarns.id, i6)
check("I6 henter TMR til stald 1", manager:getNextPickup(i6, false), 20)
check("I6 AD's mål skiftet til stald 1", i6.ad.stateModule.secondMarkerId, 30)
check("I6 skiftet logges", countLogsSince(1, "kører til Cow 1 Food") >= 1, true)
cowLevels[STRAW], cowLevels[TMR] = 99000, 99000
cow2Levels[STRAW] = 10000 -- nu mangler stald 2 halm
check("I6 næste tur: halm til stald 2", manager:getNextPickup(i6, false), 21)
check("I6 mål skiftet til stald 2", i6.ad.stateModule.secondMarkerId, 31)
ADRunsController.store = ADRuns.newStore()
-- ==== V: forkert last på vognen ====
-- V1: pålæsning starter med anden vare på vognen -> opgaven afsluttes straks (AD leverer det den har)
buildCowWorld()
cowLevels[STRAW], cowLevels[TMR] = 90000, 0 -- TMR mangler
local wv = makeSupplyDriver(markerByIndex(21), markerByIndex(30), {TMR, STRAW})
wv.carriable = {TMR, STRAW}; wv.trailers = {level = 8000, capacity = 8000}
wv.units = {{object = {getFillUnitFillLevel = function() return 8000 end, getFillUnitFillType = function() return STRAW end}, fillUnitIndex = 1}}
withDrivePath(wv); wv.targetReached = false
check("V1 TMR vælges som behov", manager:getNextPickup(wv, false), 20)
local loadTask = {vehicle = wv, trailers = wv.trailers, finished = function(self) self.isFinished = true end}
FS25_AutoDrive.LoadAtDestinationTask.update(loadTask, 16)
check("V1 pålæsningsopgave afsluttet pga. halm på vognen", loadTask.isFinished, true)
check("V1 logget", countLogsSince(1, "leverer først") >= 1, true)
-- samme vare på vognen som den der hentes -> opgaven kører normalt
wv.units[1].object.getFillUnitFillType = function() return TMR end
local loadTask2 = {vehicle = wv, trailers = wv.trailers, finished = function(self) self.isFinished = true end}
manager:getNextPickup(wv, false)
FS25_AutoDrive.LoadAtDestinationTask.update(loadTask2, 16)
check("V1 samme vare -> ikke afsluttet", loadTask2.isFinished, nil)
-- V2: ved målet med en vare målet ikke tager -> Wait; ved Wait -> silo uanset "manglende" vare
buildWaitWorld()
table.insert(unloadStations, stations[2]) -- halmladen tager halm
stations[2].unloadTriggers = {{exactFillRootNode = 121}}
stations[2].getIsFillTypeSupported = function(_, fillType) return fillType == STRAW end
stations[2].getFreeCapacity = function() return 100000 end
stations[2].owningPlaceable = {spec_silo = {storages = {{getFillLevel = function() return 0 end}}}}
waitBarnCapacity[STRAW] = nil -- stalden tager IKKE halm
local vw, tw = makeUnloadingVehicle("Fastrac Forkert", 561, 5000, 35000)
UnloadTask.update(tw, 16)
check("V2 målet tager ikke varen -> Wait", tw.destinationID, 40)
UnloadTask.update(tw, ADLoadSwap.CHECK_INTERVAL_MS + 1)
check("V2 fra Wait til silo selv uden manglende vare", tw.destinationID, 21)
waitBarnCapacity[STRAW] = 50000
-- V3: Start med forkert last -> AD's mål = silo; første getNextPickup må IKKE sætte run-målet tilbage,
-- og efter aflæsningen (finished) gendannes run-målet
buildCowWorld()
table.insert(unloadStations, stations[2]) -- halmladen tager halm
stations[2].unloadTriggers = {{exactFillRootNode = 121}}
stations[2].getIsFillTypeSupported = function(_, fillType) return fillType == STRAW end
stations[2].getFreeCapacity = function() return 100000 end
stations[2].owningPlaceable = {spec_silo = {storages = {{getFillLevel = function() return 0 end}}}}
local v3 = makeSupplyDriver(markerByIndex(21), markerByIndex(30), {TMR, STRAW})
v3.uniqueId = "vehicleV3"; v3.carriable = {TMR}; v3.trailers = {level = 4000, capacity = 8000}; v3.rootNode = 571; nodePositions[571] = {x = 0, z = 0}
v3.units = {{object = {getFillUnitFillLevel = function() return 4000 end, getFillUnitFillType = function() return STRAW end}, fillUnitIndex = 1}}
v3.ad.stateModule.secondMarkerId = 30
v3.ad.stateModule.setSecondMarkerByWayPointId = function(self, id) self.secondMarkerId = id end
v3.ad.stateModule.getSecondMarker = function(self) return markerByIndex(self.secondMarkerId) end
cowLevels[STRAW] = nil -- stald 1 tager i denne test kun TMR
unloadStations[1].getIsFillTypeSupported = function(_, fillType) return fillType == TMR end
testVehicles = {v3}
ADRunsController.store = ADRuns.newStore()
local v3Run = ADRunsController.createRun("TMR only")
ADRunsController.updateRun(v3Run.id, {targetWayPointId = 30, ingredientMode = "manual", ingredients = {"FORAGE"}})
ADRunsController.assignVehicle(v3Run.id, v3)
ADUnloadWait.setDumpTarget(v3, 21) -- som Start gør ved forkert last
check("V3 AD's mål = silo", v3.ad.stateModule.secondMarkerId, 21)
manager:getNextPickup(v3, false)
check("V3 getNextPickup rører ikke målet mens silo-aflæsning er undervejs", v3.ad.stateModule.secondMarkerId, 21)
UnloadTask.finished({vehicle = v3})
check("V3 mål gendannet efter aflæsning", v3.ad.stateModule.secondMarkerId, 30)
ADRunsController.store = ADRuns.newStore()
-- ==== E: eksponerede opslag (bruges af runs-controller og læs-bytte) ====
local eDriver = buildCowWorld()
check("E1 aflæsningsstation ved waypoint", ADSmartPickup.getUnloadStationAtWayPointId(FS25_AutoDrive, 30, 1), unloadStations[1])
check("E1 ukendt waypoint -> nil", ADSmartPickup.getUnloadStationAtWayPointId(FS25_AutoDrive, 9999, 1), nil)
check("E2 læssestation ved waypoint", ADSmartPickup.getLoadStationAtWayPointId(FS25_AutoDrive, 21, 1), stations[2])
local eLevel, eCapacity = ADSmartPickup.getStationLevelAndCapacity(unloadStations[1], TMR, 1)
check("E3 niveau i målet", eLevel, 40000)
check("E3 kapacitet i målet", eCapacity, 100000)
check("E3 vare målet ikke tager -> nil", ADSmartPickup.getStationLevelAndCapacity(unloadStations[1], MILK, 1), nil)
-- E4: FS25-stald med foder-spec: foder læses fra spec_husbandryFood (fælles kapacitet),
-- fodertyper fra animalFoodSystem, halm fra spec_husbandryStraw; outputs (mælk) tages ikke
buildCowWorld()
local foodLevels = {[TMR] = 10000, [HAY] = 5000}
local fs25Barn = {
getName = function() return "FS25-kostald" end,
spec_husbandryFood = {fillLevels = foodLevels, capacity = 100000, animalTypeIndex = 7},
spec_husbandryStraw = {fillLevel = 20000, capacity = 300000},
-- staldens egen "understøtter" siger også ja til mælk (output) — må ikke blive et mål
getHusbandryIsFillTypeSupported = function(_, fillType) return fillType == MILK or fillType == STRAW or fillType == TMR end,
getHusbandryFillLevel = function(_, fillType) if fillType == STRAW then return 20000 end return 0 end,
getHusbandryCapacity = function(_, fillType) if fillType == STRAW then return 300000 end return 0 end,
}
g_currentMission.animalFoodSystem = {
getAnimalFood = function(_, animalTypeIndex) if animalTypeIndex == 7 then return {groups = {{fillTypes = {TMR}}, {fillTypes = {HAY, GRASS}}}} end end,
getMixturesByAnimalTypeIndex = function() return {} end,
}
unloadStations[1].targetStorages = nil
unloadStations[1].owningPlaceable = fs25Barn
local tmrLevel, tmrCapacity = ADSmartPickup.getStationLevelAndCapacity(unloadStations[1], TMR, 1)
check("E4 TMR-niveau fra foder-spec", tmrLevel, 10000)
check("E4 TMR-kapacitet = eget + fælles fri plads", tmrCapacity, 10000 + (100000 - 15000))
local grassLevel, grassCapacity = ADSmartPickup.getStationLevelAndCapacity(unloadStations[1], GRASS, 1)
check("E4 græs (fodergruppe uden lager) niveau 0", grassLevel, 0)
check("E4 græs kapacitet = fælles fri plads", grassCapacity, 85000)
local strawLevel, strawCapacity = ADSmartPickup.getStationLevelAndCapacity(unloadStations[1], STRAW, 1)
check("E4 halm via husbandry-opslag", strawLevel .. "/" .. strawCapacity, "20000/300000")
check("E4 mælk (output) er ikke et mål", ADSmartPickup.getStationLevelAndCapacity(unloadStations[1], MILK, 1), nil)
check("E4 sukkerroer (ikke foder) er ikke et mål", ADSmartPickup.getStationLevelAndCapacity(unloadStations[1], SUGARBEET, 1), nil)
g_currentMission.animalFoodSystem = nil
check("E3 farmId", ADSmartPickup.getFarmId(eDriver), 1)
-- 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)