2ff0b641b0
In-game 2026-09-22: fire Fastrac'er med halm+TMR til to kostalde kørte med 'Rotér mål: kun aflæsning' og fast pålæsning ved 'TMR Load' -> AutoDrive læssede halm (fabrikkens input) dér uden at modden var i spil. Med modden i spil ville 'mindst fyldte først' have valgt samme lager. - ADSupplyPlanner.isProductionInput: vare der er input (og ikke output) i en fabriks produktioner udelukkes som kilde i findBestSource (mappe + behov). - Stald som mål: niveau/kapacitet/understøttelse fra placeable:getHusbandry* (stationen selv siger nej til halm) -> halm rangeres og hentes. - README: to stalde = kun pålæsnings-rotation (AD vælger pickup FØR deliver roteres).
555 lines
30 KiB
Lua
555 lines
30 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}
|
|
local logLines = {}
|
|
|
|
-- ---- FS25-mocks ----
|
|
MathUtil = {vector2Length = function(x, z) return math.sqrt(x * x + z * z) end}
|
|
Logging = {
|
|
info = function(fmt, ...) table.insert(logLines, string.format(fmt, ...)) end,
|
|
warning = function(fmt, ...) table.insert(logLines, "WARN " .. string.format(fmt, ...)) end,
|
|
}
|
|
local nodePositions = {}
|
|
function entityExists(node) return nodePositions[node] ~= nil end
|
|
function getWorldTranslation(node) local p = nodePositions[node]; return p.x, 0, p.z end
|
|
local listener
|
|
function addModEventListener(l) listener = l end
|
|
|
|
local function makeStation(node, x, z, levels, isBroken, ownLevels)
|
|
nodePositions[node] = {x = x, z = z}
|
|
local placeable = ownLevels ~= nil and {spec_silo = {storages = {
|
|
{getFillLevel = function(_, fillType) return ownLevels[fillType] or 0 end},
|
|
}}} or nil
|
|
return {
|
|
owningPlaceable = placeable,
|
|
loadTriggers = {{triggerNode = node}},
|
|
getFillLevel = function(_, fillType)
|
|
if isBroken then error("boom") end
|
|
return levels[fillType] or 0
|
|
end,
|
|
}
|
|
end
|
|
|
|
-- Aflæsningsmål (fx TMR-blander): ét lager med niveau + kapacitet pr. vare.
|
|
local function makeUnloadStation(node, x, z, levels, capacities, placeable, isBroken)
|
|
nodePositions[node] = {x = x, z = z}
|
|
local storage = {
|
|
getFillLevel = function(_, fillType)
|
|
if isBroken then error("boom") end
|
|
return levels[fillType] or 0
|
|
end,
|
|
getFreeCapacity = function(_, fillType) return (capacities[fillType] or 0) - (levels[fillType] or 0) end,
|
|
}
|
|
return {
|
|
owningPlaceable = placeable,
|
|
unloadTriggers = {{exactFillRootNode = node}},
|
|
targetStorages = {[storage] = storage},
|
|
getIsFillTypeSupported = function(_, fillType) return capacities[fillType] ~= nil end,
|
|
}
|
|
end
|
|
|
|
local stations, unloadStations = {}, {}
|
|
g_currentMission = {
|
|
storageSystem = {
|
|
getLoadingStations = function() return stations end,
|
|
getUnloadingStations = function() return unloadStations end,
|
|
},
|
|
accessHandler = {canFarmAccess = function() return true end},
|
|
}
|
|
|
|
-- ---- AutoDrive-mocks ----
|
|
local markers, wayPoints = {}, {}
|
|
-- fromX/fromZ: forrige waypoint på ruten ind til markøren (giver ankomstretningen)
|
|
local function addMarker(index, name, group, x, z, fromX, fromZ)
|
|
wayPoints[index] = {x = x, z = z, incoming = {}}
|
|
if fromX ~= nil then
|
|
wayPoints[1000 + index] = {x = fromX, z = fromZ, incoming = {}}
|
|
wayPoints[index].incoming = {1000 + index}
|
|
end
|
|
table.insert(markers, {id = index, markerIndex = index, name = name, group = group})
|
|
end
|
|
|
|
local function resetWorld()
|
|
stations, unloadStations, markers, wayPoints, nodePositions, logLines = {}, {}, {}, {}, {}, {}
|
|
FS25_AutoDrive.ADMultipleTargetsManager.pickups = {}
|
|
end
|
|
|
|
local RT_NONE, RT_ONLYPICKUP = 1, 2
|
|
FS25_AutoDrive = {
|
|
AutoDrive = {
|
|
RT_NONE = RT_NONE,
|
|
getSetting = function(settingName) return settingName == "rotateTargets" and RT_ONLYPICKUP or "andet" end,
|
|
getAllFillLevels = function(trailers) return trailers.level end,
|
|
},
|
|
-- opgaven noterer hvad AutoDrive "ser" af indstillinger mens den opdaterer
|
|
LoadAtDestinationTask = {update = function(self)
|
|
self.seenRotate = FS25_AutoDrive.AutoDrive.getSetting("rotateTargets", self.vehicle)
|
|
self.seenOther = FS25_AutoDrive.AutoDrive.getSetting("useFolders", self.vehicle)
|
|
if self.shouldFail then error("task boom") end
|
|
end},
|
|
ADMultipleTargetsManager = {pickups = {}, getNextPickup = function() return "ORIGINAL" end},
|
|
ADGraphManager = {
|
|
getMapMarkersInGroup = function(_, group)
|
|
local result = {}
|
|
for _, m in ipairs(markers) do if m.group == group then table.insert(result, m) end end
|
|
return result
|
|
end,
|
|
getWayPointById = function(_, id) return wayPoints[id] end,
|
|
getMapMarkers = function() return markers end,
|
|
},
|
|
}
|
|
|
|
local function makeDriver(firstMarker, fillTypes)
|
|
return {
|
|
getName = function() return "Testtraktor" end,
|
|
getOwnerFarmId = function() return 1 end,
|
|
ad = {stateModule = {
|
|
getFirstMarker = function() return firstMarker end,
|
|
getSecondMarker = function() return nil end,
|
|
getSelectedFillTypes = function() return fillTypes end,
|
|
getFillType = function() return fillTypes[1] end,
|
|
}},
|
|
}
|
|
end
|
|
|
|
-- Køretøjet er "fremme ved markøren" medmindre testen siger andet.
|
|
local function withDrivePath(driver)
|
|
driver.targetReached = true
|
|
driver.ad.drivePathModule = {isTargetReached = function() return driver.targetReached end}
|
|
return driver
|
|
end
|
|
|
|
-- Muterbar stateModule-mock: behovsstyringen skifter vare og indsnævrer AD's valg.
|
|
local function makeSupplyDriver(firstMarker, secondMarker, fillTypes)
|
|
local stateModule = {selectedFillTypes = fillTypes, fillType = fillTypes[1], active = true, dirtyCount = 0}
|
|
stateModule.getFirstMarker = function() return firstMarker end
|
|
stateModule.getSecondMarker = function() return secondMarker end
|
|
stateModule.getSelectedFillTypes = function(self) return self.selectedFillTypes end
|
|
stateModule.getFillType = function(self) return self.fillType end
|
|
stateModule.setFillType = function(self, fillType) self.fillType = fillType end
|
|
stateModule.raiseDirtyFlag = function(self) self.dirtyCount = self.dirtyCount + 1 end
|
|
stateModule.isActive = function(self) return self.active end
|
|
return {
|
|
getName = function() return "Fodertraktor" end,
|
|
getOwnerFarmId = function() return 1 end,
|
|
ad = {stateModule = stateModule},
|
|
}
|
|
end
|
|
|
|
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
|
|
dofile("FS25_ADSmartPickup/adSmartPickup.lua")
|
|
listener:update(16) -- installerer hook'en
|
|
local manager = FS25_AutoDrive.ADMultipleTargetsManager
|
|
|
|
local failures = 0
|
|
local function check(name, actual, expected)
|
|
if actual == expected then
|
|
print("OK " .. name)
|
|
else
|
|
failures = failures + 1
|
|
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
|
end
|
|
end
|
|
|
|
-- 1: vælger mindst fyldte silo med varen; springer tomme og forkert vare over
|
|
resetWorld()
|
|
addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[WHEAT] = 900000}))
|
|
addMarker(2, "Silo 2", "Siloer", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[SUGARBEET] = 800000}))
|
|
addMarker(3, "Silo 3", "Siloer", 1000, 0); table.insert(stations, makeStation(103, 1000, 15, {[SUGARBEET] = 12000}))
|
|
addMarker(4, "Silo 4", "Siloer", 1500, 0); table.insert(stations, makeStation(104, 1500, 15, {[SUGARBEET] = 0}))
|
|
addMarker(5, "Fabrik", "Andet", 1000, 5); -- anden mappe, skal ignoreres
|
|
check("mindst fyldte med varen vælges", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 3)
|
|
check("AD-rotationstilstand opdateret", manager.pickups["Siloer"].lastTarget, 3)
|
|
|
|
-- 2: ingen silo har varen -> AutoDrives originale rotation
|
|
check("fallback når varen ikke findes", manager:getNextPickup(makeDriver(markers[1], {99}), false), "ORIGINAL")
|
|
|
|
-- 3: markør uden silo i nærheden ignoreres
|
|
resetWorld()
|
|
addMarker(1, "Langt væk", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 200, {[SUGARBEET] = 5}))
|
|
addMarker(2, "Silo 2", "Siloer", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[SUGARBEET] = 700}))
|
|
check("markør uden nær silo ignoreres", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 2)
|
|
|
|
-- 4: flere valgte varer lægges sammen
|
|
resetWorld()
|
|
addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[SUGARBEET] = 100, [WHEAT] = 5000}))
|
|
addMarker(2, "Silo 2", "Siloer", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[SUGARBEET] = 3000}))
|
|
check("flere varetyper summeres", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET, WHEAT}), false), 2)
|
|
|
|
-- 4b: klynge med delt lager — eget indhold slår nabo-sum, også når nabo-summen er mindre
|
|
resetWorld()
|
|
addMarker(1, "Nabo", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[SUGARBEET] = 800}, false, {}))
|
|
addMarker(2, "Ejer stor", "Siloer", 14, 0); table.insert(stations, makeStation(102, 14, 15, {[SUGARBEET] = 900000}, false, {[SUGARBEET] = 500000}))
|
|
addMarker(3, "Ejer lille", "Siloer", 28, 0);table.insert(stations, makeStation(103, 28, 15, {[SUGARBEET] = 900000}, false, {[SUGARBEET] = 20000}))
|
|
check("eget lager foran nabo-lån, mindst først", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 3)
|
|
|
|
-- 4c: kun nabo-lån muligt -> bruges stadig
|
|
resetWorld()
|
|
addMarker(1, "Nabo", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[SUGARBEET] = 800}, false, {}))
|
|
check("nabo-lån bruges når ingen ejer varen", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), 1)
|
|
|
|
-- 5: fejl i spil-API -> fallback + advarsel, aldrig crash
|
|
resetWorld()
|
|
addMarker(1, "Silo 1", "Siloer", 0, 0); table.insert(stations, makeStation(101, 0, 15, {}, true))
|
|
check("fejl giver fallback", manager:getNextPickup(makeDriver(markers[1], {SUGARBEET}), false), "ORIGINAL")
|
|
check("fejl logges som advarsel", logLines[#logLines]:sub(1, 4), "WARN")
|
|
|
|
-- 6: gruppen "All" og manglende første markør røres ikke
|
|
check("gruppe All -> fallback", manager:getNextPickup(makeDriver({group = "All", markerIndex = 1}, {SUGARBEET}), false), "ORIGINAL")
|
|
check("ingen første markør -> fallback", manager:getNextPickup(makeDriver(nil, {SUGARBEET}), false), "ORIGINAL")
|
|
|
|
-- ==== Behovsstyret forsyning (flere varer valgt + aflæsningsmål med lager) ====
|
|
local BLANDER = {name = "TMR-blander placeable"}
|
|
local FULL = {[HAY] = 100000, [GRASS] = 100000, [SILAGE] = 100000}
|
|
local blanderLevels
|
|
|
|
local function buildFeedWorld()
|
|
resetWorld()
|
|
blanderLevels = {[HAY] = 80000, [GRASS] = 20000, [SILAGE] = 50000}
|
|
addMarker(1, "Hølade", "Lader", 0, 0); table.insert(stations, makeStation(101, 0, 15, {[HAY] = 50000}))
|
|
addMarker(2, "Græssilo stor", "Siloer", 500, 0); table.insert(stations, makeStation(102, 500, 15, {[GRASS] = 300000}))
|
|
addMarker(3, "Græssilo lille", "Andet", 1000, 0);table.insert(stations, makeStation(103, 1000, 15, {[GRASS] = 40000}))
|
|
addMarker(4, "Ensilagesilo", "Siloer", 1500, 0); table.insert(stations, makeStation(104, 1500, 15, {[SILAGE] = 200000}))
|
|
addMarker(10, "TMR-blander", "Produktion", 5000, 0)
|
|
table.insert(unloadStations, makeUnloadStation(110, 5000, 10, blanderLevels, FULL, BLANDER))
|
|
end
|
|
local function markerByIndex(index)
|
|
for _, m in ipairs(markers) do if m.markerIndex == index then return m end end
|
|
end
|
|
|
|
-- S1: mest trængende ingrediens vælges, kilde findes på tværs af mapper, AD's vare skiftes
|
|
buildFeedWorld()
|
|
local feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS, SILAGE})
|
|
check("S1 mest trængende vare, mindst fyldte kilde", manager:getNextPickup(feeder, false), 3)
|
|
check("S1 AD's vare skiftet til græs", feeder.ad.stateModule.fillType, GRASS)
|
|
check("S1 AD's valg indsnævret til én vare", #feeder.ad.stateModule.selectedFillTypes, 1)
|
|
check("S1 ændring synkroniseres (dirty-flag)", feeder.ad.stateModule.dirtyCount > 0, true)
|
|
check("S1 rotationstilstand følger valgt markørs mappe", manager.pickups["Andet"].lastTarget, 3)
|
|
|
|
-- S2: næste tur — gemt liste genbruges; græs er nu fuldt og springes over -> ensilage
|
|
blanderLevels[GRASS] = 99500
|
|
check("S2 fuld ingrediens springes over", manager:getNextPickup(feeder, false), 4)
|
|
check("S2 vare skiftet til ensilage", feeder.ad.stateModule.fillType, SILAGE)
|
|
|
|
-- S3: ingrediens uden kilde springes over -> næste i behovs-rækkefølgen
|
|
blanderLevels[SILAGE] = 10000
|
|
stations[4] = makeStation(104, 1500, 15, {[SILAGE] = 0})
|
|
check("S3 ingen kilde -> næste ingrediens", manager:getNextPickup(feeder, false), 1)
|
|
check("S3 vare skiftet til hø", feeder.ad.stateModule.fillType, HAY)
|
|
|
|
-- S4: AD stopper -> hele ingredienslisten gendannes i AD's varevalg
|
|
feeder.ad.stateModule.active = false
|
|
listener:update(16)
|
|
check("S4 valg gendannet ved stop", #feeder.ad.stateModule.selectedFillTypes, 3)
|
|
|
|
-- S5: aflæsningsmålets egen læssestation bruges ikke som kilde
|
|
buildFeedWorld()
|
|
addMarker(11, "Blander udtag", "Produktion", 5030, 0)
|
|
table.insert(stations, makeStation(111, 5030, 15, {[GRASS] = 10}, false, nil))
|
|
stations[#stations].owningPlaceable = BLANDER
|
|
feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS})
|
|
check("S5 målets egen station udelukkes som kilde", manager:getNextPickup(feeder, false), 3)
|
|
|
|
-- S6: brugeren vælger selv en anden enkelt vare -> behovsstyring fra (mappe-logik)
|
|
feeder.ad.stateModule.selectedFillTypes = {HAY}
|
|
feeder.ad.stateModule.fillType = HAY
|
|
check("S6 brugerens enkelt-valg -> mappe-logik", manager:getNextPickup(feeder, false), 1)
|
|
check("S6 vare røres ikke", feeder.ad.stateModule.fillType, HAY)
|
|
|
|
-- S7: intet behov (alt fuldt) -> mappe-logik, valg indsnævres ikke
|
|
buildFeedWorld()
|
|
blanderLevels[HAY], blanderLevels[GRASS], blanderLevels[SILAGE] = 99900, 99900, 99900
|
|
feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS, SILAGE})
|
|
check("S7 intet behov -> mappe-logik", manager:getNextPickup(feeder, false), 1)
|
|
check("S7 valg ikke indsnævret", #feeder.ad.stateModule.selectedFillTypes, 3)
|
|
|
|
-- S8: ingen aflæsningsstation ved målet -> mappe-logik
|
|
buildFeedWorld()
|
|
unloadStations = {}
|
|
feeder = makeSupplyDriver(markerByIndex(2), markerByIndex(10), {GRASS, SILAGE})
|
|
check("S8 ingen station ved mål -> mappe-logik", manager:getNextPickup(feeder, false), 4)
|
|
|
|
-- S9: fejl i målets lager-API -> AutoDrives original + advarsel
|
|
buildFeedWorld()
|
|
unloadStations = {makeUnloadStation(110, 5000, 10, blanderLevels, FULL, BLANDER, true)}
|
|
feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS})
|
|
check("S9 fejl giver fallback", manager:getNextPickup(feeder, false), "ORIGINAL")
|
|
check("S9 fejl logges som advarsel", logLines[#logLines]:sub(1, 4), "WARN")
|
|
|
|
-- S10: stationen eksponerer ikke targetStorages -> lageret læses fra produktionspunktet
|
|
buildFeedWorld()
|
|
local bareStation = unloadStations[1]
|
|
local productionStorage = next(bareStation.targetStorages)
|
|
bareStation.targetStorages = nil
|
|
bareStation.owningPlaceable = {spec_productionPoint = {productionPoint = {storage = productionStorage}}}
|
|
feeder = makeSupplyDriver(markerByIndex(1), markerByIndex(10), {HAY, GRASS, SILAGE})
|
|
check("S10 lager læses fra produktionspunktet", manager:getNextPickup(feeder, false), 3)
|
|
|
|
-- ==== 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"
|
|
FS25_AutoDrive.AutoDrive.getSupportedFillTypesOfAllUnitsAlphabetically = function(vehicle) return vehicle.carriable end
|
|
|
|
-- 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)
|
|
|
|
-- 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)
|
|
|
|
-- 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)
|