diff --git a/FS25_ADSmartPickup/adOutbound.lua b/FS25_ADSmartPickup/adOutbound.lua
index 30ab12c..f444e97 100644
--- a/FS25_ADSmartPickup/adOutbound.lua
+++ b/FS25_ADSmartPickup/adOutbound.lua
@@ -144,7 +144,17 @@ local function findSellQuote(adEnv, vehicle, product, sellMarkers, sellAtPercent
end
local function findStoreMarker(adEnv, vehicle, product, capacity, sources, litersOverride)
- if product.isPallet or ADLoadSwap == nil or ADLoadSwap.findDumpMarker == nil then
+ local isReachable = function(markerId)
+ return ADWaitPool.hasRouteTo(adEnv, vehicle, markerId)
+ end
+ -- paller lagres i et palleskur (objektlager) — UAL læsser af dér
+ if product.isPallet then
+ if ADSources.findPalletStore == nil then
+ return nil
+ end
+ return ADSources.findPalletStore(adEnv, vehicle, product.fillType, ADSources.getSourcePlaceables(sources), isReachable, ADSmartPickup.getFarmId(vehicle))
+ end
+ if ADLoadSwap == nil or ADLoadSwap.findDumpMarker == nil then
return nil
end
local available = litersOverride or 0
@@ -154,9 +164,6 @@ local function findStoreMarker(adEnv, vehicle, product, capacity, sources, liter
end
end
local liters = math.max(1, math.min(available, capacity or available))
- local isReachable = function(markerId)
- return ADWaitPool.hasRouteTo(adEnv, vehicle, markerId)
- end
local marker = ADLoadSwap.findDumpMarker(adEnv, vehicle, product.fillType, liters, nil, isReachable, ADSources.getSourcePlaceables(sources))
if marker == nil then
return nil
@@ -210,6 +217,11 @@ function ADOutbound.buildPlanInput(adEnv, run, vehicle)
local homeId = product.sources[1] ~= nil and product.sources[1].wayPointId or nil
local quote, unreachableName = findSellQuote(adEnv, vehicle, product, sellMarkers, run.sellAtPercent, homeId)
product.quote = quote
+ -- gårdens eget behov (fabrikker/stalde) sælges ikke; kilderne selv tæller ikke som forbrugere
+ if ADSources.getFarmDemand ~= nil then
+ local demand = ADSources.getFarmDemand(adEnv, product.fillType, farmId, ADSources.getSourcePlaceables(sources))
+ product.reserve, product.reservedFor = demand.liters, demand.consumers
+ end
if quote == nil and unreachableName ~= nil then
warnOnce(adEnv, vehicle, "sell:" .. product.name, string.format("'%s' kan ikke nå salgssted '%s' for %s – tjek vejnettet", tostring(vehicle:getName()), unreachableName, product.name))
else
diff --git a/FS25_ADSmartPickup/adOutboundPlanner.lua b/FS25_ADSmartPickup/adOutboundPlanner.lua
index a198675..ca9f252 100644
--- a/FS25_ADSmartPickup/adOutboundPlanner.lua
+++ b/FS25_ADSmartPickup/adOutboundPlanner.lua
@@ -8,7 +8,8 @@
-- products = { { fillType, name, policy = "store"|"sell"|"sellElseStore", isPallet,
-- sources = { {wayPointId, available, level, capacity} ... }, -- kilder med varen
-- quote = {percent, price, wayPointId, stationName} | nil, -- bedste NÅELIGE salgssted
--- store = {wayPointId, name, free} | nil } ... } } -- silo med plads, nåelig
+-- store = {wayPointId, name, free} | nil, -- silo/palleskur med plads, nåelig
+-- reserve = liter | nil, reservedFor = {navne} | nil } ... } } -- gårdens eget behov (sælges ikke)
-- plan(input) -> trip | nil, reasons
-- trip = {kind = "sell"|"store", fillTypes, names, sourceIds, targetWayPointId, targetName, percent}
-- reasons = én linje pr. vare der ikke gav en tur ("SUGAR 78 % af top (kræver 90), lager 41 %")
@@ -43,8 +44,11 @@ end
function ADOutboundPlanner.evaluate(product, input)
local available, storagePercent = sumSources(product)
local percent = product.quote ~= nil and product.quote.percent or nil
+ local reserve = math.max(0, product.reserve or 0)
return {
available = available,
+ reserve = reserve,
+ sellable = math.max(0, available - reserve),
fullLoad = input.capacity ~= nil and input.capacity > 0 and available >= input.capacity,
storagePercent = storagePercent,
storageHigh = storagePercent >= (input.storeAbovePercent or 80),
@@ -53,10 +57,33 @@ function ADOutboundPlanner.evaluate(product, input)
}
end
+local function formatLiters(liters)
+ if liters >= 1000000 then
+ return string.format("%.1fM", liters / 1000000)
+ elseif liters >= 1000 then
+ return string.format("%.1fk", liters / 1000)
+ end
+ return string.format("%d", liters)
+end
+
+-- Salg må ikke røre reserven: enten er intet reserveret, eller et helt læs kan tages uden om den
+-- (AutoDrive læsser altid vognen fuld, så en delvis mængde kan ikke styres).
+local function canSellWithoutReserve(e, input)
+ if e.reserve <= 0 then
+ return e.sellable >= ADOutboundPlanner.MIN_AVAILABLE_LITERS
+ end
+ local capacity = input.capacity or 0
+ return e.sellable >= ADOutboundPlanner.MIN_AVAILABLE_LITERS and (capacity <= 0 or e.sellable >= capacity)
+end
+
local function reasonFor(product, e, input)
if e.available < ADOutboundPlanner.MIN_AVAILABLE_LITERS then
return product.name .. ": intet på lager"
end
+ if wantsSell(product.policy) and e.priceOk and product.quote ~= nil and not canSellWithoutReserve(e, input) then
+ local names = table.concat(product.reservedFor or {}, ", ")
+ return string.format("%s: %s reserveret til %s", product.name, formatLiters(e.reserve), names ~= "" and names or "gårdens eget forbrug")
+ end
local canStore = wantsStore(product.policy) and product.store ~= nil
if wantsSell(product.policy) and product.quote == nil and not canStore then
return product.name .. ": intet nåeligt salgssted"
@@ -79,7 +106,7 @@ local function collectCandidates(input)
local e = ADOutboundPlanner.evaluate(product, input)
local added = false
if e.available >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then
- if wantsSell(product.policy) and e.priceOk and product.quote ~= nil then
+ if wantsSell(product.policy) and e.priceOk and product.quote ~= nil and canSellWithoutReserve(e, input) then
table.insert(sell, {product = product, e = e, kind = "sell", targetWayPointId = product.quote.wayPointId, targetName = product.quote.stationName, percent = e.percent})
added = true
elseif wantsStore(product.policy) and (e.fullLoad or e.storageHigh) and product.store ~= nil then
diff --git a/FS25_ADSmartPickup/adPrices.lua b/FS25_ADSmartPickup/adPrices.lua
index 2756e36..2f7032a 100644
--- a/FS25_ADSmartPickup/adPrices.lua
+++ b/FS25_ADSmartPickup/adPrices.lua
@@ -1,14 +1,18 @@
-- AD Prices
-- Prisregel for udkørsel: hvor tæt er salgsstedets pris på årets toppris for varen?
--- Toppris = stationens grundpris lige nu × (årets højeste periodefaktor ÷ nuværende periodes
--- faktor) — dvs. det Priser-menuen ville vise som årets bedste måned for netop den station.
--- Effektiv pris (inkl. prisfald efter salg og stor efterspørgsel) ÷ toppris = pris-%;
--- stor efterspørgsel giver > 100 % og opfylder dermed altid reglen.
+-- Grundlaget er spillets faste sæsonkurve (fillType.economy.factors[1..12], samme data som
+-- prisgrafen i Priser-menuen; metode fra Distribution Redux): kurve-% = faktor nu ÷ årets højeste.
+-- Oven på det ganges stationens egen afvigelse (effektiv pris ÷ stationens grundpris nu), så
+-- prisfald efter salg og stor efterspørgsel tæller med — stor efterspørgsel giver > 100 %.
+-- Sværhedsgraden skalerer begge priser ens og falder ud af regnestykket. Flade kurver
+-- (top/bund under FLAT_THRESHOLD) regnes som 100 %, så varer uden sæson aldrig strander.
-- De rene funktioner øverst har ingen spil-API; getQuote er spil-adapteren.
ADPrices = {}
ADPrices.LOG_PREFIX = "ADSmartPickup: "
+-- top/bund under dette = ingen reel sæson (Distribution Redux bruger samme grænse)
+ADPrices.FLAT_THRESHOLD = 1.02
function ADPrices.maxFactor(factors)
local best = nil
@@ -37,6 +41,29 @@ function ADPrices.percentOfTop(price, top)
return math.floor(100 * price / top + 0.5)
end
+function ADPrices.minFactor(factors)
+ local best = nil
+ for _, factor in pairs(factors or {}) do
+ if type(factor) == "number" and (best == nil or factor < best) then
+ best = factor
+ end
+ end
+ return best or 1
+end
+
+-- Kurve-%: nuværende periodes faktor ÷ årets højeste, afrundet. 100 uden kurve, uden periode
+-- eller når kurven er flad.
+function ADPrices.curvePercent(factors, period)
+ if factors == nil or period == nil then
+ return 100
+ end
+ local now, max, min = tonumber(factors[period]), ADPrices.maxFactor(factors), ADPrices.minFactor(factors)
+ if now == nil or now <= 0 or max <= 0 or min <= 0 or (max / min) < ADPrices.FLAT_THRESHOLD then
+ return 100
+ end
+ return math.floor(100 * now / max + 0.5)
+end
+
-- Rigtige salgssteder: SellingStation der sælger (fabrikkers aflæsningsstationer er
-- også SellingStation-objekter, men med isSellingPoint = false) og vises i Priser-menuen.
function ADPrices.isSellingStation(station)
@@ -49,18 +76,8 @@ local function getCurrentPeriod()
return environment ~= nil and environment.currentPeriod or nil
end
-local function getDifficultyMultiplier()
- local economy = g_currentMission ~= nil and g_currentMission.economyManager or nil
- if economy ~= nil and economy.getPriceMultiplier ~= nil then
- local ok, value = pcall(economy.getPriceMultiplier, economy)
- if ok and type(value) == "number" then
- return value
- end
- end
- return 1
-end
-
-- {price, top, percent} for varen ved stationen; nil hvis stationen ikke kan prissætte den.
+-- percent = kurve-% × (effektiv ÷ stationens grundpris nu); top = den pris der svarer til 100 %.
function ADPrices.getQuote(station, fillType)
if station == nil or station.getEffectiveFillTypePrice == nil then
return nil
@@ -71,14 +88,12 @@ function ADPrices.getQuote(station, fillType)
end
local fillTypeDesc = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil
local factors = fillTypeDesc ~= nil and fillTypeDesc.economy ~= nil and fillTypeDesc.economy.factors or nil
- local period = getCurrentPeriod()
- local factorNow = factors ~= nil and period ~= nil and tonumber(factors[period]) or 1
+ local curve = ADPrices.curvePercent(factors, getCurrentPeriod())
local baseNow = station.fillTypePrices ~= nil and tonumber(station.fillTypePrices[fillType]) or nil
- if baseNow == nil or baseNow <= 0 then
- baseNow = (fillTypeDesc ~= nil and tonumber(fillTypeDesc.pricePerLiter) or 0) * factorNow * getDifficultyMultiplier()
- end
- local top = ADPrices.topPrice(baseNow, factorNow, ADPrices.maxFactor(factors))
- return {price = effective, top = top, percent = ADPrices.percentOfTop(effective, top)}
+ local stationRatio = (baseNow ~= nil and baseNow > 0) and (effective / baseNow) or 1
+ local percent = math.floor(curve * stationRatio + 0.5)
+ local top = percent > 0 and (effective * 100 / percent) or effective
+ return {price = effective, top = top, percent = percent}
end
-- Højeste pris-%, ved lighed højeste pris. nil ved tom liste.
diff --git a/FS25_ADSmartPickup/adSmartPickup.lua b/FS25_ADSmartPickup/adSmartPickup.lua
index 03630d2..4799a88 100644
--- a/FS25_ADSmartPickup/adSmartPickup.lua
+++ b/FS25_ADSmartPickup/adSmartPickup.lua
@@ -15,6 +15,7 @@ if ADSmartPickup.dir ~= nil and source ~= nil then
source(ADSmartPickup.dir .. "gui/SmartPickupFrame.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupSupplyTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupOutboundTab.lua")
+ source(ADSmartPickup.dir .. "gui/SmartPickupStorageTab.lua")
end
-- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et
diff --git a/FS25_ADSmartPickup/adSources.lua b/FS25_ADSmartPickup/adSources.lua
index abebad2..1750b6e 100644
--- a/FS25_ADSmartPickup/adSources.lua
+++ b/FS25_ADSmartPickup/adSources.lua
@@ -317,6 +317,281 @@ function ADSources.getSellMarkers(adEnv, farmId)
return result
end
+-- ---- paller til lager: objektlager (palleskur) som mål ----
+
+function ADSources.objectStorageAccepts(placeable, fillType)
+ local spec = placeable ~= nil and placeable.spec_objectStorage or nil
+ if spec == nil or spec.supportsPallets == false then
+ return false
+ end
+ if placeable.getObjectStorageSupportsFillType ~= nil then
+ local ok, value = pcall(placeable.getObjectStorageSupportsFillType, placeable, fillType)
+ return ok and value == true
+ end
+ return true
+end
+
+-- Ledige pladser (objekter); uendeligt når lageret ingen kapacitet opgiver.
+function ADSources.objectStorageFreeSlots(placeable)
+ local spec = placeable ~= nil and placeable.spec_objectStorage or nil
+ if spec == nil then
+ return 0
+ end
+ local capacity = spec.capacity or 0
+ if capacity <= 0 then
+ return math.huge
+ end
+ local stored = spec.storedObjects ~= nil and #spec.storedObjects or (spec.numStoredObjects or 0)
+ return math.max(0, capacity - stored)
+end
+
+-- Nærmeste markør ved et objektlager der tager varen og har plads, med rute fra traktoren;
+-- kilderne selv udelukkes. {wayPointId, name, free = pladser} eller nil.
+function ADSources.findPalletStore(adEnv, vehicle, fillType, excludedPlaceables, isReachable, farmId)
+ local x, z = getNodePosition(vehicle.rootNode)
+ if x == nil then
+ return nil
+ end
+ local candidates = {}
+ for _, marker in pairs(adEnv.ADGraphManager:getMapMarkers()) do
+ local source = ADSources.identify(adEnv, marker.id, farmId)
+ if source ~= nil and source.kind == "objectStorage" and not (excludedPlaceables ~= nil and excludedPlaceables[source.placeable])
+ and ADSources.objectStorageAccepts(source.placeable, fillType) and ADSources.objectStorageFreeSlots(source.placeable) >= 1 then
+ local wayPoint = adEnv.ADGraphManager:getWayPointById(marker.id)
+ table.insert(candidates, {marker = marker, source = source, distance = distance2(x, z, wayPoint.x, wayPoint.z)})
+ end
+ end
+ table.sort(candidates, function(a, b) return a.distance < b.distance end)
+ for _, candidate in ipairs(candidates) do
+ if isReachable == nil or isReachable(candidate.marker.id) then
+ return {wayPointId = candidate.marker.id, name = tostring(candidate.marker.name), free = ADSources.objectStorageFreeSlots(candidate.source.placeable)}
+ end
+ end
+ return nil
+end
+
+-- ---- reserve: hvad gårdens egne fabrikker og stalde skal bruge det næste døgn ----
+
+ADSources.RESERVE_HOURS = 24
+
+local function isOwnedBy(placeable, farmId)
+ if placeable.getOwnerFarmId ~= nil then
+ local ok, owner = pcall(placeable.getOwnerFarmId, placeable)
+ return ok and owner == farmId
+ end
+ return placeable.ownerFarmId == farmId
+end
+
+local function isProductionWanted(productionPoint, production)
+ local isEnabled = false
+ if productionPoint.getIsProductionEnabled ~= nil then
+ isEnabled = productionPoint:getIsProductionEnabled(production.id) == true
+ else
+ for _, active in ipairs(productionPoint.activeProductions or {}) do
+ if active == production then isEnabled = true end
+ end
+ end
+ local missingInputs = ProductionPoint ~= nil and ProductionPoint.PROD_STATUS ~= nil and ProductionPoint.PROD_STATUS.MISSING_INPUTS or nil
+ return isEnabled or (missingInputs ~= nil and production.status == missingInputs)
+end
+
+-- Fabrik: døgnforbrug af varen i ønskede produktioner minus det den har, højst fri plads.
+local function getProductionReserve(placeable, fillType)
+ local productionPoint = getProductionPoint(placeable)
+ if productionPoint == nil or productionPoint.storage == nil then
+ return 0
+ end
+ local perMinute = 0
+ for _, production in ipairs(productionPoint.productions or {}) do
+ if isProductionWanted(productionPoint, production) then
+ for _, input in ipairs(production.inputs or {}) do
+ if input.type == fillType then
+ local cyclesPerMinute = production.cyclesPerMinute or ((production.cyclesPerHour or 0) / 60)
+ perMinute = perMinute + (input.amount or 0) * cyclesPerMinute
+ end
+ end
+ end
+ end
+ if perMinute <= 0 then
+ return 0
+ end
+ local storage = productionPoint.storage
+ local level = storage.getFillLevel ~= nil and (storage:getFillLevel(fillType) or 0) or ((storage.fillLevels or {})[fillType] or 0)
+ local free = storage.getFreeCapacity ~= nil and storage:getFreeCapacity(fillType) or nil
+ if free == nil then
+ local capacity = storage.getCapacity ~= nil and (storage:getCapacity(fillType) or 0) or 0
+ free = math.max(0, capacity - level)
+ end
+ local need = perMinute * 60 * ADSources.RESERVE_HOURS - level
+ return math.max(0, math.min(need, free))
+end
+
+-- Stald: foder/halm/vand-rate × timer minus beholdning, højst fri plads (spec-felter som
+-- Distribution Redux læser dem); automatisk vandforsyning = intet behov.
+local function getHusbandryReserve(placeable, fillType)
+ if placeable.getHusbandryIsFillTypeSupported == nil or placeable.getHusbandryFillLevel == nil then
+ return 0
+ end
+ local okSupported, supported = pcall(placeable.getHusbandryIsFillTypeSupported, placeable, fillType)
+ if not okSupported or supported ~= true then
+ return 0
+ end
+ local name = g_fillTypeManager ~= nil and g_fillTypeManager:getFillTypeNameByIndex(fillType) or ""
+ local rate, level = 0, placeable:getHusbandryFillLevel(fillType) or 0
+ local food, straw, water = placeable.spec_husbandryFood, placeable.spec_husbandryStraw, placeable.spec_husbandryWater
+ if name == "WATER" then
+ if water == nil or water.automaticWaterSupply then return 0 end
+ rate = water.litersPerHour or 0
+ elseif name == "STRAW" and straw ~= nil then
+ rate = straw.inputLitersPerHour or 0
+ elseif food ~= nil then
+ rate = food.litersPerHour or 0
+ level = 0
+ for _, value in pairs(food.fillLevels or {}) do level = level + (value or 0) end
+ end
+ if rate <= 0 then
+ return 0
+ end
+ local free = placeable.getHusbandryFreeCapacity ~= nil and (placeable:getHusbandryFreeCapacity(fillType) or 0) or math.huge
+ return math.max(0, math.min(rate * ADSources.RESERVE_HOURS - level, free))
+end
+
+-- {liters, consumers = {navne}} — gårdens samlede behov for varen; udelukkede placeables (kilderne) tæller ikke.
+function ADSources.getFarmDemand(adEnv, fillType, farmId, excludedPlaceables)
+ local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
+ local total, consumers = 0, {}
+ for _, placeable in pairs(placeables) do
+ if isOwnedBy(placeable, farmId) and not (excludedPlaceables ~= nil and excludedPlaceables[placeable]) then
+ local reserve = getProductionReserve(placeable, fillType) + getHusbandryReserve(placeable, fillType)
+ if reserve > 0 then
+ total = total + reserve
+ table.insert(consumers, getPlaceableName(placeable, "?"))
+ end
+ end
+ end
+ return {liters = total, consumers = consumers}
+end
+
+-- ---- inventar: alt gården har liggende, pr. bygning ----
+
+local function addEntry(entries, fillType, level, capacity, extra)
+ local entry = newProduct(fillType, level, level, capacity, false)
+ entry.isOutput = extra ~= nil and extra.isOutput or false
+ entry.count = extra ~= nil and extra.count or nil
+ table.insert(entries, entry)
+end
+
+local function factoryEntries(placeable)
+ local productionPoint = getProductionPoint(placeable)
+ local storage = productionPoint ~= nil and productionPoint.storage or nil
+ local entries = {}
+ if storage == nil then return entries end
+ local outputs = {}
+ for _, production in ipairs(productionPoint.productions or {}) do
+ for _, output in ipairs(production.outputs or {}) do outputs[output.type] = true end
+ end
+ for fillType, level in pairs(storage.fillLevels or {}) do
+ if (level or 0) > 0 then
+ local capacity = storage.getCapacity ~= nil and (storage:getCapacity(fillType) or 0) or 0
+ addEntry(entries, fillType, level, capacity, {isOutput = outputs[fillType] == true})
+ end
+ end
+ return entries
+end
+
+local function siloEntries(placeable)
+ local entries, levels, capacities = {}, {}, {}
+ for _, storage in pairs(placeable.spec_silo.storages or {}) do
+ for fillType, level in pairs(storage.fillLevels or {}) do
+ if (level or 0) > 0 then
+ levels[fillType] = (levels[fillType] or 0) + level
+ capacities[fillType] = (capacities[fillType] or 0) + (storage.getCapacity ~= nil and (storage:getCapacity(fillType) or 0) or 0)
+ end
+ end
+ end
+ for fillType, level in pairs(levels) do addEntry(entries, fillType, level, capacities[fillType] or 0) end
+ return entries
+end
+
+local function objectStorageEntries(placeable)
+ local entries = {}
+ local spec = placeable.spec_objectStorage
+ for _, objectInfo in ipairs(spec.objectInfos or {}) do
+ local fillType, perObject = getObjectInfoContent(objectInfo)
+ local count = objectInfo.numObjects or 0
+ if fillType ~= nil and count > 0 then
+ addEntry(entries, fillType, count * perObject, spec.capacity or 0, {count = count})
+ end
+ end
+ return entries
+end
+
+local function husbandryEntries(placeable)
+ local entries, seen = {}, {}
+ local food = placeable.spec_husbandryFood
+ if food ~= nil then
+ local total = 0
+ for _, level in pairs(food.fillLevels or {}) do total = total + (level or 0) end
+ for fillType, level in pairs(food.fillLevels or {}) do
+ if (level or 0) > 0 then
+ seen[fillType] = true
+ addEntry(entries, fillType, level, level + math.max(0, (food.capacity or 0) - total))
+ end
+ end
+ end
+ for _, name in ipairs({"STRAW", "WATER", "MILK", "LIQUIDMANURE", "MANURE"}) do
+ local fillType = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeIndexByName ~= nil and g_fillTypeManager:getFillTypeIndexByName(name) or nil
+ if fillType == nil and g_fillTypeManager ~= nil then
+ -- test-fakes uden getFillTypeIndexByName: slå navnet op baglæns
+ for index = 1, 64 do
+ if g_fillTypeManager:getFillTypeNameByIndex(index) == name then fillType = index break end
+ end
+ end
+ if fillType ~= nil and not seen[fillType] and placeable.getHusbandryIsFillTypeSupported ~= nil and placeable:getHusbandryIsFillTypeSupported(fillType) then
+ local level = placeable.getHusbandryFillLevel ~= nil and (placeable:getHusbandryFillLevel(fillType) or 0) or 0
+ if level > 0 then
+ local free = placeable.getHusbandryFreeCapacity ~= nil and (placeable:getHusbandryFreeCapacity(fillType) or 0) or 0
+ addEntry(entries, fillType, level, level + free)
+ end
+ end
+ end
+ return entries
+end
+
+-- { {name, kind = "factory"|"silo"|"objectStorage"|"husbandry", entries = {{fillType, name, title, level, capacity, isOutput, count}}} }
+-- kun gårdens bygninger med indhold; sorteret på slags, dernæst navn.
+function ADSources.getFarmInventory(adEnv, farmId)
+ local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
+ local result = {}
+ for _, placeable in pairs(placeables) do
+ if isOwnedBy(placeable, farmId) then
+ local kind, entries = nil, nil
+ if getProductionPoint(placeable) ~= nil then
+ kind, entries = "factory", factoryEntries(placeable)
+ elseif placeable.spec_silo ~= nil then
+ kind, entries = "silo", siloEntries(placeable)
+ elseif placeable.spec_objectStorage ~= nil then
+ kind, entries = "objectStorage", objectStorageEntries(placeable)
+ elseif placeable.getHusbandryFillLevel ~= nil then
+ kind, entries = "husbandry", husbandryEntries(placeable)
+ end
+ if entries ~= nil and #entries > 0 then
+ table.sort(entries, function(a, b)
+ if a.isOutput ~= b.isOutput then return a.isOutput end
+ return tostring(a.title) < tostring(b.title)
+ end)
+ table.insert(result, {name = getPlaceableName(placeable, "?"), kind = kind, entries = entries, placeable = placeable})
+ end
+ end
+ end
+ local order = {factory = 1, husbandry = 2, objectStorage = 3, silo = 4}
+ table.sort(result, function(a, b)
+ if order[a.kind] ~= order[b.kind] then return order[a.kind] < order[b.kind] end
+ return a.name < b.name
+ end)
+ return result
+end
+
function ADSources.getSourcePlaceables(sources)
local set = {}
for _, source in ipairs(sources or {}) do
diff --git a/FS25_ADSmartPickup/gui/SmartPickupFrame.lua b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
index 63c9f56..a0b6d8f 100644
--- a/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
+++ b/FS25_ADSmartPickup/gui/SmartPickupFrame.lua
@@ -9,10 +9,10 @@ SmartPickupFrame._mt = Class(SmartPickupFrame, TabbedMenuFrameElement)
SmartPickupFrame.REFRESH_INTERVAL_MS = 1000
SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: "
SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2
-SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_SUPPLY, SmartPickupFrame.TAB_OUTBOUND = 1, 2, 3
-SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_supply", "spu_tab_outbound"}
--- Hver fane viser kun runs af sin egen slags; Oversigt opretter forsynings-runs.
-SmartPickupFrame.TAB_KIND = {"supply", "supply", "outbound"}
+SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_SUPPLY, SmartPickupFrame.TAB_OUTBOUND, SmartPickupFrame.TAB_STORAGE = 1, 2, 3, 4
+SmartPickupFrame.TAB_NAMES = {"spu_tab_overview", "spu_tab_supply", "spu_tab_outbound", "spu_tab_storage"}
+-- Hver fane viser kun runs af sin egen slags; Oversigt og Lagre opretter forsynings-runs.
+SmartPickupFrame.TAB_KIND = {"supply", "supply", "outbound", "supply"}
SmartPickupFrame.POLICIES = {"off", "store", "sell", "sellElseStore"}
function SmartPickupFrame.new(i18n, getAdEnv)
@@ -296,6 +296,7 @@ function SmartPickupFrame:clearRows()
self.needsRows = {}
self.overviewRows = {}
self.productRows, self.outboundStatusRows = {}, {}
+ self.storageRows = {}
end
function SmartPickupFrame:addRow(prefab, data)
@@ -345,6 +346,17 @@ function SmartPickupFrame:rebuild()
self:clearRows()
local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW
self:setStaticRowsVisible(self.currentTab)
+ if self.currentTab == SmartPickupFrame.TAB_STORAGE then
+ if adEnv ~= nil then
+ self:fillStorageRows(adEnv)
+ end
+ self.statusRow = self:addTextRow("")
+ self:colorRows()
+ self.boxLayout:invalidateLayout()
+ self:restoreFocusIfLost()
+ self:refreshLive()
+ return
+ end
if isOverview then
if adEnv ~= nil then
self:fillOverviewRows(adEnv)
@@ -665,6 +677,7 @@ function SmartPickupFrame:refreshLive()
return
end
self:refreshOverview(adEnv)
+ self:refreshStorageRows(adEnv)
local isRunning = false
if run ~= nil then
self:syncOptionsFromModel(adEnv, run)
diff --git a/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua b/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua
index 0daf289..53e3c29 100644
--- a/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua
+++ b/FS25_ADSmartPickup/gui/SmartPickupOutboundTab.lua
@@ -43,11 +43,9 @@ function SmartPickupFrame:describeProduct(adEnv, product)
return string.format(self.i18n:getText("spu_product_row"), tostring(product.title), SmartPickupFrame.formatCompact(product.available or 0), price)
end
--- Paller kan ikke lagres i en silo: kun Fra/Salg for pallevarer.
-SmartPickupFrame.PALLET_POLICIES = {"off", "sell"}
-
+-- Paller lagres i et palleskur (objektlager) i stedet for en silo — samme politikker som bulk.
function SmartPickupFrame.getPoliciesFor(product)
- return product.isPallet and SmartPickupFrame.PALLET_POLICIES or SmartPickupFrame.POLICIES
+ return SmartPickupFrame.POLICIES
end
function SmartPickupFrame.getPolicyIndex(policies, policy)
diff --git a/FS25_ADSmartPickup/gui/SmartPickupStorageTab.lua b/FS25_ADSmartPickup/gui/SmartPickupStorageTab.lua
new file mode 100644
index 0000000..9f6e243
--- /dev/null
+++ b/FS25_ADSmartPickup/gui/SmartPickupStorageTab.lua
@@ -0,0 +1,55 @@
+-- Smart Pickup — fanen Lagre
+-- Alt gården har liggende, pr. bygning: fabrikker (outputs først), stalde, palleskure, siloer.
+-- Kun bygninger med indhold vises; teksterne opdateres hvert sekund uden genopbygning.
+-- Funktionerne lægges på SmartPickupFrame (indlæses efter SmartPickupFrame.lua).
+
+-- "Sugar mill (fabrik): Sukker 150.0k/180.0k ▸ · Roer 356.8k/360.0k" (▸ = output)
+function SmartPickupFrame:describeInventory(entry)
+ local parts = {}
+ for _, item in ipairs(entry.entries) do
+ local amount
+ if item.count ~= nil then
+ amount = string.format(self.i18n:getText("spu_storage_pallets"), item.count)
+ else
+ amount = string.format("%s/%s", SmartPickupFrame.formatCompact(item.level or 0), SmartPickupFrame.formatCompact(item.capacity or 0))
+ end
+ table.insert(parts, string.format("%s %s%s", tostring(item.title), amount, item.isOutput and " ▸" or ""))
+ end
+ local kindText = self.i18n:getText("spu_source_kind_" .. tostring(entry.kind))
+ return string.format("%s (%s): %s", tostring(entry.name), kindText, table.concat(parts, " · "))
+end
+
+function SmartPickupFrame:getInventory(adEnv)
+ if ADSources == nil or ADSources.getFarmInventory == nil then
+ return {}
+ end
+ return ADSources.getFarmInventory(adEnv, g_currentMission:getFarmId())
+end
+
+function SmartPickupFrame:fillStorageRows(adEnv)
+ self.storageRows = {}
+ self:addTextRow(self.i18n:getText("spu_section_storage"))
+ local inventory = self:getInventory(adEnv)
+ for _, entry in ipairs(inventory) do
+ table.insert(self.storageRows, {placeable = entry.placeable, title = self:addTextRow(self:describeInventory(entry))})
+ end
+ if #inventory == 0 then
+ self:addTextRow(self.i18n:getText("spu_storage_empty"))
+ end
+end
+
+function SmartPickupFrame:refreshStorageRows(adEnv)
+ if self.currentTab ~= SmartPickupFrame.TAB_STORAGE or #(self.storageRows or {}) == 0 then
+ return
+ end
+ local byPlaceable = {}
+ for _, entry in ipairs(self:getInventory(adEnv)) do
+ byPlaceable[entry.placeable] = entry
+ end
+ for _, row in ipairs(self.storageRows) do
+ local entry = byPlaceable[row.placeable]
+ if entry ~= nil then
+ row.title:setText(self:describeInventory(entry))
+ end
+ end
+end
diff --git a/FS25_ADSmartPickup/l10n/l10n_da.xml b/FS25_ADSmartPickup/l10n/l10n_da.xml
index 6620fe0..d58b915 100644
--- a/FS25_ADSmartPickup/l10n/l10n_da.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_da.xml
@@ -60,6 +60,11 @@
+
+
+
+
+
@@ -72,7 +77,7 @@
-
+
diff --git a/FS25_ADSmartPickup/l10n/l10n_en.xml b/FS25_ADSmartPickup/l10n/l10n_en.xml
index 463b8f6..e972231 100644
--- a/FS25_ADSmartPickup/l10n/l10n_en.xml
+++ b/FS25_ADSmartPickup/l10n/l10n_en.xml
@@ -60,6 +60,11 @@
+
+
+
+
+
@@ -72,7 +77,7 @@
-
+
diff --git a/FS25_ADSmartPickup/modDesc.xml b/FS25_ADSmartPickup/modDesc.xml
index 3a9d20d..b4ef959 100644
--- a/FS25_ADSmartPickup/modDesc.xml
+++ b/FS25_ADSmartPickup/modDesc.xml
@@ -1,7 +1,7 @@
masterdraco
- 1.12.0.0
+ 1.12.1.0
AD Smart Pickup
diff --git a/README.md b/README.md
index 85da420..7019888 100644
--- a/README.md
+++ b/README.md
@@ -183,6 +183,23 @@ tjek vejnettet`), samme tekst i AD's notifikations-historik, i log.txt (WARN) og
Traktoren holder ved Wait og kører selv når vejen er koblet på. Advarslen gentages først når
traktor, station eller vare ændrer sig.
+**Prisregel (v1.12.1, metode fra Distribution Redux):** pris-% = nuværende periodes faktor ÷ årets
+højeste faktor i spillets faste sæsonkurve (samme data som prisgrafen) × stationens egen afvigelse
+(effektiv pris ÷ stationens grundpris nu). Sværhedsgraden falder ud; flade kurver (top/bund < 1,02)
+regnes som 100 %; stor efterspørgsel giver > 100 %.
+
+**Reserve (v1.12.1):** før salg fratrækkes det gårdens egne fabrikker og stalde skal bruge det næste
+døgn (forbrug × 24 t minus det de allerede har, højst deres fri plads; fabrikker = ønskede
+produktioners inputs, stalde = foder/halm/vand-rate). Der sælges kun når et helt læs kan tages uden
+at røre reserven. Loggen/UI'et siger fx `SUGARBEET: 196.0k reserveret til TMR-fabrik`. Lager-politik
+rører ikke reserven (varen bliver på gården).
+
+**Paller til lager (v1.12.1):** Lager-mål for pallevarer = nærmeste objektlager (palleskur) der tager
+varen og har plads; AutoDrive læsser UAL-vognen af dér.
+
+**Fanen Lagre (v1.12.1):** gårdens beholdning pr. bygning (fabrikker med ▸ ved outputs, stalde,
+palleskure som antal paller, siloer), opdateret hvert sekund.
+
**Opsætning:** Udkørsel-fanen → Nyt run → afkryds kilder → sæt politik pr. vare → Ja på traktorer
→ Start. AD-tilstand pr. traktor: PickupAndDeliver, 1./2. markør = første kilde (pladsholder,
sættes pr. tur), rotation "kun pålæsning". Runs gemmes i `ADSmartPickup_runs.xml` version 2;
diff --git a/tests/test_adOutbound.lua b/tests/test_adOutbound.lua
index d3baad0..412f103 100644
--- a/tests/test_adOutbound.lua
+++ b/tests/test_adOutbound.lua
@@ -394,6 +394,25 @@ ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
check("O16 næste spawn via fyldniveau", sources[3].spawnCalls, 2)
+-- O17 (reserve + paller til lager): buildPlanInput henter reserve og palle-lager fra ADSources
+local demandCalls = {}
+ADSources.getFarmDemand = function(_, ft, farmId, excluded) table.insert(demandCalls, ft) return {liters = ft == SUGAR and 4000 or 0, consumers = {"TMR-fabrik"}} end
+local palletStoreMarker = nil
+ADSources.findPalletStore = function(_, vehicle, ft, excluded, isReachable, farmId) return palletStoreMarker end
+local v17 = newVehicle("Reserve", 0, 0, 20000)
+runs[v17] = outboundRun(17, {1}, {{fillType = "SUGAR", policy = "sell"}})
+local input17 = ADOutbound.buildPlanInput(adEnv, runs[v17], v17)
+check("O17 reserve på salgsvare", input17.products[1].reserve, 4000)
+check("O17 reserveret til", input17.products[1].reservedFor[1], "TMR-fabrik")
+local v18 = newVehicle("PalletStore", 100, 0, 8000, true)
+runs[v18] = outboundRun(18, {3}, {{fillType = "STRAW_BASKET", policy = "store"}})
+palletStoreMarker = {wayPointId = 40, name = "Palleskur", free = 100}
+sources[3].stored[BASKET] = 4
+products[3][1].available, products[3][1].level, products[3][1].capacity = 4000, 3900, 4000
+local input18 = ADOutbound.buildPlanInput(adEnv, runs[v18], v18)
+check("O18 palle-lager som store-mål", input18.products[1].store ~= nil and input18.products[1].store.wayPointId, 40)
+check("O18 reserve spørges ikke for lager-politik", demandCalls[#demandCalls] ~= BASKET, true)
+
-- O10: install wrapper: fejl i hook stopper ikke AD
local calls = {}
adEnv.LoadAtDestinationTask.update = function(self, dt) calls.update = (calls.update or 0) + 1 end
diff --git a/tests/test_adOutboundPlanner.lua b/tests/test_adOutboundPlanner.lua
index 647d484..df0f45b 100644
--- a/tests/test_adOutboundPlanner.lua
+++ b/tests/test_adOutboundPlanner.lua
@@ -109,5 +109,26 @@ check("P12 storageHigh", e.storageHigh, true)
check("P12 fullLoad", e.fullLoad, true)
check("P12 priceOk", e.priceOk, true)
+-- P13 (reserve): det gårdens fabrikker/stalde skal bruge, sælges ikke
+local BEET = 10
+local withReserve = product("SUGARBEET", BEET, "sell", {{wayPointId = 3, available = 268000, level = 268000, capacity = 12000000}}, quoteS, nil)
+withReserve.reserve, withReserve.reservedFor = 196000, {"TMR-fabrik"}
+local trip13 = P.plan(input({withReserve}))
+check("P13 fuldt læs uden om reserven -> salg", trip13 ~= nil and trip13.kind, "sell")
+withReserve.reserve = 260000 -- kun 8.000 til overs: mindre end et læs
+local trip13b, reasons13b = P.plan(input({withReserve}))
+check("P13 reserven ville blive rørt -> ingen tur", trip13b, nil)
+check("P13 begrundelse", reasons13b[1], "SUGARBEET: 260.0k reserveret til TMR-fabrik")
+withReserve.reserve = 300000 -- alt reserveret
+check("P13 alt reserveret -> ingen tur", P.plan(input({withReserve})), nil)
+withReserve.reserve = 0
+check("P13 reserve 0 -> salg uanset mængde", P.plan(input({withReserve})).kind, "sell")
+-- lager-politik ignorerer reserven (varen bliver på gården)
+local storeReserve = product("SUGARBEET", BEET, "store", {{wayPointId = 3, available = 268000, level = 11000000, capacity = 12000000}}, nil, storeSilo)
+storeReserve.reserve = 300000
+check("P13 lager rører ikke reserven", P.plan(input({storeReserve})).kind, "store")
+local e13 = P.evaluate(withReserve, input({}, {capacity = 20000}))
+check("P13 evaluate sellable", e13.sellable, 268000)
+
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)
diff --git a/tests/test_adPrices.lua b/tests/test_adPrices.lua
index ad2ec65..4a6d515 100644
--- a/tests/test_adPrices.lua
+++ b/tests/test_adPrices.lua
@@ -42,17 +42,29 @@ local station = {
}
local quote = ADPrices.getQuote(station, SUGAR)
check("P4 pris", quote.price, 470)
-check("P4 top", quote.top, 625)
+check("P4 top (pris ved 100 %)", math.floor(quote.top + 0.5), 627)
check("P4 procent", quote.percent, 75)
check("P4 er salgssted", ADPrices.isSellingStation(station), true)
check("P4 skjult = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = true, hideFromPricesMenu = true}), false)
check("P4 fabrik-station = ikke salgssted", ADPrices.isSellingStation({isa = function() return true end, isSellingPoint = false}), false)
--- P5: uden stationens grundpris: pricePerLiter × faktor nu × sværhedsgrad
+-- P5: uden stationens grundpris: kun kurven tæller (faktor nu ÷ faktor max)
local bare = {getEffectiveFillTypePrice = function() return 470 end}
local quote2 = ADPrices.getQuote(bare, SUGAR)
-check("P5 top fra pricePerLiter", quote2.top, 500)
-check("P5 procent", quote2.percent, 94)
+check("P5 procent = kurven alene", quote2.percent, 80)
+check("P5 top = effektiv ÷ kurve", math.floor(quote2.top + 0.5), 588)
+-- P7: kurven er grundlaget (Distribution Redux-metoden): faktor nu ÷ årets max, uafhængigt af sværhedsgrad
+check("P7 curvePercent", ADPrices.curvePercent({1.0, 1.25, 0.9}, 1), 80)
+check("P7 curvePercent ved top", ADPrices.curvePercent({1.0, 1.25, 0.9}, 2), 100)
+check("P7 flad kurve = 100", ADPrices.curvePercent({1.0, 1.01, 1.0}, 3), 100)
+check("P7 ingen kurve = 100", ADPrices.curvePercent(nil, 1), 100)
+check("P7 ukendt periode = 100", ADPrices.curvePercent({1.0, 1.25}, nil), 100)
+-- P8: stor efterspørgsel ganger stationens egen faktor på: 500 grundpris, 700 effektiv, kurve 80 % -> 112
+local demandStation = {fillTypePrices = {[SUGAR] = 500}, getEffectiveFillTypePrice = function() return 700 end}
+check("P8 stor efterspørgsel > 100", ADPrices.getQuote(demandStation, SUGAR).percent, 112)
+-- P9: sværhedsgraden påvirker ikke procenten (begge priser skaleres ens)
+local hardStation = {fillTypePrices = {[SUGAR] = 250}, getEffectiveFillTypePrice = function() return 235 end}
+check("P9 uafhængig af sværhedsgrad", ADPrices.getQuote(hardStation, SUGAR).percent, 75)
check("P5 fejlende station -> nil", ADPrices.getQuote({getEffectiveFillTypePrice = function() error("x") end}, SUGAR), nil)
-- P6: bedste tilbud = højeste procent, dernæst pris
diff --git a/tests/test_adSources.lua b/tests/test_adSources.lua
index c36e8ad..9de4137 100644
--- a/tests/test_adSources.lua
+++ b/tests/test_adSources.lua
@@ -199,5 +199,82 @@ check("S9 fabrik foretrækkes frem for objektlager i nærheden", ADSources.ident
local excluded = ADSources.getSourcePlaceables({source1, source2})
check("S8 udelukkede placeables", excluded[factory] and excluded[silo], true)
+-- S10 (paller til lager): objektlager med plads der tager varen; nærmeste nåelige fra traktoren
+objectStorage.getObjectStorageSupportsFillType = function(self, ft) return ft == BASKET or ft == FORAGE end
+check("S10 tager kurve", ADSources.objectStorageAccepts(objectStorage, BASKET), true)
+check("S10 tager ikke sukker", ADSources.objectStorageAccepts(objectStorage, SUGAR), false)
+check("S10 fri pladser", ADSources.objectStorageFreeSlots(objectStorage), 3990)
+local fullShed = {rootNode = {x = 700, z = 700}, spec_objectStorage = {capacity = 10, numStoredObjects = 10, objectInfos = {}}, getName = function() return "Fuldt skur" end}
+table.insert(g_currentMission.placeableSystem.placeables, fullShed)
+addMarker(8, "Fuldt skur", 700, 690)
+check("S10 fuldt skur 0 pladser", ADSources.objectStorageFreeSlots(fullShed), 0)
+local truck = {rootNode = {x = 650, z = 650}}
+local store = ADSources.findPalletStore(adEnv, truck, BASKET, {}, function() return true end, 1)
+check("S10 nærmeste med plads = Objektlager (skuret er fuldt)", store.wayPointId, 3)
+check("S10 navn", store.name, "Objektlager")
+check("S10 udelukket kilde -> nil", ADSources.findPalletStore(adEnv, truck, BASKET, {[objectStorage] = true}, function() return true end, 1), nil)
+check("S10 uopnåelig -> nil", ADSources.findPalletStore(adEnv, truck, BASKET, {}, function() return false end, 1), nil)
+check("S10 sukker: intet skur tager det", ADSources.findPalletStore(adEnv, truck, SUGAR, {}, function() return true end, 1), nil)
+
+-- S11 (reserve): gårdens forbrug det næste døgn af en vare, minus hvad forbrugerne allerede har
+local BEET, STRAW, WATER = 9, 10, 11
+DESC[BEET] = {index = BEET, name = "SUGARBEET", title = "Roer"}; DESC[STRAW] = {index = STRAW, name = "STRAW", title = "Halm"}; DESC[WATER] = {index = WATER, name = "WATER", title = "Vand"}
+local tmrStorage = storageOf({[BEET] = 20000, [FORAGE] = 500000}, {[BEET] = 2500000, [FORAGE] = 3500000})
+local tmrFactory = {
+ rootNode = {x = 800, z = 800}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "TMR-fabrik" end,
+ spec_productionPoint = {productionPoint = {
+ productions = {{id = "tmr3", cyclesPerMinute = 200 / 60, inputs = {{type = BEET, amount = 45}}, outputs = {{type = FORAGE, amount = 240}}}},
+ storage = tmrStorage,
+ getIsProductionEnabled = function(self, id) return id == "tmr3" end,
+ }},
+}
+local cowBarn = {
+ rootNode = {x = 900, z = 900}, ownerFarmId = 1, getOwnerFarmId = function() return 1 end, getName = function() return "Kostald" end,
+ spec_husbandryStraw = {inputLitersPerHour = 500}, spec_husbandryWater = {litersPerHour = 100, automaticWaterSupply = true},
+ getHusbandryIsFillTypeSupported = function(self, ft) return ft == STRAW or ft == WATER end,
+ getHusbandryFillLevel = function(self, ft) return ft == STRAW and 2000 or 0 end,
+ getHusbandryFreeCapacity = function(self, ft) return ft == STRAW and 19375 or 0 end,
+}
+local otherFarmBarn = {rootNode = {x = 950, z = 950}, ownerFarmId = 2, getOwnerFarmId = function() return 2 end, spec_husbandryStraw = {inputLitersPerHour = 500}, getHusbandryIsFillTypeSupported = function() return true end, getHusbandryFillLevel = function() return 0 end, getHusbandryFreeCapacity = function() return 99999 end}
+table.insert(g_currentMission.placeableSystem.placeables, tmrFactory)
+table.insert(g_currentMission.placeableSystem.placeables, cowBarn)
+table.insert(g_currentMission.placeableSystem.placeables, otherFarmBarn)
+local demand = ADSources.getFarmDemand(adEnv, BEET, 1, {})
+-- 45 l × (200/60) pr. min × 1440 min = 216.000 l/døgn; har 20.000 -> 196.000
+check("S11 roer: døgnforbrug minus beholdning", demand.liters, 196000)
+check("S11 forbruger-navn", demand.consumers[1], "TMR-fabrik")
+local strawDemand = ADSources.getFarmDemand(adEnv, STRAW, 1, {})
+-- 500 l/t × 24 = 12.000; har 2.000 -> 10.000 (under fri plads 19.375)
+check("S11 halm: stald", strawDemand.liters, 10000)
+check("S11 anden farm tæller ikke", #strawDemand.consumers, 1)
+check("S11 automatisk vand = intet behov", ADSources.getFarmDemand(adEnv, WATER, 1, {}).liters, 0)
+check("S11 output er ikke behov", ADSources.getFarmDemand(adEnv, FORAGE, 1, {}).liters, 0)
+check("S11 udelukket forbruger", ADSources.getFarmDemand(adEnv, BEET, 1, {[tmrFactory] = true}).liters, 0)
+tmrStorage.fillLevels[BEET] = 300000
+check("S11 nok på lager -> 0", ADSources.getFarmDemand(adEnv, BEET, 1, {}).liters, 0)
+tmrStorage.fillLevels[BEET] = 20000
+tmrFactory.spec_productionPoint.productionPoint.getIsProductionEnabled = function() return false end
+check("S11 slukket produktion -> 0", ADSources.getFarmDemand(adEnv, BEET, 1, {}).liters, 0)
+
+-- S12 (inventar): alt gården har liggende, pr. bygning, kun bygninger med indhold
+factory.ownerFarmId = 1; silo.ownerFarmId = 1; objectStorage.ownerFarmId = 1
+local inventory = ADSources.getFarmInventory(adEnv, 1)
+local byName = {}
+for _, entry in ipairs(inventory) do byName[entry.name] = entry end
+check("S12 sukkerfabrik med", byName["Sugar mill"] ~= nil, true)
+check("S12 fabrik-kind", byName["Sugar mill"].kind, "factory")
+check("S12 fabrik sukker", byName["Sugar mill"].entries[1].level, 150000)
+check("S12 fabrik sukker er output", byName["Sugar mill"].entries[1].isOutput, true)
+local beetEntry
+for _, e in ipairs(byName["TMR-fabrik"].entries) do if e.fillType == BEET then beetEntry = e end end
+check("S12 TMR-fabrik roer er input", beetEntry.isOutput, false)
+check("S12 outputs først", byName["TMR-fabrik"].entries[1].isOutput, true)
+check("S12 silo", byName["Farma"].entries[1].fillType, FORAGE)
+check("S12 objektlager kurve som antal", byName["Objektlager"].entries[1].count, 8)
+check("S12 stald halm", byName["Kostald"].entries[1].level, 2000)
+check("S12 stald kapacitet = niveau + fri", byName["Kostald"].entries[1].capacity, 21375)
+check("S12 tomt skur udeladt", byName["Fuldt skur"], nil)
+check("S12 anden farm udeladt", #inventory, 5)
+
print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1)