diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua
index 33e381c..ef60141 100644
--- a/FS25_ADSmartPickup/adSmartPickup.lua
+++ b/FS25_ADSmartPickup/adSmartPickup.lua
@@ -32,6 +32,7 @@ if ADSmartPickup.dir ~= nil and source ~= nil then
source(ADSmartPickup.dir .. "gui/SmartPickupFieldGuide.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupFlowGuide.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupEditPanel.lua")
+ source(ADSmartPickup.dir .. "gui/SmartPickupAttentionFlow.lua")
end
-- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et
diff --git a/FS25_ADSmartPickup/adStorageStats.lua b/FS25_ADSmartPickup/adStorageStats.lua
index b6f2192..a4fe73f 100644
--- a/FS25_ADSmartPickup/adStorageStats.lua
+++ b/FS25_ADSmartPickup/adStorageStats.lua
@@ -283,3 +283,40 @@ function ADStorageStats.fraction(percent)
if percent == nil then return 0 end
return math.max(0, math.min(1, percent / 100))
end
+
+-- ---- "kræver handling" -> flow ----
+
+-- Bygninger der BRUGER varen (fabrik-input, stald-foder) — silo/objektlager lagrer kun. -> {{name, placeable, level}}
+function ADStorageStats.consumersOf(inventory, fillType)
+ local result = {}
+ for _, building in ipairs(inventory or {}) do
+ if building.kind == "factory" or building.kind == "husbandry" then
+ for _, entry in ipairs(building.entries or {}) do
+ if entry.fillType == fillType and not entry.isOutput then
+ table.insert(result, {name = building.name, placeable = building.placeable, level = entry.level or 0})
+ end
+ end
+ end
+ end
+ return result
+end
+
+-- Hjælper et flow? Et flow flytter kun varer: det nytter når varen ligger andre steder end hos forbrugerne.
+-- s = {farmLevel, consumerLevel, perDay, consumerCount, existingRunId} -> "open" | "flow" | "produce" | "none"
+ADStorageStats.MIN_FLOW_LITERS = 5000
+ADStorageStats.MIN_FLOW_DAY_SHARE = 0.25 -- mindst en kvart dags forbrug skal kunne flyttes
+
+function ADStorageStats.flowAdvice(s)
+ if (s.consumerCount or 0) == 0 then return "none" end
+ if s.existingRunId ~= nil then return "open" end
+ local elsewhere = (s.farmLevel or 0) - (s.consumerLevel or 0)
+ local needed = math.max(ADStorageStats.MIN_FLOW_LITERS, (s.perDay or 0) * ADStorageStats.MIN_FLOW_DAY_SHARE)
+ return elsewhere >= needed and "flow" or "produce"
+end
+
+-- "Sælg nu" -> udkørsels-flow: s = {sourceCount, existingRunId} -> "open" | "flow" | "none"
+function ADStorageStats.sellAdvice(s)
+ if (s.sourceCount or 0) == 0 then return "none" end
+ if s.existingRunId ~= nil then return "open" end
+ return "flow"
+end
diff --git a/FS25_ADSmartPickup/gui/SmartPickupAttentionFlow.lua b/FS25_ADSmartPickup/gui/SmartPickupAttentionFlow.lua
new file mode 100644
index 0000000..a058e4e
--- /dev/null
+++ b/FS25_ADSmartPickup/gui/SmartPickupAttentionFlow.lua
@@ -0,0 +1,130 @@
+-- Smart Pickup — "Kræver handling" -> flow (v1.60)
+-- En "løber tør"-linje i lagerfanerne kan klikkes: findes der et flow der leverer varen til forbrugerne, åbnes
+-- det; ligger varen andre steder på gården, åbnes "Nyt flow" udfyldt (ind til forbrugerne, varen valgt); ellers
+-- 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).
+
+local function fillTypeName(fillType)
+ return g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or tostring(fillType)
+end
+
+-- Forsyningsflow der allerede leverer varen til en af forbrugerne -> run eller nil.
+local function existingSupplyRun(consumerIds, name)
+ for _, run in ipairs(ADRunsController.store.runs) do
+ if run.kind ~= "outbound" then
+ local carries = run.ingredientMode ~= "manual"
+ for _, ingredient in ipairs(run.ingredients or {}) do
+ if ingredient == name then carries = true end
+ end
+ if carries then
+ for _, id in ipairs(run.targetBuildingIds or {}) do
+ if consumerIds[id] then return run end
+ end
+ end
+ end
+ end
+ return nil
+end
+
+-- Udkørselsflow der allerede sælger varen fra en af bygningerne -> run eller nil.
+local function existingSellRun(sourceIds, name)
+ for _, run in ipairs(ADRunsController.store.runs) do
+ if run.kind == "outbound" then
+ local sells = false
+ for _, product in ipairs(run.products or {}) do
+ if product.fillType == name and (product.policy == "sell" or product.policy == "sellElseStore") then sells = true end
+ end
+ if sells then
+ for _, id in ipairs(run.sourceBuildingIds or {}) do
+ if sourceIds[id] then return run end
+ end
+ end
+ end
+ end
+ return nil
+end
+
+-- "Sælg nu": kilder = fanens bygninger med varen.
+local function sellAdviceFor(stat)
+ local sourceIds, ids = {}, {}
+ for _, building in ipairs(stat.buildings or {}) do
+ local id = building.placeable ~= nil and ADBuildings.getId(building.placeable) or nil
+ if id ~= nil and not sourceIds[id] then
+ sourceIds[id] = true
+ table.insert(ids, id)
+ end
+ end
+ local name = fillTypeName(stat.fillType)
+ local run = existingSellRun(sourceIds, name)
+ local advice = ADStorageStats.sellAdvice({sourceCount = #ids, existingRunId = run ~= nil and run.id or nil})
+ return advice, {buildingIds = ids, run = run, name = name, stat = stat, sell = true}
+end
+
+-- entry fra ADStorageStats.attentionList -> advice ("open"|"flow"|"produce"|"none"), info
+function SmartPickupFrame:attentionAdvice(entry)
+ 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
+ local consumers = ADStorageStats.consumersOf(self.lastFullInventory, stat.fillType)
+ local consumerIds, consumerLevel, ids = {}, 0, {}
+ for _, consumer in ipairs(consumers) do
+ consumerLevel = consumerLevel + consumer.level
+ local id = consumer.placeable ~= nil and ADBuildings.getId(consumer.placeable) or nil
+ if id ~= nil and not consumerIds[id] then
+ consumerIds[id] = true
+ table.insert(ids, id)
+ end
+ end
+ local name = fillTypeName(stat.fillType)
+ local run = existingSupplyRun(consumerIds, name)
+ local advice = ADStorageStats.flowAdvice({farmLevel = stat.farmLevel or stat.level, consumerLevel = consumerLevel,
+ perDay = stat.consumption, consumerCount = #ids, existingRunId = run ~= nil and run.id or nil})
+ return advice, {consumers = consumers, buildingIds = ids, run = run, name = name, stat = stat}
+end
+
+function SmartPickupFrame:attentionHint(advice, info)
+ 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
+ return ""
+end
+
+function SmartPickupFrame:onAttentionClick(entry)
+ self:safe(function(frame)
+ local adEnv = frame.getAdEnv()
+ local advice, info = frame:attentionAdvice(entry)
+ if adEnv == nil or advice == "none" then return end
+ if advice == "open" then
+ frame.selectedRunId = info.run.id
+ frame:switchTab(SmartPickupFrame.TAB_FLOWS)
+ frame:openEditPanel("flow", info.run.id)
+ return
+ end
+ if advice == "produce" then
+ local text = string.format(frame.i18n:getText("spu_attention_produce"), tostring(info.stat.title),
+ SmartPickupFrame.formatCompact(info.stat.farmLevel or 0))
+ frame.statusOverride, frame.statusOverrideTicks = text, SmartPickupFrame.STATUS_OVERRIDE_TICKS
+ if ADFieldJobs ~= nil and ADFieldJobs.notify ~= nil then ADFieldJobs.notify(text, false) end
+ return
+ end
+ local guide
+ if info.sell then
+ -- udfyldt "Nyt flow": ud af bygningerne med varen, varen til salg
+ local name = string.format(frame.i18n:getText("spu_attention_sellFlowName"), tostring(info.stat.title))
+ guide = ADFlowGuide.openNew(name, {})
+ guide.draft.kind = "outbound"
+ guide.draft.sourceBuildingIds = info.buildingIds
+ guide.draft.products = {{fillType = info.name, policy = "sell"}}
+ else
+ -- udfyldt "Nyt flow": ind til forbrugerne, kun denne vare; guiden åbnes på oversigten (Gem/Start eller [Ret])
+ local name = string.format(frame.i18n:getText("spu_attention_flowName"), tostring(info.stat.title), tostring(info.consumers[1].name))
+ guide = ADFlowGuide.openNew(name, {})
+ guide.draft.targetBuildingIds = info.buildingIds
+ guide.draft.ingredientMode = "manual"
+ guide.draft.ingredients = {info.name}
+ end
+ guide = ADGuide.new(ADFlowGuide.DEF, guide.draft, frame:flowGuideContext(adEnv, guide.draft), {step = "summary"})
+ frame:switchTab(SmartPickupFrame.TAB_FLOWS)
+ frame:openGuide("flow", guide)
+ end)
+end
diff --git a/FS25_ADSmartPickup/gui/SmartPickupStorageView.lua b/FS25_ADSmartPickup/gui/SmartPickupStorageView.lua
index d9aff7c..38129c3 100644
--- a/FS25_ADSmartPickup/gui/SmartPickupStorageView.lua
+++ b/FS25_ADSmartPickup/gui/SmartPickupStorageView.lua
@@ -76,6 +76,7 @@ end
-- Fanens varer med statistik; gårdens totaler/kapacitet/rater regnes over HELE gården.
function SmartPickupFrame:computeItemStats(adEnv, inventory, fullInventory)
+ self.lastFullInventory = fullInventory -- til "kræver handling" -> flow (hvem bruger varen)
local farmId = g_currentMission:getFarmId()
local totals = ADStorageStats.farmTotals(fullInventory)
local capacities = ADStorageStats.farmCapacities(fullInventory)
@@ -196,7 +197,13 @@ function SmartPickupFrame:alertText(entry)
end
function SmartPickupFrame:setAlertRow(row, entry)
- row.c1:setText(self:alertText(entry))
+ row.entry = entry
+ local hint = ""
+ if SmartPickupFrame.attentionAdvice ~= nil then
+ local ok, advice, info = pcall(self.attentionAdvice, self, entry)
+ if ok then hint = self:attentionHint(advice, info) end
+ end
+ row.c1:setText(self:alertText(entry) .. hint)
if row.dot ~= nil and row.dot.setImageColor ~= nil then
row.dot:setImageColor(nil, unpackFn(SmartPickupFrame.LEVEL_COLORS[entry.level] or SmartPickupFrame.LEVEL_COLORS.green))
end
@@ -267,6 +274,8 @@ function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory)
local element = self:addRow(self.alertPrefab, {kind = "text"})
local row = {c1 = element:getDescendantByName("c1"), dot = element:getDescendantByName("dot")}
self:setAlertRow(row, entry)
+ -- klik: åbn/opret flow til varen (gui/SmartPickupAttentionFlow.lua)
+ self:makeRowClickable(element, function(frame) frame:onAttentionClick(row.entry) end)
table.insert(self.itemView.alertRows, row)
end
self:addTextRow(i18n:getText("spu_section_storage_" .. tostring(SmartPickupFrame.getTabKind(self.currentTab))) .. " " .. i18n:getText("spu_items_hint"))
diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml
index 16d486e..3f27092 100644
--- a/FS25_ADSmartPickup/l10n/l10n_da.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_da.xml
@@ -549,6 +549,13 @@
+
+
+
+
+
+
+
diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml
index 4ec628a..52b8fe4 100644
--- a/FS25_ADSmartPickup/l10n/l10n_en.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_en.xml
@@ -549,6 +549,13 @@
+
+
+
+
+
+
+
diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml
index 29394cd..abfd84f 100644
--- a/FS25_ADSmartPickup/modDesc.xml
+++ b/FS25_ADSmartPickup/modDesc.xml
@@ -1,7 +1,7 @@
masterdraco
- 1.59.11.0
+ 1.60.0.0
AD Smart Pickup
diff --git a/tests/test_adStorageStats.lua b/tests/test_adStorageStats.lua
index af628a9..80486d3 100644
--- a/tests/test_adStorageStats.lua
+++ b/tests/test_adStorageStats.lua
@@ -182,5 +182,25 @@ local shedGroups = S.groupByFillType({
}, titleOf)
check("SV11 procent nil ved ubegrænset skur", shedGroups[1].percent, nil)
+-- "kræver handling" -> opret flow: hvem bruger varen, og hjælper et flow overhovedet? (brugerønske 29/9)
+do
+ local inv = {
+ {name = "Mølle", kind = "factory", placeable = "pMill", entries = {{fillType = 7, level = 1000, isOutput = false}, {fillType = 8, level = 50, isOutput = true}}},
+ {name = "Silo", kind = "silo", placeable = "pSilo", entries = {{fillType = 7, level = 90000}}},
+ {name = "Kostald", kind = "husbandry", placeable = "pBarn", entries = {{fillType = 7, level = 500, isOutput = false}}},
+ }
+ local consumers = S.consumersOf(inv, 7)
+ check("forbrugere: fabrik + stald (ikke silo)", #consumers, 2)
+ check("forbrugere: output tæller ikke", #S.consumersOf(inv, 8), 0)
+ check("råd: eksisterende flow -> åbn det", S.flowAdvice({farmLevel = 91500, consumerLevel = 1500, perDay = 20000, consumerCount = 2, existingRunId = 4}), "open")
+ check("råd: lager andre steder -> opret flow", S.flowAdvice({farmLevel = 91500, consumerLevel = 1500, perDay = 20000, consumerCount = 2}), "flow")
+ check("råd: intet at flytte -> producér/køb", S.flowAdvice({farmLevel = 12700, consumerLevel = 12700, perDay = 228000, consumerCount = 1}), "produce")
+ check("råd: lidt andre steder, stort forbrug -> producér/køb", S.flowAdvice({farmLevel = 20000, consumerLevel = 12000, perDay = 228000, consumerCount = 1}), "produce")
+ check("råd: ingen forbrugere -> intet", S.flowAdvice({farmLevel = 5000, consumerLevel = 0, perDay = 0, consumerCount = 0}), "none")
+end
+-- "sælg nu" -> udkørsels-flow (brugerønske 29/9)
+check("salg: eksisterende salgsflow -> åbn", S.sellAdvice({sourceCount = 1, existingRunId = 3}), "open")
+check("salg: ingen -> opret flow", S.sellAdvice({sourceCount = 2}), "flow")
+check("salg: ingen bygning med varen -> intet", S.sellAdvice({sourceCount = 0}), "none")
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)