From 3702073c8c4c02755b74b69a43ab7611f6fce3ba Mon Sep 17 00:00:00 2001 From: masterdraco Date: Tue, 29 Sep 2026 18:30:54 +0200 Subject: [PATCH] =?UTF-8?q?feat(lagre):=20'Kr=C3=A6ver=20handling'=20?= =?UTF-8?q?=E2=86=92=20flow:=20l=C3=B8ber=20t=C3=B8r=20=3D=20=C3=A5bn/opre?= =?UTF-8?q?t=20forsyningsflow=20(eller=20'produc=C3=A9r/k=C3=B8b'=20n?= =?UTF-8?q?=C3=A5r=20et=20flow=20ikke=20hj=C3=A6lper),=20s=C3=A6lg=20nu=20?= =?UTF-8?q?=3D=20=C3=A5bn/opret=20salgsflow;=20hint=20p=C3=A5=20hver=20lin?= =?UTF-8?q?je=20(v1.60.0.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adSmartPickup.lua | 1 + FS25_ADSmartPickup/adStorageStats.lua | 37 +++++ .../gui/SmartPickupAttentionFlow.lua | 130 ++++++++++++++++++ .../gui/SmartPickupStorageView.lua | 11 +- FS25_ADSmartPickup/l10n/l10n_da.xml | 7 + FS25_ADSmartPickup/l10n/l10n_en.xml | 7 + FS25_ADSmartPickup/modDesc.xml | 2 +- tests/test_adStorageStats.lua | 20 +++ 8 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 FS25_ADSmartPickup/gui/SmartPickupAttentionFlow.lua 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 <en>AD Smart Pickup</en> 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)