fix(kæde): markvarer som hø og hakkelse kommer fra markerne

ADChainData leverer data.fieldGoods (vindue-varer, spillets konvertere, tørrede
varianter og kendte markvarer der findes som fill type). En markvare uden
producent/stald får kilden "fra marker" uden afgrødeplan og uden problem, så
fermenteringssiloernes hø/lucerne/kløver/hakkelse ikke meldes som køb.
This commit is contained in:
2026-09-30 15:25:47 +02:00
parent 928299c8ee
commit d2a4cf0d23
6 changed files with 96 additions and 7 deletions
+2 -2
View File
@@ -290,11 +290,11 @@ end
local function plan(data, state)
local recipes = ADChain.modelRecipes(data.recipes, state.paused, state.userOff)
local chain = ADChainModel.build(recipes, data.husbandry, data.crops)
local chain = ADChainModel.build(recipes, data.husbandry, data.crops, data.fieldGoods)
-- pausede rødder: det de bruger, er beskyttet mod slukning (modellen regnet som om de ikke var pauset)
local protected = {}
if next(state.paused) ~= nil then
protected = ADChainModel.build(ADChain.modelRecipes(data.recipes, {}, state.userOff), data.husbandry, data.crops).needRecipes
protected = ADChainModel.build(ADChain.modelRecipes(data.recipes, {}, state.userOff), data.husbandry, data.crops, data.fieldGoods).needRecipes
end
local used = ADChain.unionFruits(data.usedFruits, chain.fieldDemand)
-- ufuldstændig markliste: udbuddet ville være undervurderet -> ingen mangler, forslag eller markskift endnu
+72 -1
View File
@@ -197,6 +197,76 @@ local function readCrops(data, nameOf)
end
end
-- ---- markvarer (hø, hakkelse, halm …) ----
-- Varer der høstes/laves på marken uden at være en afgrødes hovedvare. Spillets konvertere hedder forskelligt
-- mellem versioner/kort (FORAGEHARVESTER/FORAGE_HARVESTER), så alle navne prøves; et ukendt navn giver nil.
ADChainData.FIELD_CONVERTERS = {"FORAGEHARVESTER", "FORAGE_HARVESTER", "MOWER", "COMBINE", "CUTTER"}
-- Kendte markvarer (basisspil, FermentingSiloPack, Dakota); kun dem der findes som fill type bruges.
ADChainData.KNOWN_FIELD_GOODS = {"STRAW", "CHAFF", "CROP_WINDROW", "GRASS_WINDROW", "ALFALFA_WINDROW", "CLOVER_WINDROW",
"DRYGRASS_WINDROW", "DRYALFALFA_WINDROW", "DRYCLOVER_WINDROW", "CCM", "CCMRAW", "WCS"}
-- fruit.windrowFillType kan være et indeks eller en fill type-beskrivelse
local function windrowIndex(fruit)
local windrow = fruit.windrowFillType
if type(windrow) == "number" then return windrow end
if type(windrow) == "table" then
if windrow.index ~= nil then return windrow.index end
if windrow.name ~= nil and g_fillTypeManager.getFillTypeIndexByName ~= nil then
return g_fillTypeManager:getFillTypeIndexByName(windrow.name)
end
end
return nil
end
-- Én konverters mål-indekser ({[fra] = {targetFillTypeIndex|fillTypeIndex}}, samme form som adFields.buildMowable)
local function converterTargets(converter, add)
for _, entry in pairs(converter or {}) do
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
end
end
-- data.fieldGoods = {[navn] = true}. Kun eksisterende fill types; fejl logges én gang og gør ikke læsningen delvis
-- (en ændret spil-API må ikke blokere kæden for altid) — værst tænkeligt meldes en markvare som "køb".
local function readFieldGoods(data, nameOf)
local indices = {}
local function add(index) if type(index) == "number" then indices[index] = true end end
local function guarded(key, fn)
local ok, err = pcall(fn)
if not ok then ADChainData.warnOnce("fieldGoods:" .. key, "markvarer (%s) kunne ikke læses: %s", key, tostring(err)) end
end
if g_fillTypeManager == nil then return end
local fruitManager = g_fruitTypeManager
for _, fruit in ipairs(fruitManager ~= nil and fruitManager.fruitTypes or {}) do
guarded("windrow:" .. tostring(fruit.name), function() add(windrowIndex(fruit)) end)
end
if fruitManager ~= nil and fruitManager.getConverterDataByName ~= nil then
for _, name in ipairs(ADChainData.FIELD_CONVERTERS) do
guarded(name, function() converterTargets(fruitManager:getConverterDataByName(name), add) end)
end
end
if g_fillTypeManager.getFillTypeIndexByName ~= nil then
for _, name in ipairs(ADChainData.KNOWN_FIELD_GOODS) do
guarded(name, function() add(g_fillTypeManager:getFillTypeIndexByName(name)) end)
end
end
-- tørrede varianter (vender: græs-vindue -> hø)
if g_fillTypeManager.getConverterDataByName ~= nil then
guarded("TEDDER", function()
local tedder = g_fillTypeManager:getConverterDataByName("TEDDER") or {}
local windrows = {}
for index in pairs(indices) do table.insert(windrows, index) end
for _, index in ipairs(windrows) do
local entry = tedder[index]
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
end
end)
end
for index in pairs(indices) do
local ok, name = pcall(nameOf, index)
if ok and name ~= nil and data.crops[name] == nil then data.fieldGoods[name] = true end
end
end
local function readStock(data, adEnv, farmId, nameOf)
local ok, err = pcall(function()
for index, liters in pairs(ADStorageStats.farmTotals(ADSources.getFarmInventory(adEnv, farmId))) do
@@ -369,11 +439,12 @@ end
-- ikke har nået alle marker.
function ADChainData.collect(adEnv, farmId, byChain)
local data = {recipes = {}, husbandry = {}, crops = {}, stock = {}, fields = {}, yields = {}, fruitFillIndex = {},
usedFruits = {}, points = {}, fillTitles = {}, fillIndex = {}, placeables = {}, barnFood = {}, stockByFill = {}, rates = {}}
usedFruits = {}, points = {}, fillTitles = {}, fillIndex = {}, placeables = {}, barnFood = {}, stockByFill = {}, rates = {}, fieldGoods = {}}
local nameOf = nameResolver(data)
data.priceOf = priceResolver(data)
readPlaceables(data, farmId, byChain, nameOf)
readCrops(data, nameOf)
readFieldGoods(data, nameOf)
readStock(data, adEnv, farmId, nameOf)
readRates(data, adEnv, farmId, nameOf)
readUsedFruits(data, adEnv, farmId, nameOf)
+8 -1
View File
@@ -47,7 +47,9 @@ local function sortedNames(map)
return names
end
function ADChainModel.build(recipes, husbandry, crops)
-- fieldGoods = {[vare] = true}: varer der høstes/laves på marken uden at være en afgrødes hovedvare (hø, hakkelse,
-- halm …). Uden producent/stald er kilden "field" uden afgrøde: intet markbehov og intet problem.
function ADChainModel.build(recipes, husbandry, crops, fieldGoods)
local chain = {roots = {}, demand = {}, fieldDemand = {}, needRecipes = {}, problems = {}}
local producers, herds = indexProducers(recipes), indexHerds(husbandry)
-- Rødderne kører for sig selv med fuld fart: deres egne input kræves af deres eget træ. Som leverandør til en
@@ -136,6 +138,11 @@ function ADChainModel.build(recipes, husbandry, crops)
chain.fieldDemand[node.fruit] = (chain.fieldDemand[node.fruit] or 0) + rest
rest = 0
end
-- c2) markvare uden egen afgrøde (fx hø, hakkelse): kommer fra markerne, ingen afgrødeplan
if rest > 1e-9 and #node.producers == 0 and #node.herds == 0 and fieldGoods ~= nil and fieldGoods[name] then
node.source, node.fruit = "field", nil
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"
+12 -3
View File
@@ -22,7 +22,7 @@ ADChainView.TIGHT_MONTHS = 3
-- nøgler rækkerne bruger (til l10n-testen)
ADChainView.TEXT_KEYS = {"spu_chain_empty", "spu_chain_approve", "spu_chain_dismiss", "spu_chain_auto",
"spu_chain_flowOk", "spu_chain_flowMissing", "spu_chain_flowPallet", "spu_chain_flowNone", "spu_chain_flowStopped", "spu_chain_bottleneck",
"spu_chain_buy", "spu_chain_cycle", "spu_chain_fieldShort", "spu_chain_tight", "spu_chain_noField", "spu_chain_buySource",
"spu_chain_buy", "spu_chain_cycle", "spu_chain_fieldShort", "spu_chain_tight", "spu_chain_noField", "spu_chain_buySource", "spu_chain_fromFields",
"spu_chain_pause", "spu_chain_resume", "spu_chain_scanning", "spu_chain_rootOk", "spu_chain_rootError", "spu_chain_rootWaiting",
"spu_chain_rootPaused", "spu_chain_rootSummary", "spu_chain_free", "spu_chain_switches", "spu_chain_gone", "spu_chain_col_item",
"spu_chain_col_source", "spu_chain_col_perMonth", "spu_chain_col_status", "spu_chain_col_stock",
@@ -94,7 +94,14 @@ end
local function stockOf(last, fruit)
local stock = last.data ~= nil and last.data.stock or nil
return stock ~= nil and stock[fruit] or nil
return stock ~= nil and fruit ~= nil and stock[fruit] or nil
end
-- Lager for en mark-node: afgrødens lager, eller varens eget lager for en markvare uden afgrøde (hø, hakkelse …).
local function fieldStockOf(last, node)
if node.fruit ~= nil then return stockOf(last, node.fruit) end
local byFill = last.data ~= nil and last.data.stockByFill or nil
return byFill ~= nil and byFill[node.fillType] or nil
end
-- Mark-afgrøde uden mangel men med lager under TIGHT_MONTHS måneders behov.
@@ -187,6 +194,8 @@ local function sourceText(node, last, textOf)
elseif node.source == "husbandry" then
for _, herd in ipairs(node.herds or {}) do table.insert(names, tostring(herd.buildingName)) end
elseif node.source == "field" then
-- markvare uden egen afgrøde (hø, hakkelse …): ingen afgrødeplan, bare "fra marker"
if node.fruit == nil then return textOf("spu_chain_fromFields") end
for _, field in ipairs(last.fields or {}) do
if plannedFruit(field) == node.fruit then table.insert(names, "F" .. tostring(field.id)) end
end
@@ -294,7 +303,7 @@ local function rootRows(rows, root, last, ctx, isExpanded)
end
eachNode(root.children, function(node, depth)
local text, nodeLevel = nodeStatus(node, last, ctx.fmt, ctx.textOf, ctx.short)
local stock = node.source == "field" and stockOf(last, node.fruit) or nil
local stock = node.source == "field" and fieldStockOf(last, node) or nil
local row = makeRow("node", depth, nodeLevel, {string.rep(ADChainView.INDENT, depth) .. ctx.titleOf(node.fillType),
sourceText(node, last, ctx.textOf), ctx.fmt(node.perMonth or 0), text, stock ~= nil and ctx.fmt(stock) or ""})
row.fillType = node.fillType
+1
View File
@@ -598,6 +598,7 @@
<text name="spu_chain_tight" text="lager &lt; 3 md"/>
<text name="spu_chain_noField" text="ingen mark"/>
<text name="spu_chain_buySource" text="køb"/>
<text name="spu_chain_fromFields" text="fra marker"/>
<text name="spu_chain_pause" text="[Pause]"/>
<text name="spu_chain_resume" text="[Start]"/>
<text name="spu_chain_rootOk" text="dækket"/>
+1
View File
@@ -598,6 +598,7 @@
<text name="spu_chain_tight" text="stock &lt; 3 mo"/>
<text name="spu_chain_noField" text="no field"/>
<text name="spu_chain_buySource" text="buy"/>
<text name="spu_chain_fromFields" text="from fields"/>
<text name="spu_chain_pause" text="[Pause]"/>
<text name="spu_chain_resume" text="[Resume]"/>
<text name="spu_chain_rootOk" text="covered"/>