diff --git a/FS25_ADSmartPickup/adFieldSilos.lua b/FS25_ADSmartPickup/adFieldSilos.lua
new file mode 100644
index 0000000..11fb6cd
--- /dev/null
+++ b/FS25_ADSmartPickup/adFieldSilos.lua
@@ -0,0 +1,315 @@
+-- AD Field Silos
+-- Marksilo-flowet i spillet: samler marksiloer, gårdlagre og ledige vogne, lader ADFieldSiloPlanner vælge,
+-- låner vogne (AutoDrive "Hent og aflever", én tur, parkering bagefter) og frigiver dem igen.
+-- Lån ligger kun i hukommelsen; opsætningen gemmes i ADSmartPickup_fieldsilos.xml.
+
+ADFieldSilos = {}
+
+ADFieldSilos.PLAN_INTERVAL_MS = 60000
+ADFieldSilos.CHECK_INTERVAL_MS = 2000
+ADFieldSilos.STUCK_MS = 120000
+ADFieldSilos.PAUSE_MS = 600000
+ADFieldSilos.MOVING_SPEED = 0.00028 -- m/ms, samme grænse som AutoDrive
+ADFieldSilos.FILE_NAME = "ADSmartPickup_fieldsilos.xml"
+ADFieldSilos.ROOT = "ADSmartPickupFieldSilos"
+ADFieldSilos.LOG_PREFIX = "ADSmartPickup: marksilo: "
+
+ADFieldSilos.config = ADFieldSiloConfig.new()
+ADFieldSilos.lastPlan = nil
+local loans = setmetatable({}, {__mode = "k"}) -- vehicle -> {siloId, siloName, fillType, liters, startedMs, lastProgressMs, lastFill}
+local paused = {} -- key -> untilMs
+local planTimerMs, checkTimerMs, nowMs = ADFieldSilos.PLAN_INTERVAL_MS, 0, 0
+
+-- ---- ren frigivelsesregel ----
+
+-- vehicleState = {adActive, controlled, fill, moving}
+function ADFieldSilos.releaseReason(loan, vehicleState, now)
+ if vehicleState.controlled then return "player" end
+ if not vehicleState.adActive then return "done" end
+ if vehicleState.moving then return nil end
+ if (vehicleState.fill or 0) > (loan.lastFill or 0) then return nil end
+ if now - (loan.lastProgressMs or loan.startedMs or 0) > ADFieldSilos.STUCK_MS then return "stuck" end
+ return nil
+end
+
+-- ---- opsætning ----
+
+function ADFieldSilos.setConfig(cfg)
+ local wasEnabled = ADFieldSilos.config.enabled
+ ADFieldSilos.config = cfg
+ if wasEnabled and not cfg.enabled then ADFieldSilos.stopAll(ADFieldSilos.adEnv) end
+ planTimerMs = ADFieldSilos.PLAN_INTERVAL_MS -- planlæg straks ved næste tick
+end
+
+function ADFieldSilos.isLoaned(vehicle)
+ return loans[vehicle] ~= nil
+end
+
+function ADFieldSilos.loanCount()
+ local n = 0
+ for _ in pairs(loans) do n = n + 1 end
+ return n
+end
+
+-- ---- spil-input ----
+
+local function farmId()
+ return g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1
+end
+
+local function position(node)
+ if node == nil then return 0, 0 end
+ local x, _, z = getWorldTranslation(node)
+ return x, z
+end
+
+local function siloStock(placeable)
+ local stock = {}
+ local spec = placeable ~= nil and placeable.spec_silo or nil
+ for _, storage in pairs(spec ~= nil and spec.storages or {}) do
+ for fillType, level in pairs(storage.fillLevels or {}) do
+ if (level or 0) > 0 then stock[fillType] = (stock[fillType] or 0) + level end
+ end
+ end
+ return stock
+end
+
+local function siloFree(placeable)
+ local free = {}
+ local spec = placeable ~= nil and placeable.spec_silo or nil
+ for _, storage in pairs(spec ~= nil and spec.storages or {}) do
+ for fillType in pairs(storage.fillTypes or {}) do
+ local amount = storage.getFreeCapacity ~= nil and (storage:getFreeCapacity(fillType) or 0) or 0
+ free[fillType] = (free[fillType] or 0) + amount
+ end
+ end
+ return free
+end
+
+local function firstMarker(building, role)
+ local markers = ADBuildings.getMarkers(building, role)
+ return markers[1] ~= nil and markers[1].id or nil
+end
+
+-- Gårdlagre fra opsætningen + marksiloer (alle markers marksiloer, uden dubletter og uden gårdlagre).
+function ADFieldSilos.collectBuildings(adEnv)
+ local fid = farmId()
+ local stores, byId = {}, {}
+ for _, building in ipairs(ADBuildings.list(adEnv, fid)) do
+ byId[building.id] = building
+ if building.kind == "silo" and ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id) then
+ local x, z = position(building.placeable ~= nil and building.placeable.rootNode or nil)
+ table.insert(stores, {id = building.id, name = building.name, markerId = firstMarker(building, "unload"),
+ x = x, z = z, free = siloFree(building.placeable)})
+ end
+ end
+ local silos, seen = {}, {}
+ for _, info in ipairs(ADFields.list()) do
+ for _, building in ipairs(ADFields.fieldSilos(adEnv, fid, info)) do
+ if not seen[building.id] and not ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id) then
+ seen[building.id] = true
+ local x, z = position(building.placeable ~= nil and building.placeable.rootNode or nil)
+ table.insert(silos, {id = building.id, name = building.name, markerId = firstMarker(building, "load"),
+ x = x, z = z, stock = siloStock(building.placeable)})
+ end
+ end
+ end
+ return silos, stores
+end
+
+local function trailerInfo(adEnv, vehicle)
+ local capacity, carries, cargo = 0, {}, nil
+ for _, unit in ipairs(adEnv.AutoDrive.getAllDischargeableUnits(vehicle) or {}) do
+ local object, index = unit.object, unit.fillUnitIndex
+ if object ~= nil and object.getFillUnitCapacity ~= nil then
+ capacity = capacity + (object:getFillUnitCapacity(index) or 0)
+ for fillType in pairs(object.getFillUnitSupportedFillTypes ~= nil and object:getFillUnitSupportedFillTypes(index) or {}) do
+ carries[fillType] = true
+ end
+ if (object:getFillUnitFillLevel(index) or 0) > 0 then cargo = object:getFillUnitFillType(index) end
+ end
+ end
+ return capacity, carries, cargo
+end
+
+local function fillLevel(adEnv, vehicle)
+ local ok, level = pcall(function() return (adEnv.AutoDrive.getAllFillLevels(adEnv.AutoDrive.getAllUnits(vehicle))) end)
+ return ok and tonumber(level) or 0
+end
+
+function ADFieldSilos.collectVehicles(adEnv)
+ local vehicles, byId = {}, {}
+ for _, rig in ipairs(ADFieldWork.getRigs(adEnv)) do
+ if rig.role == "unloader" and rig.enabled and not rig.busy and not rig.controlled then
+ local capacity, carries, cargo = trailerInfo(adEnv, rig.vehicle)
+ table.insert(vehicles, {id = rig.id, x = rig.x, z = rig.z, capacity = capacity, carries = carries, cargo = cargo})
+ byId[rig.id] = rig.vehicle
+ end
+ end
+ return vehicles, byId
+end
+
+-- ---- lån ----
+
+local function fillTypeName(fillType)
+ local desc = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil
+ return desc ~= nil and (desc.title or desc.name) or tostring(fillType)
+end
+
+local function dispatchOne(adEnv, order, vehicle, silosById, storesById)
+ local module = vehicle.ad.stateModule
+ module:setMode(adEnv.AutoDrive.MODE_PICKUPANDDELIVER)
+ module:setFirstMarkerByWayPointId(order.siloMarkerId)
+ module:setSecondMarkerByWayPointId(order.storeMarkerId)
+ module.selectedFillTypes = {order.fillType}
+ module:setFillType(order.fillType)
+ module.loopCounter = 1
+ if module.setLoopsDone ~= nil then module:setLoopsDone(0) end
+ ADFieldWork.enableParkAtJobFinished(vehicle)
+ module:raiseDirtyFlag()
+ ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_PICKUPANDDELIVER, order.siloMarkerId, order.storeMarkerId, order.fillType)
+ local silo, store = silosById[order.siloId], storesById[order.storeId]
+ loans[vehicle] = {siloId = order.siloId, siloName = silo ~= nil and silo.name or order.siloId, fillType = order.fillType,
+ liters = order.liters, startedMs = nowMs, lastProgressMs = nowMs, lastFill = fillLevel(adEnv, vehicle)}
+ Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' henter %d l %s i %s -> %s", tostring(ADRuns.vehicleLabel(vehicle)),
+ math.floor(order.liters), fillTypeName(order.fillType), tostring(silo ~= nil and silo.name or order.siloId),
+ tostring(store ~= nil and store.name or order.storeId))
+end
+
+local function release(adEnv, vehicle, reason)
+ local loan = loans[vehicle]
+ loans[vehicle] = nil
+ if loan == nil then return end
+ if reason == "stuck" then
+ local k = ADFieldSiloPlanner.key(loan.siloId, loan.fillType)
+ paused[k] = nowMs + ADFieldSilos.PAUSE_MS
+ pcall(vehicle.stopAutoDrive, vehicle)
+ Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' kom ikke i gang ved %s (%s) på 2 min — stoppet; siloen springes over i 10 min",
+ tostring(ADRuns.vehicleLabel(vehicle)), tostring(loan.siloName), fillTypeName(loan.fillType))
+ elseif reason == "stopped" then
+ pcall(vehicle.stopAutoDrive, vehicle)
+ end
+ Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' frigivet (%s)", tostring(ADRuns.vehicleLabel(vehicle)), tostring(reason))
+end
+
+function ADFieldSilos.stopAll(adEnv)
+ local list = {}
+ for vehicle in pairs(loans) do table.insert(list, vehicle) end
+ for _, vehicle in ipairs(list) do release(adEnv, vehicle, "stopped") end
+end
+
+local function checkLoans(adEnv)
+ local list = {}
+ for vehicle in pairs(loans) do table.insert(list, vehicle) end
+ for _, vehicle in ipairs(list) do
+ local loan = loans[vehicle]
+ local stateModule = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
+ local fill = fillLevel(adEnv, vehicle)
+ local vehicleState = {
+ adActive = stateModule ~= nil and stateModule:isActive(),
+ controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() or false,
+ fill = fill, moving = (vehicle.lastSpeedReal or 0) >= ADFieldSilos.MOVING_SPEED,
+ }
+ if vehicleState.moving or fill > (loan.lastFill or 0) then loan.lastProgressMs = nowMs end
+ loan.lastFill = math.max(loan.lastFill or 0, fill)
+ local reason = ADFieldSilos.releaseReason(loan, vehicleState, nowMs)
+ if reason ~= nil then release(adEnv, vehicle, reason) end
+ end
+end
+
+local function currentPaused()
+ local result = {}
+ for k, untilMs in pairs(paused) do
+ if untilMs > nowMs then result[k] = true else paused[k] = nil end
+ end
+ return result
+end
+
+local function loanList()
+ local list = {}
+ for _, loan in pairs(loans) do table.insert(list, {siloId = loan.siloId, fillType = loan.fillType, liters = loan.liters}) end
+ return list
+end
+
+function ADFieldSilos.planNow(adEnv)
+ local cfg = ADFieldSilos.config
+ local silos, stores = ADFieldSilos.collectBuildings(adEnv)
+ local vehicles, vehiclesById = ADFieldSilos.collectVehicles(adEnv)
+ local plan = ADFieldSiloPlanner.plan({silos = silos, stores = stores, loans = loanList(), vehicles = vehicles,
+ paused = currentPaused(), maxVehicles = cfg.maxVehicles, minLiters = cfg.minLiters})
+ ADFieldSilos.lastPlan = plan
+ if not cfg.enabled then return plan end
+ local silosById, storesById = {}, {}
+ for _, silo in ipairs(silos) do silosById[silo.id] = silo end
+ for _, store in ipairs(stores) do storesById[store.id] = store end
+ for _, order in ipairs(plan.dispatch) do
+ local vehicle = vehiclesById[order.vehicleId]
+ if vehicle ~= nil then
+ local ok, err = pcall(dispatchOne, adEnv, order, vehicle, silosById, storesById)
+ if not ok then
+ loans[vehicle] = nil
+ Logging.warning(ADFieldSilos.LOG_PREFIX .. "kunne ikke sende '%s': %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
+ end
+ end
+ end
+ return plan
+end
+
+function ADFieldSilos.tick(adEnv, dt)
+ if adEnv == nil then return end
+ ADFieldSilos.adEnv = adEnv
+ nowMs = nowMs + (dt or 0)
+ checkTimerMs = checkTimerMs + (dt or 0)
+ if checkTimerMs >= ADFieldSilos.CHECK_INTERVAL_MS then
+ checkTimerMs = 0
+ checkLoans(adEnv)
+ end
+ planTimerMs = planTimerMs + (dt or 0)
+ if planTimerMs >= ADFieldSilos.PLAN_INTERVAL_MS then
+ planTimerMs = 0
+ ADFieldSilos.planNow(adEnv)
+ end
+end
+
+-- ---- gem / indlæs (samme sti-opslag som runs) ----
+
+local function getPath()
+ local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
+ if path == nil then return nil end
+ return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADFieldSilos.FILE_NAME))
+end
+
+function ADFieldSilos.save()
+ local path = getPath()
+ if path == nil then return false end
+ local xmlFile = XMLFile.create("ADSmartPickupFieldSilos", path, ADFieldSilos.ROOT)
+ if xmlFile == nil then return false end
+ local record = ADFieldSiloConfig.toRecord(ADFieldSilos.config)
+ xmlFile:setInt(ADFieldSilos.ROOT .. "#enabled", record.enabled)
+ xmlFile:setString(ADFieldSilos.ROOT .. "#stores", record.stores)
+ xmlFile:setInt(ADFieldSilos.ROOT .. "#maxVehicles", record.maxVehicles)
+ xmlFile:setInt(ADFieldSilos.ROOT .. "#minLiters", record.minLiters)
+ xmlFile:save()
+ xmlFile:delete()
+ return true
+end
+
+function ADFieldSilos.load()
+ local path = getPath()
+ if path == nil or not fileExists(path) then
+ ADFieldSilos.config = ADFieldSiloConfig.new()
+ return
+ end
+ local xmlFile = XMLFile.load("ADSmartPickupFieldSilos", path)
+ if xmlFile == nil then
+ ADFieldSilos.config = ADFieldSiloConfig.new()
+ return
+ end
+ ADFieldSilos.config = ADFieldSiloConfig.fromRecord({
+ enabled = xmlFile:getInt(ADFieldSilos.ROOT .. "#enabled"),
+ stores = xmlFile:getString(ADFieldSilos.ROOT .. "#stores"),
+ maxVehicles = xmlFile:getInt(ADFieldSilos.ROOT .. "#maxVehicles"),
+ minLiters = xmlFile:getInt(ADFieldSilos.ROOT .. "#minLiters"),
+ })
+ xmlFile:delete()
+end
diff --git a/FS25_ADSmartPickup/adFieldWork.lua b/FS25_ADSmartPickup/adFieldWork.lua
index 9ecfaad..60dc928 100644
--- a/FS25_ADSmartPickup/adFieldWork.lua
+++ b/FS25_ADSmartPickup/adFieldWork.lua
@@ -88,6 +88,9 @@ local function startAd(adEnv, vehicle, mode, firstId, secondId, fillType)
wake(vehicle, true)
end
+ADFieldWork.startAd = startAd
+ADFieldWork.enableParkAtJobFinished = enableParkAtJobFinished
+
-- AD-parkering: sidder på redskabet og kopieres af AD til traktorens stateModule (markør-INDEKS).
local function parkMarkerId(adEnv, vehicle)
local module = stateModule(vehicle)
@@ -130,7 +133,8 @@ function ADFieldWork.getRigs(adEnv)
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id),
-- optaget: vores egen opgave, eller AutoDrive/Courseplay kører den allerede (fx spillerens
-- egen parkeringstur) — den må ikke kapres midt i en anden kørsel
- busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle),
+ busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)
+ or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)),
controlled = isControlledByPlayer(vehicle),
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
-- tom spreder kan flere ting (gødning og kalk): alle dens roller
diff --git a/FS25_ADSmartPickup/adRunsController.lua b/FS25_ADSmartPickup/adRunsController.lua
index cffc0b1..9ef8897 100644
--- a/FS25_ADSmartPickup/adRunsController.lua
+++ b/FS25_ADSmartPickup/adRunsController.lua
@@ -1318,6 +1318,7 @@ function ADRunsController.save()
if ADFieldJobs ~= nil and ADFieldJobs.loaded then
pcall(ADFieldJobs.save)
end
+ if ADFieldSilos ~= nil then pcall(ADFieldSilos.save) end
return saved
end
@@ -1330,6 +1331,7 @@ function ADRunsController.load(adEnv)
end
ADRunsController.store = store
pcall(ADRunsController.migrateBuildings, adEnv)
+ if ADFieldSilos ~= nil then pcall(ADFieldSilos.load) end
local vehiclesById = getFarmVehiclesById(adEnv)
for _, run in ipairs(store.runs) do
for _, vehicleId in ipairs(run.vehicleIds) do
diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua
index 8620a46..a62b66c 100644
--- a/FS25_ADSmartPickup/adSmartPickup.lua
+++ b/FS25_ADSmartPickup/adSmartPickup.lua
@@ -1262,6 +1262,12 @@ function ADSmartPickup:update(dt)
Logging.warning(ADSmartPickup.LOG_PREFIX .. "markarbejde fejlede: %s", tostring(errFields))
end
end
+ if ADFieldSilos ~= nil then
+ local okSilos, errSilos = pcall(ADFieldSilos.tick, getAutoDriveEnv(), dt)
+ if not okSilos then
+ Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos))
+ end
+ end
if ADStuckDiag ~= nil then pcall(ADStuckDiag.tick, dt) end
if ADRunsController ~= nil and ADRunsController.adoptLoadedThrottled ~= nil then
pcall(ADRunsController.adoptLoadedThrottled, getAutoDriveEnv(), dt)
diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml
index d637af2..aff94c1 100644
--- a/FS25_ADSmartPickup/modDesc.xml
+++ b/FS25_ADSmartPickup/modDesc.xml
@@ -39,6 +39,9 @@
+
+
+
diff --git a/tests/test_adFieldSilos.lua b/tests/test_adFieldSilos.lua
new file mode 100644
index 0000000..b81b144
--- /dev/null
+++ b/tests/test_adFieldSilos.lua
@@ -0,0 +1,81 @@
+-- Kør: luajit tests/test_adFieldSilos.lua (fra repo-roden)
+dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
+dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
+dofile("FS25_ADSmartPickup/adFieldSilos.lua")
+local S = ADFieldSilos
+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
+local loan = {startedMs = 0, lastProgressMs = 0, lastFill = 0}
+check("kører, tidligt -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 5000), nil)
+check("AD slukket -> done", S.releaseReason(loan, {adActive = false, controlled = false, fill = 0}, 5000), "done")
+check("spiller i vognen -> player", S.releaseReason(loan, {adActive = true, controlled = true, fill = 0}, 5000), "player")
+check("står stille uden fyld i 120 s -> stuck", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = false}, 120001), "stuck")
+check("kører stadig efter 120 s -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 120001), nil)
+local loading = {startedMs = 0, lastProgressMs = 110000, lastFill = 0}
+check("fyldning steg for nylig -> behold", S.releaseReason(loading, {adActive = true, controlled = false, fill = 20000, moving = false}, 125000), nil)
+
+-- Røgtest af spil-laget med mocks: planNow låner nærmeste tømmevogn, isLoaned = optaget,
+-- frakobling (setConfig enabled=false) stopper AD og frigiver (Review Focus).
+do
+ local WHEAT = 1
+ Logging = {info = function() end, warning = function() end}
+ g_currentMission = {getFarmId = function() return 1 end}
+ g_fillTypeManager = {getFillTypeByIndex = function() return {title = "Hvede"} end}
+ function getWorldTranslation(node) return node.x, 0, node.z end
+ ADRuns = {vehicleLabel = function(v) return v.name end}
+ local function storage(levels, free) return {fillLevels = levels, fillTypes = {[WHEAT] = true}, getFreeCapacity = function() return free end} end
+ local fieldSilo = {id = "fs", name = "Marksilo 1", kind = "silo", placeable = {rootNode = {x = 1000, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
+ local farm = {id = "farm", name = "Gårdsilo", kind = "silo", placeable = {rootNode = {x = 0, z = 0}, spec_silo = {storages = {storage({}, 500000)}}}}
+ ADBuildings = {list = function() return {fieldSilo, farm} end,
+ getMarkers = function(b, role) return {{id = b.id .. "-" .. role}} end}
+ ADFields = {list = function() return {{id = 1}} end, fieldSilos = function() return {fieldSilo, farm} end}
+ local started = {}
+ local function makeVehicle(name, x)
+ local module = {active = false}
+ function module:setMode() end
+ function module:setFirstMarkerByWayPointId(id) self.first = id end
+ function module:setSecondMarkerByWayPointId(id) self.second = id end
+ function module:setFillType() end
+ function module:setLoopsDone() end
+ function module:raiseDirtyFlag() end
+ function module:isActive() return self.active end
+ return {name = name, x = x, lastSpeedReal = 0, ad = {stateModule = module},
+ stopAutoDrive = function(self) self.ad.stateModule.active = false; self.stopped = true end}
+ end
+ local near, far = makeVehicle("Nær", 900), makeVehicle("Fjern", 5000)
+ local adEnv = {AutoDrive = {MODE_PICKUPANDDELIVER = 2,
+ getAllDischargeableUnits = function() return {{fillUnitIndex = 1, object = {
+ getFillUnitCapacity = function() return 40000 end,
+ getFillUnitSupportedFillTypes = function() return {[WHEAT] = true} end,
+ getFillUnitFillLevel = function() return 0 end, getFillUnitFillType = function() return WHEAT end}}} end,
+ getAllUnits = function() return {} end, getAllFillLevels = function() return 0 end}}
+ ADFieldWork = {
+ getRigs = function() return {
+ {id = "n", vehicle = near, role = "unloader", enabled = true, busy = S.isLoaned(near), controlled = false, x = 900, z = 0},
+ {id = "f", vehicle = far, role = "unloader", enabled = true, busy = S.isLoaned(far), controlled = false, x = 5000, z = 0},
+ {id = "h", vehicle = far, role = "harvester", enabled = true, busy = false, controlled = false, x = 1000, z = 0}} end,
+ enableParkAtJobFinished = function() end,
+ startAd = function(_, v, _, first, second, fillType) v.ad.stateModule.active = true; table.insert(started, {v = v, first = first, second = second, fillType = fillType}) end,
+ }
+ S.setConfig(ADFieldSiloConfig.with(ADFieldSiloConfig.toggleStore(ADFieldSiloConfig.new(), "farm"), {enabled = true, maxVehicles = 1}))
+ local plan = S.planNow(adEnv)
+ check("RØG én afsendelse (x=1)", #started, 1)
+ check("RØG nærmeste tømmevogn", started[1] ~= nil and started[1].v.name, "Nær")
+ check("RØG fra marksilo-markør", started[1] ~= nil and started[1].first, "fs-load")
+ check("RØG til gårdlager-markør", started[1] ~= nil and started[1].second, "farm-unload")
+ check("RØG varen med", started[1] ~= nil and started[1].fillType, WHEAT)
+ check("RØG gårdlager er ikke marksilo", #plan.status, 1)
+ check("RØG vognen er lånt", S.isLoaned(near), true)
+ check("RØG x nået -> ingen flere", #S.planNow(adEnv).dispatch, 0)
+ S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
+ check("RØG frakobling stopper AD", near.stopped, true)
+ check("RØG frakobling frigiver", S.isLoaned(near), false)
+end
+
+print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
+os.exit(failures == 0 and 0 or 1)