feat(marksilo): låner ledige tømmevogne hvert minut, AD hent-og-aflever til gårdlager, frigiver ved færdig/spiller/2 min stilstand; opsætning gemmes
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user