379 lines
17 KiB
Lua
379 lines
17 KiB
Lua
-- 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.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
|
|
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
|
|
|
|
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
|
|
|
|
-- 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 = {}, runBuildingIds()
|
|
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
|
|
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) >= 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 = {}, {}
|
|
for _, rig in ipairs(ADFieldWork.getRigs(adEnv)) 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" 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
|
|
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)
|
|
-- 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
|
|
|
|
local function release(adEnv, vehicle, reason)
|
|
local loan = loans[vehicle]
|
|
loans[vehicle] = nil
|
|
if loan == nil then return 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))
|
|
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
|
|
if vehicle.isDeleted then
|
|
loans[vehicle] = nil -- solgt/slettet mens den var lånt
|
|
else
|
|
local ok, err = pcall(function()
|
|
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,
|
|
}
|
|
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 = 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 and not hasRoutes(adEnv, vehicle, order) then
|
|
vehicle = nil
|
|
end
|
|
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
|