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