feat(kæde): advarsel når en fabrik gården er afhængig af slukkes

Slukker spilleren en produktion, hvis vare et tændt flow kører (manuel vare
eller målet tager den i auto), en stald æder eller en aktiv kæde bruger,
kommer én besked pr. slukning med brugere og lagerdage, og en rød post
'Slukket men nødvendig' i Kræver handling og statusvinduet så længe den er
slukket og nødvendig. Ingen advarsel når en anden tændt produktion dækker,
lageret rækker over 60 dage, kæden selv slukkede den eller den var slukket
ved indlæsning. Tjekket kører i kædens tick også uden kæder; ren logik i
adDependency.lua.
This commit is contained in:
2026-09-30 15:08:28 +02:00
parent ba2a3e94f1
commit 979b7f72d5
14 changed files with 488 additions and 14 deletions
+55 -1
View File
@@ -1,6 +1,7 @@
-- AD Chain (kæde-kørsel)
-- Hvert minut (efter opstartsfristen): læs gården (ADChainData), byg kæden (ADChainModel), find manglende
-- afgrøder (ADCropPlan) og lad ADChainRun skifte frie marker, tænde/slukke mellemled og oprette/stoppe flows.
-- Samme tick advarer (ADDependency), når spilleren slukker en produktion gården er afhængig af — også uden kæder.
-- Resultatet gemmes i ADChain.last til Kæder-fanen. Tilstanden (byChain, pausede rødder, afviste forslag,
-- userOff og det sidst kendte sæt af rødder) gemmes i ADSmartPickup_chain.xml under spillets gem.
-- Spiladgang kun inde i funktioner (filen dofile's i test).
@@ -31,6 +32,9 @@ ADChain.timer = 0
ADChain.collectFailed = false
-- {[rodnøgle|vare|art] = true}: manglende led der allerede er meldt som besked (kun i hukommelsen; nulstilles ved indlæsning)
ADChain.problemKeys = {}
-- {[nøgle] = true}: produktioner (ikke kædens) der var tændt ved sidste afhængighedstjek (kun kædens tick, kun på
-- hele data). nil efter indlæsning: det der allerede er slukket ved første tjek, advares der ikke om.
ADChain.depPrevEnabled = nil
local function contains(list, value)
for _, item in ipairs(list or {}) do if item == value then return true end end
@@ -320,6 +324,30 @@ local function applyReconcile(state, data)
ADChain.afterLoad = false
end
-- Afhængighedstjek (spec afsnit 8): nye slukninger (kun act) og "Slukket men nødvendig"-posterne. Uafhængigt af
-- kædens rødder og af markscanningen; springes over ved læsefejl (så beholdes forrige poster, og slukninger i
-- mellemtiden meldes ved næste hele tjek).
-- Produktioner kæden selv er ved at tænde (plan.prod.enable), er ikke "slukket men nødvendig".
local function dependencyCheck(data, plan, runs, act, previous)
if ADDependency == nil or ADChainData.dependencyInputs == nil then return nil end
if data.readFailed then
return previous ~= nil and {warnings = {}, offNeeded = previous.offNeeded or {}} or nil
end
local inputs = ADChainData.dependencyInputs(data, plan.chain, runs)
local enabling, recipes = {}, {}
for _, key in ipairs(plan.prod ~= nil and plan.prod.enable or {}) do enabling[key] = true end
for _, recipe in ipairs(data.recipes or {}) do
if not enabling[recipe.key] then table.insert(recipes, recipe) end
end
local prev = act and ADChain.depPrevEnabled or nil
local result = {
warnings = ADDependency.warnings(prev, recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
offNeeded = ADDependency.offNeeded(recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
}
if act then ADChain.depPrevEnabled = ADDependency.enabledKeys(data.recipes) end
return result
end
-- act = true: udfør handlingerne (tick). Uden act kun en frisk visning (GUI).
function ADChain.evaluate(adEnv, act)
local state = ADChain.state
@@ -341,11 +369,17 @@ function ADChain.evaluate(adEnv, act)
end
if act and ADChainRun ~= nil then ADChainRun.apply(adEnv, data, result) end
local runs = ADChain.runsView(ADRunsController.store.runs)
local okDep, dependency = pcall(dependencyCheck, data, result, runs, act, ADChain.last ~= nil and ADChain.last.dependency or nil)
if not okDep then
Logging.warning(ADChain.LOG_PREFIX .. "afhængighedstjek fejlede: %s", tostring(dependency))
dependency = nil
end
local paused = {}
for key in pairs(state.paused) do paused[key] = true end
ADChain.last = {chain = result.chain, shortages = result.shortages, actions = result.actions, edges = result.edges,
missingFlows = ADChainActions.missingFlows(result.edges, runs), flowStatus = ADChain.flowStatus(result.edges, runs, ADChain.noVehicle),
paused = paused, userOff = copySet(state.userOff), data = data, fields = data.fields, fieldsIncomplete = data.fieldsIncomplete == true, titles = data.fillTitles, at = ADChain.now, switchCount = ADChain.switchCount}
paused = paused, userOff = copySet(state.userOff), data = data, fields = data.fields, fieldsIncomplete = data.fieldsIncomplete == true, titles = data.fillTitles, at = ADChain.now, switchCount = ADChain.switchCount,
dependency = dependency}
return ADChain.last
end
@@ -378,6 +412,23 @@ function ADChain.notifyProblems(last)
return #fresh
end
-- Én besked pr. slukning af en nødvendig produktion (kun fra tick; advarslerne tømmes, når de er meldt).
function ADChain.notifyDependencies(last)
local dependency = last ~= nil and last.dependency or nil
if dependency == nil or #(dependency.warnings or {}) == 0 or ADChain.now < ADChain.STARTUP_GRACE_MS then return 0 end
local warnings = dependency.warnings
dependency.warnings = {}
if ADFieldJobs == nil or ADFieldJobs.notify == nil then return 0 end
local textOf = function(key) return g_i18n ~= nil and g_i18n:getText(key) or key end
local titleOf = function(name) return tostring((last.titles or {})[name] or name) end
for _, warning in ipairs(warnings) do
local text = ADDependency.notifyText(warning, textOf, titleOf)
Logging.info(ADChain.LOG_PREFIX .. "nødvendig produktion slukket: %s", text)
ADFieldJobs.notify(text, true)
end
return #warnings
end
function ADChain.tick(adEnv, dt)
if adEnv == nil then return end
ADChain.now = ADChain.now + (dt or 0)
@@ -387,6 +438,8 @@ function ADChain.tick(adEnv, dt)
local last = ADChain.evaluate(adEnv, true)
local ok, err = pcall(ADChain.notifyProblems, last)
if not ok then Logging.warning(ADChain.LOG_PREFIX .. "besked om manglende led fejlede: %s", tostring(err)) end
local okDep, errDep = pcall(ADChain.notifyDependencies, last)
if not okDep then Logging.warning(ADChain.LOG_PREFIX .. "advarsel om slukket produktion fejlede: %s", tostring(errDep)) end
end
-- ---- GUI-handlinger ----
@@ -482,6 +535,7 @@ function ADChain.load()
ADChain.noVehicle = {}
ADChain.prevEnabled, ADChain.afterLoad = nil, true
ADChain.problemKeys = {}
ADChain.depPrevEnabled = nil
local path = getPath()
if path == nil or not fileExists(path) then return end
local xmlFile = XMLFile.load("ADSmartPickupChain", path)
+127 -4
View File
@@ -32,8 +32,9 @@ end
-- En læsefejl i dette kald: data.partial = true (også når advarslen allerede er logget), så kæden ikke
-- slukker eller stopper noget på et ufuldstændigt billede af gården.
-- data.readFailed skelner en ægte læsefejl fra en ufærdig markscanning (afhængighedstjekket bruger ikke markerne).
local function failed(data, key, fmt, ...)
data.partial = true
data.partial, data.readFailed = true, true
ADChainData.warnOnce(key, fmt, ...)
end
@@ -118,11 +119,27 @@ local function readHerd(placeable, nameOf)
return {building = uid, buildingName = placeableName(placeable, uid), outputs = outputs}
end
-- Stald-foder: {[vare] = {staldnavn...}} for det truget æder nu (forbrug > 0) — til afhængighedstjekket.
local function readBarnFood(data, placeable, nameOf)
if ADSources.getHusbandryConsumption == nil then return end
local barnName
for index, perDay in pairs(ADSources.getHusbandryConsumption(placeable) or {}) do
local name = nameOf(index)
if name ~= nil and (perDay or 0) > 0 then
barnName = barnName or placeableName(placeable, placeableId(placeable) or "?")
data.barnFood[name] = data.barnFood[name] or {}
table.insert(data.barnFood[name], barnName)
end
end
end
local function readPlaceables(data, farmId, byChain, nameOf)
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
for index, placeable in pairs(placeables) do
local ok, err = pcall(function()
if not ADPrices.isOwnedBy(placeable, farmId) then return end
local uid = placeableId(placeable)
if uid ~= nil then data.placeables[uid] = placeable end
local point = placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil
if point ~= nil then
local recipes, points = readFactory(placeable, point, byChain, nameOf)
@@ -131,6 +148,7 @@ local function readPlaceables(data, farmId, byChain, nameOf)
elseif placeable.spec_husbandry ~= nil or placeable.getHusbandryFillLevel ~= nil then
local herd = readHerd(placeable, nameOf)
if herd ~= nil then table.insert(data.husbandry, herd) end
readBarnFood(data, placeable, nameOf)
end
end)
if not ok then
@@ -182,13 +200,30 @@ 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
local fruit = data.crops[nameOf(index) or ""]
local name = nameOf(index)
if name ~= nil then data.stockByFill[name] = (data.stockByFill[name] or 0) + liters end
local fruit = data.crops[name or ""]
if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
end
end)
if not ok then failed(data, "stock", "lager kunne ikke læses: %s", tostring(err)) end
end
-- Gårdens forbrug og produktion pr. døgn (fabrikker der kører + stalde) pr. varenavn og spillets tidsfaktor
-- (rå rater pr. måned × faktor = pr. døgn) — til lagerdage og dækning i afhængighedstjekket.
local function readRates(data, adEnv, farmId, nameOf)
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
local adjustment = environment ~= nil and tonumber(environment.timeAdjustment) or nil
data.timeAdjustment = adjustment ~= nil and adjustment > 0 and adjustment or 1
local ok, err = pcall(function()
for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId, false) or {}) do
local name = nameOf(index)
if name ~= nil then data.rates[name] = {consumption = rate.consumption or 0, production = rate.production or 0} end
end
end)
if not ok then failed(data, "allRates", "gårdens forbrug kunne ikke læses: %s", tostring(err)) end
end
-- Afgrøder gården selv bruger: input i en tændt produktion eller foder/halm i en stald (kun staldenes forbrug
-- læses: feedOnly springer fabrikkernes rater over — dem dækker opskrifterne allerede).
local function readUsedFruits(data, adEnv, farmId, nameOf)
@@ -231,19 +266,107 @@ local function readFields(data)
end
end
-- ---- afhængighedstjek (spec afsnit 8) ----
local function unloadStationsOf(placeable)
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
local result = {}
if storage == nil or storage.getUnloadingStations == nil then return result end
for _, station in pairs(storage:getUnloadingStations() or {}) do
if station.owningPlaceable == placeable then table.insert(result, station) end
end
return result
end
-- Hvad auto-flowenes mål tager af varerne `goods` (navne): {[bygnings-id] = {[vare] = true}}.
-- Kun tændte forsyningsflows i auto-tilstand; hvert mål slås op én gang.
function ADChainData.acceptsMap(data, flows, goods)
local result = {}
for _, flow in ipairs(flows or {}) do
if flow.kind ~= "outbound" and not flow.stopped and flow.ingredientMode ~= "manual" then
for _, id in ipairs(flow.targetBuildingIds or {}) do
local placeable = data.placeables[id]
if result[id] == nil and placeable ~= nil then
result[id] = {}
local ok, err = pcall(function()
local stations = unloadStationsOf(placeable)
for _, name in ipairs(goods) do
local index = data.fillIndex[name]
for _, station in ipairs(stations) do
if index ~= nil and ADSources.stationAccepts(station, index) then result[id][name] = true; break end
end
end
end)
if not ok then ADChainData.warnOnce("accepts:" .. tostring(id), "mål %s kunne ikke læses: %s", tostring(id), tostring(err)) end
end
end
end
end
return result
end
-- Kædens input-varer (alle varer i de aktive kæders træer) -> {[vare] = {kædenavn...}}
function ADChainData.chainInputs(chain, titles)
local result = {}
local function walk(nodes, rootTitle)
for _, node in ipairs(nodes or {}) do
result[node.fillType] = result[node.fillType] or {}
local list = result[node.fillType]
if list[#list] ~= rootTitle then table.insert(list, rootTitle) end
walk(node.children, rootTitle)
end
end
for _, root in ipairs(chain ~= nil and chain.roots or {}) do
walk(root.children, tostring((titles or {})[root.name] or root.name))
end
return result
end
-- Alt afhængighedstjekket regner på, i liter pr. døgn: {needed, stockDays, capacity, demand}.
-- flows = ADChain.runsView(...); chain = kædemodellen (kan være uden rødder).
function ADChainData.dependencyInputs(data, chain, flows)
local goods, seen = {}, {}
for _, recipe in ipairs(data.recipes or {}) do
if not recipe.enabled and not recipe.byChain then
for name in pairs(recipe.outputs or {}) do
if not seen[name] then seen[name] = true; table.insert(goods, name) end
end
end
end
table.sort(goods)
local needed = ADDependency.neededOutputs(flows, data.barnFood, ADChainData.chainInputs(chain, data.fillTitles),
#goods > 0 and ADChainData.acceptsMap(data, flows, goods) or {})
local adjustment = data.timeAdjustment or 1
local capacity, demand, stockDays = {}, {}, {}
for name, perMonth in pairs(ADDependency.capacity(data.recipes)) do capacity[name] = perMonth * adjustment end
for _, name in ipairs(goods) do
local rate = (data.rates or {})[name] or {}
local consumption, production = rate.consumption or 0, rate.production or 0
local chainDemand = ((chain ~= nil and chain.demand or {})[name] or 0) * adjustment
demand[name] = math.max(consumption, chainDemand)
if consumption > production then
stockDays[name] = ((data.stockByFill or {})[name] or 0) / (consumption - production)
elseif consumption > 0 then
stockDays[name] = math.huge
end
end
return {needed = needed, stockDays = stockDays, capacity = capacity, demand = demand}
end
-- ---- samlet ----
-- byChain = {[recipeKey] = true} for produktioner kæden selv har tændt (ikke rødder).
-- data.partial = true når noget ikke kunne læses i dette kald; data.fieldsIncomplete = true mens markscanningen
-- data.partial = true når noget ikke kunne læses i dette kald (data.readFailed) eller markscanningen kører; data.fieldsIncomplete = true mens markscanningen
-- 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 = {}}
usedFruits = {}, points = {}, fillTitles = {}, fillIndex = {}, placeables = {}, barnFood = {}, stockByFill = {}, rates = {}}
local nameOf = nameResolver(data)
data.priceOf = priceResolver(data)
readPlaceables(data, farmId, byChain, nameOf)
readCrops(data, nameOf)
readStock(data, adEnv, farmId, nameOf)
readRates(data, adEnv, farmId, nameOf)
readUsedFruits(data, adEnv, farmId, nameOf)
readFields(data)
return data
+160
View File
@@ -0,0 +1,160 @@
-- AD Dependency (ren logik)
-- Advarsel når spilleren slukker en produktion gården er afhængig af (spec 2026-09-30-produktionskaeder afsnit 8).
-- En vare er "nødvendig" når et tændt forsyningsflow kører den (manuel vare, eller flowets mål tager den i
-- auto-tilstand), en stald æder den, eller en aktiv kæde har den som input. Varer er NAVNE; kapacitet og forbrug
-- i samme enhed (adapteren i adChain.lua vælger), lagerdage i spillets døgn. Ingen spiladgang.
ADDependency = {}
ADDependency.STOCK_DAYS_OK = 60 -- lager til mere end så mange døgn: ingen advarsel
ADDependency.MAX_USERS = 2 -- brugere nævnt i beskeden; resten "+k"
local function contains(list, value)
for _, item in ipairs(list or {}) do if item == value then return true end end
return false
end
local function sortedKeys(map)
local keys = {}
for key in pairs(map or {}) do table.insert(keys, key) end
table.sort(keys)
return keys
end
-- flows = {{name, kind, stopped, targetBuildingIds, ingredientMode, ingredients}}
-- barnFood = {[vare] = {staldnavn...}}, chainInputs = {[vare] = {kædenavn...}}
-- accepts = {[bygnings-id] = {[vare] = true}}: hvad auto-flowenes mål tager (adapteren slår op)
-- -> {[vare] = {users = {navn...}, onlyChain}} (brugere i rækkefølgen flows, stalde, kæder; hver kun én gang)
function ADDependency.neededOutputs(flows, barnFood, chainInputs, accepts)
local result = {}
local function add(name, user, isChain)
if name == nil or user == nil then return end
local entry = result[name]
if entry == nil then
entry = {users = {}, onlyChain = true}
result[name] = entry
end
if not isChain then entry.onlyChain = false end
if not contains(entry.users, user) then table.insert(entry.users, user) end
end
for _, flow in ipairs(flows or {}) do
if flow.kind ~= "outbound" and not flow.stopped then
local user = tostring(flow.name or "?")
if flow.ingredientMode == "manual" then
for _, name in ipairs(flow.ingredients or {}) do add(name, user, false) end
else
for _, building in ipairs(flow.targetBuildingIds or {}) do
for _, name in ipairs(sortedKeys((accepts or {})[building])) do add(name, user, false) end
end
end
end
end
for _, name in ipairs(sortedKeys(barnFood)) do
for _, barn in ipairs(barnFood[name]) do add(name, tostring(barn), false) end
end
for _, name in ipairs(sortedKeys(chainInputs)) do
for _, root in ipairs(chainInputs[name]) do add(name, tostring(root), true) end
end
return result
end
-- Tændte produktioner der ikke er kædens (samme regel som ADChain.enabledKeys) -> {[nøgle] = true}
function ADDependency.enabledKeys(recipes)
local result = {}
for _, recipe in ipairs(recipes or {}) do
if recipe.enabled and not recipe.byChain then result[recipe.key] = true end
end
return result
end
-- Tændte produktioners kapacitet pr. vare (outputs × cyklusser pr. måned) -> {[vare] = mængde}
function ADDependency.capacity(recipes)
local result = {}
for _, recipe in ipairs(recipes or {}) do
if recipe.enabled then
for name, amount in pairs(recipe.outputs or {}) do
result[name] = (result[name] or 0) + (amount or 0) * (recipe.cyclesPerMonth or 0)
end
end
end
return result
end
-- Dækker andre tændte produktioner varen? Ukendt/intet forbrug: enhver kapacitet dækker.
local function isCovered(name, otherCapacity, demand)
local capacity = (otherCapacity or {})[name] or 0
return capacity > 0 and capacity >= ((demand or {})[name] or 0)
end
-- Rækker lageret længe nok? nil = ukendt (advar, uden dage).
local function stockLasts(name, stockDays)
local days = (stockDays or {})[name]
return days ~= nil and days > ADDependency.STOCK_DAYS_OK
end
-- Første nødvendige, udækkede vare (efter navn) for en slukket produktion (ikke kædens) -> advarsel eller nil.
local function check(recipe, needed, stockDays, otherCapacity, demand)
if recipe.enabled or recipe.byChain then return nil end
for _, name in ipairs(sortedKeys(recipe.outputs)) do
local need = (needed or {})[name]
if need ~= nil and (recipe.outputs[name] or 0) > 0 and not isCovered(name, otherCapacity, demand)
and not stockLasts(name, stockDays) then
local days = (stockDays or {})[name]
return {recipeKey = recipe.key, building = recipe.building, buildingName = recipe.buildingName, fillType = name,
users = need.users, onlyChain = need.onlyChain == true, days = days}
end
end
return nil
end
-- Alle slukkede, nødvendige produktioner (én pr. fabrik) — "Slukket men nødvendig" i Kræver handling.
function ADDependency.offNeeded(recipes, needed, stockDays, otherCapacity, demand)
local result, seen = {}, {}
for _, recipe in ipairs(recipes or {}) do
local warning = check(recipe, needed, stockDays, otherCapacity, demand)
if warning ~= nil and not seen[tostring(warning.building)] then
seen[tostring(warning.building)] = true
table.insert(result, warning)
end
end
return result
end
-- Nye slukninger siden sidste evaluering: tændt i prevEnabled, slukket nu, nødvendig og ikke dækket.
-- prevEnabled = nil (første evaluering efter indlæsning) -> ingen. Én pr. fabrik og vare.
function ADDependency.warnings(prevEnabled, recipes, needed, stockDays, otherCapacity, demand)
local result, seen = {}, {}
if prevEnabled == nil then return result end
for _, recipe in ipairs(recipes or {}) do
if prevEnabled[recipe.key] then
local warning = check(recipe, needed, stockDays, otherCapacity, demand)
local id = warning ~= nil and (tostring(warning.building) .. "|" .. warning.fillType) or nil
if warning ~= nil and not seen[id] then
seen[id] = true
table.insert(result, warning)
end
end
end
return result
end
-- "A, B +2"
function ADDependency.usersText(users)
local shown = {}
for index = 1, math.min(#(users or {}), ADDependency.MAX_USERS) do shown[index] = tostring(users[index]) end
if #shown == 0 then return "?" end
local text = table.concat(shown, ", ")
if #users > #shown then text = text .. " +" .. tostring(#users - #shown) end
return text
end
-- Beskedtekst (spu_dep_warn med dage, spu_dep_warnNoDays uden).
function ADDependency.notifyText(warning, textOf, titleOf)
local title = tostring(titleOf ~= nil and titleOf(warning.fillType) or warning.fillType)
local users = ADDependency.usersText(warning.users)
local building = tostring(warning.buildingName or warning.building or "?")
local days = warning.days
if days ~= nil and days < math.huge then
return string.format(textOf("spu_dep_warn"), building, title, users, math.max(0, math.floor(days)))
end
return string.format(textOf("spu_dep_warnNoDays"), building, title, users)
end
+12
View File
@@ -963,6 +963,18 @@ function ADSources.getFarmRates(adEnv, farmId, feedOnly)
return rates
end
-- Én stalds forbrug (foder efter trugets indhold, halm, vand): {[fillType] = liter} kun for consumption > 0.
-- Rå rate uden timeAdjustment (som getHusbandryOutputs).
function ADSources.getHusbandryConsumption(placeable)
local rates, result = {}, {}
if placeable == nil then return result end
pcall(addHusbandryRates, rates, placeable)
for fillType, rate in pairs(rates) do
if (rate.consumption or 0) > 0 then result[fillType] = rate.consumption end
end
return result
end
-- Én stalds produktion (mælk, gødning, ...): {[fillType] = liter} kun for production > 0. Spillets rå rate
-- (pr. måned = én 24-timers dag) UDEN timeAdjustment — samme enhed som fabrikkernes cyklusser pr. måned i kæden.
function ADSources.getHusbandryOutputs(placeable)
+6 -2
View File
@@ -168,10 +168,11 @@ function ADStatusHud.fieldEntries(proxy)
return entries
end
-- Kæder med manglende led + ventende forslag (samme punkter og tekster som "Kræver handling").
-- Kæder med manglende led + ventende forslag + slukkede nødvendige fabrikker (samme punkter og tekster som
-- "Kræver handling"; de to sidste står under Advarsler).
function ADStatusHud.chainEntries(proxy)
local chains, suggestions = {}, {}
local result = {chains = chains, suggestions = suggestions}
local result = {chains = chains, suggestions = suggestions, dependencies = {}}
if ADChain == nil or ADChain.last == nil or ADStorageStats == nil or ADStorageStats.chainEntries == nil or proxy == nil then
return result
end
@@ -180,6 +181,8 @@ function ADStatusHud.chainEntries(proxy)
table.insert(chains, {text = proxy:alertText(entry), level = "error"})
elseif entry.kind == "chainSuggest" then
table.insert(suggestions, {text = proxy:alertText(entry), level = "waiting"})
elseif entry.kind == "dependencyOff" then
table.insert(result.dependencies, {text = proxy:alertText(entry), level = "error"})
end
end
return result
@@ -212,6 +215,7 @@ function ADStatusHud.snapshot(adEnv)
snapshot.fields = guarded("fields", ADStatusHud.fieldEntries, proxy) or {}
local chainResult = guarded("chains", ADStatusHud.chainEntries, proxy) or {}
snapshot.chains = chainResult.chains or {}
for _, entry in ipairs(chainResult.dependencies or {}) do table.insert(snapshot.warnings, entry) end
for _, entry in ipairs(guarded("stuck", ADStatusHud.stuckEntries) or {}) do table.insert(snapshot.warnings, entry) end
for _, entry in ipairs(chainResult.suggestions or {}) do table.insert(snapshot.warnings, entry) end
return snapshot
+27
View File
@@ -336,9 +336,36 @@ local function chainProblemEntries(last)
return result
end
-- "Slukket men nødvendig" (ADChain.last.dependency.offNeeded): én rød post pr. fabrik, højst MAX_DEPENDENCY_ENTRIES
-- (den sidste får more = antal skjulte). Score DEPENDENCY_SCORE + hast (0-99, færre lagerdage = højere; ukendt = 50):
-- over manglende led (3500-3599), under "rød tom" (3900-4000).
ADStorageStats.DEPENDENCY_SCORE = 3700
ADStorageStats.MAX_DEPENDENCY_ENTRIES = 2
local function urgency(days)
if days == nil then return 50 end
if days == math.huge then return 0 end
return math.max(0, 99 - math.min(99, math.floor(days)))
end
function ADStorageStats.dependencyEntries(last)
local entries = {}
for _, item in ipairs(last ~= nil and last.dependency ~= nil and last.dependency.offNeeded or {}) do
local name = tostring(item.buildingName or item.building or "?")
table.insert(entries, {kind = "dependencyOff", level = "red", score = ADStorageStats.DEPENDENCY_SCORE + urgency(item.days),
building = item.building, buildingName = name, fillType = item.fillType, onlyChain = item.onlyChain == true, text = name})
end
entries = ADStorageStats.rankAttention(entries)
local result = {}
for index = 1, math.min(#entries, ADStorageStats.MAX_DEPENDENCY_ENTRIES) do result[index] = entries[index] end
if #entries > #result then result[#result].more = #entries - #result end
return result
end
function ADStorageStats.chainEntries(last)
if last == nil then return {} end
local result = chainProblemEntries(last)
for _, entry in ipairs(ADStorageStats.dependencyEntries(last)) do table.insert(result, entry) end
local count = 0
for _, action in ipairs(last.actions or {}) do
if action.mode == "suggest" then count = count + 1 end
@@ -4,6 +4,14 @@
-- siges det at et flow ikke hjælper (producér/køb). Afgørelsen er ADStorageStats.flowAdvice (testet).
-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
-- Lagerfanen for en bygningstype (TAB_KINDS), fx "factory" -> fanen Fabrikker.
function SmartPickupFrame.tabOfKind(kind)
for tab, tabKind in pairs(SmartPickupFrame.TAB_KINDS or {}) do
if tabKind == kind then return tab end
end
return SmartPickupFrame.TAB_OVERVIEW
end
local function fillTypeName(fillType)
return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
end
@@ -64,6 +72,8 @@ end
function SmartPickupFrame:attentionAdvice(entry)
-- kæde-forslag og manglende led har ingen vare (stat): klik fører til fanen Kæder
if entry ~= nil and (entry.kind == "chainSuggest" or entry.kind == "chainProblem") then return "chain", nil end
-- slukket men nødvendig: fanen Fabrikker (Kæder når kun en kæde bruger varen)
if entry ~= nil and entry.kind == "dependencyOff" then return entry.onlyChain and "chain" or "factories", nil end
if entry ~= nil and entry.kind == "sell" and entry.stat ~= nil then return sellAdviceFor(entry.stat) end
if entry == nil or entry.kind ~= "empty" or entry.stat == nil then return "none", nil end
local stat = entry.stat
@@ -86,6 +96,7 @@ end
function SmartPickupFrame:attentionHint(advice, info)
if advice == "chain" then return self.i18n:getText("spu_attention_hintChain") end
if advice == "factories" then return self.i18n:getText("spu_attention_hintFactories") end
if advice == "open" then return string.format(self.i18n:getText("spu_attention_hintOpen"), tostring(info.run.name)) end
if advice == "flow" then return self.i18n:getText(info.sell and "spu_attention_hintSellFlow" or "spu_attention_hintFlow") end
if advice == "produce" then return self.i18n:getText("spu_attention_hintProduce") end
@@ -103,6 +114,10 @@ function SmartPickupFrame:onAttentionClick(entry)
frame:switchTab(SmartPickupFrame.TAB_CHAINS)
return
end
if entry ~= nil and entry.kind == "dependencyOff" then
frame:switchTab(entry.onlyChain and SmartPickupFrame.TAB_CHAINS or SmartPickupFrame.tabOfKind("factory"))
return
end
local adEnv = frame.getAdEnv()
local advice, info = frame:attentionAdvice(entry)
if adEnv == nil or advice == "none" then return end
@@ -183,10 +183,15 @@ function SmartPickupFrame:addKpiRow(stats)
return row
end
-- Varepunkterne + kædens punkter (røde manglende led, højst 2, og ét samlet punkt for afgrødeforslag; ADChain.last,
-- ingen evaluering her), samme loft MAX_ATTENTION.
-- Varepunkterne + kædens punkter (røde manglende led, højst 2, røde "Slukket men nødvendig", højst 2, og ét samlet
-- punkt for afgrødeforslag), samme loft MAX_ATTENTION. ADChain.last genbruges; er det ældre end CHAIN_MAX_AGE_MS,
-- evalueres der (visning, handler ikke), så en slukket nødvendig fabrik ses straks.
function SmartPickupFrame:attentionFor(stats)
local attention = ADStorageStats.attentionList(stats, SmartPickupFrame.MAX_ATTENTION)
if ADChain ~= nil and ADChain.evaluate ~= nil and self.chainLast ~= nil and self.getAdEnv ~= nil then
local okEnv, adEnv = pcall(self.getAdEnv)
if okEnv and adEnv ~= nil then pcall(self.chainLast, self, adEnv) end
end
if ADChain == nil or ADChain.last == nil then return attention end
return ADStorageStats.mergeAttention(attention, ADStorageStats.chainEntries(ADChain.last), SmartPickupFrame.MAX_ATTENTION)
end
@@ -201,6 +206,11 @@ function SmartPickupFrame:alertText(entry)
if (entry.more or 0) > 0 then text = text .. string.format(" (+%d)", entry.more) end
return text
end
if entry.kind == "dependencyOff" then
local text = string.format(i18n:getText("spu_attention_depOff"), tostring(entry.buildingName))
if (entry.more or 0) > 0 then text = text .. string.format(" (+%d)", entry.more) end
return text
end
if entry.kind == "empty" then
return string.format(i18n:getText("spu_alert_empty"), stat.title, self:formatDays(stat.days), fc(stat.farmLevel or 0), fc(math.abs(stat.net or 0)))
elseif entry.kind == "full" then
+4
View File
@@ -632,5 +632,9 @@
<text name="spu_status_act_emptying" text="tømmer vognen"/>
<text name="spu_status_act_homing" text="på vej hjem"/>
<text name="spu_status_stuck" text="%s står stille (%d min)"/>
<text name="spu_dep_warn" text="Advarsel: du slukkede %s (%s) — %s bruger det. Lageret rækker ca. %d dage."/>
<text name="spu_dep_warnNoDays" text="Advarsel: du slukkede %s (%s) — %s bruger det."/>
<text name="spu_attention_depOff" text="Slukket men nødvendig: %s"/>
<text name="spu_attention_hintFactories" text=" → klik: se Fabrikker"/>
</texts>
</l10n>
+4
View File
@@ -632,5 +632,9 @@
<text name="spu_status_act_emptying" text="emptying the wagon"/>
<text name="spu_status_act_homing" text="heading home"/>
<text name="spu_status_stuck" text="%s standing still (%d min)"/>
<text name="spu_dep_warn" text="Warning: you switched off %s (%s) — %s uses it. Stock lasts about %d days."/>
<text name="spu_dep_warnNoDays" text="Warning: you switched off %s (%s) — %s uses it."/>
<text name="spu_attention_depOff" text="Switched off but needed: %s"/>
<text name="spu_attention_hintFactories" text=" → click: see Factories"/>
</texts>
</l10n>
+1
View File
@@ -58,6 +58,7 @@
<sourceFile filename="adChainModel.lua"/>
<sourceFile filename="adCropPlan.lua"/>
<sourceFile filename="adChainActions.lua"/>
<sourceFile filename="adDependency.lua"/>
<sourceFile filename="adChainData.lua"/>
<sourceFile filename="adChain.lua"/>
<sourceFile filename="adChainRun.lua"/>