From f813d4b3d82906d24c925f1e371d95c77fb09aaa Mon Sep 17 00:00:00 2001 From: masterdraco Date: Wed, 30 Sep 2026 13:24:04 +0200 Subject: [PATCH] =?UTF-8?q?feat(k=C3=A6de):=20k=C3=A6demodel=20sporer=20pr?= =?UTF-8?q?odukter=20bagl=C3=A6ns=20til=20r=C3=A5varer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adChainModel.lua | 145 ++++++++++++++++++++++++++++ FS25_ADSmartPickup/modDesc.xml | 1 + 2 files changed, 146 insertions(+) create mode 100644 FS25_ADSmartPickup/adChainModel.lua diff --git a/FS25_ADSmartPickup/adChainModel.lua b/FS25_ADSmartPickup/adChainModel.lua new file mode 100644 index 0000000..44bdb83 --- /dev/null +++ b/FS25_ADSmartPickup/adChainModel.lua @@ -0,0 +1,145 @@ +-- 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 + 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) + if 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 diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml index e441228..8a16652 100644 --- a/FS25_ADSmartPickup/modDesc.xml +++ b/FS25_ADSmartPickup/modDesc.xml @@ -55,6 +55,7 @@ +