Files
fs25-adsmartpickup/FS25_ADSmartPickup/adChainModel.lua
T

153 lines
6.4 KiB
Lua

-- AD Chain Model (ren logik)
-- Sporer baglæns fra de produktioner spilleren selv har tændt (rødder) gennem gårdens egne fabrikker og stalde
-- til råvarerne. Alle mængder er liter pr. måned; varer og afgrøder er NAVNE. Se spec 2026-09-30-produktionskaeder.
ADChainModel = {}
ADChainModel.MAX_DEPTH = 8
local function byKey(a, b) return tostring(a.key) < tostring(b.key) end
-- producenter pr. vare: tændte først, så stabil rækkefølge
local function indexProducers(recipes)
local index = {}
for _, recipe in ipairs(recipes or {}) do
for name, amount in pairs(recipe.outputs or {}) do
if (amount or 0) > 0 and (recipe.cyclesPerMonth or 0) > 0 then
index[name] = index[name] or {}
table.insert(index[name], recipe)
end
end
end
for _, list in pairs(index) do
table.sort(list, function(a, b)
if (a.enabled == true) ~= (b.enabled == true) then return a.enabled == true end
return byKey(a, b)
end)
end
return index
end
local function indexHerds(husbandry)
local index = {}
for _, herd in ipairs(husbandry or {}) do
for name, perMonth in pairs(herd.outputs or {}) do
if (perMonth or 0) > 0 then
index[name] = index[name] or {}
table.insert(index[name], {building = herd.building, buildingName = herd.buildingName, left = perMonth})
end
end
end
return index
end
local function sortedNames(map)
local names = {}
for name in pairs(map or {}) do table.insert(names, name) end
table.sort(names)
return names
end
function ADChainModel.build(recipes, husbandry, crops)
local chain = {roots = {}, demand = {}, fieldDemand = {}, needRecipes = {}, problems = {}}
local producers, herds = indexProducers(recipes), indexHerds(husbandry)
local capacity, roots = {}, {}
for _, recipe in ipairs(recipes or {}) do
capacity[recipe.key] = recipe.cyclesPerMonth or 0
if recipe.enabled and not recipe.byChain then table.insert(roots, recipe) end
end
table.sort(roots, byKey)
for _, root in ipairs(roots) do capacity[root.key] = 0 end -- rødderne kører for sig selv
local function problem(kind, fillType, rootKey, detail)
table.insert(chain.problems, {kind = kind, fillType = fillType, rootKey = rootKey, detail = detail})
end
local need
local function inputsOf(recipe, cycles, depth, path, rootKey)
local children = {}
for _, name in ipairs(sortedNames(recipe.inputs)) do
local amount = recipe.inputs[name] or 0
if amount > 0 and cycles > 0 then
local child = need(name, amount * cycles, depth + 1, path, rootKey)
child.consumer, child.consumerName = recipe.building, recipe.buildingName
table.insert(children, child)
end
end
return children
end
need = function(name, perMonth, depth, path, rootKey)
local node = {fillType = name, perMonth = perMonth, coveredPerMonth = 0, producers = {}, herds = {}, children = {}}
chain.demand[name] = (chain.demand[name] or 0) + perMonth
if depth > ADChainModel.MAX_DEPTH or path[name] then
node.source = "cycle"
problem("cycle", name, rootKey)
return node
end
path[name] = true
local rest = perMonth
-- a) gårdens fabrikker
local makers = producers[name]
if makers ~= nil then
node.source = "production"
for _, recipe in ipairs(makers) do
local perCycle = recipe.outputs[name]
local cycles = math.min(rest / perCycle, capacity[recipe.key] or 0)
if cycles > 0 then
capacity[recipe.key] = capacity[recipe.key] - cycles
rest = rest - cycles * perCycle
chain.needRecipes[recipe.key] = (chain.needRecipes[recipe.key] or 0) + cycles
table.insert(node.producers, {key = recipe.key, building = recipe.building, buildingName = recipe.buildingName, cycles = cycles})
for _, child in ipairs(inputsOf(recipe, cycles, depth, path, rootKey)) do table.insert(node.children, child) end
end
if rest <= 1e-9 then break end
end
if rest > 1e-9 and #node.producers > 0 then
problem("bottleneck", name, rootKey, rest)
node.shortPerMonth = rest
end
if rest > 1e-9 and #node.producers == 0 then
node.source = nil
end
end
-- b) stalde
if rest > 1e-9 and #node.producers == 0 and herds[name] ~= nil then
node.source = "husbandry"
for _, herd in ipairs(herds[name]) do
local take = math.min(rest, herd.left)
if take > 0 then
herd.left = herd.left - take
rest = rest - take
table.insert(node.herds, {building = herd.building, buildingName = herd.buildingName, perMonth = take})
end
end
end
-- c) mark
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and crops ~= nil and crops[name] ~= nil then
node.source, node.fruit = "field", crops[name]
chain.fieldDemand[node.fruit] = (chain.fieldDemand[node.fruit] or 0) + rest
rest = 0
end
-- d) ingen kilde (eller rest efter stald), eller exhausted producers
if rest > 1e-9 and makers ~= nil and #node.producers == 0 and #node.herds == 0 then
node.source = "production"
node.shortPerMonth = rest
problem("bottleneck", name, rootKey, rest)
elseif rest > 1e-9 and (node.source == nil or node.source == "husbandry") then
if node.source == nil then node.source = "buy" end
node.shortPerMonth = rest
problem("buy", name, rootKey, rest)
end
node.coveredPerMonth = perMonth - (node.shortPerMonth or 0)
path[name] = nil
return node
end
for _, root in ipairs(roots) do
local cycles = root.cyclesPerMonth or 0
table.insert(chain.roots, {key = root.key, name = root.name, building = root.building, buildingName = root.buildingName,
children = inputsOf(root, cycles, 0, {}, root.key)})
end
return chain
end