diff --git a/FS25_ADSmartPickup/adSources.lua b/FS25_ADSmartPickup/adSources.lua
index 4a332d8..e61ac47 100644
--- a/FS25_ADSmartPickup/adSources.lua
+++ b/FS25_ADSmartPickup/adSources.lua
@@ -624,6 +624,92 @@ function ADSources.getFarmDemand(adEnv, fillType, farmId, excludedPlaceables)
return {liters = total, consumers = consumers}
end
+-- ---- forbrug og produktion pr. døgn (fanen Lagre) ----
+
+local function addRate(rates, fillType, field, liters)
+ if fillType == nil or liters == nil or liters <= 0 then return end
+ rates[fillType] = rates[fillType] or {consumption = 0, production = 0}
+ rates[fillType][field] = rates[fillType][field] + liters
+end
+
+local function addFactoryRates(rates, placeable)
+ local productionPoint = getProductionPoint(placeable)
+ for _, production in ipairs(productionPoint ~= nil and productionPoint.productions or {}) do
+ if isProductionWanted(productionPoint, production) then
+ local perDay = (production.cyclesPerMinute or ((production.cyclesPerHour or 0) / 60)) * 1440
+ for _, input in ipairs(production.inputs or {}) do addRate(rates, input.type, "consumption", (input.amount or 0) * perDay) end
+ for _, output in ipairs(production.outputs or {}) do addRate(rates, output.type, "production", (output.amount or 0) * perDay) end
+ end
+ end
+end
+
+local function getFoodWeights(placeable)
+ local foodSpec = placeable.spec_husbandryFood
+ local system = g_currentMission ~= nil and g_currentMission.animalFoodSystem or nil
+ local weights = {}
+ local food = system ~= nil and system.getAnimalFood ~= nil and foodSpec ~= nil and system:getAnimalFood(foodSpec.animalTypeIndex) or nil
+ for _, group in pairs(food ~= nil and food.groups or {}) do
+ local weight = tonumber(group.productionWeight) or tonumber(group.eatWeight) or 1
+ for _, fillType in pairs(group.fillTypes or {}) do weights[fillType] = math.max(weights[fillType] or 0, weight) end
+ end
+ return weights
+end
+
+local function addHusbandryRates(rates, placeable)
+ local food, straw, water = placeable.spec_husbandryFood, placeable.spec_husbandryStraw, placeable.spec_husbandryWater
+ local byName = function(name) return g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeIndexByName ~= nil and g_fillTypeManager:getFillTypeIndexByName(name) or nil end
+ local nameToIndex = function(name)
+ local index = byName(name)
+ if index == nil and g_fillTypeManager ~= nil then
+ for i = 1, 256 do if g_fillTypeManager:getFillTypeNameByIndex(i) == name then return i end end
+ end
+ return index
+ end
+ if food ~= nil and (food.litersPerHour or 0) > 0 then
+ for fillType, liters in pairs(ADStorageStats.splitTroughConsumption(food.litersPerHour * 24, food.fillLevels, getFoodWeights(placeable))) do
+ addRate(rates, fillType, "consumption", liters)
+ end
+ end
+ if straw ~= nil and (straw.inputLitersPerHour or 0) > 0 then
+ addRate(rates, nameToIndex("STRAW"), "consumption", straw.inputLitersPerHour * 24)
+ end
+ if water ~= nil and not water.automaticWaterSupply and (water.litersPerHour or 0) > 0 then
+ addRate(rates, nameToIndex("WATER"), "consumption", water.litersPerHour * 24)
+ end
+end
+
+-- {[fillType] = {consumption, production}} i liter pr. døgn for gårdens fabrikker og stalde.
+function ADSources.getFarmRates(adEnv, farmId)
+ local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
+ local rates = {}
+ for _, placeable in pairs(placeables) do
+ if isOwnedBy(placeable, farmId) then
+ if getProductionPoint(placeable) ~= nil then
+ addFactoryRates(rates, placeable)
+ elseif placeable.getHusbandryFillLevel ~= nil then
+ addHusbandryRates(rates, placeable)
+ end
+ end
+ end
+ return rates
+end
+
+-- Bedste pris lige nu blandt alle spillets salgssteder der tager varen (uanset markør/vejnet):
+-- {price, top} pr. liter; top = prisen ved årets højeste periode. nil når ingen tager varen.
+function ADSources.getMarketQuote(fillType)
+ local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
+ local best = nil
+ for _, station in pairs(storage ~= nil and storage:getUnloadingStations() or {}) do
+ if ADPrices.isSellingStation(station) and ADSources.stationAccepts(station, fillType) then
+ local quote = ADPrices.getQuote(station, fillType)
+ if quote ~= nil and (best == nil or quote.price > best.price) then
+ best = {price = quote.price, top = quote.top}
+ end
+ end
+ end
+ return best
+end
+
-- ---- inventar: alt gården har liggende, pr. bygning ----
local function addEntry(entries, fillType, level, capacity, extra)
diff --git a/FS25_ADSmartPickup/adStorageStats.lua b/FS25_ADSmartPickup/adStorageStats.lua
new file mode 100644
index 0000000..a457a07
--- /dev/null
+++ b/FS25_ADSmartPickup/adStorageStats.lua
@@ -0,0 +1,67 @@
+-- AD Storage Stats
+-- Rene beregninger (ingen spil-API) til fanen Lagre: pr. vare i en bygning fyldning, værdi nu og
+-- ved årets toppris, gårdens samlede beholdning og hvor mange døgn den rækker ved gårdens forbrug.
+
+ADStorageStats = {}
+
+-- item = {fillType, level, capacity}; farmTotal = liter af varen på hele gården;
+-- rate = {consumption, production} liter pr. døgn (eller nil); quote = {price, top} pr. liter (eller nil).
+function ADStorageStats.itemStats(item, farmTotal, rate, quote)
+ local level = item.level or 0
+ local capacity = item.capacity or 0
+ local consumption = rate ~= nil and rate.consumption or 0
+ local production = rate ~= nil and rate.production or 0
+ return {
+ fillType = item.fillType,
+ level = level,
+ capacity = capacity,
+ percent = capacity > 0 and math.floor(100 * level / capacity + 0.5) or nil,
+ valueNow = quote ~= nil and quote.price ~= nil and math.floor(level * quote.price + 0.5) or nil,
+ valueTop = quote ~= nil and quote.top ~= nil and math.floor(level * quote.top + 0.5) or nil,
+ farmTotal = farmTotal or level,
+ consumption = consumption,
+ production = production,
+ net = production - consumption,
+ days = consumption > 0 and (farmTotal or level) / consumption or nil,
+ }
+end
+
+function ADStorageStats.buildingTotals(stats)
+ local now, top = 0, 0
+ for _, stat in ipairs(stats) do
+ now = now + (stat.valueNow or 0)
+ top = top + (stat.valueTop or 0)
+ end
+ return {valueNow = now, valueTop = top}
+end
+
+-- inventory = { {entries = {{fillType, level}}} } -> {[fillType] = liter}
+function ADStorageStats.farmTotals(inventory)
+ local totals = {}
+ for _, building in ipairs(inventory or {}) do
+ for _, entry in ipairs(building.entries or {}) do
+ totals[entry.fillType] = (totals[entry.fillType] or 0) + (entry.level or 0)
+ end
+ end
+ return totals
+end
+
+-- Et stald-trug æder `perDay` liter i alt; fordel på fodertyperne efter hvad truget indeholder.
+-- Tomt trug: alt på foderet med højest vægt (det der hentes først).
+function ADStorageStats.splitTroughConsumption(perDay, levels, weights)
+ local total = 0
+ for _, level in pairs(levels or {}) do total = total + (level or 0) end
+ local result = {}
+ if total > 0 then
+ for fillType, level in pairs(levels) do
+ if (level or 0) > 0 then result[fillType] = perDay * level / total end
+ end
+ return result
+ end
+ local best, bestWeight = nil, -1
+ for fillType, weight in pairs(weights or {}) do
+ if weight > bestWeight or (weight == bestWeight and fillType < best) then best, bestWeight = fillType, weight end
+ end
+ if best ~= nil then result[best] = perDay end
+ return result
+end
diff --git a/FS25_ADSmartPickup/gui/SmartPickupFrame.lua b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
index 5385b81..7f1d091 100644
--- a/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
+++ b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
@@ -288,6 +288,7 @@ function SmartPickupFrame:clearRows()
self.overviewRows = {}
self.productRows, self.outboundStatusRows = {}, {}
self.storageRows = {}
+ self.storageDetailRows = {}
self.boardRows = {}
self.ingredientModeRowOption, self.collectRoundRowOption, self.directionRowOption = nil, nil, nil
end
diff --git a/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua b/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua
index b365c24..6c33959 100644
--- a/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua
+++ b/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua
@@ -178,6 +178,10 @@ function SmartPickupFrame:onClickSource(state, element)
end
function SmartPickupFrame:onClickProduct(state, element)
+ local clicked = self.rowData[element]
+ if clicked ~= nil and clicked.kind == "storageBuilding" then
+ return self:safe(function(frame) frame:onClickStorageBuilding(state) end)
+ end
self:safe(function(frame)
local run, data = frame:getRun(), frame.rowData[element]
if run == nil or data == nil or data.kind ~= "product" then return end
diff --git a/FS25_ADSmartPickup/gui/SmartPickupStorageTab.lua b/FS25_ADSmartPickup/gui/SmartPickupStorageTab.lua
index 02472f3..8a6b018 100644
--- a/FS25_ADSmartPickup/gui/SmartPickupStorageTab.lua
+++ b/FS25_ADSmartPickup/gui/SmartPickupStorageTab.lua
@@ -26,10 +26,114 @@ function SmartPickupFrame:getInventory(adEnv)
return ADSources.getFarmInventory(adEnv, g_currentMission:getFarmId())
end
+-- ---- detaljer for den valgte bygning ----
+
+function SmartPickupFrame.formatMoney(value)
+ if value == nil then return "-" end
+ if g_i18n ~= nil and g_i18n.formatMoney ~= nil then
+ local ok, text = pcall(g_i18n.formatMoney, g_i18n, value, 0, true, true)
+ if ok and text ~= nil then return text end
+ end
+ return string.format("%d", math.floor(value + 0.5))
+end
+
+function SmartPickupFrame:fillTypeTitle(fillType)
+ local title = g_fillTypeManager.getFillTypeTitleByIndex ~= nil and g_fillTypeManager:getFillTypeTitleByIndex(fillType) or nil
+ return tostring(title or g_fillTypeManager:getFillTypeNameByIndex(fillType))
+end
+
+-- Beregner stats for alle varer i bygningen (gårdens totaler og rater regnes én gang pr. opdatering).
+function SmartPickupFrame:computeBuildingStats(adEnv, entry, inventory)
+ local farmId = g_currentMission:getFarmId()
+ local totals = ADStorageStats.farmTotals(inventory)
+ local rates = ADSources.getFarmRates ~= nil and ADSources.getFarmRates(adEnv, farmId) or {}
+ local stats = {}
+ for _, item in ipairs(entry.entries) do
+ local quote = ADSources.getMarketQuote ~= nil and ADSources.getMarketQuote(item.fillType) or nil
+ table.insert(stats, ADStorageStats.itemStats(item, totals[item.fillType], rates[item.fillType], quote))
+ end
+ return stats
+end
+
+-- "Sukker — 150.0k/180.0k (83 %) · nu 240.300 kr · top 300.400 kr · gården 412.0k = 3.2 døgn · forbrug 130.0k/døgn · prod. 48.0k/døgn"
+function SmartPickupFrame:describeStat(stat)
+ local i18n = self.i18n
+ local fc = SmartPickupFrame.formatCompact
+ local parts = {string.format("%s — %s/%s%s", self:fillTypeTitle(stat.fillType), fc(stat.level), fc(stat.capacity), stat.percent ~= nil and string.format(" (%d %%)", stat.percent) or "")}
+ if stat.valueNow ~= nil then
+ table.insert(parts, string.format(i18n:getText("spu_stat_now"), SmartPickupFrame.formatMoney(stat.valueNow)))
+ table.insert(parts, string.format(i18n:getText("spu_stat_top"), SmartPickupFrame.formatMoney(stat.valueTop)))
+ else
+ table.insert(parts, i18n:getText("spu_stat_noSell"))
+ end
+ local days = stat.days ~= nil and string.format(i18n:getText("spu_stat_days"), stat.days) or i18n:getText("spu_stat_noUse")
+ table.insert(parts, string.format(i18n:getText("spu_stat_farm"), fc(stat.farmTotal), days))
+ if stat.consumption > 0 then table.insert(parts, string.format(i18n:getText("spu_stat_use"), fc(stat.consumption))) end
+ if stat.production > 0 then table.insert(parts, string.format(i18n:getText("spu_stat_prod"), fc(stat.production))) end
+ return " " .. table.concat(parts, " · ")
+end
+
+function SmartPickupFrame:describeStatTotals(stats)
+ local totals = ADStorageStats.buildingTotals(stats)
+ return string.format(" " .. self.i18n:getText("spu_stat_total"), SmartPickupFrame.formatMoney(totals.valueNow), SmartPickupFrame.formatMoney(totals.valueTop))
+end
+
+-- Navne til vælgeren; ens navne (40 Farma-siloer) får løbenummer.
+function SmartPickupFrame:storageLabels(inventory)
+ local counts, seen, labels = {}, {}, {}
+ for _, entry in ipairs(inventory) do counts[entry.name] = (counts[entry.name] or 0) + 1 end
+ for index, entry in ipairs(inventory) do
+ seen[entry.name] = (seen[entry.name] or 0) + 1
+ labels[index] = counts[entry.name] > 1 and string.format("%s #%d", entry.name, seen[entry.name]) or entry.name
+ end
+ return labels
+end
+
+function SmartPickupFrame:fillStorageDetail(adEnv, inventory)
+ self.storageDetailRows = {}
+ if #inventory == 0 then
+ return
+ end
+ self.storageIndex = math.max(1, math.min(self.storageIndex or 1, #inventory))
+ local row = self:addRow(self.productPrefab, {kind = "storageBuilding"})
+ local option = row:getDescendantByName("option")
+ option:setTexts(self:storageLabels(inventory))
+ option:setState(self.storageIndex, false)
+ row:getDescendantByName("title"):setText(self.i18n:getText("spu_storage_select"))
+ local entry = inventory[self.storageIndex]
+ self.storageDetailPlaceable = entry.placeable
+ local stats = self:computeBuildingStats(adEnv, entry, inventory)
+ for _, stat in ipairs(stats) do
+ table.insert(self.storageDetailRows, self:addTextRow(self:describeStat(stat)))
+ end
+ table.insert(self.storageDetailRows, self:addTextRow(self:describeStatTotals(stats)))
+end
+
+function SmartPickupFrame:refreshStorageDetail(adEnv, inventory)
+ local entry = nil
+ for _, candidate in ipairs(inventory) do
+ if candidate.placeable == self.storageDetailPlaceable then entry = candidate end
+ end
+ if entry == nil or #(self.storageDetailRows or {}) ~= #entry.entries + 1 then
+ return
+ end
+ local stats = self:computeBuildingStats(adEnv, entry, inventory)
+ for index, stat in ipairs(stats) do
+ self.storageDetailRows[index]:setText(self:describeStat(stat))
+ end
+ self.storageDetailRows[#stats + 1]:setText(self:describeStatTotals(stats))
+end
+
+function SmartPickupFrame:onClickStorageBuilding(state)
+ self.storageIndex = state
+ self:requestRebuild()
+end
+
function SmartPickupFrame:fillStorageRows(adEnv)
self.storageRows = {}
- self:addTextRow(self.i18n:getText("spu_section_storage"))
local inventory = self:getInventory(adEnv)
+ self:fillStorageDetail(adEnv, inventory)
+ self:addTextRow(self.i18n:getText("spu_section_storage"))
for _, entry in ipairs(inventory) do
table.insert(self.storageRows, {placeable = entry.placeable, title = self:addTextRow(self:describeInventory(entry))})
end
@@ -42,10 +146,12 @@ function SmartPickupFrame:refreshStorageRows(adEnv)
if self.currentTab ~= SmartPickupFrame.TAB_STORAGE or #(self.storageRows or {}) == 0 then
return
end
+ local inventory = self:getInventory(adEnv)
local byPlaceable = {}
- for _, entry in ipairs(self:getInventory(adEnv)) do
+ for _, entry in ipairs(inventory) do
byPlaceable[entry.placeable] = entry
end
+ pcall(self.refreshStorageDetail, self, adEnv, inventory)
for _, row in ipairs(self.storageRows) do
local entry = byPlaceable[row.placeable]
if entry ~= nil then
diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml
index e751e91..b1a1387 100644
--- a/FS25_ADSmartPickup/l10n/l10n_da.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_da.xml
@@ -124,5 +124,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml
index c579027..4a4c3e8 100644
--- a/FS25_ADSmartPickup/l10n/l10n_en.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_en.xml
@@ -124,5 +124,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml
index 534a112..e5efe2b 100644
--- a/FS25_ADSmartPickup/modDesc.xml
+++ b/FS25_ADSmartPickup/modDesc.xml
@@ -1,7 +1,7 @@
masterdraco
- 1.14.3.0
+ 1.15.0.0
AD Smart Pickup
@@ -20,6 +20,7 @@
+
diff --git a/tests/test_adSources.lua b/tests/test_adSources.lua
index cef8805..23bf8b9 100644
--- a/tests/test_adSources.lua
+++ b/tests/test_adSources.lua
@@ -20,6 +20,7 @@ SellingStation = {}
dofile("FS25_ADSmartPickup/adSupplyPlanner.lua")
dofile("FS25_ADSmartPickup/adPrices.lua")
+dofile("FS25_ADSmartPickup/adStorageStats.lua")
dofile("FS25_ADSmartPickup/adSources.lua")
local failures = 0
@@ -383,5 +384,26 @@ check("S17 gylle-enden: gylle", slurryEnd[1].fillType, SLURRY)
twoEndStation.loadTriggers[1].fillTypes = nil
check("S17 uden punkt-varer: stationens varer", #ADSources.getProducts(adEnv, ADSources.identify(adEnv, 40, 1), 1), 2)
+-- S18: gårdens forbrug og produktion pr. døgn (fabrikker + stalde) og bedste markedspris
+tmrFactory.spec_productionPoint.productionPoint.getIsProductionEnabled = function(self, id) return id == "tmr3" end
+tmrFactory.spec_productionPoint.productionPoint.productions[1].outputs = {{type = FORAGE, amount = 240}}
+cowBarn.spec_husbandryWater.automaticWaterSupply = false
+local rates = ADSources.getFarmRates(adEnv, 1)
+-- TMR-fabrik: 45 l roer × 200/60 cyklus/min × 1440 = 216.000 l/døgn; TMR ud 240 × 200/60 × 1440 = 1.152.000
+check("S18 roer forbrug", math.floor(rates[BEET].consumption + 0.5), 216000)
+check("S18 TMR produktion", math.floor(rates[FORAGE].production + 0.5), 1152000)
+check("S18 halm i stald 500 l/t", rates[STRAW].consumption, 12000)
+check("S18 vand 100 l/t", rates[WATER].consumption, 2400)
+check("S18 anden farm tæller ikke", rates[STRAW].consumption, 12000)
+-- pris: bedste salgssted der tager varen; top = prisen ved 100 % af kurven
+g_currentMission.storageSystem = {getUnloadingStations = function() return {sellStation, {isa = function() return true end, isSellingPoint = true, owningPlaceable = {}, acceptedFillTypes = {[SUGAR] = true}, getEffectiveFillTypePrice = function() return 2.0 end}} end}
+sellStation.getEffectiveFillTypePrice = function() return 1.5 end
+g_currentMission.environment = {currentPeriod = 1}
+DESC[SUGAR].economy = {factors = {0.8, 1.0}}
+local quote = ADSources.getMarketQuote(SUGAR)
+check("S18 bedste pris", quote.price, 2.0)
+check("S18 top ved 80 % af kurven", quote.top, 2.5)
+check("S18 intet salgssted -> nil", ADSources.getMarketQuote(FORAGE), nil)
+
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
diff --git a/tests/test_adStorageStats.lua b/tests/test_adStorageStats.lua
new file mode 100644
index 0000000..d0b48e8
--- /dev/null
+++ b/tests/test_adStorageStats.lua
@@ -0,0 +1,46 @@
+-- Kør: luajit tests/test_adStorageStats.lua (fra repo-roden)
+-- Lagerstatistik pr. vare: fyldning, værdi nu / ved toppris, gårdens samlede beholdning, dage tilbage.
+
+dofile("FS25_ADSmartPickup/adStorageStats.lua")
+local S = ADStorageStats
+
+local failures = 0
+local function check(name, actual, expected)
+ if actual == expected then print("OK " .. name) else failures = failures + 1; print(string.format("FAIL %s: forventede %s, fik %s", name, tostring(expected), tostring(actual))) end
+end
+
+-- ST1: fuld beregning
+local st = S.itemStats({fillType = 5, level = 150000, capacity = 180000}, 412000, {consumption = 130000, production = 48000}, {price = 1.6, top = 2.0})
+check("ST1 procent", st.percent, 83)
+check("ST1 værdi nu", st.valueNow, 240000)
+check("ST1 værdi top", st.valueTop, 300000)
+check("ST1 gårdens beholdning", st.farmTotal, 412000)
+check("ST1 dage", math.floor(st.days * 10 + 0.5) / 10, 3.2)
+check("ST1 netto pr. døgn", st.net, -82000)
+
+-- ST2: ingen salgssted, intet forbrug, ingen kapacitet
+local st2 = S.itemStats({fillType = 6, level = 5000, capacity = 0}, 5000, nil, nil)
+check("ST2 ingen pris -> nil", st2.valueNow, nil)
+check("ST2 intet forbrug -> dage nil", st2.days, nil)
+check("ST2 kapacitet 0 -> procent nil", st2.percent, nil)
+
+-- ST3: bygningens samlede værdi (kun varer med pris)
+local total = S.buildingTotals({st, st2, S.itemStats({fillType = 7, level = 1000, capacity = 2000}, 1000, nil, {price = 2, top = 3})})
+check("ST3 samlet nu", total.valueNow, 242000)
+check("ST3 samlet top", total.valueTop, 303000)
+
+-- ST4: gårdens totaler fra inventar
+local totals = S.farmTotals({{entries = {{fillType = 5, level = 100}, {fillType = 6, level = 5}}}, {entries = {{fillType = 5, level = 50}}}})
+check("ST4 sum pr. vare", totals[5], 150)
+check("ST4 anden vare", totals[6], 5)
+
+-- ST5: foder-forbrug i et fælles trug fordeles efter truget indhold; tomt trug -> bedste foder
+local split = S.splitTroughConsumption(2400, {[116] = 3000, [7] = 1000}, {[116] = 1.0, [7] = 0.8})
+check("ST5 TMR 75 %", split[116], 1800)
+check("ST5 ensilage 25 %", split[7], 600)
+local empty = S.splitTroughConsumption(2400, {}, {[116] = 1.0, [7] = 0.8})
+check("ST5 tomt trug -> bedste foder", empty[116], 2400)
+check("ST5 tomt trug -> intet på ensilage", empty[7], nil)
+
+print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
+os.exit(failures == 0 and 0 or 1)