-- 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