183 lines
8.6 KiB
Lua
183 lines
8.6 KiB
Lua
-- 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 eller (med "kør til produktion") hvilken fabrik. 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
|
||
|
||
local function isProduction(store)
|
||
return store ~= nil and store.kind == "production"
|
||
end
|
||
|
||
-- Nærmeste lager af den ønskede slags (fabrik/gård) med plads til hele turen; ellers det med mest plads.
|
||
local function chooseStore(stores, silo, fillType, liters, minLiters, reserved, production)
|
||
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 and isProduction(store) == production 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
|
||
-- fabrikker kun med plads til hele læsset: en fabrik tager kun det den forbruger, så en vogn med resten
|
||
-- holder ved den i timevis (in-game 28/9: 252.000 l kartofler ved en fuld snackfabrik)
|
||
if production then return best end
|
||
return best or most
|
||
end
|
||
|
||
-- Tom silo: en lånt vogn med last, der har stået stille ved en tom marksilo i REST_IDLE_MS, kører resten videre
|
||
-- (AutoDrive venter ellers til vognen er fuld). s = {atSilo, stillMs, siloStock, cargo}
|
||
ADFieldSiloPlanner.REST_IDLE_MS = 30000
|
||
ADFieldSiloPlanner.REST_MIN_LITERS = 100
|
||
function ADFieldSiloPlanner.restDue(s)
|
||
return s.atSilo == true and (s.stillMs or 0) >= ADFieldSiloPlanner.REST_IDLE_MS
|
||
and (s.cargo or 0) >= ADFieldSiloPlanner.REST_MIN_LITERS and (s.siloStock or 0) < ADFieldSiloPlanner.REST_MIN_LITERS
|
||
end
|
||
|
||
-- Lager til en rest: fabrik kun med plads til hele resten, ellers gårdlager (nærmeste med plads, ellers mest plads).
|
||
function ADFieldSiloPlanner.restStore(stores, silo, fillType, liters)
|
||
return chooseStore(stores, silo, fillType, liters, 1, {}, true) or chooseStore(stores, silo, fillType, liters, 1, {}, false)
|
||
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
|
||
|
||
-- Foder først: mangler gården stadig foderreserve for varen (reserve − beholdning − læs på vej hjem), køres til
|
||
-- gårdlager; ellers til fabrik. Findes kun den ene slags, bruges den.
|
||
local function pickStore(input, farmIncoming, task, liters, minLiters, reserved)
|
||
local fillType = task.fillType
|
||
local short = ((input.feedReserve or {})[fillType] or 0) - ((input.farmStock or {})[fillType] or 0) - (farmIncoming[fillType] or 0)
|
||
local wantProduction = short <= 0
|
||
return chooseStore(input.stores, task.silo, fillType, liters, minLiters, reserved, wantProduction)
|
||
or chooseStore(input.stores, task.silo, fillType, liters, minLiters, reserved, not wantProduction)
|
||
end
|
||
|
||
-- Høsten først: for hvert behov {fillType, count} (høstere i arbejde, der mangler tømmevogne) holdes så mange
|
||
-- vogne der kan bære afgrøden tilbage; resten må marksilo-flowet låne. Ny liste; input muteres ikke.
|
||
function ADFieldSiloPlanner.withoutHarvestReserve(vehicles, needs)
|
||
local held = {}
|
||
for _, need in ipairs(needs or {}) do
|
||
local left = need.count or 0
|
||
for _, vehicle in ipairs(vehicles or {}) do
|
||
if left <= 0 then break end
|
||
if not held[vehicle.id] and canCarry(vehicle, need.fillType) then
|
||
held[vehicle.id] = true
|
||
left = left - 1
|
||
end
|
||
end
|
||
end
|
||
local free = {}
|
||
for _, vehicle in ipairs(vehicles or {}) do
|
||
if not held[vehicle.id] then table.insert(free, vehicle) end
|
||
end
|
||
return free
|
||
end
|
||
|
||
function ADFieldSiloPlanner.plan(input)
|
||
local minLiters = input.minLiters or 10000
|
||
local storesById = {}
|
||
for _, store in ipairs(input.stores or {}) do storesById[store.id] = store end
|
||
local reserved, onWay, farmIncoming = {}, {}, {}
|
||
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)
|
||
if loan.storeId ~= nil and not isProduction(storesById[loan.storeId]) then
|
||
farmIncoming[loan.fillType] = (farmIncoming[loan.fillType] or 0) + (loan.liters or 0)
|
||
end
|
||
-- 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, statusByKey = {}, {}, {}
|
||
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)
|
||
statusByKey[k] = 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 = pickStore(input, farmIncoming, task, liters, minLiters, reserved)
|
||
if store == nil then break end
|
||
if isProduction(store) then
|
||
statusByKey[key(task.silo.id, task.fillType)].toProduction = true
|
||
else
|
||
farmIncoming[task.fillType] = (farmIncoming[task.fillType] or 0) + liters
|
||
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
|