Files
fs25-adsmartpickup/FS25_ADSmartPickup/adFieldSilos.lua
T

721 lines
36 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- AD Field Silos
-- Marksilo-flowet i spillet: samler marksiloer, gårdlagre og ledige vogne, lader ADFieldSiloPlanner vælge,
-- med "kør til produktion" går overskud ud over gårdens foderreserve til gårdens fabrikker;
-- 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.FAIL_MS = 30000 -- AD slukket inden for 30 s uden fremdrift = turen kunne ikke køres
ADFieldSilos.CARGO_MIN_LITERS = 100 -- rest under dette tæller ikke som last
-- Silo så tæt på et gårdlager er en del af gården (Farma-siloer deler lager inden for 50 m): ikke marksilo.
-- (in-game 27/9: sukkerroer blev kørt fra én Farma-silo til nabosiloen i samme klynge)
ADFieldSilos.FARM_RADIUS = 100
-- Siloer højst så langt fra en nabo hører til samme klynge (Farma-rækker: 15 m mellem siloer, 18 m mellem rækker)
ADFieldSilos.CLUSTER_LINK = 60
ADFieldSilos.REST_SILO_DISTANCE = 40 -- m fra marksiloens markør: vognen holder ved siloen
ADFieldSilos.FETCH_TIMEOUT_MS = 300000 -- så længe må en traktor være om at hente en puljevogn
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}. Fremdrift = kører, eller fyldningen ændrer sig (læsser
-- eller aflæsser). Vagten gælder kun før turen er kommet i gang — kø ved siloen/lageret er normalt.
function ADFieldSilos.releaseReason(loan, vehicleState, now)
if vehicleState.controlled then return "player" end
local progressing = vehicleState.moving or (vehicleState.fill or 0) ~= (loan.lastFill or 0)
local hasProgressed = loan.hasProgressed or progressing
local age = now - (loan.startedMs or 0)
if not vehicleState.adActive then
return (not hasProgressed and age < ADFieldSilos.FAIL_MS) and "failed" or "done"
end
if hasProgressed then return nil end
if age > 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
-- Bygninger der er kilde, mål eller lager i et flow: dem tømmer marksilo-flowet ikke (de hører til den drift).
local function runBuildingIds()
local ids = {}
local store = ADRunsController ~= nil and ADRunsController.store or nil
for _, run in ipairs(store ~= nil and store.runs or {}) do
for _, list in ipairs({run.sourceBuildingIds or {}, run.targetBuildingIds or {}, run.storeBuildingIds or {}}) do
for _, id in ipairs(list) do ids[id] = true end
end
end
return ids
end
-- Alle siloer i samme klynge som et gårdlager (kæde af naboer ≤ CLUSTER_LINK): gårdens, aldrig marksiloer.
-- (in-game 27/9: gårdens Farma-klynge er ~300 m lang; siloer over 100 m fra et valgt lager blev marksiloer)
local function farmCluster(allSilos, storeIds)
local farm, queue = {}, {}
for _, silo in ipairs(allSilos) do
if storeIds[silo.id] then farm[silo.id] = true; table.insert(queue, silo) end
end
local link2 = ADFieldSilos.CLUSTER_LINK ^ 2
while #queue > 0 do
local current = table.remove(queue)
for _, silo in ipairs(allSilos) do
if not farm[silo.id] and (silo.x - current.x) ^ 2 + (silo.z - current.z) ^ 2 <= link2 then
farm[silo.id] = true
table.insert(queue, silo)
end
end
end
return farm
end
local function nearStore(stores, x, z)
for _, store in ipairs(stores) do
if (store.x - x) ^ 2 + (store.z - z) ^ 2 <= ADFieldSilos.FARM_RADIUS ^ 2 then return true end
end
return false
end
-- Fabrikkens input-varer (dem aflæsningen tager imod) med fri plads i dens lager.
local function factoryFree(placeable)
local spec = placeable ~= nil and placeable.spec_productionPoint or nil
local point = spec ~= nil and spec.productionPoint or nil
local storage = point ~= nil and point.storage or nil
if storage == nil or storage.getFreeCapacity == nil then return {} end
local inputs = {}
for fillType in pairs(point.inputFillTypeIds or {}) do inputs[fillType] = true end
for _, production in ipairs(point.productions or {}) do
for _, input in ipairs(production.inputs or {}) do inputs[input.type] = true end
end
local free = {}
for fillType in pairs(inputs) do free[fillType] = storage:getFreeCapacity(fillType) or 0 end
return free
end
-- "Kør til produktion": gårdens fabrikker med aflæsningsmarkør bliver mål ved siden af gårdlagrene.
local function addFactories(stores, buildings)
for _, building in ipairs(buildings) do
local markerId = building.kind == "factory" and firstMarker(building, "unload") or nil
if markerId ~= nil then
local x, z = position(building.placeable ~= nil and building.placeable.rootNode or nil)
table.insert(stores, {id = building.id, name = building.name, kind = "production", markerId = markerId,
x = x, z = z, free = factoryFree(building.placeable)})
end
end
end
-- Foderreserve (staldenes døgnforbrug × døgn) og gårdlagrenes beholdning pr. vare.
function ADFieldSilos.collectFeed(stores, byId, fid, reserveDays)
local reserve, stock = {}, {}
local rates = ADSources ~= nil and ADSources.getFarmRates ~= nil and ADSources.getFarmRates(ADFieldSilos.adEnv, fid, true) or {}
for fillType, rate in pairs(rates) do reserve[fillType] = (rate.consumption or 0) * reserveDays end
local counted = {} -- Farma-siloer i samme klynge deler lager: tæl hvert lager én gang
for _, store in ipairs(stores) do
local building = byId[store.id]
local spec = store.kind ~= "production" and building ~= nil and building.placeable ~= nil and building.placeable.spec_silo or nil
for _, storage in pairs(spec ~= nil and spec.storages or {}) do
if not counted[storage] then
counted[storage] = true
for fillType, level in pairs(storage.fillLevels or {}) do stock[fillType] = (stock[fillType] or 0) + (level or 0) end
end
end
end
return reserve, stock
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, allSilos, storeIds = {}, {}, {}, {}
for _, building in ipairs(ADBuildings.list(adEnv, fid)) do
byId[building.id] = building
if building.kind == "silo" then
local sx, sz = position(building.placeable ~= nil and building.placeable.rootNode or nil)
table.insert(allSilos, {id = building.id, x = sx, z = sz})
if ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id) then storeIds[building.id] = true end
end
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
if ADFieldSilos.config.toProduction then addFactories(stores, ADBuildings.list(adEnv, fid)) end
local silos, seen = {}, runBuildingIds()
for id in pairs(farmCluster(allSilos, storeIds)) do seen[id] = true end
for _, info in ipairs(ADFields.list()) do
for _, building in ipairs(ADFields.fieldSilos(adEnv, fid, info)) do
local x, z = position(building.placeable ~= nil and building.placeable.rootNode or nil)
if not seen[building.id] and not ADFieldSiloConfig.hasStore(ADFieldSilos.config, building.id) and not nearStore(stores, x, z) then
seen[building.id] = true
-- marknummeret først: navnet skæres af i fanen, og mange siloer hedder ens ("Farma 500 …")
-- marknummeret først (medmindre navnet allerede starter med det, fx "F22 marksilo")
local siloName = tostring(building.name)
if not siloName:match("^F" .. tostring(info.id) .. "%f[%D]") then siloName = string.format("F%s %s", tostring(info.id), siloName) end
table.insert(silos, {id = building.id, name = siloName, markerId = firstMarker(building, "load"),
x = x, z = z, stock = siloStock(building.placeable)})
end
end
end
return silos, stores, byId
end
-- Marksiloernes placeables som sæt (til at udelukke dem som tømmested — de er til markens høst).
function ADFieldSilos.fieldSiloPlaceableSet(adEnv)
local set = {}
local silos, _, byId = ADFieldSilos.collectBuildings(adEnv)
for _, silo in ipairs(silos or {}) do
local building = byId[silo.id]
if building ~= nil and building.placeable ~= nil then set[building.placeable] = true end
end
return set
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) >= ADFieldSilos.CARGO_MIN_LITERS 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 = {}, {}
-- hvorfor tømmevogne ikke kan lånes (til fanens statuslinje)
local why = {notEnabled = 0, busy = 0, controlled = 0, noPark = 0}
local rigs = ADFieldWork.getRigs(adEnv)
for _, rig in ipairs(rigs) do
-- uden AD-parkeringspunkt bliver vognen holdende på lagerets markør og spærrer for de næste
local canPark = ADFieldWork.parkMarkerId == nil or ADFieldWork.parkMarkerId(adEnv, rig.vehicle) ~= nil
if rig.role == "unloader" then
if not rig.enabled then why.notEnabled = why.notEnabled + 1
elseif rig.controlled then why.controlled = why.controlled + 1
elseif rig.busy then why.busy = why.busy + 1
elseif not canPark then why.noPark = why.noPark + 1 end
end
if rig.role == "unloader" and rig.enabled and not rig.busy and not rig.controlled and canPark 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
-- puljevogne: en ledig solo-traktor henter vognen først (maskinpulje etape 3)
local poolById = {}
if ADPool ~= nil then
local okPool, implements, tractors = pcall(ADPool.collect, adEnv, function(v)
return ADFieldWork.isBusyVehicle(v, ADRunsController.getVehicleId(v))
end)
if okPool then
for _, pair in ipairs(ADPool.pairAll("unloader", implements, tractors, ADPool.blocked(ADFieldJobs.now or 0))) do
local entryId = "pool:" .. tostring(pair.implement.id)
table.insert(vehicles, {id = entryId, x = pair.implement.x, z = pair.implement.z,
capacity = pair.implement.capacity or 0, carries = pair.implement.carries or {}})
byId[entryId] = pair.tractor.vehicle
poolById[entryId] = pair
end
else
Logging.warning(ADFieldSilos.LOG_PREFIX .. "maskinpuljen kunne ikke læses: %s", tostring(implements))
end
end
local ok, needs = pcall(ADFieldSilos.harvestNeeds, adEnv, rigs)
if ok and #needs > 0 then
local before = #vehicles
vehicles = ADFieldSiloPlanner.withoutHarvestReserve(vehicles, needs)
why.harvest = before - #vehicles
elseif not ok then
Logging.warning(ADFieldSilos.LOG_PREFIX .. "høstreserve kunne ikke beregnes: %s", tostring(needs))
end
why.eligible = #vehicles
ADFieldSilos.lastVehicleReasons = why
return vehicles, byId, poolById
end
-- Høsten først: pr. høster der arbejder (CP kører) holdes dens planlagte antal tømmevogne tilbage, minus dem
-- markarbejdet allerede har sendt til marken. (in-game 28/9: marksilo-flowet havde lånt begge kartoffelvogne,
-- og ingen kom til roeoptagerne.)
function ADFieldSilos.harvestNeeds(adEnv, rigs)
local assignments = ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.assignments or {}
local sent = {}
for _, assignment in pairs(assignments) do
if assignment.role == "unloader" and assignment.fieldId ~= nil then sent[assignment.fieldId] = (sent[assignment.fieldId] or 0) + 1 end
end
local needs = {}
for _, rig in ipairs(rigs) do
if rig.role == "harvester" and ADCpBridge.isWorking(rig.vehicle) then
local assignment = assignments[rig.id]
local fieldId = assignment ~= nil and assignment.fieldId or nil
local field = fieldId ~= nil and ADFieldJobs.getField(fieldId) or nil
local info = fieldId ~= nil and ADFields.getInfo(fieldId) or nil
local count = field ~= nil and ADFieldModel.maxHelpers("harvesting", field.plan) or 1
count = count - (fieldId ~= nil and (sent[fieldId] or 0) or 0)
local fillTypes = {}
if info ~= nil and info.harvestFillType ~= nil then
fillTypes[info.harvestFillType] = true
else
for fruitIndex in pairs(rig.fruits or {}) do
local fruit = g_fruitTypeManager ~= nil and g_fruitTypeManager:getFruitTypeByIndex(fruitIndex) or nil
if fruit ~= nil and fruit.fillType ~= nil then fillTypes[fruit.fillType.index] = true end
end
end
for fillType in pairs(fillTypes) do
if count > 0 then table.insert(needs, {fillType = fillType, count = count}) end
end
end
end
return needs
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, isPool)
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
-- puljevogn: ingen AD-parkering — ved turens slut sætter returnHome vognen af og kører traktoren hjem
-- (med AD-parkering kørte den først hjem med vognen og så tilbage efter den)
if not isPool then ADFieldWork.enableParkAtJobFinished(vehicle) end
-- rotation fra: ellers kan modens egne pickup-hooks sende vognen til en anden silo i mappen
if ADRunsController ~= nil and ADRunsController.setSetting ~= nil then
ADRunsController.setSetting(adEnv, vehicle, "rotateTargets", adEnv.AutoDrive.RT_NONE)
end
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, storeId = order.storeId, 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
-- Puljevogn: traktoren henter vognen ved "Connect X" og kører derefter turen som en almindelig tømmevogn.
local function dispatchPool(adEnv, order, pair, silosById, storesById)
local tractor = pair.tractor.vehicle
local silo = silosById[order.siloId]
local owner = {siloId = order.siloId, storeId = order.storeId, siloName = silo ~= nil and silo.name or order.siloId,
fillType = order.fillType, liters = order.liters, startedMs = nowMs, lastProgressMs = nowMs, lastFill = 0, fetching = true}
loans[tractor] = owner
ADHitch.fetch(adEnv, tractor, pair.implement.connectId, function(_, ok, detail)
ADPool.release(owner)
if loans[tractor] ~= owner then return end -- lånet blev afbrudt imens (Stop alt / flow slået fra)
if not ok then
loans[tractor] = nil
local blamed = ADPool.failureBlames(detail) == "tractor" and pair.tractor.id or pair.implement.id
ADPool.quarantine(blamed, ADFieldJobs.now or 0)
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' kunne ikke hente puljevognen '%s' (%s) — prøver en anden om lidt",
tostring(ADRuns.vehicleLabel(tractor)), tostring(pair.implement.name), tostring(detail))
local park = ADFieldWork.parkMarkerId(adEnv, tractor)
if park ~= nil then ADFieldWork.startAd(adEnv, tractor, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
return
end
pcall(ADHitch.foldForTransport, tractor)
local okDispatch, err = pcall(dispatchOne, adEnv, order, tractor, silosById, storesById, true)
if not okDispatch then
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' kunne ikke starte turen med puljevognen: %s", tostring(ADRuns.vehicleLabel(tractor)), tostring(err))
ADFieldSilos.releaseVehicle(adEnv, tractor, "failed")
end
end)
-- reservationen først når hentningen er sat i gang (fejler fetch, er der intet at frigive)
ADPool.reserve(owner, {pair.implement.id, pair.tractor.id})
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' henter puljevognen '%s' til %d l %s i %s", tostring(ADRuns.vehicleLabel(tractor)),
tostring(pair.implement.name), math.floor(order.liters), fillTypeName(order.fillType), tostring(silo ~= nil and silo.name or order.siloId))
end
local function release(adEnv, vehicle, reason)
local loan = loans[vehicle]
loans[vehicle] = nil
if loan == nil then return end
if loan.fetching then
-- henter stadig puljevognen: afbryd koblingen og frigiv reservationen
if ADHitch ~= nil then ADHitch.cancel(vehicle) end
if ADPool ~= nil then ADPool.release(loan) end
end
if reason == "stuck" or reason == "failed" then
local k = ADFieldSiloPlanner.key(loan.siloId, loan.fillType)
paused[k] = nowMs + ADFieldSilos.PAUSE_MS
pcall(vehicle.stopAutoDrive, vehicle)
local why = reason == "stuck" and "kom ikke i gang på 2 min — stoppet" or "AutoDrive stoppede turen med det samme (rute/læssested?)"
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' ved %s (%s): %s; siloen springes over i 10 min",
tostring(ADRuns.vehicleLabel(vehicle)), tostring(loan.siloName), fillTypeName(loan.fillType), why)
elseif reason == "stopped" then
pcall(vehicle.stopAutoDrive, vehicle)
end
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' frigivet (%s)", tostring(ADRuns.vehicleLabel(vehicle)), tostring(reason))
-- puljevogn bag traktoren: sæt den af på sin plads og kør hjem (ikke når spilleren har overtaget)
if reason ~= "player" and ADHitch ~= nil then
local okHome, pooled = pcall(ADHitch.returnHome, adEnv, vehicle)
if not (okHome and pooled) and loan.fetching then
local park = ADFieldWork.parkMarkerId(adEnv, vehicle)
if park ~= nil then ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DRIVETO, park, nil) end
end
end
end
function ADFieldSilos.releaseVehicle(adEnv, vehicle, reason)
release(adEnv, vehicle, 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
-- Efter genindlæsning genoptager AutoDrive selv "Hent og aflever" fra marksiloerne, men lånene er glemt: sådanne
-- vogne overtages igen som lån (tæller i "højst x ude", og tom silo-vagten gælder).
function ADFieldSilos.adoptOrphans(adEnv, silos)
local byMarker = {}
for _, silo in ipairs(silos or {}) do
if silo.markerId ~= nil then byMarker[silo.markerId] = silo end
end
for _, vehicle in ipairs(ADRunsController.getFarmVehicles(adEnv)) do
local module = vehicle.ad ~= nil and vehicle.ad.stateModule or nil
if loans[vehicle] == nil and module ~= nil and module:isActive() and module:getMode() == adEnv.AutoDrive.MODE_PICKUPANDDELIVER then
local first = module:getFirstMarker()
local silo = first ~= nil and byMarker[first.id] or nil
if silo ~= nil then
local fillType = module.getFillType ~= nil and module:getFillType() or nil
local second = module.getSecondMarker ~= nil and module:getSecondMarker() or nil
local storeId = nil
for _, store in ipairs(ADFieldSilos.lastStores or {}) do
if second ~= nil and store.markerId == second.id then storeId = store.id end
end
loans[vehicle] = {siloId = silo.id, storeId = storeId, siloName = silo.name, fillType = fillType, liters = 0, startedMs = nowMs,
lastProgressMs = nowMs, lastFill = fillLevel(adEnv, vehicle), hasProgressed = true, adopted = true}
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s' overtaget igen (AutoDrive kører fra %s efter genindlæsning)",
tostring(ADRuns.vehicleLabel(vehicle)), tostring(silo.name))
end
end
end
end
-- Tom silo-vagt: en lånt vogn med last, der står stille ved en tom marksilo, kører resten videre — til en fabrik
-- med plads til hele resten, ellers til et gårdlager (AutoDrive venter ellers til vognen er fuld).
local warnedNoRestStore = {}
local function isNear(adEnv, vehicle, markerId)
local wayPoint = markerId ~= nil and adEnv.ADGraphManager:getWayPointById(markerId) or nil
if wayPoint == nil or vehicle.rootNode == nil then return false end
local x, _, z = getWorldTranslation(vehicle.rootNode)
return (x - wayPoint.x) ^ 2 + (z - wayPoint.z) ^ 2 <= ADFieldSilos.REST_SILO_DISTANCE ^ 2
end
local function findById(list, id)
for _, item in ipairs(list or {}) do
if item.id == id then return item end
end
return nil
end
local function checkRest(adEnv, vehicle, loan, moving, fill)
if loan.fetching then return end
local silo = findById(ADFieldSilos.lastSilos, loan.siloId)
if silo == nil then return end
if moving then loan.stillSince = nil return end
loan.stillSince = loan.stillSince or nowMs
local stillMs = nowMs - loan.stillSince
local fullStore = nil
if not loan.delivering and isNear(adEnv, vehicle, silo.markerId) then
-- tom marksilo med last på
local state = {atSilo = true, stillMs = stillMs, siloStock = (silo.stock or {})[loan.fillType] or 0, cargo = fill}
if not ADFieldSiloPlanner.restDue(state) then return end
else
-- fuldt lager (fabrik der kun tager sit forbrug) med last på
local current = findById(ADFieldSilos.lastStores, loan.storeId)
if current == nil or not isNear(adEnv, vehicle, current.markerId) then return end
local state = {atStore = true, stillMs = stillMs, cargo = fill, storeFree = (current.free or {})[loan.fillType] or 0}
if not ADFieldSiloPlanner.storeFullDue(state) then return end
fullStore = current
end
local store = ADFieldSiloPlanner.restStore(ADFieldSilos.lastStores, silo, loan.fillType, fill, fullStore ~= nil and fullStore.id or nil)
if store == nil then
if not warnedNoRestStore[vehicle] then
warnedNoRestStore[vehicle] = true
Logging.warning(ADFieldSilos.LOG_PREFIX .. "'%s' har %d l %s fra tom %s, men intet lager har plads",
tostring(ADRuns.vehicleLabel(vehicle)), math.floor(fill), fillTypeName(loan.fillType), tostring(silo.name))
end
return
end
warnedNoRestStore[vehicle] = nil
loan.delivering, loan.storeId, loan.stillSince = true, store.id, nil
loan.startedMs, loan.hasProgressed = nowMs, true
local isPool = ADHitch ~= nil and #ADHitch.poolParkIds(adEnv, vehicle) > 0
if not isPool then ADFieldWork.enableParkAtJobFinished(vehicle) end
ADFieldWork.startAd(adEnv, vehicle, adEnv.AutoDrive.MODE_DELIVERTO, store.markerId, nil, loan.fillType)
Logging.info(ADFieldSilos.LOG_PREFIX .. "'%s': %s — kører resten (%d l %s) til %s",
tostring(ADRuns.vehicleLabel(vehicle)), fullStore ~= nil and (tostring(fullStore.name) .. " er fuld") or (tostring(silo.name) .. " er tom"),
math.floor(fill), fillTypeName(loan.fillType), tostring(store.name))
end
local function checkLoans(adEnv)
local list = {}
for vehicle in pairs(loans) do table.insert(list, vehicle) end
for _, vehicle in ipairs(list) do
if vehicle.isDeleted then
loans[vehicle] = nil -- solgt/slettet mens den var lånt
else
local ok, err = pcall(function()
local loan = loans[vehicle]
-- henter puljevognen: AD holder stille under koblingen — ikke "færdig"; men spilleren kan overtage,
-- og en hentning der ikke bliver færdig (trafik, rute), opgives
if loan.fetching then
local controlled = vehicle.getIsControlled ~= nil and vehicle:getIsControlled() or false
if controlled then
release(adEnv, vehicle, "player")
elseif nowMs - (loan.startedMs or nowMs) > ADFieldSilos.FETCH_TIMEOUT_MS then
release(adEnv, vehicle, "stuck")
end
return
end
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,
}
checkRest(adEnv, vehicle, loan, vehicleState.moving, fill)
if loans[vehicle] ~= loan then return end
vehicleState.adActive = stateModule ~= nil and stateModule:isActive()
local reason = ADFieldSilos.releaseReason(loan, vehicleState, nowMs)
if vehicleState.moving or fill ~= (loan.lastFill or 0) then loan.hasProgressed = true end
loan.lastFill = fill
if reason ~= nil then release(adEnv, vehicle, reason) end
end)
if not ok then
loans[vehicle] = nil
Logging.warning(ADFieldSilos.LOG_PREFIX .. "lån opgivet (fejl): %s", tostring(err))
end
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, storeId = loan.storeId, fillType = loan.fillType, liters = loan.liters}) end
return list
end
-- Rute fra vognen til siloen og videre til lageret (samme opslag som Wait-punkterne); én advarsel pr. kombination.
local warnedRoute = {}
local function hasRoutes(adEnv, vehicle, order)
if ADUnloadWait == nil or ADUnloadWait.hasRouteTo == nil then return true end
local ok = ADUnloadWait.hasRouteTo(adEnv, vehicle, order.siloMarkerId) and ADUnloadWait.hasRouteTo(adEnv, vehicle, order.storeMarkerId)
local warnKey = tostring(order.vehicleId) .. ">" .. tostring(order.siloMarkerId)
if not ok and not warnedRoute[warnKey] then
warnedRoute[warnKey] = true
Logging.warning(ADFieldSilos.LOG_PREFIX .. "ingen AutoDrive-rute for '%s' til marksilo-markør %s eller gårdlager-markør %s",
tostring(ADRuns.vehicleLabel(vehicle)), tostring(order.siloMarkerId), tostring(order.storeMarkerId))
end
return ok
end
function ADFieldSilos.planNow(adEnv)
local cfg = ADFieldSilos.config
local silos, stores, byId = ADFieldSilos.collectBuildings(adEnv)
ADFieldSilos.lastSilos, ADFieldSilos.lastStores = silos, stores
if cfg.enabled then
local okAdopt, errAdopt = pcall(ADFieldSilos.adoptOrphans, adEnv, silos)
if not okAdopt then Logging.warning(ADFieldSilos.LOG_PREFIX .. "overtagelse efter genindlæsning fejlede: %s", tostring(errAdopt)) end
end
local vehicles, vehiclesById, poolById = ADFieldSilos.collectVehicles(adEnv)
local feedReserve, farmStock = nil, nil
if cfg.toProduction then
local ok, reserve, stock = pcall(ADFieldSilos.collectFeed, stores, byId, farmId(), cfg.reserveDays)
if ok then feedReserve, farmStock = reserve, stock
else Logging.warning(ADFieldSilos.LOG_PREFIX .. "foderreserve kunne ikke beregnes: %s", tostring(reserve)) end
end
local plan = ADFieldSiloPlanner.plan({silos = silos, stores = stores, loans = loanList(), vehicles = vehicles,
paused = currentPaused(), maxVehicles = cfg.maxVehicles, minLiters = cfg.minLiters,
feedReserve = feedReserve, farmStock = farmStock})
ADFieldSilos.lastFeed = {reserve = feedReserve, stock = farmStock}
ADFieldSilos.lastPlan = plan
local paused = ADFieldJobs ~= nil and ADFieldJobs.state ~= nil and ADFieldJobs.state.paused
if not cfg.enabled or paused 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 and not hasRoutes(adEnv, vehicle, order) then
vehicle = nil
end
local pair = poolById ~= nil and poolById[order.vehicleId] or nil
if vehicle ~= nil and pair ~= nil then
local ok, err = pcall(dispatchPool, adEnv, order, pair, silosById, storesById)
if not ok then
loans[vehicle] = nil
Logging.warning(ADFieldSilos.LOG_PREFIX .. "kunne ikke sende '%s' efter puljevogn: %s", tostring(ADRuns.vehicleLabel(vehicle)), tostring(err))
end
elseif 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:setInt(ADFieldSilos.ROOT .. "#toProduction", record.toProduction)
xmlFile:setInt(ADFieldSilos.ROOT .. "#reserveDays", record.reserveDays)
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"),
toProduction = xmlFile:getInt(ADFieldSilos.ROOT .. "#toProduction"),
reserveDays = xmlFile:getInt(ADFieldSilos.ROOT .. "#reserveDays"),
})
xmlFile:delete()
end