feat: v1.12.1.0 — prisregel via sæsonkurven, reserve til gårdens eget forbrug, paller til palleskur, fanen Lagre (idéer fra Distribution Redux)

This commit is contained in:
2026-09-23 10:34:41 +02:00
parent 43483bda76
commit 2d6e6dd7bf
16 changed files with 595 additions and 43 deletions
+16 -4
View File
@@ -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
+29 -2
View File
@@ -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
+37 -22
View File
@@ -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.
+1
View File
@@ -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
+275
View File
@@ -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
+17 -4
View File
@@ -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)
@@ -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)
@@ -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
+6 -1
View File
@@ -60,6 +60,11 @@
<text name="spu_canCarry" text="kan bære: %s"/>
<text name="spu_tab_supply" text="Forsyning"/>
<text name="spu_tab_outbound" text="Udkørsel"/>
<text name="spu_tab_storage" text="Lagre"/>
<text name="spu_section_storage" text="— Gårdens beholdning (▸ = fabrikkens output) —"/>
<text name="spu_storage_pallets" text="%d paller"/>
<text name="spu_storage_empty" text="Ingen bygninger med indhold"/>
<text name="spu_source_kind_husbandry" text="stald"/>
<text name="spu_section_run" text="— Run —"/>
<text name="spu_section_targets" text="— Mål og varer —"/>
<text name="spu_section_sources" text="— Kilder (fabrik, silo eller objektlager) —"/>
@@ -72,7 +77,7 @@
<text name="spu_policy_store" text="Lager"/>
<text name="spu_policy_sell" text="Salg"/>
<text name="spu_policy_sellElseStore" text="Salg, ellers lager"/>
<text name="spu_policy_tooltip" text="Lager: kør til en silo når der er et fuldt læs eller lageret er højt. Salg: sælg når prisen er tæt på årets top. Salg, ellers lager: sælg ved god pris, ellers til silo når lageret er højt."/>
<text name="spu_policy_tooltip" text="Lager: kør til en silo (paller: et palleskur) når der er et fuldt læs eller lageret er højt. Salg: sælg når prisen er tæt på årets top — men aldrig det gårdens fabrikker og stalde skal bruge det næste døgn. Salg, ellers lager: sælg ved god pris, ellers til lager når lageret er højt."/>
<text name="spu_sellAt" text="Sælg fra (% af årets toppris)"/>
<text name="spu_sellAt_tooltip" text="Der køres til salg når salgsstedets pris er mindst denne andel af varens toppris i årets prisforløb — uanset mængde. Stor efterspørgsel tæller altid."/>
<text name="spu_storeAbove" text="Kør til lager over"/>
+6 -1
View File
@@ -60,6 +60,11 @@
<text name="spu_canCarry" text="can carry: %s"/>
<text name="spu_tab_supply" text="Supply"/>
<text name="spu_tab_outbound" text="Outbound"/>
<text name="spu_tab_storage" text="Storage"/>
<text name="spu_section_storage" text="— Farm inventory (▸ = production output) —"/>
<text name="spu_storage_pallets" text="%d pallets"/>
<text name="spu_storage_empty" text="No buildings with contents"/>
<text name="spu_source_kind_husbandry" text="barn"/>
<text name="spu_section_run" text="— Run —"/>
<text name="spu_section_targets" text="— Targets and goods —"/>
<text name="spu_section_sources" text="— Sources (factory, silo or object storage) —"/>
@@ -72,7 +77,7 @@
<text name="spu_policy_store" text="Store"/>
<text name="spu_policy_sell" text="Sell"/>
<text name="spu_policy_sellElseStore" text="Sell, else store"/>
<text name="spu_policy_tooltip" text="Store: drive to a silo when there is a full load or the storage is high. Sell: sell when the price is near the yearly top. Sell, else store: sell at a good price, otherwise to a silo when storage is high."/>
<text name="spu_policy_tooltip" text="Store: drive to a silo (pallets: a pallet shed) when there is a full load or the storage is high. Sell: sell when the price is near the yearly top — never what the farm's factories and barns need for the next day. Sell, else store: sell at a good price, otherwise to storage when storage is high."/>
<text name="spu_sellAt" text="Sell from (% of yearly top price)"/>
<text name="spu_sellAt_tooltip" text="Drive to sell when the selling point pays at least this share of the good's top price in the yearly price curve — regardless of amount. Great demand always qualifies."/>
<text name="spu_storeAbove" text="Drive to storage above"/>
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107">
<author>masterdraco</author>
<version>1.12.0.0</version>
<version>1.12.1.0</version>
<title>
<en>AD Smart Pickup</en>
</title>