-- 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 -- 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.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 -- 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 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 …") table.insert(silos, {id = building.id, name = string.format("F%s %s", tostring(info.id), tostring(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 = {}, {} -- hvorfor tømmevogne ikke kan lånes (til fanens statuslinje) local why = {notEnabled = 0, busy = 0, controlled = 0, noPark = 0} 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" 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 why.eligible = #vehicles ADFieldSilos.lastVehicleReasons = why 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 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 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