feat: kæder respekterer fabrikkens udgang (Fordel = intet flow, Sælg på mellemvare -> Behold)

This commit is contained in:
2026-09-30 22:01:42 +02:00
parent 21b48d7664
commit 1bda82a270
9 changed files with 120 additions and 14 deletions
+1
View File
@@ -8,6 +8,7 @@ står i [docs/BRUGERMANUAL.md](docs/BRUGERMANUAL.md) (dansk) og [docs/USER_MANUA
## 1.62 — Kæder slås selv til
- **1.62.2** Kæderne læser fabrikkernes udgange (Sælg / Fordel / Behold). **Fordel:** spillet leverer selv til gårdens fabrikker, så kæden opretter intet flow, og fanen Kæder viser *fordeles af spillet*. **Behold** (spawn eller lager): et flow henter varen ved fabrikken som hidtil. **Sælg på en mellemvare** betyder, at næste led i kæden intet får, så kæden sætter den til Behold og henter den.
- **1.62.1** Rettet: udkørsel fra objektlager (fx Sell Egg fra Æglageret) stoppede halvfuld med "spawner ikke … (30 s uden fremdrift)". Lageret deler samme vare i flere grupper (paller med forskellig fyldning), og moden talte kun den første gruppe — når en lille gruppe blev tom, så lageret ud til at vokse. Nu tælles alle grupper. Advarslen viser lager- og vogntal, hvis den alligevel opstår.
- **1.62.0** **En kæde er noget du selv slår til:** tændte produktioner er ikke længere automatisk kæder (FS25 tænder nye produktioner som standard — 7 ensilagetårne gav 56 kæder og fik AIO-fabrikkens høproduktion tændt). Fanen **Kæder** har nederst *N tændte produktioner uden kæde* (foldet sammen); **[Kæde til]** gør en produktion til en kæde. En pauset kæde har **[Fjern]**. Ikke-valgte produktioner bruges stadig som kilde. Når en kæde fjernes, slettes dens tomme kædeflows (uden traktor), og mellemled kæden selv tændte slukkes igen. Rettet: kædens flowoversigt manglede traktorerne, så sammenlægning/oprydning kunne ramme et kædeflow med traktor. Udkørsel fra palleskur holder AutoDrive, mens moden tager paller ud.
+5 -4
View File
@@ -244,12 +244,13 @@ function ADChain.stillNeeded(runView, edges)
return nil
end
-- {[forbruger|vare] = "ok"|"missing"|"stopped"|"noVehicle"|"pallet"} for hver kant.
-- {[forbruger|vare] = "ok"|"auto"|"missing"|"stopped"|"noVehicle"|"pallet"} for hver kant.
function ADChain.flowStatus(edges, runs, noVehicle)
local result = {}
for _, edge in ipairs(edges or {}) do
local status = "missing"
for _, run in ipairs(runs or {}) do
-- udgangen står på Fordel: spillet leverer selv, intet flow
local status = edge.delivery == "auto" and "auto" or "missing"
for _, run in ipairs(edge.delivery == "auto" and {} or runs or {}) do
if run.kind == "supply" and covers(run, edge) then
if not run.stopped then status = "ok"; break end
local reason = (noVehicle or {})[run.id]
@@ -318,7 +319,7 @@ local function plan(data, state)
end
-- de ægte opskrifter (ikke kopien): en pauset rod er stadig tændt og må ikke "tændes" og overtages af kæden
local prod = ADChainActions.productions(data.recipes, chain.needRecipes, protected)
return {chain = chain, shortages = shortages, actions = actions, prod = prod, edges = ADChainActions.edges(chain)}
return {chain = chain, shortages = shortages, actions = actions, prod = prod, edges = ADChainActions.edges(chain, data.recipes)}
end
local function logInfo(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end
+60 -4
View File
@@ -17,14 +17,53 @@ end
local SUPPLIED = {production = true, husbandry = true, field = true}
function ADChainActions.edges(chain)
local list, seen = {}, {}
-- Hvordan varen når frem, ud fra producenternes udgang: "auto" = alle fordeler (spillet leverer selv til gårdens
-- fabrikker; kædens forbrugere er altid fabrikker) -> intet flow; "sold" = en producent sælger mellemvaren væk;
-- ellers "haul" (Behold: spawn/lager ved fabrikken, eller stald/mark) -> et flow henter. recipesByKey = {[nøgle] = opskrift}.
function ADChainActions.delivery(edge, recipesByKey)
local producers = edge.producers or {}
if #producers == 0 or recipesByKey == nil then return "haul" end
local all, sold = true, false
for _, key in ipairs(producers) do
local mode = ADChainActions.producerMode(recipesByKey, key, edge.fillType)
if mode == "sell" then sold = true end
if mode ~= "distribute" then all = false end
end
if sold then return "sold" end
return all and "auto" or "haul"
end
function ADChainActions.producerMode(recipesByKey, key, fillType)
local recipe = (recipesByKey or {})[key]
return recipe ~= nil and (recipe.outputModes or {})[fillType] or nil
end
local function producerKeys(node)
local keys = {}
for _, producer in ipairs(node.producers or {}) do table.insert(keys, producer.key) end
return keys
end
-- recipes (valgfri): gårdens opskrifter med outputModes -> kanten får delivery (se ADChainActions.delivery).
function ADChainActions.edges(chain, recipes)
local list, seen, byKey = {}, {}, nil
if recipes ~= nil then
byKey = {}
for _, recipe in ipairs(recipes) do byKey[recipe.key] = recipe end
end
local function walk(nodes, rootKey)
for _, node in ipairs(nodes or {}) do
local id = tostring(node.consumer) .. "|" .. tostring(node.fillType)
if SUPPLIED[node.source] and node.consumer ~= nil and not seen[id] then
seen[id] = true
table.insert(list, {fillType = node.fillType, consumer = node.consumer, consumerName = node.consumerName, rootKey = rootKey})
local edge = {fillType = node.fillType, consumer = node.consumer, consumerName = node.consumerName, rootKey = rootKey,
producers = producerKeys(node)}
edge.delivery = ADChainActions.delivery(edge, byKey)
edge.soldBy = {}
for _, key in ipairs(edge.producers) do
if ADChainActions.producerMode(byKey, key, edge.fillType) == "sell" then table.insert(edge.soldBy, key) end
end
table.insert(list, edge)
end
walk(node.children, rootKey)
end
@@ -51,7 +90,24 @@ end
function ADChainActions.missingFlows(edges, runs)
local list = {}
for _, edge in ipairs(edges or {}) do
if not ADChainActions.isCovered(edge, runs) then table.insert(list, edge) end
if edge.delivery ~= "auto" and not ADChainActions.isCovered(edge, runs) then table.insert(list, edge) end
end
return list
end
-- Mellemvarer en producent sælger væk (kanten er "sold") -> {{key, fillType}} én gang hver; kæden sætter dem til Behold.
function ADChainActions.soldIntermediates(edges)
local list, seen = {}, {}
for _, edge in ipairs(edges or {}) do
if edge.delivery == "sold" then
for _, key in ipairs(edge.soldBy or {}) do
local id = key .. "|" .. edge.fillType
if not seen[id] then
seen[id] = true
table.insert(list, {key = key, fillType = edge.fillType})
end
end
end
end
return list
end
+25 -2
View File
@@ -92,6 +92,28 @@ end
-- ---- læsning pr. del ----
-- En fabriks produktioner -> opskrifter + punkter (samles først, så en fejl midtvejs ikke efterlader halve).
-- Udgangens fordeling (spillets Sælg/Fordel/Behold) -> "sell"|"distribute"|"keep". Behold dækker både spawn (paller
-- ved fabrikken) og lager (STORE fra ProductionStorageControl): begge dele skal hentes af et flow.
function ADChainData.outputModeName(mode)
local modes = ProductionPoint ~= nil and ProductionPoint.OUTPUT_MODE or {}
if mode ~= nil and mode == modes.DIRECT_SELL then return "sell" end
if mode ~= nil and mode == modes.AUTO_DELIVER then return "distribute" end
return "keep"
end
local function outputModes(point, outputs, nameOf)
local modes = {}
if point.getOutputDistributionMode == nil then return modes end
for _, output in ipairs(outputs or {}) do
local name = output.type ~= nil and nameOf(output.type) or nil
if name ~= nil then
local ok, mode = pcall(point.getOutputDistributionMode, point, output.type)
modes[name] = ADChainData.outputModeName(ok and mode or nil)
end
end
return modes
end
local function readFactory(placeable, point, byChain, nameOf)
local uid = placeableId(placeable)
if uid == nil then return {}, {} end
@@ -102,8 +124,9 @@ local function readFactory(placeable, point, byChain, nameOf)
table.insert(recipes, {key = key, name = tostring(production.name or production.id), building = uid,
buildingName = buildingName, enabled = isEnabled(point, production), byChain = (byChain or {})[key] == true,
cyclesPerMonth = ADChainData.cyclesPerMonth(production),
inputs = amountsByName(production.inputs, nameOf), outputs = amountsByName(production.outputs, nameOf)})
points[key] = {point = point, productionId = production.id, placeable = placeable}
inputs = amountsByName(production.inputs, nameOf), outputs = amountsByName(production.outputs, nameOf),
outputModes = outputModes(point, production.outputs, nameOf)})
points[key] = {point = point, productionId = production.id, placeable = placeable, buildingName = buildingName}
end
return recipes, points
end
+21
View File
@@ -281,6 +281,25 @@ function ADChainRun.applyFinalSales(data, chain, edges)
return changed
end
-- Mellemvarer der står på Sælg: næste led i kæden fik intet. Sættes til Behold (spawn/lager), så kædens flow henter
-- dem ved fabrikken; tages ud af soldByChain, så slutprodukt-logikken ikke sætter dem til salg igen.
function ADChainRun.keepIntermediates(data, edges)
if ProductionPoint == nil or ProductionPoint.OUTPUT_MODE == nil then return 0 end
local changed = 0
for _, item in ipairs(ADChainActions.soldIntermediates(edges)) do
local entry, fillIndex = (data.points or {})[item.key], data.fillIndex[item.fillType]
if entry ~= nil and entry.point ~= nil and fillIndex ~= nil and entry.point.setOutputDistributionMode ~= nil
and outputMode(entry.point, fillIndex) == ProductionPoint.OUTPUT_MODE.DIRECT_SELL then
entry.point:setOutputDistributionMode(fillIndex, ProductionPoint.OUTPUT_MODE.KEEP)
ADChain.state.soldByChain[item.key .. "|" .. item.fillType] = nil
changed = changed + 1
log("mellemvare %s i %s sælges ikke længere (næste led i kæden bruger den) -> Behold", tostring(item.fillType),
tostring(entry.buildingName or item.key))
end
end
return changed
end
-- Flows for rødder der er slået fra: flyt til en anden rod der stadig bruger kanten, ellers stop og slip (chain
-- ryddes, så spilleren kan starte flowet igen uden at kæden stopper det næste minut; det slettes ikke).
function ADChainRun.stopFlows(adEnv, chain, edges)
@@ -326,6 +345,8 @@ function ADChainRun.apply(adEnv, data, result)
local okNew, created = pcall(ADChainRun.createFlows, adEnv, data, result.edges)
local okSell, sellErr = pcall(ADChainRun.applyFinalSales, data, result.chain, result.edges)
if not okSell then warn("slutprodukter fejlede: %s", tostring(sellErr)) end
local okKeep, keepErr = pcall(ADChainRun.keepIntermediates, data, result.edges)
if not okKeep then warn("mellemvarer fejlede: %s", tostring(keepErr)) end
if okMerge and mergedCount > 0 then created = (okNew and created or 0) + mergedCount end
if not okNew then warn("nye flows fejlede: %s", tostring(created)) end
-- delvise data: en rod der ikke kunne læses, må ikke få sine flows stoppet
+5 -3
View File
@@ -26,10 +26,10 @@ ADChainView.TEXT_KEYS = {"spu_chain_optIn", "spu_chain_optOut", "spu_chain_notCh
"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",
"spu_chain_problem_buy", "spu_chain_problem_bottleneck", "spu_chain_problem_off", "spu_chain_problem_cycle", "spu_chain_problem_barn"}
"spu_chain_flowAuto", "spu_chain_problem_buy", "spu_chain_problem_bottleneck", "spu_chain_problem_off", "spu_chain_problem_cycle", "spu_chain_problem_barn"}
local FLOW_KEYS = {ok = "spu_chain_flowOk", missing = "spu_chain_flowNone", stopped = "spu_chain_flowStopped", noVehicle = "spu_chain_flowMissing",
pallet = "spu_chain_flowPallet"}
pallet = "spu_chain_flowPallet", auto = "spu_chain_flowAuto"}
local ROOT_STATUS = {running = "spu_chain_rootOk", error = "spu_chain_rootError", waiting = "spu_chain_rootWaiting", stopped = "spu_chain_rootPaused"}
local function identity(value) return tostring(value) end
@@ -129,7 +129,7 @@ function ADChainView.rootLevel(root, last)
if node.fruit ~= nil and short[node.fruit] ~= nil then hasShortage = true end
if (node.shortPerMonth or 0) > 0 then hasShortage = true end
local status = node.consumer ~= nil and flowStatus[edgeKey(node)] or nil
if (status ~= nil and status ~= "ok") or isTight(node, last, short) then hasWaiting = true end
if (status ~= nil and status ~= "ok" and status ~= "auto") or isTight(node, last, short) then hasWaiting = true end
end)
if hasShortage then return "error" end
if hasWaiting then return "waiting" end
@@ -218,6 +218,8 @@ local function nodeStatus(node, last, fmt, textOf, short)
local shortage = node.fruit ~= nil and short[node.fruit] or nil
if shortage ~= nil then return string.format(textOf("spu_chain_fieldShort"), haText(shortage.missingHa)), "error" end
local status = node.consumer ~= nil and (last.flowStatus or {})[edgeKey(node)] or nil
-- udgangen står på Fordel: spillet leverer selv
if status == "auto" then return textOf("spu_chain_flowAuto"), "running" end
if status ~= nil and status ~= "ok" then return textOf(FLOW_KEYS[status] or "spu_chain_flowNone"), "waiting" end
if isTight(node, last, short) then return textOf("spu_chain_tight"), "waiting" end
if status == "ok" then return textOf("spu_chain_flowOk"), "running" end
+1
View File
@@ -606,6 +606,7 @@
<text name="spu_chain_flowOk" text="flow kører"/>
<text name="spu_chain_flowMissing" text="mangler traktor/vogn"/>
<text name="spu_chain_flowNone" text="intet flow"/>
<text name="spu_chain_flowAuto" text="fordeles af spillet"/>
<text name="spu_chain_flowPallet" text="palle: vælg UAL-vogn"/>
<text name="spu_chain_flowStopped" text="flow stoppet"/>
<text name="spu_chain_bottleneck" text="flaskehals: -%s/md"/>
+1
View File
@@ -606,6 +606,7 @@
<text name="spu_chain_flowOk" text="flow running"/>
<text name="spu_chain_flowMissing" text="no tractor/trailer"/>
<text name="spu_chain_flowNone" text="no flow"/>
<text name="spu_chain_flowAuto" text="distributed by game"/>
<text name="spu_chain_flowPallet" text="pallet: UAL trailer"/>
<text name="spu_chain_flowStopped" text="flow stopped"/>
<text name="spu_chain_bottleneck" text="bottleneck: -%s/mo"/>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.62.1.0</version>
<version>1.62.2.0</version>
<title>
<en>AD Smart Pickup</en>
</title>