merge: marksilo-flow (v1.36.0.0)
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
-- AD Field Silo Config
|
||||||
|
-- Marksilo-flowets opsætning som ren model (ny tabel ud, input muteres ikke) + flade records til XML-laget.
|
||||||
|
|
||||||
|
ADFieldSiloConfig = {}
|
||||||
|
|
||||||
|
ADFieldSiloConfig.MAX_VEHICLES = 10
|
||||||
|
ADFieldSiloConfig.DEFAULT_MAX_VEHICLES = 2
|
||||||
|
ADFieldSiloConfig.MIN_LITERS_CHOICES = {5000, 10000, 25000, 50000}
|
||||||
|
ADFieldSiloConfig.DEFAULT_MIN_LITERS = 10000
|
||||||
|
|
||||||
|
local function copy(cfg)
|
||||||
|
local ids = {}
|
||||||
|
for index, id in ipairs(cfg.storeIds or {}) do ids[index] = id end
|
||||||
|
return {enabled = cfg.enabled == true, storeIds = ids, maxVehicles = cfg.maxVehicles, minLiters = cfg.minLiters}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function clampVehicles(value)
|
||||||
|
value = tonumber(value) or ADFieldSiloConfig.DEFAULT_MAX_VEHICLES
|
||||||
|
return math.max(1, math.min(ADFieldSiloConfig.MAX_VEHICLES, math.floor(value)))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function snapLiters(value)
|
||||||
|
value = tonumber(value)
|
||||||
|
if value == nil then return ADFieldSiloConfig.DEFAULT_MIN_LITERS end
|
||||||
|
local best, bestDiff = nil, nil
|
||||||
|
for _, choice in ipairs(ADFieldSiloConfig.MIN_LITERS_CHOICES) do
|
||||||
|
local diff = math.abs(choice - value)
|
||||||
|
if bestDiff == nil or diff < bestDiff then best, bestDiff = choice, diff end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldSiloConfig.new()
|
||||||
|
return {enabled = false, storeIds = {}, maxVehicles = ADFieldSiloConfig.DEFAULT_MAX_VEHICLES, minLiters = ADFieldSiloConfig.DEFAULT_MIN_LITERS}
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldSiloConfig.with(cfg, changes)
|
||||||
|
local new = copy(cfg)
|
||||||
|
if changes.enabled ~= nil then new.enabled = changes.enabled == true end
|
||||||
|
if changes.maxVehicles ~= nil then new.maxVehicles = clampVehicles(changes.maxVehicles) end
|
||||||
|
if changes.minLiters ~= nil then new.minLiters = snapLiters(changes.minLiters) end
|
||||||
|
return new
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldSiloConfig.hasStore(cfg, id)
|
||||||
|
for _, storeId in ipairs(cfg.storeIds or {}) do
|
||||||
|
if storeId == id then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldSiloConfig.toggleStore(cfg, id)
|
||||||
|
local new = copy(cfg)
|
||||||
|
local kept, found = {}, false
|
||||||
|
for _, storeId in ipairs(new.storeIds) do
|
||||||
|
if storeId == id then found = true else table.insert(kept, storeId) end
|
||||||
|
end
|
||||||
|
if not found then table.insert(kept, id) end
|
||||||
|
new.storeIds = kept
|
||||||
|
return new
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldSiloConfig.toRecord(cfg)
|
||||||
|
return {enabled = cfg.enabled and 1 or 0, stores = table.concat(cfg.storeIds or {}, " "),
|
||||||
|
maxVehicles = cfg.maxVehicles, minLiters = cfg.minLiters}
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldSiloConfig.fromRecord(record)
|
||||||
|
local cfg = ADFieldSiloConfig.new()
|
||||||
|
if record == nil then return cfg end
|
||||||
|
for token in string.gmatch(tostring(record.stores or ""), "%S+") do table.insert(cfg.storeIds, token) end
|
||||||
|
cfg.enabled = tonumber(record.enabled) == 1
|
||||||
|
cfg.maxVehicles = clampVehicles(record.maxVehicles)
|
||||||
|
cfg.minLiters = snapLiters(record.minLiters)
|
||||||
|
return cfg
|
||||||
|
end
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
-- AD Field Silo Planner
|
||||||
|
-- Ren planlægning for marksilo-flowet: hvilke (silo, vare) skal hjem, hvor mange lånte vogne sendes, og til
|
||||||
|
-- hvilket gårdlager. Ingen spil-kald; input muteres ikke.
|
||||||
|
|
||||||
|
ADFieldSiloPlanner = {}
|
||||||
|
|
||||||
|
function ADFieldSiloPlanner.key(siloId, fillType)
|
||||||
|
return tostring(siloId) .. ":" .. tostring(fillType)
|
||||||
|
end
|
||||||
|
local key = ADFieldSiloPlanner.key
|
||||||
|
|
||||||
|
local function dist2(a, b)
|
||||||
|
return ((a.x or 0) - (b.x or 0)) ^ 2 + ((a.z or 0) - (b.z or 0)) ^ 2
|
||||||
|
end
|
||||||
|
|
||||||
|
local function freeIn(store, fillType, reserved)
|
||||||
|
return ((store.free or {})[fillType] or 0) - (reserved[store.id .. ":" .. tostring(fillType)] or 0)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function anyStoreTakes(stores, fillType, minLiters, reserved)
|
||||||
|
for _, store in ipairs(stores or {}) do
|
||||||
|
if store.markerId ~= nil and freeIn(store, fillType, reserved) >= minLiters then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Nærmeste lager med plads til hele turen; ellers det med mest plads (over tærsklen).
|
||||||
|
local function chooseStore(stores, silo, fillType, liters, minLiters, reserved)
|
||||||
|
local best, bestD, most, mostFree = nil, nil, nil, nil
|
||||||
|
for _, store in ipairs(stores or {}) do
|
||||||
|
local free = freeIn(store, fillType, reserved)
|
||||||
|
if store.markerId ~= nil and free >= minLiters then
|
||||||
|
local d = dist2(store, silo)
|
||||||
|
if free >= liters and (bestD == nil or d < bestD) then best, bestD = store, d end
|
||||||
|
if mostFree == nil or free > mostFree then most, mostFree = store, free end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return best or most
|
||||||
|
end
|
||||||
|
|
||||||
|
local function canCarry(vehicle, fillType)
|
||||||
|
if (vehicle.capacity or 0) <= 0 then return false end
|
||||||
|
if vehicle.cargo ~= nil and vehicle.cargo ~= fillType then return false end
|
||||||
|
return (vehicle.carries or {})[fillType] == true
|
||||||
|
end
|
||||||
|
|
||||||
|
local function nearestVehicle(vehicles, used, silo, fillType)
|
||||||
|
local best, bestD = nil, nil
|
||||||
|
for _, vehicle in ipairs(vehicles or {}) do
|
||||||
|
if not used[vehicle.id] and canCarry(vehicle, fillType) then
|
||||||
|
local d = dist2(vehicle, silo)
|
||||||
|
if bestD == nil or d < bestD then best, bestD = vehicle, d end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return best
|
||||||
|
end
|
||||||
|
|
||||||
|
function ADFieldSiloPlanner.plan(input)
|
||||||
|
local minLiters = input.minLiters or 10000
|
||||||
|
local reserved, onWay = {}, {}
|
||||||
|
for _, loan in ipairs(input.loans or {}) do
|
||||||
|
local k = key(loan.siloId, loan.fillType)
|
||||||
|
onWay[k] = (onWay[k] or 0) + (loan.liters or 0)
|
||||||
|
-- læs på vej til et lager optager pladsen, til de er aflæsset (også i senere runder)
|
||||||
|
if loan.storeId ~= nil then
|
||||||
|
local reserveKey = loan.storeId .. ":" .. tostring(loan.fillType)
|
||||||
|
reserved[reserveKey] = (reserved[reserveKey] or 0) + (loan.liters or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local status, tasks = {}, {}
|
||||||
|
for _, silo in ipairs(input.silos or {}) do
|
||||||
|
for fillType, liters in pairs(silo.stock or {}) do
|
||||||
|
if (liters or 0) >= minLiters then
|
||||||
|
local k = key(silo.id, fillType)
|
||||||
|
local entry = {siloId = silo.id, siloName = silo.name, fillType = fillType, liters = liters, onWay = onWay[k] or 0}
|
||||||
|
if silo.markerId == nil then
|
||||||
|
entry.state = "noMarker"
|
||||||
|
elseif not anyStoreTakes(input.stores, fillType, minLiters, reserved) then
|
||||||
|
entry.state = "noFarmStore"
|
||||||
|
elseif (input.paused or {})[k] then
|
||||||
|
entry.state = "paused"
|
||||||
|
else
|
||||||
|
entry.state = "ok"
|
||||||
|
local rest = liters - entry.onWay
|
||||||
|
if rest >= minLiters then table.insert(tasks, {silo = silo, fillType = fillType, rest = rest}) end
|
||||||
|
end
|
||||||
|
table.insert(status, entry)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(tasks, function(a, b)
|
||||||
|
if a.rest ~= b.rest then return a.rest > b.rest end
|
||||||
|
if tostring(a.silo.id) ~= tostring(b.silo.id) then return tostring(a.silo.id) < tostring(b.silo.id) end
|
||||||
|
return a.fillType < b.fillType
|
||||||
|
end)
|
||||||
|
table.sort(status, function(a, b)
|
||||||
|
if tostring(a.siloId) ~= tostring(b.siloId) then return tostring(a.siloId) < tostring(b.siloId) end
|
||||||
|
return a.fillType < b.fillType
|
||||||
|
end)
|
||||||
|
local freeSlots = math.max(0, (input.maxVehicles or 2) - #(input.loans or {}))
|
||||||
|
local used, dispatch = {}, {}
|
||||||
|
for _, task in ipairs(tasks) do
|
||||||
|
while freeSlots > 0 and task.rest >= minLiters do
|
||||||
|
local vehicle = nearestVehicle(input.vehicles, used, task.silo, task.fillType)
|
||||||
|
if vehicle == nil then break end
|
||||||
|
local liters = math.min(vehicle.capacity, task.rest)
|
||||||
|
local store = chooseStore(input.stores, task.silo, task.fillType, liters, minLiters, reserved)
|
||||||
|
if store == nil then break end
|
||||||
|
used[vehicle.id] = true
|
||||||
|
local reserveKey = store.id .. ":" .. tostring(task.fillType)
|
||||||
|
reserved[reserveKey] = (reserved[reserveKey] or 0) + liters
|
||||||
|
table.insert(dispatch, {vehicleId = vehicle.id, siloId = task.silo.id, siloMarkerId = task.silo.markerId,
|
||||||
|
fillType = task.fillType, storeId = store.id, storeMarkerId = store.markerId, liters = liters})
|
||||||
|
task.rest = task.rest - liters
|
||||||
|
freeSlots = freeSlots - 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return {dispatch = dispatch, status = status, freeSlots = freeSlots}
|
||||||
|
end
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
-- 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
|
||||||
|
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
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
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) >= 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
|
||||||
@@ -88,6 +88,9 @@ local function startAd(adEnv, vehicle, mode, firstId, secondId, fillType)
|
|||||||
wake(vehicle, true)
|
wake(vehicle, true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ADFieldWork.startAd = startAd
|
||||||
|
ADFieldWork.enableParkAtJobFinished = enableParkAtJobFinished
|
||||||
|
|
||||||
-- AD-parkering: sidder på redskabet og kopieres af AD til traktorens stateModule (markør-INDEKS).
|
-- AD-parkering: sidder på redskabet og kopieres af AD til traktorens stateModule (markør-INDEKS).
|
||||||
local function parkMarkerId(adEnv, vehicle)
|
local function parkMarkerId(adEnv, vehicle)
|
||||||
local module = stateModule(vehicle)
|
local module = stateModule(vehicle)
|
||||||
@@ -96,6 +99,7 @@ local function parkMarkerId(adEnv, vehicle)
|
|||||||
local marker = adEnv.ADGraphManager:getMapMarkerById(index)
|
local marker = adEnv.ADGraphManager:getMapMarkerById(index)
|
||||||
return marker ~= nil and marker.id or nil
|
return marker ~= nil and marker.id or nil
|
||||||
end
|
end
|
||||||
|
ADFieldWork.parkMarkerId = parkMarkerId
|
||||||
|
|
||||||
local function restLoad(adEnv, vehicle)
|
local function restLoad(adEnv, vehicle)
|
||||||
local ok, level = pcall(function()
|
local ok, level = pcall(function()
|
||||||
@@ -130,7 +134,8 @@ function ADFieldWork.getRigs(adEnv)
|
|||||||
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id),
|
enabled = ADFieldFleet.isEnabled(state.fleetMode, state.fleet, id),
|
||||||
-- optaget: vores egen opgave, eller AutoDrive/Courseplay kører den allerede (fx spillerens
|
-- optaget: vores egen opgave, eller AutoDrive/Courseplay kører den allerede (fx spillerens
|
||||||
-- egen parkeringstur) — den må ikke kapres midt i en anden kørsel
|
-- egen parkeringstur) — den må ikke kapres midt i en anden kørsel
|
||||||
busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle),
|
busy = state.assignments[id] ~= nil or isAdActive(vehicle) or ADCpBridge.isWorking(vehicle)
|
||||||
|
or (ADFieldSilos ~= nil and ADFieldSilos.isLoaned(vehicle)),
|
||||||
controlled = isControlledByPlayer(vehicle),
|
controlled = isControlledByPlayer(vehicle),
|
||||||
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
width = ADCpBridge.getWorkWidth(vehicle), x = x, z = z,
|
||||||
-- tom spreder kan flere ting (gødning og kalk): alle dens roller
|
-- tom spreder kan flere ting (gødning og kalk): alle dens roller
|
||||||
|
|||||||
@@ -911,6 +911,7 @@ local function setSetting(adEnv, vehicle, settingName, value)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
ADRunsController.setSetting = setSetting
|
||||||
|
|
||||||
-- Nærmeste markør med en læssestation: kun en gyldig start — moden vælger den reelle kilde pr. tur.
|
-- Nærmeste markør med en læssestation: kun en gyldig start — moden vælger den reelle kilde pr. tur.
|
||||||
local function findNearestLoadMarker(adEnv, vehicle, farmId)
|
local function findNearestLoadMarker(adEnv, vehicle, farmId)
|
||||||
@@ -1318,6 +1319,7 @@ function ADRunsController.save()
|
|||||||
if ADFieldJobs ~= nil and ADFieldJobs.loaded then
|
if ADFieldJobs ~= nil and ADFieldJobs.loaded then
|
||||||
pcall(ADFieldJobs.save)
|
pcall(ADFieldJobs.save)
|
||||||
end
|
end
|
||||||
|
if ADFieldSilos ~= nil then pcall(ADFieldSilos.save) end
|
||||||
return saved
|
return saved
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -1330,6 +1332,7 @@ function ADRunsController.load(adEnv)
|
|||||||
end
|
end
|
||||||
ADRunsController.store = store
|
ADRunsController.store = store
|
||||||
pcall(ADRunsController.migrateBuildings, adEnv)
|
pcall(ADRunsController.migrateBuildings, adEnv)
|
||||||
|
if ADFieldSilos ~= nil then pcall(ADFieldSilos.load) end
|
||||||
local vehiclesById = getFarmVehiclesById(adEnv)
|
local vehiclesById = getFarmVehiclesById(adEnv)
|
||||||
for _, run in ipairs(store.runs) do
|
for _, run in ipairs(store.runs) do
|
||||||
for _, vehicleId in ipairs(run.vehicleIds) do
|
for _, vehicleId in ipairs(run.vehicleIds) do
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ if ADSmartPickup.dir ~= nil and source ~= nil then
|
|||||||
source(ADSmartPickup.dir .. "gui/SmartPickupFlowTab.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupFlowTab.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupFieldTab.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupMonthTab.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupMonthTab.lua")
|
||||||
|
source(ADSmartPickup.dir .. "gui/SmartPickupFieldSiloTab.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupGuide.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
|
||||||
source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")
|
source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")
|
||||||
@@ -1262,6 +1263,12 @@ function ADSmartPickup:update(dt)
|
|||||||
Logging.warning(ADSmartPickup.LOG_PREFIX .. "markarbejde fejlede: %s", tostring(errFields))
|
Logging.warning(ADSmartPickup.LOG_PREFIX .. "markarbejde fejlede: %s", tostring(errFields))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if ADFieldSilos ~= nil then
|
||||||
|
local okSilos, errSilos = pcall(ADFieldSilos.tick, getAutoDriveEnv(), dt)
|
||||||
|
if not okSilos then
|
||||||
|
Logging.warning(ADSmartPickup.LOG_PREFIX .. "marksilo-flow fejlede: %s", tostring(errSilos))
|
||||||
|
end
|
||||||
|
end
|
||||||
if ADStuckDiag ~= nil then pcall(ADStuckDiag.tick, dt) end
|
if ADStuckDiag ~= nil then pcall(ADStuckDiag.tick, dt) end
|
||||||
if ADRunsController ~= nil and ADRunsController.adoptLoadedThrottled ~= nil then
|
if ADRunsController ~= nil and ADRunsController.adoptLoadedThrottled ~= nil then
|
||||||
pcall(ADRunsController.adoptLoadedThrottled, getAutoDriveEnv(), dt)
|
pcall(ADRunsController.adoptLoadedThrottled, getAutoDriveEnv(), dt)
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
-- Smart Pickup — fanen Marksiloer
|
||||||
|
-- Opsætning (til/fra, maks. vogne, mindste mængde, gårdlagre) og status for marksilo-flowet.
|
||||||
|
-- Klik går til onClickFieldSiloRow; reglerne ligger i ADFieldSilos/ADFieldSiloPlanner.
|
||||||
|
|
||||||
|
SmartPickupFrame.FIELDSILO_ROW_KINDS = {fsEnabled = true, fsMaxVehicles = true, fsMinLiters = true, fsStore = true}
|
||||||
|
|
||||||
|
local function numberTexts(from, to)
|
||||||
|
local texts = {}
|
||||||
|
for n = from, to do table.insert(texts, tostring(n)) end
|
||||||
|
return texts
|
||||||
|
end
|
||||||
|
|
||||||
|
local function literTexts()
|
||||||
|
local texts = {}
|
||||||
|
for _, liters in ipairs(ADFieldSiloConfig.MIN_LITERS_CHOICES) do table.insert(texts, string.format("%d l", liters)) end
|
||||||
|
return texts
|
||||||
|
end
|
||||||
|
|
||||||
|
local function indexOf(list, value)
|
||||||
|
for index, entry in ipairs(list) do if entry == value then return index end end
|
||||||
|
return 1
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Valg med flere værdier: productPrefab (MultiTextOption). ingredientPrefab er en BinaryOption med kun to sider.
|
||||||
|
function SmartPickupFrame:addFieldSiloChoiceRow(kind, title, texts, state)
|
||||||
|
local element = self:addRow(self.productPrefab, {kind = kind})
|
||||||
|
local option = element:getDescendantByName("option")
|
||||||
|
option:setTexts(texts)
|
||||||
|
option:setState(state, false)
|
||||||
|
element:getDescendantByName("title"):setText(title)
|
||||||
|
local tooltip = option:getDescendantByName("ignore")
|
||||||
|
if tooltip ~= nil then tooltip:setText("") end
|
||||||
|
return option
|
||||||
|
end
|
||||||
|
|
||||||
|
function SmartPickupFrame:fillFieldSiloRows(adEnv)
|
||||||
|
local i18n, cfg = self.i18n, ADFieldSilos.config
|
||||||
|
local onOff = {i18n:getText("ui_no"), i18n:getText("ui_yes")}
|
||||||
|
self:addToggleRow("fsEnabled", i18n:getText("spu_fs_enabled"), onOff, cfg.enabled)
|
||||||
|
self:addFieldSiloChoiceRow("fsMaxVehicles", i18n:getText("spu_fs_maxVehicles"), numberTexts(1, ADFieldSiloConfig.MAX_VEHICLES), cfg.maxVehicles)
|
||||||
|
self:addFieldSiloChoiceRow("fsMinLiters", i18n:getText("spu_fs_minLiters"), literTexts(), indexOf(ADFieldSiloConfig.MIN_LITERS_CHOICES, cfg.minLiters))
|
||||||
|
|
||||||
|
local plan = ADFieldSilos.lastPlan
|
||||||
|
local waiting = 0
|
||||||
|
for _, entry in ipairs(plan ~= nil and plan.status or {}) do if entry.state == "ok" then waiting = waiting + 1 end end
|
||||||
|
self:addTextRow(string.format(i18n:getText("spu_fs_status"), ADFieldSilos.loanCount(), cfg.maxVehicles, waiting))
|
||||||
|
if #cfg.storeIds == 0 then self:addTextRow(i18n:getText("spu_fs_noStores")) end
|
||||||
|
|
||||||
|
self:addTextRow(i18n:getText("spu_fs_silosTitle"))
|
||||||
|
if plan == nil or #plan.status == 0 then
|
||||||
|
self:addTextRow(" " .. i18n:getText("spu_fs_nothing"))
|
||||||
|
else
|
||||||
|
self:addCellRow(self.fieldPrefab, {i18n:getText("spu_fs_col_silo"), i18n:getText("spu_col_crop"), i18n:getText("spu_fs_col_liters"),
|
||||||
|
i18n:getText("spu_fs_col_onWay"), i18n:getText("spu_fs_col_state"), ""})
|
||||||
|
for _, entry in ipairs(plan.status) do
|
||||||
|
local desc = g_fillTypeManager:getFillTypeByIndex(entry.fillType)
|
||||||
|
local title = desc ~= nil and (desc.title or desc.name) or tostring(entry.fillType)
|
||||||
|
local state = i18n:getText("spu_fs_state_" .. entry.state)
|
||||||
|
if entry.state == "noFarmStore" then state = string.format(state, title) end
|
||||||
|
self:addCellRow(self.fieldPrefab, {tostring(entry.siloName), title, string.format("%d", math.floor(entry.liters)),
|
||||||
|
string.format("%d", math.floor(entry.onWay)), state, ""})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
self:addTextRow(i18n:getText("spu_fs_storesTitle"))
|
||||||
|
local farmId = g_currentMission:getFarmId()
|
||||||
|
for _, building in ipairs(ADBuildings.list(adEnv, farmId)) do
|
||||||
|
if building.kind == "silo" then
|
||||||
|
local option = self:addToggleRow("fsStore", tostring(building.name), onOff, ADFieldSiloConfig.hasStore(cfg, building.id))
|
||||||
|
self.rowData[option] = {kind = "fsStore", buildingId = building.id}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function SmartPickupFrame:onClickFieldSiloRow(state, data)
|
||||||
|
self:safe(function(frame)
|
||||||
|
local cfg = ADFieldSilos.config
|
||||||
|
if data.kind == "fsEnabled" then
|
||||||
|
cfg = ADFieldSiloConfig.with(cfg, {enabled = state == SmartPickupFrame.STATE_ON})
|
||||||
|
elseif data.kind == "fsMaxVehicles" then
|
||||||
|
cfg = ADFieldSiloConfig.with(cfg, {maxVehicles = state})
|
||||||
|
elseif data.kind == "fsMinLiters" then
|
||||||
|
cfg = ADFieldSiloConfig.with(cfg, {minLiters = ADFieldSiloConfig.MIN_LITERS_CHOICES[state]})
|
||||||
|
elseif data.kind == "fsStore" then
|
||||||
|
cfg = ADFieldSiloConfig.toggleStore(cfg, data.buildingId)
|
||||||
|
end
|
||||||
|
ADFieldSilos.setConfig(cfg)
|
||||||
|
pcall(ADFieldSilos.save)
|
||||||
|
local adEnv = frame.getAdEnv()
|
||||||
|
if adEnv ~= nil then pcall(ADFieldSilos.planNow, adEnv) end
|
||||||
|
frame:requestRebuild()
|
||||||
|
end)
|
||||||
|
end
|
||||||
@@ -11,12 +11,14 @@ SmartPickupFrame.STATUS_OVERRIDE_TICKS = 6
|
|||||||
SmartPickupFrame.MAX_STATUS_REASONS = 3
|
SmartPickupFrame.MAX_STATUS_REASONS = 3
|
||||||
SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: "
|
SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: "
|
||||||
SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2
|
SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2
|
||||||
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_FIELDS, SmartPickupFrame.TAB_MONTH = 1, 2, 3, 4
|
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_FLOWS, SmartPickupFrame.TAB_FIELDS, SmartPickupFrame.TAB_MONTH, SmartPickupFrame.TAB_FIELDSILOS = 1, 2, 3, 4, 5
|
||||||
-- Måned-fanen genopbygges hvert N. sekund (ingen klikbare rækker, så fokus går ikke tabt)
|
-- Måned-fanen genopbygges hvert N. sekund (ingen klikbare rækker, så fokus går ikke tabt)
|
||||||
SmartPickupFrame.MONTH_REBUILD_TICKS = 5
|
SmartPickupFrame.MONTH_REBUILD_TICKS = 5
|
||||||
|
-- Marksilo-fanen har klikbare valg: genopbygges sjældnere, så fokus ikke rykker midt i et valg
|
||||||
|
SmartPickupFrame.FIELDSILO_REBUILD_TICKS = 30
|
||||||
-- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory)
|
-- Lagerfaner: én pr. bygningstype (indeks -> kind i ADSources.getFarmInventory)
|
||||||
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_month", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"}
|
SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_flows", "spu_tab_fields", "spu_tab_month", "spu_tab_fieldsilos", "spu_tab_factories", "spu_tab_silos", "spu_tab_barns", "spu_tab_sheds"}
|
||||||
SmartPickupFrame.TAB_KINDS = {[5] = "factory", [6] = "silo", [7] = "husbandry", [8] = "objectStorage"}
|
SmartPickupFrame.TAB_KINDS = {[6] = "factory", [7] = "silo", [8] = "husbandry", [9] = "objectStorage"}
|
||||||
|
|
||||||
function SmartPickupFrame.getTabKind(tab)
|
function SmartPickupFrame.getTabKind(tab)
|
||||||
return SmartPickupFrame.TAB_KINDS[tab]
|
return SmartPickupFrame.TAB_KINDS[tab]
|
||||||
@@ -412,6 +414,17 @@ function SmartPickupFrame:rebuild()
|
|||||||
self:restoreFocusIfLost()
|
self:restoreFocusIfLost()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
||||||
|
if adEnv ~= nil then
|
||||||
|
self:fillFieldSiloRows(adEnv)
|
||||||
|
end
|
||||||
|
self.monthRebuildTicks = SmartPickupFrame.FIELDSILO_REBUILD_TICKS
|
||||||
|
self:colorRows()
|
||||||
|
self.boxLayout:invalidateLayout()
|
||||||
|
self:refreshButtons()
|
||||||
|
self:restoreFocusIfLost()
|
||||||
|
return
|
||||||
|
end
|
||||||
if self.currentTab == SmartPickupFrame.TAB_MONTH then
|
if self.currentTab == SmartPickupFrame.TAB_MONTH then
|
||||||
if adEnv ~= nil then
|
if adEnv ~= nil then
|
||||||
self:fillMonthRows(adEnv)
|
self:fillMonthRows(adEnv)
|
||||||
@@ -554,7 +567,7 @@ function SmartPickupFrame:refreshButtons()
|
|||||||
wanted = self.overviewButtons
|
wanted = self.overviewButtons
|
||||||
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
elseif self.currentTab == SmartPickupFrame.TAB_FIELDS then
|
||||||
wanted = self:getFieldButtons()
|
wanted = self:getFieldButtons()
|
||||||
elseif self.currentTab == SmartPickupFrame.TAB_MONTH then
|
elseif self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
||||||
wanted = self.monthButtons
|
wanted = self.monthButtons
|
||||||
end
|
end
|
||||||
if self.menuButtonInfo ~= wanted then
|
if self.menuButtonInfo ~= wanted then
|
||||||
@@ -576,7 +589,7 @@ function SmartPickupFrame:refreshLive()
|
|||||||
self:setStatusText(self.i18n:getText("spu_status_noAd"))
|
self:setStatusText(self.i18n:getText("spu_status_noAd"))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if self.currentTab == SmartPickupFrame.TAB_MONTH then
|
if self.currentTab == SmartPickupFrame.TAB_MONTH or self.currentTab == SmartPickupFrame.TAB_FIELDSILOS then
|
||||||
self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1
|
self.monthRebuildTicks = (self.monthRebuildTicks or 0) - 1
|
||||||
if self.monthRebuildTicks <= 0 then self:requestRebuild() end
|
if self.monthRebuildTicks <= 0 then self:requestRebuild() end
|
||||||
self:refreshButtons()
|
self:refreshButtons()
|
||||||
@@ -659,6 +672,8 @@ function SmartPickupFrame:onClickIngredient(state, element)
|
|||||||
local data = self.rowData[element]
|
local data = self.rowData[element]
|
||||||
if data ~= nil and data.kind == "guideRow" then
|
if data ~= nil and data.kind == "guideRow" then
|
||||||
return self:onClickGuideRow(state, data)
|
return self:onClickGuideRow(state, data)
|
||||||
|
elseif data ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS[data.kind] then
|
||||||
|
return self:onClickFieldSiloRow(state, data)
|
||||||
elseif data ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[data.kind] then
|
elseif data ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[data.kind] then
|
||||||
return self:onClickFieldRow(state, data)
|
return self:onClickFieldRow(state, data)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ function SmartPickupFrame:onClickProduct(state, element)
|
|||||||
if clicked ~= nil and clicked.kind == "storageBuilding" then
|
if clicked ~= nil and clicked.kind == "storageBuilding" then
|
||||||
return self:safe(function(frame) frame:onClickStorageBuilding(state) end)
|
return self:safe(function(frame) frame:onClickStorageBuilding(state) end)
|
||||||
end
|
end
|
||||||
|
if clicked ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS ~= nil and SmartPickupFrame.FIELDSILO_ROW_KINDS[clicked.kind] then
|
||||||
|
return self:onClickFieldSiloRow(state, clicked)
|
||||||
|
end
|
||||||
if clicked ~= nil and SmartPickupFrame.FIELD_ROW_KINDS ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[clicked.kind] then
|
if clicked ~= nil and SmartPickupFrame.FIELD_ROW_KINDS ~= nil and SmartPickupFrame.FIELD_ROW_KINDS[clicked.kind] then
|
||||||
return self:onClickFieldRow(state, clicked)
|
return self:onClickFieldRow(state, clicked)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -463,5 +463,22 @@
|
|||||||
<text name="spu_guide_flow_sumReturn" text="hjem fra Wait under %d %%"/>
|
<text name="spu_guide_flow_sumReturn" text="hjem fra Wait under %d %%"/>
|
||||||
<text name="spu_guide_flow_sumNoTractors" text="ingen"/>
|
<text name="spu_guide_flow_sumNoTractors" text="ingen"/>
|
||||||
<text name="spu_guide_flow_sumTractors" text="%d traktorer"/>
|
<text name="spu_guide_flow_sumTractors" text="%d traktorer"/>
|
||||||
|
<text name="spu_tab_fieldsilos" text="Marksiloer"/>
|
||||||
|
<text name="spu_fs_enabled" text="Hent marksiloer hjem automatisk"/>
|
||||||
|
<text name="spu_fs_maxVehicles" text="Maks. vogne ude"/>
|
||||||
|
<text name="spu_fs_minLiters" text="Mindste mængde pr. vare"/>
|
||||||
|
<text name="spu_fs_status" text="%d/%d vogne ude · %d siloer venter"/>
|
||||||
|
<text name="spu_fs_noStores" text="Vælg mindst ét gårdlager nedenfor — ellers køres intet hjem"/>
|
||||||
|
<text name="spu_fs_silosTitle" text="Marksiloer med varer"/>
|
||||||
|
<text name="spu_fs_nothing" text="Ingen marksiloer har varer over mindste mængde"/>
|
||||||
|
<text name="spu_fs_col_silo" text="Silo"/>
|
||||||
|
<text name="spu_fs_col_liters" text="Liter"/>
|
||||||
|
<text name="spu_fs_col_onWay" text="På vej"/>
|
||||||
|
<text name="spu_fs_col_state" text="Status"/>
|
||||||
|
<text name="spu_fs_state_ok" text="ok"/>
|
||||||
|
<text name="spu_fs_state_noMarker" text="mangler markør"/>
|
||||||
|
<text name="spu_fs_state_noFarmStore" text="ingen gård tager %s"/>
|
||||||
|
<text name="spu_fs_state_paused" text="pause (kom ikke i gang)"/>
|
||||||
|
<text name="spu_fs_storesTitle" text="Gårdlagre (hvor varerne køres hjem til)"/>
|
||||||
</texts>
|
</texts>
|
||||||
</l10n>
|
</l10n>
|
||||||
|
|||||||
@@ -463,5 +463,22 @@
|
|||||||
<text name="spu_guide_flow_sumReturn" text="leave Wait below %d %%"/>
|
<text name="spu_guide_flow_sumReturn" text="leave Wait below %d %%"/>
|
||||||
<text name="spu_guide_flow_sumNoTractors" text="none"/>
|
<text name="spu_guide_flow_sumNoTractors" text="none"/>
|
||||||
<text name="spu_guide_flow_sumTractors" text="%d tractors"/>
|
<text name="spu_guide_flow_sumTractors" text="%d tractors"/>
|
||||||
|
<text name="spu_tab_fieldsilos" text="Field silos"/>
|
||||||
|
<text name="spu_fs_enabled" text="Bring field silos home automatically"/>
|
||||||
|
<text name="spu_fs_maxVehicles" text="Max. vehicles out"/>
|
||||||
|
<text name="spu_fs_minLiters" text="Minimum amount per product"/>
|
||||||
|
<text name="spu_fs_status" text="%d/%d vehicles out · %d silos waiting"/>
|
||||||
|
<text name="spu_fs_noStores" text="Select at least one farm storage below — otherwise nothing is brought home"/>
|
||||||
|
<text name="spu_fs_silosTitle" text="Field silos with products"/>
|
||||||
|
<text name="spu_fs_nothing" text="No field silo has products above the minimum amount"/>
|
||||||
|
<text name="spu_fs_col_silo" text="Silo"/>
|
||||||
|
<text name="spu_fs_col_liters" text="Liters"/>
|
||||||
|
<text name="spu_fs_col_onWay" text="On the way"/>
|
||||||
|
<text name="spu_fs_col_state" text="Status"/>
|
||||||
|
<text name="spu_fs_state_ok" text="ok"/>
|
||||||
|
<text name="spu_fs_state_noMarker" text="missing marker"/>
|
||||||
|
<text name="spu_fs_state_noFarmStore" text="no farm storage takes %s"/>
|
||||||
|
<text name="spu_fs_state_paused" text="paused (did not get going)"/>
|
||||||
|
<text name="spu_fs_storesTitle" text="Farm storages (where products are brought home to)"/>
|
||||||
</texts>
|
</texts>
|
||||||
</l10n>
|
</l10n>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
<modDesc descVersion="107">
|
<modDesc descVersion="107">
|
||||||
<author>masterdraco</author>
|
<author>masterdraco</author>
|
||||||
<version>1.35.3.0</version>
|
<version>1.36.0.0</version>
|
||||||
<title>
|
<title>
|
||||||
<en>AD Smart Pickup</en>
|
<en>AD Smart Pickup</en>
|
||||||
</title>
|
</title>
|
||||||
@@ -39,6 +39,9 @@
|
|||||||
<sourceFile filename="adCourseCache.lua"/>
|
<sourceFile filename="adCourseCache.lua"/>
|
||||||
<sourceFile filename="adFieldStorage.lua"/>
|
<sourceFile filename="adFieldStorage.lua"/>
|
||||||
<sourceFile filename="adFieldWork.lua"/>
|
<sourceFile filename="adFieldWork.lua"/>
|
||||||
|
<sourceFile filename="adFieldSiloPlanner.lua"/>
|
||||||
|
<sourceFile filename="adFieldSiloConfig.lua"/>
|
||||||
|
<sourceFile filename="adFieldSilos.lua"/>
|
||||||
<sourceFile filename="adFieldJobs.lua"/>
|
<sourceFile filename="adFieldJobs.lua"/>
|
||||||
<sourceFile filename="adMonthView.lua"/>
|
<sourceFile filename="adMonthView.lua"/>
|
||||||
<sourceFile filename="adProfiler.lua"/>
|
<sourceFile filename="adProfiler.lua"/>
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ med højst x vogne ude ad gangen, og uden at tage vogne fra markarbejde, andre f
|
|||||||
|
|
||||||
- **Marksilo:** bygning af typen silo inden for `ADFields.FIELD_SILO_DISTANCE` (150 m) af en marks polygon
|
- **Marksilo:** bygning af typen silo inden for `ADFields.FIELD_SILO_DISTANCE` (150 m) af en marks polygon
|
||||||
(`ADFields.fieldSilos`), samlet over alle marker, uden dubletter. En bygning, der er valgt som gårdlager i
|
(`ADFields.fieldSilos`), samlet over alle marker, uden dubletter. En bygning, der er valgt som gårdlager i
|
||||||
flowet, er aldrig marksilo.
|
flowet, er aldrig marksilo. Det samme gælder siloer, der er kilde, mål eller lager i et flow (tilføjet efter review
|
||||||
|
2026-09-27: ellers tømte flowet gårdsiloer, som forsyningsflows bruger, fordi de ligger under 150 m fra en mark).
|
||||||
- **Gårdlager:** bygning spilleren vælger i flowet (samme bygningsvælger som udkørslens lagre, `storeBuildings`).
|
- **Gårdlager:** bygning spilleren vælger i flowet (samme bygningsvælger som udkørslens lagre, `storeBuildings`).
|
||||||
- **Lån:** en vogn, flowet har sendt på én tur. Lån ligger kun i hukommelsen.
|
- **Lån:** en vogn, flowet har sendt på én tur. Lån ligger kun i hukommelsen.
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ Output: `{dispatch = {{vehicleId, siloId, siloMarkerId, fillType, storeMarkerId,
|
|||||||
|
|
||||||
- **Takt:** planlæg hvert 60. s (spiltid via `dt`), kun når flowet kører. Lagertal læses som i dag
|
- **Takt:** planlæg hvert 60. s (spiltid via `dt`), kun når flowet kører. Lagertal læses som i dag
|
||||||
(`ADSmartPickup.getStationLevelAndCapacity` / silo-lagre).
|
(`ADSmartPickup.getStationLevelAndCapacity` / silo-lagre).
|
||||||
- **Ledige vogne:** `ADFieldWork.getRigs(adEnv)` filtreret til `role == "unloader"` (traktor + vogn, som Courseplay
|
- **Ledige vogne:** kun vogne med AD-parkeringspunkt (ellers bliver de holdende på lagerets markør); `ADFieldWork.getRigs(adEnv)` filtreret til `role == "unloader"` (traktor + vogn, som Courseplay
|
||||||
godkender som tømmevogn — ikke sprøjter/høstere med tank), `enabled and not busy and not controlled`, og
|
godkender som tømmevogn — ikke sprøjter/høstere med tank), `enabled and not busy and not controlled`, og
|
||||||
vogne der har aflæsbare enheder (`AutoDrive.getAllDischargeableUnits`), der understøtter varen.
|
vogne der har aflæsbare enheder (`AutoDrive.getAllDischargeableUnits`), der understøtter varen.
|
||||||
`getRigs.busy` udvides med `ADFieldSilos.isLoaned(vehicle)`, så markarbejdet ikke tager en lånt vogn
|
`getRigs.busy` udvides med `ADFieldSilos.isLoaned(vehicle)`, så markarbejdet ikke tager en lånt vogn
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
-- Kør: luajit tests/test_adFieldSiloConfig.lua (fra repo-roden)
|
||||||
|
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
|
||||||
|
local C = ADFieldSiloConfig
|
||||||
|
local failures = 0
|
||||||
|
local function check(name, actual, expected)
|
||||||
|
if actual == expected then print("OK " .. name) else
|
||||||
|
failures = failures + 1
|
||||||
|
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local cfg = C.new()
|
||||||
|
check("standard fra", cfg.enabled, false)
|
||||||
|
check("standard x=2", cfg.maxVehicles, 2)
|
||||||
|
check("standard tærskel 10000", cfg.minLiters, 10000)
|
||||||
|
check("x under 1 -> 1", C.with(cfg, {maxVehicles = 0}).maxVehicles, 1)
|
||||||
|
check("x over 10 -> 10", C.with(cfg, {maxVehicles = 99}).maxVehicles, 10)
|
||||||
|
check("tærskel snappes til nærmeste valg", C.with(cfg, {minLiters = 12000}).minLiters, 10000)
|
||||||
|
check("tærskel 40000 -> 50000", C.with(cfg, {minLiters = 40000}).minLiters, 50000)
|
||||||
|
check("with muterer ikke", cfg.maxVehicles, 2)
|
||||||
|
local on = C.toggleStore(cfg, "silo1")
|
||||||
|
check("toggle til", C.hasStore(on, "silo1"), true)
|
||||||
|
check("toggle muterer ikke", C.hasStore(cfg, "silo1"), false)
|
||||||
|
check("toggle fra", C.hasStore(C.toggleStore(on, "silo1"), "silo1"), false)
|
||||||
|
local full = C.with(C.toggleStore(C.toggleStore(cfg, "a"), "b"), {enabled = true, maxVehicles = 4, minLiters = 25000})
|
||||||
|
local back = C.fromRecord(C.toRecord(full))
|
||||||
|
check("record: enabled", back.enabled, true)
|
||||||
|
check("record: lagre", table.concat(back.storeIds, " "), "a b")
|
||||||
|
check("record: x", back.maxVehicles, 4)
|
||||||
|
check("record: tærskel", back.minLiters, 25000)
|
||||||
|
check("tom record -> standard", C.fromRecord(nil).maxVehicles, 2)
|
||||||
|
check("record uden tal -> standard tærskel", C.fromRecord({enabled = 1}).minLiters, 10000)
|
||||||
|
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||||
|
os.exit(failures == 0 and 0 or 1)
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
-- Kør: luajit tests/test_adFieldSiloPlanner.lua (fra repo-roden)
|
||||||
|
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
|
||||||
|
local P = ADFieldSiloPlanner
|
||||||
|
local WHEAT, CANOLA = 1, 2
|
||||||
|
local failures = 0
|
||||||
|
local function check(name, actual, expected)
|
||||||
|
if actual == expected then print("OK " .. name) else
|
||||||
|
failures = failures + 1
|
||||||
|
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function silo(id, x, stock, markerId) return {id = id, name = id, markerId = markerId == nil and (id .. "m") or markerId or nil, x = x, z = 0, stock = stock} end
|
||||||
|
local function store(id, x, free) return {id = id, markerId = id .. "m", x = x, z = 0, free = free} end
|
||||||
|
local function vehicle(id, x, capacity, carries, cargo) return {id = id, x = x, z = 0, capacity = capacity, carries = carries or {[WHEAT] = true, [CANOLA] = true}, cargo = cargo} end
|
||||||
|
local function base(over)
|
||||||
|
local input = {silos = {}, stores = {store("farm", 0, {[WHEAT] = 1000000, [CANOLA] = 1000000})}, loans = {}, vehicles = {}, paused = {}, maxVehicles = 2, minLiters = 10000}
|
||||||
|
for k, v in pairs(over or {}) do input[k] = v end
|
||||||
|
return input
|
||||||
|
end
|
||||||
|
|
||||||
|
-- kvalificering
|
||||||
|
local r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 9999})}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||||
|
check("under tærskel -> ingen afsendelse", #r.dispatch, 0)
|
||||||
|
check("under tærskel -> ingen status", #r.status, 0)
|
||||||
|
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000}, false)}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||||
|
check("uden markør -> noMarker", r.status[1].state, "noMarker")
|
||||||
|
check("uden markør -> ingen afsendelse", #r.dispatch, 0)
|
||||||
|
r = P.plan(base({stores = {store("farm", 0, {[CANOLA] = 1000000})}, silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||||
|
check("ingen gård tager varen -> noFarmStore", r.status[1].state, "noFarmStore")
|
||||||
|
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 30000})}, vehicles = {vehicle("v1", 400, 50000)}, paused = {[P.key("A", WHEAT)] = true}}))
|
||||||
|
check("pause -> paused, ingen afsendelse", r.status[1].state .. #r.dispatch, "paused0")
|
||||||
|
|
||||||
|
-- afsendelse, fuldeste først, x-loft
|
||||||
|
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 20000}), silo("B", 900, {[WHEAT] = 80000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 950, 50000), vehicle("v3", 1000, 50000)}}))
|
||||||
|
check("x=2 -> to afsendt", #r.dispatch, 2)
|
||||||
|
check("fuldeste silo først", r.dispatch[1].siloId, "B")
|
||||||
|
check("nærmeste vogn til B", r.dispatch[1].vehicleId, "v2")
|
||||||
|
check("B's rest (30000) får vogn nr. 2", r.dispatch[2].siloId, "B")
|
||||||
|
check("lager-markør med", r.dispatch[1].storeMarkerId, "farmm")
|
||||||
|
check("liter = vognens kapacitet", r.dispatch[1].liters, 50000)
|
||||||
|
check("anden tur = resten", r.dispatch[2].liters, 30000)
|
||||||
|
|
||||||
|
-- aktive lån tæller mod x og mod siloens rest
|
||||||
|
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 60000})}, loans = {{siloId = "A", fillType = WHEAT, liters = 50000}}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
|
||||||
|
check("lån på vej -> rest 10000 -> én ny vogn", #r.dispatch, 1)
|
||||||
|
check("status viser på vej", r.status[1].onWay, 50000)
|
||||||
|
r = P.plan(base({maxVehicles = 1, silos = {silo("A", 500, {[WHEAT] = 90000})}, loans = {{siloId = "Z", fillType = WHEAT, liters = 1}}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||||
|
check("x=1 med ét lån ude -> ingen", #r.dispatch, 0)
|
||||||
|
check("frie pladser 0", r.freeSlots, 0)
|
||||||
|
|
||||||
|
-- flere varer i én silo = hver sin opgave
|
||||||
|
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000, [CANOLA] = 20000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 410, 50000)}}))
|
||||||
|
check("to varer -> to afsendelser", #r.dispatch, 2)
|
||||||
|
check("fuldeste vare først", r.dispatch[1].fillType, WHEAT)
|
||||||
|
|
||||||
|
-- vogne der ikke kan
|
||||||
|
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, {[CANOLA] = true}), vehicle("v2", 2000, 50000)}}))
|
||||||
|
check("vogn der ikke kan bære varen springes over", r.dispatch[1].vehicleId, "v2")
|
||||||
|
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 50000, nil, CANOLA), vehicle("v2", 2000, 50000)}}))
|
||||||
|
check("vogn med anden vare på springes over", r.dispatch[1].vehicleId, "v2")
|
||||||
|
r = P.plan(base({silos = {silo("A", 500, {[WHEAT] = 40000})}, vehicles = {vehicle("v1", 400, 0)}}))
|
||||||
|
check("vogn uden kapacitet -> ingen", #r.dispatch, 0)
|
||||||
|
|
||||||
|
-- lagervalg
|
||||||
|
r = P.plan(base({stores = {store("near", 400, {[WHEAT] = 20000}), store("far", 5000, {[WHEAT] = 900000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
|
||||||
|
check("nærmeste lager uden plads til læsset -> det med plads", r.dispatch[1].storeId, "far")
|
||||||
|
r = P.plan(base({stores = {store("small", 400, {[WHEAT] = 30000}), store("tiny", 300, {[WHEAT] = 15000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000), vehicle("v2", 400, 50000)}}))
|
||||||
|
check("intet har plads til hele læsset -> mest plads", r.dispatch[1].storeId, "small")
|
||||||
|
check("reserveret plads: vogn 2 til det andet lager", r.dispatch[2].storeId, "tiny")
|
||||||
|
r = P.plan(base({stores = {store("full", 400, {[WHEAT] = 5000})}, silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||||
|
check("lager med under tærsklen -> noFarmStore", r.status[1].state, "noFarmStore")
|
||||||
|
|
||||||
|
-- input muteres ikke
|
||||||
|
local input = base({silos = {silo("A", 500, {[WHEAT] = 90000})}, vehicles = {vehicle("v1", 400, 50000)}})
|
||||||
|
P.plan(input)
|
||||||
|
check("input.stores urørt", input.stores[1].free[WHEAT], 1000000)
|
||||||
|
|
||||||
|
|
||||||
|
-- I1: lån på vej til et lager reserverer pladsen på tværs af planlægningsrunder
|
||||||
|
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
||||||
|
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||||
|
check("I1 lager fyldt af lån på vej -> ingen ny vogn dertil", #r.dispatch, 0)
|
||||||
|
r = P.plan(base({stores = {store("s", 400, {[WHEAT] = 30000}), store("t", 900, {[WHEAT] = 500000})}, silos = {silo("A", 500, {[WHEAT] = 90000})},
|
||||||
|
loans = {{siloId = "A", fillType = WHEAT, liters = 30000, storeId = "s"}}, vehicles = {vehicle("v1", 400, 50000)}}))
|
||||||
|
check("I1 næste vogn til det andet lager", r.dispatch[1] ~= nil and r.dispatch[1].storeId, "t")
|
||||||
|
|
||||||
|
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||||
|
os.exit(failures == 0 and 0 or 1)
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
-- Kør: luajit tests/test_adFieldSilos.lua (fra repo-roden)
|
||||||
|
dofile("FS25_ADSmartPickup/adFieldSiloPlanner.lua")
|
||||||
|
dofile("FS25_ADSmartPickup/adFieldSiloConfig.lua")
|
||||||
|
dofile("FS25_ADSmartPickup/adFieldSilos.lua")
|
||||||
|
local S = ADFieldSilos
|
||||||
|
local failures = 0
|
||||||
|
local function check(name, actual, expected)
|
||||||
|
if actual == expected then print("OK " .. name) else
|
||||||
|
failures = failures + 1
|
||||||
|
print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual)))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local loan = {startedMs = 0, lastProgressMs = 0, lastFill = 0}
|
||||||
|
check("kører, tidligt -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 5000), nil)
|
||||||
|
check("AD slukket efter 40 s -> done", S.releaseReason(loan, {adActive = false, controlled = false, fill = 0}, 40000), "done")
|
||||||
|
check("spiller i vognen -> player", S.releaseReason(loan, {adActive = true, controlled = true, fill = 0}, 5000), "player")
|
||||||
|
check("står stille uden fyld i 120 s -> stuck", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = false}, 120001), "stuck")
|
||||||
|
check("kører stadig efter 120 s -> behold", S.releaseReason(loan, {adActive = true, controlled = false, fill = 0, moving = true}, 120001), nil)
|
||||||
|
local loading = {startedMs = 0, lastProgressMs = 110000, lastFill = 0}
|
||||||
|
check("fyldning steg for nylig -> behold", S.releaseReason(loading, {adActive = true, controlled = false, fill = 20000, moving = false}, 125000), nil)
|
||||||
|
|
||||||
|
-- I2/I3: vagten gælder kun før turen er kommet i gang; AD der slukker straks uden fremdrift = fejl
|
||||||
|
local going = {startedMs = 0, lastFill = 30000, hasProgressed = true}
|
||||||
|
check("I2 i gang, står i kø 500 s -> behold", S.releaseReason(going, {adActive = true, controlled = false, fill = 30000, moving = false}, 500000), nil)
|
||||||
|
check("I2 aflæsning (fyld falder) er fremdrift", S.releaseReason({startedMs = 0, lastFill = 30000}, {adActive = true, controlled = false, fill = 20000, moving = false}, 500000), nil)
|
||||||
|
check("I3 AD slukket efter 10 s uden fremdrift -> failed", S.releaseReason({startedMs = 0, lastFill = 0}, {adActive = false, controlled = false, fill = 0}, 10000), "failed")
|
||||||
|
check("I3 AD slukket efter 10 s med fremdrift -> done", S.releaseReason({startedMs = 0, lastFill = 0, hasProgressed = true}, {adActive = false, controlled = false, fill = 0}, 10000), "done")
|
||||||
|
|
||||||
|
-- Røgtest af spil-laget med mocks: planNow låner nærmeste tømmevogn, isLoaned = optaget,
|
||||||
|
-- frakobling (setConfig enabled=false) stopper AD og frigiver (Review Focus).
|
||||||
|
do
|
||||||
|
local WHEAT = 1
|
||||||
|
Logging = {info = function() end, warning = function() end}
|
||||||
|
g_currentMission = {getFarmId = function() return 1 end}
|
||||||
|
g_fillTypeManager = {getFillTypeByIndex = function() return {title = "Hvede"} end}
|
||||||
|
function getWorldTranslation(node) return node.x, 0, node.z end
|
||||||
|
ADRuns = {vehicleLabel = function(v) return v.name end}
|
||||||
|
local function storage(levels, free) return {fillLevels = levels, fillTypes = {[WHEAT] = true}, getFreeCapacity = function() return free end} end
|
||||||
|
local fieldSilo = {id = "fs", name = "Marksilo 1", kind = "silo", placeable = {rootNode = {x = 1000, z = 0}, spec_silo = {storages = {storage({[WHEAT] = 60000}, 0)}}}}
|
||||||
|
local farm = {id = "farm", name = "Gårdsilo", kind = "silo", placeable = {rootNode = {x = 0, z = 0}, spec_silo = {storages = {storage({}, 500000)}}}}
|
||||||
|
ADBuildings = {list = function() return {fieldSilo, farm} end,
|
||||||
|
getMarkers = function(b, role) return {{id = b.id .. "-" .. role}} end}
|
||||||
|
ADFields = {list = function() return {{id = 1}} end, fieldSilos = function() return {fieldSilo, farm} end}
|
||||||
|
local started, settingsSet, noPark, noRoute, residue = {}, {}, {}, {}, 0
|
||||||
|
ADRunsController = {store = {runs = {}}, setSetting = function(_, v, name, value) settingsSet[v] = settingsSet[v] or {}; settingsSet[v][name] = value end}
|
||||||
|
ADUnloadWait = {hasRouteTo = function(_, _, id) return not noRoute[id] end}
|
||||||
|
local function makeVehicle(name, x)
|
||||||
|
local module = {active = false}
|
||||||
|
function module:setMode() end
|
||||||
|
function module:setFirstMarkerByWayPointId(id) self.first = id end
|
||||||
|
function module:setSecondMarkerByWayPointId(id) self.second = id end
|
||||||
|
function module:setFillType() end
|
||||||
|
function module:setLoopsDone() end
|
||||||
|
function module:raiseDirtyFlag() end
|
||||||
|
function module:isActive() return self.active end
|
||||||
|
return {name = name, x = x, lastSpeedReal = 0, ad = {stateModule = module},
|
||||||
|
stopAutoDrive = function(self) self.ad.stateModule.active = false; self.stopped = true end}
|
||||||
|
end
|
||||||
|
local near, far = makeVehicle("Nær", 900), makeVehicle("Fjern", 5000)
|
||||||
|
local adEnv = {AutoDrive = {MODE_PICKUPANDDELIVER = 2, RT_NONE = 0,
|
||||||
|
getAllDischargeableUnits = function() return {{fillUnitIndex = 1, object = {
|
||||||
|
getFillUnitCapacity = function() return 40000 end,
|
||||||
|
getFillUnitSupportedFillTypes = function() return {[WHEAT] = true} end,
|
||||||
|
getFillUnitFillLevel = function() return residue end, getFillUnitFillType = function() return WHEAT end}}} end,
|
||||||
|
getAllUnits = function() return {} end, getAllFillLevels = function() return 0 end}}
|
||||||
|
ADFieldWork = {
|
||||||
|
getRigs = function()
|
||||||
|
local rigs = {}
|
||||||
|
-- som spillets vehicleSystem: slettede køretøjer er ikke med
|
||||||
|
if not near.isDeleted then table.insert(rigs, {id = "n", vehicle = near, role = "unloader", enabled = true, busy = S.isLoaned(near), controlled = false, x = 900, z = 0}) end
|
||||||
|
if not far.isDeleted then table.insert(rigs, {id = "f", vehicle = far, role = "unloader", enabled = true, busy = S.isLoaned(far), controlled = false, x = 5000, z = 0}) end
|
||||||
|
table.insert(rigs, {id = "h", vehicle = far, role = "harvester", enabled = true, busy = false, controlled = false, x = 1000, z = 0})
|
||||||
|
return rigs
|
||||||
|
end,
|
||||||
|
enableParkAtJobFinished = function() end,
|
||||||
|
parkMarkerId = function(_, v) if noPark[v] then return nil end return 777 end,
|
||||||
|
startAd = function(_, v, _, first, second, fillType) v.ad.stateModule.active = true; table.insert(started, {v = v, first = first, second = second, fillType = fillType}) end,
|
||||||
|
}
|
||||||
|
S.setConfig(ADFieldSiloConfig.with(ADFieldSiloConfig.toggleStore(ADFieldSiloConfig.new(), "farm"), {enabled = true, maxVehicles = 1}))
|
||||||
|
local plan = S.planNow(adEnv)
|
||||||
|
check("RØG én afsendelse (x=1)", #started, 1)
|
||||||
|
check("RØG nærmeste tømmevogn", started[1] ~= nil and started[1].v.name, "Nær")
|
||||||
|
check("RØG fra marksilo-markør", started[1] ~= nil and started[1].first, "fs-load")
|
||||||
|
check("RØG til gårdlager-markør", started[1] ~= nil and started[1].second, "farm-unload")
|
||||||
|
check("RØG varen med", started[1] ~= nil and started[1].fillType, WHEAT)
|
||||||
|
check("RØG gårdlager er ikke marksilo", #plan.status, 1)
|
||||||
|
check("RØG vognen er lånt", S.isLoaned(near), true)
|
||||||
|
check("RØG x nået -> ingen flere", #S.planNow(adEnv).dispatch, 0)
|
||||||
|
check("I4 rotation slået fra på lånt vogn", settingsSet[near] ~= nil and settingsSet[near].rotateTargets, 0)
|
||||||
|
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
||||||
|
check("RØG frakobling stopper AD", near.stopped, true)
|
||||||
|
check("RØG frakobling frigiver", S.isLoaned(near), false)
|
||||||
|
|
||||||
|
-- I5: vogn uden parkeringspunkt lånes ikke (ville blive holdende på lagerets markør)
|
||||||
|
started = {}
|
||||||
|
noPark[near] = true
|
||||||
|
near.stopped = nil
|
||||||
|
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
|
||||||
|
S.planNow(adEnv)
|
||||||
|
check("I5 uden parkering -> fjern vogn med parkering", started[1] ~= nil and started[1].v.name, "Fjern")
|
||||||
|
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
||||||
|
noPark[near] = nil
|
||||||
|
|
||||||
|
-- I3: ingen rute til siloen -> ingen afsendelse
|
||||||
|
started = {}
|
||||||
|
noRoute["fs-load"] = true
|
||||||
|
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
|
||||||
|
S.planNow(adEnv)
|
||||||
|
check("I3 ingen rute -> ingen afsendelse", #started, 0)
|
||||||
|
noRoute["fs-load"] = nil
|
||||||
|
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
||||||
|
|
||||||
|
-- I6: silo der er kilde/mål i et flow er ikke marksilo
|
||||||
|
ADRunsController.store.runs = {{kind = "supply", sourceBuildingIds = {}, targetBuildingIds = {"fs"}, storeBuildingIds = {}}}
|
||||||
|
local silos = S.collectBuildings(adEnv)
|
||||||
|
check("I6 silo i et flow tømmes ikke", #silos, 0)
|
||||||
|
ADRunsController.store.runs = {}
|
||||||
|
|
||||||
|
-- M5: rest under 100 l tæller ikke som last
|
||||||
|
residue = 50
|
||||||
|
local vehicles = S.collectVehicles(adEnv)
|
||||||
|
check("M5 rest på 50 l er ikke last", vehicles[1].cargo, nil)
|
||||||
|
residue = 0
|
||||||
|
|
||||||
|
-- M4: slettet lånt vogn fjernes uden at vælte tick
|
||||||
|
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = true}))
|
||||||
|
started = {}
|
||||||
|
S.planNow(adEnv)
|
||||||
|
local loaned = started[1] ~= nil and started[1].v or nil
|
||||||
|
if loaned ~= nil then loaned.isDeleted = true end
|
||||||
|
local okTick = pcall(S.tick, adEnv, 2000)
|
||||||
|
check("M4 tick tåler slettet vogn", okTick, true)
|
||||||
|
check("M4 slettet vogn frigivet", loaned ~= nil and S.isLoaned(loaned), false)
|
||||||
|
S.setConfig(ADFieldSiloConfig.with(S.config, {enabled = false}))
|
||||||
|
end
|
||||||
|
|
||||||
|
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
|
||||||
|
os.exit(failures == 0 and 0 or 1)
|
||||||
Reference in New Issue
Block a user