-- 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) -- 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 = {}} 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