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 end
local function findStoreMarker(adEnv, vehicle, product, capacity, sources, litersOverride) 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 return nil
end end
local available = litersOverride or 0 local available = litersOverride or 0
@@ -154,9 +164,6 @@ local function findStoreMarker(adEnv, vehicle, product, capacity, sources, liter
end end
end end
local liters = math.max(1, math.min(available, capacity or available)) 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)) local marker = ADLoadSwap.findDumpMarker(adEnv, vehicle, product.fillType, liters, nil, isReachable, ADSources.getSourcePlaceables(sources))
if marker == nil then if marker == nil then
return nil 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 homeId = product.sources[1] ~= nil and product.sources[1].wayPointId or nil
local quote, unreachableName = findSellQuote(adEnv, vehicle, product, sellMarkers, run.sellAtPercent, homeId) local quote, unreachableName = findSellQuote(adEnv, vehicle, product, sellMarkers, run.sellAtPercent, homeId)
product.quote = quote 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 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)) 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 else
+29 -2
View File
@@ -8,7 +8,8 @@
-- products = { { fillType, name, policy = "store"|"sell"|"sellElseStore", isPallet, -- products = { { fillType, name, policy = "store"|"sell"|"sellElseStore", isPallet,
-- sources = { {wayPointId, available, level, capacity} ... }, -- kilder med varen -- sources = { {wayPointId, available, level, capacity} ... }, -- kilder med varen
-- quote = {percent, price, wayPointId, stationName} | nil, -- bedste NÅELIGE salgssted -- 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 -- plan(input) -> trip | nil, reasons
-- trip = {kind = "sell"|"store", fillTypes, names, sourceIds, targetWayPointId, targetName, percent} -- 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 %") -- 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) function ADOutboundPlanner.evaluate(product, input)
local available, storagePercent = sumSources(product) local available, storagePercent = sumSources(product)
local percent = product.quote ~= nil and product.quote.percent or nil local percent = product.quote ~= nil and product.quote.percent or nil
local reserve = math.max(0, product.reserve or 0)
return { return {
available = available, available = available,
reserve = reserve,
sellable = math.max(0, available - reserve),
fullLoad = input.capacity ~= nil and input.capacity > 0 and available >= input.capacity, fullLoad = input.capacity ~= nil and input.capacity > 0 and available >= input.capacity,
storagePercent = storagePercent, storagePercent = storagePercent,
storageHigh = storagePercent >= (input.storeAbovePercent or 80), storageHigh = storagePercent >= (input.storeAbovePercent or 80),
@@ -53,10 +57,33 @@ function ADOutboundPlanner.evaluate(product, input)
} }
end 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) local function reasonFor(product, e, input)
if e.available < ADOutboundPlanner.MIN_AVAILABLE_LITERS then if e.available < ADOutboundPlanner.MIN_AVAILABLE_LITERS then
return product.name .. ": intet på lager" return product.name .. ": intet på lager"
end 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 local canStore = wantsStore(product.policy) and product.store ~= nil
if wantsSell(product.policy) and product.quote == nil and not canStore then if wantsSell(product.policy) and product.quote == nil and not canStore then
return product.name .. ": intet nåeligt salgssted" return product.name .. ": intet nåeligt salgssted"
@@ -79,7 +106,7 @@ local function collectCandidates(input)
local e = ADOutboundPlanner.evaluate(product, input) local e = ADOutboundPlanner.evaluate(product, input)
local added = false local added = false
if e.available >= ADOutboundPlanner.MIN_AVAILABLE_LITERS then 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}) table.insert(sell, {product = product, e = e, kind = "sell", targetWayPointId = product.quote.wayPointId, targetName = product.quote.stationName, percent = e.percent})
added = true added = true
elseif wantsStore(product.policy) and (e.fullLoad or e.storageHigh) and product.store ~= nil then 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 -- AD Prices
-- Prisregel for udkørsel: hvor tæt er salgsstedets pris på årets toppris for varen? -- 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 -- Grundlaget er spillets faste sæsonkurve (fillType.economy.factors[1..12], samme data som
-- faktor) — dvs. det Priser-menuen ville vise som årets bedste måned for netop den station. -- prisgrafen i Priser-menuen; metode fra Distribution Redux): kurve-% = faktor nu ÷ årets højeste.
-- Effektiv pris (inkl. prisfald efter salg og stor efterspørgsel) ÷ toppris = pris-%; -- Oven på det ganges stationens egen afvigelse (effektiv pris ÷ stationens grundpris nu), så
-- stor efterspørgsel giver > 100 % og opfylder dermed altid reglen. -- 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. -- De rene funktioner øverst har ingen spil-API; getQuote er spil-adapteren.
ADPrices = {} ADPrices = {}
ADPrices.LOG_PREFIX = "ADSmartPickup: " 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) function ADPrices.maxFactor(factors)
local best = nil local best = nil
@@ -37,6 +41,29 @@ function ADPrices.percentOfTop(price, top)
return math.floor(100 * price / top + 0.5) return math.floor(100 * price / top + 0.5)
end 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 -- Rigtige salgssteder: SellingStation der sælger (fabrikkers aflæsningsstationer er
-- også SellingStation-objekter, men med isSellingPoint = false) og vises i Priser-menuen. -- også SellingStation-objekter, men med isSellingPoint = false) og vises i Priser-menuen.
function ADPrices.isSellingStation(station) function ADPrices.isSellingStation(station)
@@ -49,18 +76,8 @@ local function getCurrentPeriod()
return environment ~= nil and environment.currentPeriod or nil return environment ~= nil and environment.currentPeriod or nil
end 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. -- {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) function ADPrices.getQuote(station, fillType)
if station == nil or station.getEffectiveFillTypePrice == nil then if station == nil or station.getEffectiveFillTypePrice == nil then
return nil return nil
@@ -71,14 +88,12 @@ function ADPrices.getQuote(station, fillType)
end end
local fillTypeDesc = g_fillTypeManager ~= nil and g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(fillType) or nil 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 factors = fillTypeDesc ~= nil and fillTypeDesc.economy ~= nil and fillTypeDesc.economy.factors or nil
local period = getCurrentPeriod() local curve = ADPrices.curvePercent(factors, getCurrentPeriod())
local factorNow = factors ~= nil and period ~= nil and tonumber(factors[period]) or 1
local baseNow = station.fillTypePrices ~= nil and tonumber(station.fillTypePrices[fillType]) or nil local baseNow = station.fillTypePrices ~= nil and tonumber(station.fillTypePrices[fillType]) or nil
if baseNow == nil or baseNow <= 0 then local stationRatio = (baseNow ~= nil and baseNow > 0) and (effective / baseNow) or 1
baseNow = (fillTypeDesc ~= nil and tonumber(fillTypeDesc.pricePerLiter) or 0) * factorNow * getDifficultyMultiplier() local percent = math.floor(curve * stationRatio + 0.5)
end local top = percent > 0 and (effective * 100 / percent) or effective
local top = ADPrices.topPrice(baseNow, factorNow, ADPrices.maxFactor(factors)) return {price = effective, top = top, percent = percent}
return {price = effective, top = top, percent = ADPrices.percentOfTop(effective, top)}
end end
-- Højeste pris-%, ved lighed højeste pris. nil ved tom liste. -- 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/SmartPickupFrame.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupSupplyTab.lua") source(ADSmartPickup.dir .. "gui/SmartPickupSupplyTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupOutboundTab.lua") source(ADSmartPickup.dir .. "gui/SmartPickupOutboundTab.lua")
source(ADSmartPickup.dir .. "gui/SmartPickupStorageTab.lua")
end end
-- Markøren er dér hvor trækkerens næse stopper; læssetriggeren ligger et -- 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 return result
end 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) function ADSources.getSourcePlaceables(sources)
local set = {} local set = {}
for _, source in ipairs(sources or {}) do 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.REFRESH_INTERVAL_MS = 1000
SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: " SmartPickupFrame.LOG_PREFIX = "ADSmartPickup: "
SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2 SmartPickupFrame.STATE_OFF, SmartPickupFrame.STATE_ON = 1, 2
SmartPickupFrame.TAB_OVERVIEW, SmartPickupFrame.TAB_SUPPLY, SmartPickupFrame.TAB_OUTBOUND = 1, 2, 3 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"} 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 opretter forsynings-runs. -- Hver fane viser kun runs af sin egen slags; Oversigt og Lagre opretter forsynings-runs.
SmartPickupFrame.TAB_KIND = {"supply", "supply", "outbound"} SmartPickupFrame.TAB_KIND = {"supply", "supply", "outbound", "supply"}
SmartPickupFrame.POLICIES = {"off", "store", "sell", "sellElseStore"} SmartPickupFrame.POLICIES = {"off", "store", "sell", "sellElseStore"}
function SmartPickupFrame.new(i18n, getAdEnv) function SmartPickupFrame.new(i18n, getAdEnv)
@@ -296,6 +296,7 @@ function SmartPickupFrame:clearRows()
self.needsRows = {} self.needsRows = {}
self.overviewRows = {} self.overviewRows = {}
self.productRows, self.outboundStatusRows = {}, {} self.productRows, self.outboundStatusRows = {}, {}
self.storageRows = {}
end end
function SmartPickupFrame:addRow(prefab, data) function SmartPickupFrame:addRow(prefab, data)
@@ -345,6 +346,17 @@ function SmartPickupFrame:rebuild()
self:clearRows() self:clearRows()
local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW local isOverview = self.currentTab == SmartPickupFrame.TAB_OVERVIEW
self:setStaticRowsVisible(self.currentTab) 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 isOverview then
if adEnv ~= nil then if adEnv ~= nil then
self:fillOverviewRows(adEnv) self:fillOverviewRows(adEnv)
@@ -665,6 +677,7 @@ function SmartPickupFrame:refreshLive()
return return
end end
self:refreshOverview(adEnv) self:refreshOverview(adEnv)
self:refreshStorageRows(adEnv)
local isRunning = false local isRunning = false
if run ~= nil then if run ~= nil then
self:syncOptionsFromModel(adEnv, run) 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) return string.format(self.i18n:getText("spu_product_row"), tostring(product.title), SmartPickupFrame.formatCompact(product.available or 0), price)
end end
-- Paller kan ikke lagres i en silo: kun Fra/Salg for pallevarer. -- Paller lagres i et palleskur (objektlager) i stedet for en silo — samme politikker som bulk.
SmartPickupFrame.PALLET_POLICIES = {"off", "sell"}
function SmartPickupFrame.getPoliciesFor(product) function SmartPickupFrame.getPoliciesFor(product)
return product.isPallet and SmartPickupFrame.PALLET_POLICIES or SmartPickupFrame.POLICIES return SmartPickupFrame.POLICIES
end end
function SmartPickupFrame.getPolicyIndex(policies, policy) 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_canCarry" text="kan bære: %s"/>
<text name="spu_tab_supply" text="Forsyning"/> <text name="spu_tab_supply" text="Forsyning"/>
<text name="spu_tab_outbound" text="Udkørsel"/> <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_run" text="— Run —"/>
<text name="spu_section_targets" text="— Mål og varer —"/> <text name="spu_section_targets" text="— Mål og varer —"/>
<text name="spu_section_sources" text="— Kilder (fabrik, silo eller objektlager) —"/> <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_store" text="Lager"/>
<text name="spu_policy_sell" text="Salg"/> <text name="spu_policy_sell" text="Salg"/>
<text name="spu_policy_sellElseStore" text="Salg, ellers lager"/> <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" 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_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"/> <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_canCarry" text="can carry: %s"/>
<text name="spu_tab_supply" text="Supply"/> <text name="spu_tab_supply" text="Supply"/>
<text name="spu_tab_outbound" text="Outbound"/> <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_run" text="— Run —"/>
<text name="spu_section_targets" text="— Targets and goods —"/> <text name="spu_section_targets" text="— Targets and goods —"/>
<text name="spu_section_sources" text="— Sources (factory, silo or object storage) —"/> <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_store" text="Store"/>
<text name="spu_policy_sell" text="Sell"/> <text name="spu_policy_sell" text="Sell"/>
<text name="spu_policy_sellElseStore" text="Sell, else store"/> <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" 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_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"/> <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"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<modDesc descVersion="107"> <modDesc descVersion="107">
<author>masterdraco</author> <author>masterdraco</author>
<version>1.12.0.0</version> <version>1.12.1.0</version>
<title> <title>
<en>AD Smart Pickup</en> <en>AD Smart Pickup</en>
</title> </title>
+17
View File
@@ -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 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. 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 **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, → 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; sættes pr. tur), rotation "kun pålæsning". Runs gemmes i `ADSmartPickup_runs.xml` version 2;
+19
View File
@@ -394,6 +394,25 @@ ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
ADOutbound.beforeLoadUpdate(adEnv, task16, 100) ADOutbound.beforeLoadUpdate(adEnv, task16, 100)
check("O16 næste spawn via fyldniveau", sources[3].spawnCalls, 2) 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 -- O10: install wrapper: fejl i hook stopper ikke AD
local calls = {} local calls = {}
adEnv.LoadAtDestinationTask.update = function(self, dt) calls.update = (calls.update or 0) + 1 end adEnv.LoadAtDestinationTask.update = function(self, dt) calls.update = (calls.update or 0) + 1 end
+21
View File
@@ -109,5 +109,26 @@ check("P12 storageHigh", e.storageHigh, true)
check("P12 fullLoad", e.fullLoad, true) check("P12 fullLoad", e.fullLoad, true)
check("P12 priceOk", e.priceOk, 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")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)
+16 -4
View File
@@ -42,17 +42,29 @@ local station = {
} }
local quote = ADPrices.getQuote(station, SUGAR) local quote = ADPrices.getQuote(station, SUGAR)
check("P4 pris", quote.price, 470) 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 procent", quote.percent, 75)
check("P4 er salgssted", ADPrices.isSellingStation(station), true) 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 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) 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 bare = {getEffectiveFillTypePrice = function() return 470 end}
local quote2 = ADPrices.getQuote(bare, SUGAR) local quote2 = ADPrices.getQuote(bare, SUGAR)
check("P5 top fra pricePerLiter", quote2.top, 500) check("P5 procent = kurven alene", quote2.percent, 80)
check("P5 procent", quote2.percent, 94) 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) check("P5 fejlende station -> nil", ADPrices.getQuote({getEffectiveFillTypePrice = function() error("x") end}, SUGAR), nil)
-- P6: bedste tilbud = højeste procent, dernæst pris -- P6: bedste tilbud = højeste procent, dernæst pris
+77
View File
@@ -199,5 +199,82 @@ check("S9 fabrik foretrækkes frem for objektlager i nærheden", ADSources.ident
local excluded = ADSources.getSourcePlaceables({source1, source2}) local excluded = ADSources.getSourcePlaceables({source1, source2})
check("S8 udelukkede placeables", excluded[factory] and excluded[silo], true) 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")) print(failures == 0 and "\nALLE TESTS OK" or ("\n" .. failures .. " FEJL"))
os.exit(failures == 0 and 0 or 1) os.exit(failures == 0 and 0 or 1)