710 lines
36 KiB
Lua
710 lines
36 KiB
Lua
-- 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
|
||
|
||
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
|