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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
Reference in New Issue
Block a user