Files
fs25-adsmartpickup/FS25_ADSmartPickup/gui/SmartPickupStorageView.lua
T
masterdraco 979b7f72d5 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.
2026-09-30 15:08:28 +02:00

356 lines
17 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- Smart Pickup — fanerne Lagre, vareoversigten (v1.52)
-- Én række pr. vare med samlet beholdning, fyldningsbjælke, værdi og gårdens netto; klik folder varen ud
-- til bygningerne den ligger i. Øverst nøgletal og "kræver handling". Tallene regnes i ADStorageStats;
-- her tegnes kun. Indlæses efter SmartPickupStorageTab.lua (funktionerne lægges på SmartPickupFrame).
SmartPickupFrame.STORAGE_VIEW_ITEMS, SmartPickupFrame.STORAGE_VIEW_BUILDINGS = 1, 2
SmartPickupFrame.MAX_ATTENTION = 4
-- nøgletalsrækken er højere end de andre rækker (tal over label)
SmartPickupFrame.KPI_ROW_SCALE = 1.6
-- ændret struktur (ny vare, flere alarmer) genopbygges højst hvert N. sekund, så fokus ikke hopper
SmartPickupFrame.ITEM_REBUILD_TICKS = 10
SmartPickupFrame.LEVEL_COLORS = {
red = {0.90, 0.25, 0.20, 1},
yellow = {0.95, 0.72, 0.15, 1},
green = {0.30, 0.75, 0.35, 1},
}
SmartPickupFrame.NEUTRAL_BAR_COLOR = {0.35, 0.60, 0.85, 1}
SmartPickupFrame.TEXT_COLOR = {1, 1, 1, 1}
local unpackFn = table.unpack or unpack
-- ---- små tegne-hjælpere ----
local function setTextColor(element, color)
if element ~= nil and element.setTextColor ~= nil then element:setTextColor(unpackFn(color)) end
end
-- Fyld bjælken til brøken af baggrundens bredde; tom bjælke skjules (bredde 0 tegnes som en prik).
function SmartPickupFrame.setBar(bar, background, fraction, color)
if bar == nil or background == nil or background.size == nil then return end
if fraction <= 0 then
bar:setVisible(false)
return
end
bar:setVisible(true)
bar:setSize(background.size[1] * fraction, background.size[2])
if bar.setImageColor ~= nil then bar:setImageColor(nil, unpackFn(color)) end
end
-- Bjælkens farve: rød/gul ved problem, ellers grøn for varer i omløb og blå for rent lager.
function SmartPickupFrame.barColor(level, isActive)
if level ~= nil then return SmartPickupFrame.LEVEL_COLORS[level] end
return isActive and SmartPickupFrame.LEVEL_COLORS.green or SmartPickupFrame.NEUTRAL_BAR_COLOR
end
function SmartPickupFrame:formatDays(days)
return string.format(self.i18n:getText("spu_stat_days"), days)
end
-- "−14.4k/døgn · 3.2 døgn", "+3.6k/døgn · fuld om 2.0 døgn", "+1.2k/døgn · stiger" eller "-"
function SmartPickupFrame:itemRateText(stat)
local fc = SmartPickupFrame.formatCompact
if (stat.net or 0) == 0 then
return (stat.consumption or 0) > 0 and self.i18n:getText("spu_item_balanced") or "-"
end
local rate = string.format(self.i18n:getText("spu_item_perDay"), (stat.net < 0 and "-" or "+") .. fc(math.abs(stat.net)))
if stat.days ~= nil then
return rate .. " · " .. self:formatDays(stat.days)
elseif stat.daysToFull ~= nil then
return rate .. " · " .. string.format(self.i18n:getText("spu_item_fullIn"), self:formatDays(stat.daysToFull))
end
return rate .. " · " .. self.i18n:getText("spu_stat_rising")
end
-- "1.2M/1.5M · 80 %" eller "24 paller · 1 %"
function SmartPickupFrame:itemAmountText(entry)
local fc = SmartPickupFrame.formatCompact
local amount = entry.count ~= nil and string.format(self.i18n:getText("spu_storage_pallets"), entry.count)
or string.format("%s/%s", fc(entry.level or 0), fc(entry.capacity or 0))
if entry.percent ~= nil then amount = amount .. string.format(" · %d %%", entry.percent) end
return amount
end
-- ---- data ----
-- 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)
local rates = ADSources.getFarmRates ~= nil and ADSources.getFarmRates(adEnv, farmId) or {}
local stats = {}
local titleOf = function(fillType) return self:fillTypeTitle(fillType) end
for _, group in ipairs(ADStorageStats.groupByFillType(inventory, titleOf)) do
local quote = ADSources.getMarketQuote ~= nil and ADSources.getMarketQuote(group.fillType) or nil
local farm = {level = totals[group.fillType], capacity = capacities[group.fillType]}
table.insert(stats, ADStorageStats.groupStats(group, farm, rates[group.fillType], quote))
end
return stats
end
-- Nøgle der ændrer sig når rækkerne skal være anderledes (varer, udfoldede bygninger, alarmer).
function SmartPickupFrame:itemStructureKey(stats, attention)
local parts = {}
for _, stat in ipairs(stats) do
table.insert(parts, tostring(stat.fillType))
if self:isItemExpanded(stat.fillType) then
-- sorteret: at to bygninger bytter plads i mængde må ikke udløse en genopbygning
local placeables = {}
for _, building in ipairs(stat.buildings) do table.insert(placeables, tostring(building.placeable)) end
table.sort(placeables)
table.insert(parts, table.concat(placeables, ";"))
end
end
table.insert(parts, "a" .. #attention)
return table.concat(parts, ",")
end
function SmartPickupFrame:isItemExpanded(fillType)
local expanded = self.expandedItems ~= nil and self.expandedItems[self.currentTab] or nil
return expanded ~= nil and expanded[fillType] == true
end
function SmartPickupFrame:toggleItem(fillType)
self.expandedItems = self.expandedItems or {}
self.expandedItems[self.currentTab] = self.expandedItems[self.currentTab] or {}
local expanded = self.expandedItems[self.currentTab]
expanded[fillType] = not expanded[fillType] or nil
-- listen hopper ellers til toppen ved genopbygningen
local layout = self.boxLayout
local scrollY = layout ~= nil and layout.firstVisibleY or nil
self:rebuild()
if scrollY ~= nil and layout.scrollTo ~= nil then pcall(layout.scrollTo, layout, scrollY, true) end
end
-- ---- visningsvælger (Varer | Bygninger) ----
function SmartPickupFrame:getStorageView()
local views = self.storageViewByTab or {}
return views[self.currentTab] or SmartPickupFrame.STORAGE_VIEW_ITEMS
end
function SmartPickupFrame:addStorageViewRow()
local row = self:addRow(self.productPrefab, {kind = "storageView"})
local option = row:getDescendantByName("option")
option:setTexts({self.i18n:getText("spu_view_items"), self.i18n:getText("spu_view_buildings")})
option:setState(self:getStorageView(), false)
row:getDescendantByName("title"):setText(self.i18n:getText("spu_view_title"))
local tooltip = option:getDescendantByName("ignore")
if tooltip ~= nil then tooltip:setText(self.i18n:getText("spu_view_tooltip")) end
option.toolTipText = self.i18n:getText("spu_view_tooltip")
end
function SmartPickupFrame:onClickStorageView(state)
self.storageViewByTab = self.storageViewByTab or {}
self.storageViewByTab[self.currentTab] = state
self:requestRebuild()
end
-- ---- opbygning ----
function SmartPickupFrame:kpiTexts(stats)
local kpi = ADStorageStats.kpis(stats)
local i18n = self.i18n
return {
{SmartPickupFrame.formatMoney(kpi.valueNow), i18n:getText("spu_kpi_value")},
{"+" .. SmartPickupFrame.formatMoney(kpi.gain), i18n:getText("spu_kpi_gain")},
{tostring(kpi.emptyCount), i18n:getText("spu_kpi_empty")},
{tostring(kpi.fullCount), i18n:getText("spu_kpi_full")},
}, kpi
end
function SmartPickupFrame:setKpiRow(row, stats)
local texts, kpi = self:kpiTexts(stats)
for index, pair in ipairs(texts) do
local value = row:getDescendantByName("v" .. index)
if value ~= nil then value:setText(pair[1]) end
local label = row:getDescendantByName("l" .. index)
if label ~= nil then label:setText(pair[2]) end
end
local colors = SmartPickupFrame.LEVEL_COLORS
setTextColor(row:getDescendantByName("v2"), kpi.gain > 0 and colors.green or SmartPickupFrame.TEXT_COLOR)
setTextColor(row:getDescendantByName("v3"), kpi.emptyCount > 0 and colors.red or SmartPickupFrame.TEXT_COLOR)
setTextColor(row:getDescendantByName("v4"), kpi.fullCount > 0 and colors.yellow or SmartPickupFrame.TEXT_COLOR)
end
function SmartPickupFrame:addKpiRow(stats)
local row = self:addRow(self.kpiPrefab, {kind = "text"})
if row.size ~= nil then row:setSize(row.size[1], row.size[2] * SmartPickupFrame.KPI_ROW_SCALE) end
self:setKpiRow(row, stats)
return row
end
-- 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
function SmartPickupFrame:alertText(entry)
local stat, fc, i18n = entry.stat, SmartPickupFrame.formatCompact, self.i18n
if entry.kind == "chainSuggest" then
return string.format(i18n:getText("spu_attention_chainSuggests"), entry.count or 0)
end
if entry.kind == "chainProblem" then
local text = string.format(i18n:getText("spu_attention_chainProblem"), tostring(entry.rootTitle), tostring(entry.itemsText))
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
if stat.daysToFull ~= nil then
return string.format(i18n:getText("spu_alert_fullIn"), stat.title, stat.percent or 0, self:formatDays(stat.daysToFull))
end
return string.format(i18n:getText("spu_alert_full"), stat.title, stat.percent or 0)
end
return string.format(i18n:getText("spu_alert_sell"), stat.title, math.floor((stat.priceRatio or 0) * 100 + 0.5), SmartPickupFrame.formatMoney(stat.valueNow))
end
function SmartPickupFrame:setAlertRow(row, 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
end
function SmartPickupFrame:itemCells(stat)
local marker = self:isItemExpanded(stat.fillType) and "– " or "+ "
local value = stat.valueNow ~= nil and SmartPickupFrame.formatMoney(stat.valueNow) or self.i18n:getText("spu_stat_noSell")
return {marker .. stat.title, self:itemAmountText(stat), value, self:itemRateText(stat)}
end
function SmartPickupFrame:setItemRow(row, stat)
SmartPickupFrame.setCells(row.cells, self:itemCells(stat))
local isActive = (stat.consumption or 0) > 0 or (stat.production or 0) > 0
SmartPickupFrame.setBar(row.bar, row.barBg, ADStorageStats.fraction(stat.percent), SmartPickupFrame.barColor(stat.severity, isActive))
local rateColor = SmartPickupFrame.TEXT_COLOR
if stat.emptyLevel ~= nil then
rateColor = SmartPickupFrame.LEVEL_COLORS[stat.emptyLevel]
elseif stat.fullLevel ~= nil then
rateColor = SmartPickupFrame.LEVEL_COLORS[stat.fullLevel]
end
setTextColor(row.cells[4], rateColor)
end
-- Bygningens andel af varen: værdi efter samme pris som varen, bjælke efter bygningens egen fyldning.
function SmartPickupFrame:setSubRow(row, stat, building)
local kind = SmartPickupFrame.getTabKind(self.currentTab)
local isStableOutput = kind == "husbandry" and (stat.production or 0) > 0 and (stat.consumption or 0) == 0
local value = "-"
if stat.valueNow ~= nil and stat.level > 0 then
value = SmartPickupFrame.formatMoney(stat.valueNow * building.level / stat.level)
end
local share = stat.level > 0 and math.floor(100 * building.level / stat.level + 0.5) or 0
SmartPickupFrame.setCells(row.cells, {tostring(building.name), self:itemAmountText(building), value,
string.format(self.i18n:getText("spu_item_share"), share)})
local level = ADStorageStats.buildingLevel(building, kind, isStableOutput)
SmartPickupFrame.setBar(row.bar, row.barBg, ADStorageStats.fraction(building.percent), SmartPickupFrame.barColor(level, false))
end
-- Klon af item-/sub-prefab med referencer til cellerne og bjælken.
function SmartPickupFrame:addBarRow(prefab)
local row = self:addRow(prefab, {kind = "text"})
local cells = {}
for index = 1, 4 do cells[index] = row:getDescendantByName("c" .. index) end
return {element = row, cells = cells, bar = row:getDescendantByName("bar"), barBg = row:getDescendantByName("barBg")}
end
function SmartPickupFrame:addItemHeader()
local i18n = self.i18n
local header = self:addBarRow(self.itemPrefab)
SmartPickupFrame.setCells(header.cells, {i18n:getText("spu_col_item"), i18n:getText("spu_icol_amount"),
i18n:getText("spu_icol_value"), i18n:getText("spu_icol_rate")})
if header.bar ~= nil then header.bar:setVisible(false) end
if header.barBg ~= nil then header.barBg:setVisible(false) end
end
function SmartPickupFrame:fillItemOverview(adEnv, inventory, fullInventory)
local i18n = self.i18n
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
local attention = self:attentionFor(stats)
self.itemView = {kpiRow = self:addKpiRow(stats), alertRows = {}, itemRows = {}, key = self:itemStructureKey(stats, attention)}
self.itemRebuildTicks = SmartPickupFrame.ITEM_REBUILD_TICKS
self:addTextRow(i18n:getText("spu_attention"))
if #attention == 0 then
self:addTextRow(i18n:getText("spu_attention_none"))
end
for _, entry in ipairs(attention) do
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"))
if #stats == 0 then
self:addTextRow(i18n:getText("spu_storage_empty"))
return
end
self:addItemHeader()
for _, stat in ipairs(stats) do
local row = self:addBarRow(self.itemPrefab)
local fillType = stat.fillType
self:makeRowClickable(row.element, function(frame) frame:toggleItem(fillType) end)
self:setItemRow(row, stat)
row.subs = {}
if self:isItemExpanded(fillType) then
for _, building in ipairs(stat.buildings) do
local sub = self:addBarRow(self.subPrefab)
sub.placeable = building.placeable
self:setSubRow(sub, stat, building)
table.insert(row.subs, sub)
end
end
table.insert(self.itemView.itemRows, row)
end
end
-- Hvert sekund: tal og bjælker opdateres på stedet; ny struktur -> genopbygning (højst hvert 10. s).
function SmartPickupFrame:refreshItemOverview(adEnv, inventory, fullInventory)
local view = self.itemView
if view == nil then return end
local stats = self:computeItemStats(adEnv, inventory, fullInventory)
local attention = self:attentionFor(stats)
if self:itemStructureKey(stats, attention) ~= view.key then
self.itemRebuildTicks = (self.itemRebuildTicks or 0) - 1
if self.itemRebuildTicks <= 0 then self:requestRebuild() end
return
end
self:setKpiRow(view.kpiRow, stats)
for index, entry in ipairs(attention) do self:setAlertRow(view.alertRows[index], entry) end
for index, stat in ipairs(stats) do
local row = view.itemRows[index]
self:setItemRow(row, stat)
-- underrækkerne beholder rækkefølgen fra opbygningen; tallene findes via bygningen
local byPlaceable = {}
for _, building in ipairs(stat.buildings) do byPlaceable[building.placeable] = building end
for _, sub in ipairs(row.subs) do
local building = byPlaceable[sub.placeable]
if building ~= nil then self:setSubRow(sub, stat, building) end
end
end
end