Files

512 lines
25 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-- AD Chain Data (læse-adapter)
-- Omsætter spillets tilstand (gårdens fabrikker, stalde, afgrøder, lager og marker) til de rene data som
-- ADChainModel og ADCropPlan regner på. Varer og afgrøder er NAVNE; mængder er liter pr. måned (spillets
-- rå rater: én 24-timers dag pr. måned, som lagerfanen). Al spiladgang sker inde i funktionerne, så filen kan
-- dofile's uden spillet. Hver bygning/mark/afgrøde læses i pcall; en fejl logges én gang pr. nøgle.
ADChainData = {}
ADChainData.LOG_PREFIX = "ADSmartPickup: kæde: "
ADChainData.PERMANENT = {GRAPE = true, GRAPEB = true, OLIVE = true, POPLAR = true}
ADChainData.warned = {}
-- Varige afgrøder (vin, oliven, poppel) skiftes aldrig; mod-vine hedder også GRAPE…
function ADChainData.isPermanent(fruitName)
if type(fruitName) ~= "string" or fruitName == "" then return false end
return ADChainData.PERMANENT[fruitName] == true or fruitName:sub(1, 5) == "GRAPE"
end
function ADChainData.recipeKey(placeableUid, productionId)
return tostring(placeableUid) .. ":" .. tostring(productionId)
end
-- Samme omregning som lagerfanen (adSources addFactoryRates): cyklusser pr. 24-timers dag = pr. måned.
function ADChainData.cyclesPerMonth(production)
return (production.cyclesPerMinute or ((production.cyclesPerHour or 0) / 60)) * 1440
end
function ADChainData.warnOnce(key, fmt, ...)
if ADChainData.warned[key] then return end
ADChainData.warned[key] = true
if Logging ~= nil and Logging.warning ~= nil then Logging.warning(ADChainData.LOG_PREFIX .. fmt, ...) end
end
-- En læsefejl i dette kald: data.partial = true (også når advarslen allerede er logget), så kæden ikke
-- slukker eller stopper noget på et ufuldstændigt billede af gården.
-- data.readFailed skelner en ægte læsefejl fra en ufærdig markscanning (afhængighedstjekket bruger ikke markerne).
local function failed(data, key, fmt, ...)
data.partial, data.readFailed = true, true
ADChainData.warnOnce(key, fmt, ...)
end
-- ---- hjælpere ----
-- Navn for et fillType-indeks; husker indeks og titel til GUI og vognvalg.
local function nameResolver(data)
return function(index)
if index == nil or g_fillTypeManager == nil then return nil end
local name = g_fillTypeManager:getFillTypeNameByIndex(index)
if name == nil then return nil end
if data.fillIndex[name] == nil then
data.fillIndex[name] = index
local desc = g_fillTypeManager.getFillTypeByIndex ~= nil and g_fillTypeManager:getFillTypeByIndex(index) or nil
data.fillTitles[name] = tostring(desc ~= nil and desc.title or name)
end
return name
end
end
-- {{type, amount}} -> {[navn] = mængde pr. cyklus}
local function amountsByName(list, nameOf)
local result = {}
for _, entry in ipairs(list or {}) do
local name = nameOf(entry.type)
if name ~= nil and (entry.amount or 0) > 0 then result[name] = (result[name] or 0) + entry.amount end
end
return result
end
local function placeableId(placeable)
if placeable.uniqueId ~= nil then return tostring(placeable.uniqueId) end
return ADBuildings ~= nil and ADBuildings.getId ~= nil and ADBuildings.getId(placeable) or nil
end
-- Spillerens eget navn (ADNames) før spillets; samme rækkefølge som adSources/adBuildings.
local function placeableName(placeable, fallback)
local own = ADNames ~= nil and ADNames.forPlaceable ~= nil and ADNames.forPlaceable(placeable) or nil
if own ~= nil then return own end
if placeable.getName ~= nil then
local ok, name = pcall(placeable.getName, placeable)
if ok and name ~= nil then return tostring(name) end
end
return fallback
end
local function isEnabled(point, production)
if point.getIsProductionEnabled ~= nil then return point:getIsProductionEnabled(production.id) == true end
for _, active in ipairs(point.activeProductions or {}) do
if active == production then return true end
end
return false
end
-- ---- læsning pr. del ----
-- En fabriks produktioner -> opskrifter + punkter (samles først, så en fejl midtvejs ikke efterlader halve).
-- Udgangens fordeling (spillets Sælg/Fordel/Behold) -> "sell"|"distribute"|"keep". Behold dækker både spawn (paller
-- ved fabrikken) og lager (STORE fra ProductionStorageControl): begge dele skal hentes af et flow.
function ADChainData.outputModeName(mode)
local modes = ProductionPoint ~= nil and ProductionPoint.OUTPUT_MODE or {}
if mode ~= nil and mode == modes.DIRECT_SELL then return "sell" end
if mode ~= nil and mode == modes.AUTO_DELIVER then return "distribute" end
return "keep"
end
local function outputModes(point, outputs, nameOf)
local modes = {}
if point.getOutputDistributionMode == nil then return modes end
for _, output in ipairs(outputs or {}) do
local name = output.type ~= nil and nameOf(output.type) or nil
if name ~= nil then
local ok, mode = pcall(point.getOutputDistributionMode, point, output.type)
modes[name] = ADChainData.outputModeName(ok and mode or nil)
end
end
return modes
end
local function readFactory(placeable, point, byChain, nameOf)
local uid = placeableId(placeable)
if uid == nil then return {}, {} end
local buildingName = placeableName(placeable, uid)
local recipes, points = {}, {}
for _, production in ipairs(point.productions or {}) do
local key = ADChainData.recipeKey(uid, production.id)
table.insert(recipes, {key = key, name = tostring(production.name or production.id), building = uid,
buildingName = buildingName, enabled = isEnabled(point, production), byChain = (byChain or {})[key] == true,
cyclesPerMonth = ADChainData.cyclesPerMonth(production),
inputs = amountsByName(production.inputs, nameOf), outputs = amountsByName(production.outputs, nameOf),
outputModes = outputModes(point, production.outputs, nameOf)})
points[key] = {point = point, productionId = production.id, placeable = placeable, buildingName = buildingName}
end
return recipes, points
end
local function readHerd(placeable, nameOf)
local uid = placeableId(placeable)
local outputs = {}
for index, perMonth in pairs(ADSources.getHusbandryOutputs(placeable) or {}) do
local name = nameOf(index)
if name ~= nil then outputs[name] = (outputs[name] or 0) + perMonth end
end
if uid == nil or next(outputs) == nil then return nil end
return {building = uid, buildingName = placeableName(placeable, uid), outputs = outputs}
end
-- Stald-foder: {[vare] = {staldnavn...}} for det truget æder nu (forbrug > 0) — til afhængighedstjekket.
local function readBarnFood(data, placeable, nameOf)
if ADSources.getHusbandryConsumption == nil then return end
local barnName
for index, perDay in pairs(ADSources.getHusbandryConsumption(placeable) or {}) do
local name = nameOf(index)
if name ~= nil and (perDay or 0) > 0 then
barnName = barnName or placeableName(placeable, placeableId(placeable) or "?")
data.barnFood[name] = data.barnFood[name] or {}
table.insert(data.barnFood[name], barnName)
end
end
end
local function readPlaceables(data, farmId, byChain, nameOf)
local placeables = g_currentMission ~= nil and g_currentMission.placeableSystem ~= nil and g_currentMission.placeableSystem.placeables or {}
for index, placeable in pairs(placeables) do
local ok, err = pcall(function()
if not ADPrices.isOwnedBy(placeable, farmId) then return end
local uid = placeableId(placeable)
if uid ~= nil then data.placeables[uid] = placeable end
local point = placeable.spec_productionPoint ~= nil and placeable.spec_productionPoint.productionPoint or nil
if point ~= nil then
local recipes, points = readFactory(placeable, point, byChain, nameOf)
for _, recipe in ipairs(recipes) do table.insert(data.recipes, recipe) end
for key, entry in pairs(points) do data.points[key] = entry end
elseif placeable.spec_husbandry ~= nil or placeable.getHusbandryFillLevel ~= nil then
local herd = readHerd(placeable, nameOf)
if herd ~= nil then table.insert(data.husbandry, herd) end
readBarnFood(data, placeable, nameOf)
end
end)
if not ok then
local key = tostring(placeable ~= nil and placeable.uniqueId or index)
failed(data, "placeable:" .. key, "bygning %s kunne ikke læses: %s", key, tostring(err))
end
end
table.sort(data.recipes, function(a, b) return a.key < b.key end)
end
-- Markedspris for en afgrøde, først når kæden spørger (kun kandidatmarkernes afgrøder, kun ved mangel);
-- hver afgrøde slås højst op én gang pr. læsning. Fejl -> 0 (logget én gang).
local function priceResolver(data)
local cache = {}
return function(fruitName)
if cache[fruitName] ~= nil then return cache[fruitName] end
local index = data.fruitFillIndex[fruitName]
local price = 0
if index ~= nil then
local ok, quote = pcall(ADSources.getMarketQuote, index)
if ok then
price = quote ~= nil and quote.price or 0
else
ADChainData.warnOnce("price:" .. tostring(fruitName), "pris for %s kunne ikke læses: %s", tostring(fruitName), tostring(quote))
end
end
cache[fruitName] = price
return price
end
end
local function readCrops(data, nameOf)
local fruits = g_fruitTypeManager ~= nil and g_fruitTypeManager.fruitTypes or {}
for _, fruit in ipairs(fruits) do
if fruit.fillType ~= nil and fruit.allowsSeeding ~= false and fruit.name ~= nil then
local ok, err = pcall(function()
local fillName = fruit.fillType.name or nameOf(fruit.fillType.index)
if fillName == nil then return end
if fruit.fillType.index ~= nil then nameOf(fruit.fillType.index) end
data.crops[fillName] = fruit.name
data.yields[fruit.name] = (tonumber(fruit.literPerSqm) or 0) * 10000
data.fruitFillIndex[fruit.name] = fruit.fillType.index
end)
if not ok then failed(data, "fruit:" .. tostring(fruit.name), "afgrøde %s kunne ikke læses: %s", tostring(fruit.name), tostring(err)) end
end
end
end
-- ---- markvarer (hø, hakkelse, halm …) ----
-- Varer der høstes/laves på marken uden at være en afgrødes hovedvare. Spillets konvertere hedder forskelligt
-- mellem versioner/kort (FORAGEHARVESTER/FORAGE_HARVESTER), så alle navne prøves; et ukendt navn giver nil.
ADChainData.FIELD_CONVERTERS = {"FORAGEHARVESTER", "FORAGE_HARVESTER", "MOWER", "COMBINE", "CUTTER"}
-- Kendte markvarer (basisspil, FermentingSiloPack, Dakota); kun dem der findes som fill type bruges.
ADChainData.KNOWN_FIELD_GOODS = {"STRAW", "CHAFF", "CROP_WINDROW", "GRASS_WINDROW", "ALFALFA_WINDROW", "CLOVER_WINDROW",
"DRYGRASS_WINDROW", "DRYALFALFA_WINDROW", "DRYCLOVER_WINDROW", "CCM", "CCMRAW", "WCS"}
-- fruit.windrowFillType kan være et indeks eller en fill type-beskrivelse
local function windrowIndex(fruit)
local windrow = fruit.windrowFillType
if type(windrow) == "number" then return windrow end
if type(windrow) == "table" then
if windrow.index ~= nil then return windrow.index end
if windrow.name ~= nil and g_fillTypeManager.getFillTypeIndexByName ~= nil then
return g_fillTypeManager:getFillTypeIndexByName(windrow.name)
end
end
return nil
end
-- Én konverters mål-indekser ({[fra] = {targetFillTypeIndex|fillTypeIndex}}, samme form som adFields.buildMowable)
local function converterTargets(converter, add)
for _, entry in pairs(converter or {}) do
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
end
end
-- data.fieldGoods = {[navn] = true}. Kun eksisterende fill types; fejl logges én gang og gør ikke læsningen delvis
-- (en ændret spil-API må ikke blokere kæden for altid) — værst tænkeligt meldes en markvare som "køb".
local function readFieldGoods(data, nameOf)
local indices = {}
local function add(index) if type(index) == "number" then indices[index] = true end end
local function guarded(key, fn)
local ok, err = pcall(fn)
if not ok then ADChainData.warnOnce("fieldGoods:" .. key, "markvarer (%s) kunne ikke læses: %s", key, tostring(err)) end
end
if g_fillTypeManager == nil then return end
local fruitManager = g_fruitTypeManager
for _, fruit in ipairs(fruitManager ~= nil and fruitManager.fruitTypes or {}) do
guarded("windrow:" .. tostring(fruit.name), function() add(windrowIndex(fruit)) end)
end
if fruitManager ~= nil and fruitManager.getConverterDataByName ~= nil then
for _, name in ipairs(ADChainData.FIELD_CONVERTERS) do
guarded(name, function() converterTargets(fruitManager:getConverterDataByName(name), add) end)
end
end
if g_fillTypeManager.getFillTypeIndexByName ~= nil then
for _, name in ipairs(ADChainData.KNOWN_FIELD_GOODS) do
guarded(name, function() add(g_fillTypeManager:getFillTypeIndexByName(name)) end)
end
end
-- tørrede varianter (vender: græs-vindue -> hø)
if g_fillTypeManager.getConverterDataByName ~= nil then
guarded("TEDDER", function()
local tedder = g_fillTypeManager:getConverterDataByName("TEDDER") or {}
local windrows = {}
for index in pairs(indices) do table.insert(windrows, index) end
for _, index in ipairs(windrows) do
local entry = tedder[index]
if type(entry) == "table" then add(entry.targetFillTypeIndex or entry.fillTypeIndex) end
end
end)
end
for index in pairs(indices) do
local ok, name = pcall(nameOf, index)
if ok and name ~= nil and data.crops[name] == nil then data.fieldGoods[name] = true end
end
end
-- Læs i gårdens køretøjer (vogne, høstertanke, marksilo-vogne) tæller som lager: kun afgrøder og markvarer, ikke
-- diesel o.l. loads = {{name, liters}} (in-game 30/9: høst på vej hjem gav falsk mangel og forslag).
function ADChainData.addVehicleLoads(data, loads)
for _, load in ipairs(loads or {}) do
local name, liters = load.name, load.liters or 0
local fruit = name ~= nil and (data.crops or {})[name] or nil
if liters > 0 and (fruit ~= nil or (data.fieldGoods or {})[name]) then
data.stockByFill[name] = (data.stockByFill[name] or 0) + liters
if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
end
end
end
local function readVehicleLoads(data, farmId, nameOf)
local ok, err = pcall(function()
local loads = {}
for _, vehicle in pairs(g_currentMission.vehicleSystem.vehicles or {}) do
local spec = vehicle.spec_fillUnit
if spec ~= nil and vehicle.getOwnerFarmId ~= nil and vehicle:getOwnerFarmId() == farmId then
for index, unit in ipairs(spec.fillUnits or {}) do
local level = vehicle:getFillUnitFillLevel(index) or 0
if level > 0 then
table.insert(loads, {name = nameOf(vehicle:getFillUnitFillType(index) or unit.fillType), liters = level})
end
end
end
end
ADChainData.addVehicleLoads(data, loads)
end)
if not ok then ADChainData.warnOnce("vehicleLoads", "læs i køretøjer kunne ikke læses: %s", tostring(err)) end
end
local function readStock(data, adEnv, farmId, nameOf)
local ok, err = pcall(function()
for index, liters in pairs(ADStorageStats.farmTotals(ADSources.getFarmInventory(adEnv, farmId))) do
local name = nameOf(index)
if name ~= nil then data.stockByFill[name] = (data.stockByFill[name] or 0) + liters end
local fruit = data.crops[name or ""]
if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end
end
end)
if not ok then failed(data, "stock", "lager kunne ikke læses: %s", tostring(err)) end
end
-- Gårdens forbrug og produktion pr. døgn (fabrikker der kører + stalde) pr. varenavn og spillets tidsfaktor
-- (rå rater pr. måned × faktor = pr. døgn) — til lagerdage og dækning i afhængighedstjekket.
local function readRates(data, adEnv, farmId, nameOf)
local environment = g_currentMission ~= nil and g_currentMission.environment or nil
local adjustment = environment ~= nil and tonumber(environment.timeAdjustment) or nil
data.timeAdjustment = adjustment ~= nil and adjustment > 0 and adjustment or 1
local ok, err = pcall(function()
for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId, false) or {}) do
local name = nameOf(index)
if name ~= nil then data.rates[name] = {consumption = rate.consumption or 0, production = rate.production or 0} end
end
end)
if not ok then failed(data, "allRates", "gårdens forbrug kunne ikke læses: %s", tostring(err)) end
end
-- Afgrøder gården selv bruger: input i en tændt produktion eller foder/halm i en stald (kun staldenes forbrug
-- læses: feedOnly springer fabrikkernes rater over — dem dækker opskrifterne allerede).
local function readUsedFruits(data, adEnv, farmId, nameOf)
for _, recipe in ipairs(data.recipes) do
if recipe.enabled then
for name in pairs(recipe.inputs) do
if data.crops[name] ~= nil then data.usedFruits[data.crops[name]] = true end
end
end
end
local ok, err = pcall(function()
for index, rate in pairs(ADSources.getFarmRates(adEnv, farmId, true) or {}) do
local fruit = data.crops[nameOf(index) or ""]
if fruit ~= nil and (rate.consumption or 0) > 0 then data.usedFruits[fruit] = true end
end
end)
if not ok then failed(data, "rates", "staldforbrug kunne ikke læses: %s", tostring(err)) end
end
-- Markscanningen har ikke nået alle marker endnu (efter indlæsning ~én mark pr. 2 s): marklisten er ufuldstændig,
-- så udbud undervurderes -> fieldsIncomplete (kæden rører ingen marker) og partial (intet slukkes/stoppes).
local function readFields(data)
if ADFields.isScanComplete ~= nil and not ADFields.isScanComplete() then
data.partial, data.fieldsIncomplete = true, true
end
for _, info in ipairs(ADFields.list() or {}) do
local ok, entry = pcall(function()
local field = ADFieldJobs.getField(info.id)
local plan = field.plan or {}
local fruit = info.growing and info.fruitName or field.lastFruit
-- høst i gang: ingen forslag/skift for afgrøden, mens læssene er på vej hjem
if field.stage == "harvesting" and info.fruitName ~= nil then data.harvesting[info.fruitName] = true end
return {id = info.id, areaHa = ADFields.polygonAreaHa(info.verts), fruit = fruit,
targetFruit = plan.targetFruit, targetSource = plan.targetSource, chainFree = plan.chainFree == true,
permanent = ADChainData.isPermanent(info.fruitName or field.lastFruit), excluded = info.kind == "mowable"}
end)
if ok then
table.insert(data.fields, entry)
else
failed(data, "field:" .. tostring(info.id), "mark %s kunne ikke læses: %s", tostring(info.id), tostring(entry))
end
end
end
-- ---- afhængighedstjek (spec afsnit 8) ----
local function unloadStationsOf(placeable)
local storage = g_currentMission ~= nil and g_currentMission.storageSystem or nil
local result = {}
if storage == nil or storage.getUnloadingStations == nil then return result end
for _, station in pairs(storage:getUnloadingStations() or {}) do
if station.owningPlaceable == placeable then table.insert(result, station) end
end
return result
end
-- Hvad auto-flowenes mål tager af varerne `goods` (navne): {[bygnings-id] = {[vare] = true}}.
-- Kun tændte forsyningsflows i auto-tilstand; hvert mål slås op én gang.
function ADChainData.acceptsMap(data, flows, goods)
local result = {}
for _, flow in ipairs(flows or {}) do
if flow.kind ~= "outbound" and not flow.stopped and flow.ingredientMode ~= "manual" then
for _, id in ipairs(flow.targetBuildingIds or {}) do
local placeable = data.placeables[id]
if result[id] == nil and placeable ~= nil then
result[id] = {}
local ok, err = pcall(function()
local stations = unloadStationsOf(placeable)
for _, name in ipairs(goods) do
local index = data.fillIndex[name]
for _, station in ipairs(stations) do
if index ~= nil and ADSources.stationAccepts(station, index) then result[id][name] = true; break end
end
end
end)
if not ok then ADChainData.warnOnce("accepts:" .. tostring(id), "mål %s kunne ikke læses: %s", tostring(id), tostring(err)) end
end
end
end
end
return result
end
-- Kædens input-varer (alle varer i de aktive kæders træer) -> {[vare] = {kædenavn...}}
function ADChainData.chainInputs(chain, titles)
local result = {}
local function walk(nodes, rootTitle)
for _, node in ipairs(nodes or {}) do
result[node.fillType] = result[node.fillType] or {}
local list = result[node.fillType]
if list[#list] ~= rootTitle then table.insert(list, rootTitle) end
walk(node.children, rootTitle)
end
end
for _, root in ipairs(chain ~= nil and chain.roots or {}) do
walk(root.children, tostring((titles or {})[root.name] or root.name))
end
return result
end
-- Alt afhængighedstjekket regner på, i liter pr. døgn: {needed, stockDays, capacity, demand}.
-- flows = ADChain.runsView(...); chain = kædemodellen (kan være uden rødder).
function ADChainData.dependencyInputs(data, chain, flows)
local goods, seen = {}, {}
for _, recipe in ipairs(data.recipes or {}) do
if not recipe.enabled and not recipe.byChain then
for name in pairs(recipe.outputs or {}) do
if not seen[name] then seen[name] = true; table.insert(goods, name) end
end
end
end
table.sort(goods)
-- auto-flowets mål tager mange varer (fx biogas: korn, ensilage, gylle): kun varer gården faktisk forbruger
-- (stald eller fabrik) eller en kæde vil have, tæller
local consumed = {}
for _, name in ipairs(goods) do
local rate = (data.rates or {})[name]
if (rate ~= nil and (rate.consumption or 0) > 0) or ((chain ~= nil and chain.demand or {})[name] or 0) > 0 then
table.insert(consumed, name)
end
end
local needed = ADDependency.neededOutputs(flows, data.barnFood, ADChainData.chainInputs(chain, data.fillTitles),
#consumed > 0 and ADChainData.acceptsMap(data, flows, consumed) or {})
local adjustment = data.timeAdjustment or 1
local capacity, demand, stockDays = {}, {}, {}
for name, perMonth in pairs(ADDependency.capacity(data.recipes)) do capacity[name] = perMonth * adjustment end
for _, name in ipairs(goods) do
local rate = (data.rates or {})[name] or {}
local consumption, production = rate.consumption or 0, rate.production or 0
local chainDemand = ((chain ~= nil and chain.demand or {})[name] or 0) * adjustment
demand[name] = math.max(consumption, chainDemand)
if consumption > production then
stockDays[name] = ((data.stockByFill or {})[name] or 0) / (consumption - production)
elseif consumption > 0 then
stockDays[name] = math.huge
end
end
return {needed = needed, stockDays = stockDays, capacity = capacity, demand = demand}
end
-- ---- samlet ----
-- byChain = {[recipeKey] = true} for produktioner kæden selv har tændt (ikke rødder).
-- data.partial = true når noget ikke kunne læses i dette kald (data.readFailed) eller markscanningen kører; data.fieldsIncomplete = true mens markscanningen
-- ikke har nået alle marker.
function ADChainData.collect(adEnv, farmId, byChain)
local data = {recipes = {}, husbandry = {}, crops = {}, stock = {}, fields = {}, yields = {}, fruitFillIndex = {},
usedFruits = {}, points = {}, fillTitles = {}, fillIndex = {}, placeables = {}, barnFood = {}, stockByFill = {}, rates = {}, fieldGoods = {}, harvesting = {}}
local nameOf = nameResolver(data)
data.priceOf = priceResolver(data)
readPlaceables(data, farmId, byChain, nameOf)
readCrops(data, nameOf)
readFieldGoods(data, nameOf)
readStock(data, adEnv, farmId, nameOf)
readVehicleLoads(data, farmId, nameOf)
readRates(data, adEnv, farmId, nameOf)
readUsedFruits(data, adEnv, farmId, nameOf)
readFields(data)
return data
end