979b7f72d5
Slukker spilleren en produktion, hvis vare et tændt flow kører (manuel vare eller målet tager den i auto), en stald æder eller en aktiv kæde bruger, kommer én besked pr. slukning med brugere og lagerdage, og en rød post 'Slukket men nødvendig' i Kræver handling og statusvinduet så længe den er slukket og nødvendig. Ingen advarsel når en anden tændt produktion dækker, lageret rækker over 60 dage, kæden selv slukkede den eller den var slukket ved indlæsning. Tjekket kører i kædens tick også uden kæder; ren logik i adDependency.lua.
374 lines
18 KiB
Lua
374 lines
18 KiB
Lua
-- 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).
|
||
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)})
|
||
points[key] = {point = point, productionId = production.id, placeable = placeable}
|
||
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
|
||
|
||
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
|
||
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)
|
||
local needed = ADDependency.neededOutputs(flows, data.barnFood, ADChainData.chainInputs(chain, data.fillTitles),
|
||
#goods > 0 and ADChainData.acceptsMap(data, flows, goods) 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 = {}}
|
||
local nameOf = nameResolver(data)
|
||
data.priceOf = priceResolver(data)
|
||
readPlaceables(data, farmId, byChain, nameOf)
|
||
readCrops(data, nameOf)
|
||
readStock(data, adEnv, farmId, nameOf)
|
||
readRates(data, adEnv, farmId, nameOf)
|
||
readUsedFruits(data, adEnv, farmId, nameOf)
|
||
readFields(data)
|
||
return data
|
||
end
|