Files
fs25-adsmartpickup/FS25_ADSmartPickup/adChain.lua
T
masterdraco 979b7f72d5 feat(kæde): advarsel når en fabrik gården er afhængig af slukkes
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.
2026-09-30 15:08:28 +02:00

567 lines
27 KiB
Lua

-- AD Chain (kæde-kørsel)
-- Hvert minut (efter opstartsfristen): læs gården (ADChainData), byg kæden (ADChainModel), find manglende
-- afgrøder (ADCropPlan) og lad ADChainRun skifte frie marker, tænde/slukke mellemled og oprette/stoppe flows.
-- Samme tick advarer (ADDependency), når spilleren slukker en produktion gården er afhængig af — også uden kæder.
-- Resultatet gemmes i ADChain.last til Kæder-fanen. Tilstanden (byChain, pausede rødder, afviste forslag,
-- userOff og det sidst kendte sæt af rødder) gemmes i ADSmartPickup_chain.xml under spillets gem.
-- Spiladgang kun inde i funktioner (filen dofile's i test).
ADChain = {}
ADChain.FILE_NAME = "ADSmartPickup_chain.xml"
ADChain.ROOT = "ADSmartPickupChain"
ADChain.TICK_MS = 60000
ADChain.STARTUP_GRACE_MS = 120000
ADChain.MAX_FIELD_SWITCHES = 2
ADChain.MAX_NEW_FLOWS = 2
ADChain.LOG_PREFIX = "ADSmartPickup: kæde: "
-- Et afvist forslag vises igen, når manglen for afgrøden har ændret sig mere end dette.
ADChain.DISMISS_CHANGE = 0.25
ADChain.state = {byChain = {}, paused = {}, dismissed = {}, userOff = {}}
ADChain.last = nil
-- {[nøgle] = true}: produktioner (ikke kædens) der var tændt ved sidste tick — til at se spillerens slukninger
ADChain.prevEnabled = nil
-- true fra indlæsning til første tick: kædens produktioner der er slukket, glemmes (spillet blev ikke gemt)
ADChain.afterLoad = false
-- run-id -> "noVehicle"|"pallet" (true = "noVehicle"): flows kæden oprettede stoppet, fordi der ikke var
-- traktor/vogn eller varen er på paller (kun i hukommelsen)
ADChain.noVehicle = {}
ADChain.switchCount = 0
ADChain.now = 0
ADChain.timer = 0
ADChain.collectFailed = false
-- {[rodnøgle|vare|art] = true}: manglende led der allerede er meldt som besked (kun i hukommelsen; nulstilles ved indlæsning)
ADChain.problemKeys = {}
-- {[nøgle] = true}: produktioner (ikke kædens) der var tændt ved sidste afhængighedstjek (kun kædens tick, kun på
-- hele data). nil efter indlæsning: det der allerede er slukket ved første tjek, advares der ikke om.
ADChain.depPrevEnabled = nil
local function contains(list, value)
for _, item in ipairs(list or {}) do if item == value then return true end end
return false
end
local function sortedKeys(map)
local keys = {}
for key in pairs(map or {}) do table.insert(keys, key) end
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
return keys
end
-- ---- rene hjælpere ----
-- userOff = {[recipeKey] = true}: produktioner spilleren slukkede selv — kæden tænder dem ikke igen.
-- roots = {[recipeKey] = true} rødderne ved sidste tick (nil = ukendt, fx gem fra en ældre version).
function ADChain.newState()
return {byChain = {}, paused = {}, dismissed = {}, userOff = {}, roots = nil}
end
function ADChain.stateToRecords(state)
local records = {byChain = sortedKeys(state.byChain), paused = sortedKeys(state.paused), userOff = sortedKeys(state.userOff), dismissed = {},
roots = state.roots ~= nil and sortedKeys(state.roots) or nil}
local ids = {}
for id in pairs(state.dismissed or {}) do table.insert(ids, id) end
table.sort(ids)
for _, id in ipairs(ids) do
local entry = state.dismissed[id]
table.insert(records.dismissed, {id = id, fruit = entry.fruit, missingHa = entry.missingHa})
end
return records
end
function ADChain.stateFromRecords(records)
local state = ADChain.newState()
if records == nil then return state end
for _, key in ipairs(records.byChain or {}) do state.byChain[key] = true end
for _, key in ipairs(records.paused or {}) do state.paused[key] = true end
for _, key in ipairs(records.userOff or {}) do state.userOff[key] = true end
if records.roots ~= nil then
state.roots = {}
for _, key in ipairs(records.roots) do state.roots[key] = true end
end
for _, entry in ipairs(records.dismissed or {}) do
local id = tonumber(entry.id)
if id ~= nil and entry.fruit ~= nil and entry.fruit ~= "" then
state.dismissed[id] = {fruit = entry.fruit, missingHa = tonumber(entry.missingHa) or 0}
end
end
return state
end
-- Afviste forslag beholdes kun mens manglen for afgrøden findes og har ændret sig højst 25 %.
function ADChain.pruneDismissed(dismissed, shortages)
local byFruit = {}
for _, shortage in ipairs(shortages or {}) do byFruit[shortage.fruit] = shortage.missingHa end
local kept = {}
for id, entry in pairs(dismissed or {}) do
local now = byFruit[entry.fruit]
local before = entry.missingHa or 0
if now ~= nil and math.abs(now - before) <= math.abs(before) * ADChain.DISMISS_CHANGE then
kept[id] = {fruit = entry.fruit, missingHa = before}
end
end
return kept
end
-- {[mark] = {fruit, missingHa}} -> {[mark] = afgrøde} (ADCropPlan.choose's dismissed)
function ADChain.dismissedFruits(dismissed)
local result = {}
for id, entry in pairs(dismissed or {}) do result[id] = entry.fruit end
return result
end
function ADChain.unionFruits(usedFruits, fieldDemand)
local result = {}
for fruit in pairs(usedFruits or {}) do result[fruit] = true end
for fruit in pairs(fieldDemand or {}) do result[fruit] = true end
return result
end
-- Modellens kopi af opskrifterne: pausede rødder markeres paused (kører for sig selv, men skaber intet nyt), og
-- slukkede userOff-produktioner får 0 cyklusser (de dækker intet behov). Originalerne røres ikke.
function ADChain.modelRecipes(recipes, paused, userOff)
local result = {}
for index, recipe in ipairs(recipes or {}) do
local copy = {}
for key, value in pairs(recipe) do copy[key] = value end
if (paused or {})[recipe.key] then copy.paused = true end
if (userOff or {})[recipe.key] and not recipe.enabled then copy.cyclesPerMonth = 0 end
result[index] = copy
end
return result
end
local function copySet(map)
local result = {}
for key, value in pairs(map or {}) do result[key] = value end
return result
end
-- Tændte produktioner der ikke er kædens (= rødderne, inkl. pausede).
function ADChain.enabledKeys(recipes)
local result = {}
for _, recipe in ipairs(recipes or {}) do
if recipe.enabled and not recipe.byChain then result[recipe.key] = true end
end
return result
end
-- Spillerens valg og forældede nøgler, før kæden regner (rent: state røres ikke).
-- opts = {prevEnabled, isPartial, firstEval}. -> {userOff, byChain, paused, roots, added, forgotten, droppedPaused, cleared}
-- * kædens produktion er slukket: af spilleren -> userOff (added); første tick efter indlæsning -> glemt (forgotten)
-- * en produktion (ikke kædens) der var tændt sidste tick, er slukket -> userOff (added)
-- * userOff ryddes kun når der kommer en ny rod (spilleren tænder noget) — ikke når noget "ikke længere bruges"
-- (det gav tænd/sluk-løkker); nye slukninger i samme tick beholdes
-- * pausede nøgler for produktioner der er væk eller slukket, droppes (droppedPaused)
-- Delvise data: userOff ryddes ikke, rødderne opdateres ikke og pausede droppes ikke.
function ADChain.reconcile(state, recipes, opts)
opts = opts or {}
local roots = ADChain.enabledKeys(recipes)
local rootAdded = false
if state.roots ~= nil then
for key in pairs(roots) do if not state.roots[key] then rootAdded = true end end
end
local result = {userOff = copySet(state.userOff), byChain = copySet(state.byChain), paused = copySet(state.paused),
roots = opts.isPartial and state.roots or roots, added = {}, forgotten = {}, droppedPaused = {}, cleared = false}
if rootAdded and not opts.isPartial and next(result.userOff) ~= nil then
result.userOff, result.cleared = {}, true
end
local present = {}
for _, recipe in ipairs(recipes or {}) do
present[recipe.key] = recipe
if not recipe.enabled and recipe.byChain then
result.byChain[recipe.key] = nil
if opts.firstEval then
table.insert(result.forgotten, recipe.key)
elseif not result.userOff[recipe.key] then
result.userOff[recipe.key] = true
table.insert(result.added, recipe.key)
end
elseif not recipe.enabled and (opts.prevEnabled or {})[recipe.key] and not result.userOff[recipe.key] then
result.userOff[recipe.key] = true
table.insert(result.added, recipe.key)
end
end
if not opts.isPartial then
for key in pairs(state.paused or {}) do
if present[key] == nil or not present[key].enabled then
result.paused[key] = nil
table.insert(result.droppedPaused, key)
end
end
end
table.sort(result.added); table.sort(result.forgotten); table.sort(result.droppedPaused)
return result
end
-- Rødder hvis flows ikke må stoppes: kædens aktuelle rødder + pausede rødder (pause rører intet).
function ADChain.activeRoots(chain, paused)
local result = {}
for _, root in ipairs((chain or {}).roots or {}) do result[root.key] = true end
for key in pairs(paused or {}) do result[key] = true end
return result
end
-- Mest brugte Wait-pulje blandt forsyningsflows til bygningen, ellers "".
function ADChain.waitPoolFor(runs, building)
local counts, best, bestCount = {}, "", 0
for _, run in ipairs(runs or {}) do
local group = run.waitPoolGroup
if run.kind == "supply" and group ~= nil and group ~= "" and contains(run.targetBuildingIds, building) then
counts[group] = (counts[group] or 0) + 1
end
end
for _, group in ipairs(sortedKeys(counts)) do
if counts[group] > bestCount then best, bestCount = group, counts[group] end
end
return best
end
function ADChain.flowName(fillTitle, buildingName)
return "Kæde: " .. tostring(fillTitle) .. " → " .. tostring(buildingName)
end
local function covers(run, edge)
return contains(run.targetBuildingIds, edge.consumer)
and (run.ingredientMode ~= "manual" or contains(run.ingredients, edge.fillType))
end
-- Bruger en af de aktuelle kanter stadig flowet? -> kantens rodnøgle (flowet flyttes dertil), ellers nil.
function ADChain.stillNeeded(runView, edges)
for _, edge in ipairs(edges or {}) do
if covers(runView, edge) then return edge.rootKey end
end
return nil
end
-- {[forbruger|vare] = "ok"|"missing"|"stopped"|"noVehicle"|"pallet"} for hver kant.
function ADChain.flowStatus(edges, runs, noVehicle)
local result = {}
for _, edge in ipairs(edges or {}) do
local status = "missing"
for _, run in ipairs(runs or {}) do
if run.kind == "supply" and covers(run, edge) then
if not run.stopped then status = "ok"; break end
local reason = (noVehicle or {})[run.id]
if reason == "pallet" then status = "pallet"
elseif reason then status = "noVehicle"
elseif status == "missing" then status = "stopped" end
end
end
result[tostring(edge.consumer) .. "|" .. tostring(edge.fillType)] = status
end
return result
end
-- Flows i det format ADChainActions regner på.
function ADChain.runsView(runs)
local list = {}
for _, run in ipairs(runs or {}) do
table.insert(list, {id = run.id, name = run.name, kind = run.kind, targetBuildingIds = run.targetBuildingIds,
ingredientMode = run.ingredientMode, ingredients = run.ingredients, stopped = run.stopped == true,
chain = run.chain, waitPoolGroup = run.waitPoolGroup})
end
return list
end
-- Manglende led der ikke var der sidst (nøgle = rodnøgle|vare|art). -> nye problemer (hver nøgle én gang), nøglerne nu.
-- Et problem der forsvinder, glemmes; kommer det igen, er det nyt. prevKeys røres ikke.
function ADChain.problemKey(problem)
return tostring(problem.rootKey) .. "|" .. tostring(problem.fillType) .. "|" .. tostring(problem.kind)
end
function ADChain.newProblems(prevKeys, problems)
local fresh, keys = {}, {}
for _, problem in ipairs(problems or {}) do
local key = ADChain.problemKey(problem)
if not keys[key] then
keys[key] = true
if not (prevKeys or {})[key] then table.insert(fresh, problem) end
end
end
return fresh, keys
end
-- ---- evaluering ----
local function farmId()
return g_currentMission ~= nil and g_currentMission.getFarmId ~= nil and g_currentMission:getFarmId() or 1
end
local function plan(data, state)
local recipes = ADChain.modelRecipes(data.recipes, state.paused, state.userOff)
local chain = ADChainModel.build(recipes, data.husbandry, data.crops)
-- pausede rødder: det de bruger, er beskyttet mod slukning (modellen regnet som om de ikke var pauset)
local protected = {}
if next(state.paused) ~= nil then
protected = ADChainModel.build(ADChain.modelRecipes(data.recipes, {}, state.userOff), data.husbandry, data.crops).needRecipes
end
local used = ADChain.unionFruits(data.usedFruits, chain.fieldDemand)
-- ufuldstændig markliste: udbuddet ville være undervurderet -> ingen mangler, forslag eller markskift endnu
local shortages, actions = {}, {}
if not data.fieldsIncomplete then
shortages = ADCropPlan.shortages(chain.fieldDemand, data.stock, data.fields, data.yields)
state.dismissed = ADChain.pruneDismissed(state.dismissed, shortages)
actions = ADCropPlan.choose(shortages, data.fields, used, data.yields, data.priceOf or data.prices, ADChain.dismissedFruits(state.dismissed))
end
-- de ægte opskrifter (ikke kopien): en pauset rod er stadig tændt og må ikke "tændes" og overtages af kæden
local prod = ADChainActions.productions(data.recipes, chain.needRecipes, protected)
return {chain = chain, shortages = shortages, actions = actions, prod = prod, edges = ADChainActions.edges(chain)}
end
local function logInfo(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end
-- Spillerens valg og forældede nøgler ind i tilstanden (kun ved tick, før der regnes).
local function applyReconcile(state, data)
local result = ADChain.reconcile(state, data.recipes, {prevEnabled = ADChain.prevEnabled, isPartial = data.partial == true,
firstEval = ADChain.afterLoad})
if result.cleared then logInfo("ny produktion tændt — slukkede produktioner må bruges igen") end
for _, key in ipairs(result.added) do logInfo("spilleren slukkede %s — kæden tænder den ikke igen", key) end
for _, key in ipairs(result.forgotten) do logInfo("kæden glemmer %s (ikke tændt efter indlæsning)", key) end
for _, key in ipairs(result.droppedPaused) do logInfo("pause for %s fjernet: produktionen er væk eller slukket", key) end
state.userOff, state.byChain, state.paused, state.roots = result.userOff, result.byChain, result.paused, result.roots
ADChain.prevEnabled = ADChain.enabledKeys(data.recipes)
ADChain.afterLoad = false
end
-- Afhængighedstjek (spec afsnit 8): nye slukninger (kun act) og "Slukket men nødvendig"-posterne. Uafhængigt af
-- kædens rødder og af markscanningen; springes over ved læsefejl (så beholdes forrige poster, og slukninger i
-- mellemtiden meldes ved næste hele tjek).
-- Produktioner kæden selv er ved at tænde (plan.prod.enable), er ikke "slukket men nødvendig".
local function dependencyCheck(data, plan, runs, act, previous)
if ADDependency == nil or ADChainData.dependencyInputs == nil then return nil end
if data.readFailed then
return previous ~= nil and {warnings = {}, offNeeded = previous.offNeeded or {}} or nil
end
local inputs = ADChainData.dependencyInputs(data, plan.chain, runs)
local enabling, recipes = {}, {}
for _, key in ipairs(plan.prod ~= nil and plan.prod.enable or {}) do enabling[key] = true end
for _, recipe in ipairs(data.recipes or {}) do
if not enabling[recipe.key] then table.insert(recipes, recipe) end
end
local prev = act and ADChain.depPrevEnabled or nil
local result = {
warnings = ADDependency.warnings(prev, recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
offNeeded = ADDependency.offNeeded(recipes, inputs.needed, inputs.stockDays, inputs.capacity, inputs.demand),
}
if act then ADChain.depPrevEnabled = ADDependency.enabledKeys(data.recipes) end
return result
end
-- act = true: udfør handlingerne (tick). Uden act kun en frisk visning (GUI).
function ADChain.evaluate(adEnv, act)
local state = ADChain.state
local ok, data = pcall(ADChainData.collect, adEnv, farmId(), state.byChain)
if not ok then
if not ADChain.collectFailed then Logging.warning(ADChain.LOG_PREFIX .. "gården kunne ikke læses: %s", tostring(data)) end
ADChain.collectFailed = true
return ADChain.last
end
ADChain.collectFailed = false
if act then
local okState, errState = pcall(applyReconcile, state, data)
if not okState then Logging.warning(ADChain.LOG_PREFIX .. "tilstanden kunne ikke opdateres: %s", tostring(errState)) end
end
local okPlan, result = pcall(plan, data, state)
if not okPlan then
Logging.warning(ADChain.LOG_PREFIX .. "kæden kunne ikke beregnes: %s", tostring(result))
return ADChain.last
end
if act and ADChainRun ~= nil then ADChainRun.apply(adEnv, data, result) end
local runs = ADChain.runsView(ADRunsController.store.runs)
local okDep, dependency = pcall(dependencyCheck, data, result, runs, act, ADChain.last ~= nil and ADChain.last.dependency or nil)
if not okDep then
Logging.warning(ADChain.LOG_PREFIX .. "afhængighedstjek fejlede: %s", tostring(dependency))
dependency = nil
end
local paused = {}
for key in pairs(state.paused) do paused[key] = true end
ADChain.last = {chain = result.chain, shortages = result.shortages, actions = result.actions, edges = result.edges,
missingFlows = ADChainActions.missingFlows(result.edges, runs), flowStatus = ADChain.flowStatus(result.edges, runs, ADChain.noVehicle),
paused = paused, userOff = copySet(state.userOff), data = data, fields = data.fields, fieldsIncomplete = data.fieldsIncomplete == true, titles = data.fillTitles, at = ADChain.now, switchCount = ADChain.switchCount,
dependency = dependency}
return ADChain.last
end
-- Alder (ms, spillets ur) af det sidste resultat; math.huge når der intet er. Fanen genbruger et friskt resultat.
function ADChain.lastAge()
if ADChain.last == nil then return math.huge end
return ADChain.now - (ADChain.last.at or 0)
end
-- Engangsbesked pr. nyt manglende led (kun fra tick). Ikke i opstartsfristen og ikke mens gården er læst delvist
-- eller markscanningen kører (ellers falske alarmer lige efter indlæsning) — så huskes intet, og det meldes senere.
function ADChain.notifyProblems(last)
if last == nil or ADChain.now < ADChain.STARTUP_GRACE_MS then return 0 end
if last.fieldsIncomplete or (last.data ~= nil and last.data.partial) then return 0 end
local fresh, keys = ADChain.newProblems(ADChain.problemKeys, last.chain ~= nil and last.chain.problems or {})
ADChain.problemKeys = keys
if #fresh == 0 or ADFieldJobs == nil or ADFieldJobs.notify == nil then return 0 end
local textOf = function(key) return g_i18n ~= nil and g_i18n:getText(key) or key end
local fmt = SmartPickupFrame ~= nil and SmartPickupFrame.formatCompact or nil
local rootNames = {}
for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do
rootNames[root.key] = tostring((last.titles or {})[root.name] or root.name)
end
for _, problem in ipairs(fresh) do
-- kædens navn foran, så to kæder der mangler samme vare kan skelnes
local text = tostring(rootNames[problem.rootKey] or problem.rootKey) .. ": " .. ADChainView.problemText(problem, last, textOf, nil, fmt)
Logging.info(ADChain.LOG_PREFIX .. "manglende led: %s", text)
ADFieldJobs.notify(text, true)
end
return #fresh
end
-- Én besked pr. slukning af en nødvendig produktion (kun fra tick; advarslerne tømmes, når de er meldt).
function ADChain.notifyDependencies(last)
local dependency = last ~= nil and last.dependency or nil
if dependency == nil or #(dependency.warnings or {}) == 0 or ADChain.now < ADChain.STARTUP_GRACE_MS then return 0 end
local warnings = dependency.warnings
dependency.warnings = {}
if ADFieldJobs == nil or ADFieldJobs.notify == nil then return 0 end
local textOf = function(key) return g_i18n ~= nil and g_i18n:getText(key) or key end
local titleOf = function(name) return tostring((last.titles or {})[name] or name) end
for _, warning in ipairs(warnings) do
local text = ADDependency.notifyText(warning, textOf, titleOf)
Logging.info(ADChain.LOG_PREFIX .. "nødvendig produktion slukket: %s", text)
ADFieldJobs.notify(text, true)
end
return #warnings
end
function ADChain.tick(adEnv, dt)
if adEnv == nil then return end
ADChain.now = ADChain.now + (dt or 0)
ADChain.timer = ADChain.timer + (dt or 0)
if ADChain.now < ADChain.STARTUP_GRACE_MS or ADChain.timer < ADChain.TICK_MS then return end
ADChain.timer = 0
local last = ADChain.evaluate(adEnv, true)
local ok, err = pcall(ADChain.notifyProblems, last)
if not ok then Logging.warning(ADChain.LOG_PREFIX .. "besked om manglende led fejlede: %s", tostring(err)) end
local okDep, errDep = pcall(ADChain.notifyDependencies, last)
if not okDep then Logging.warning(ADChain.LOG_PREFIX .. "advarsel om slukket produktion fejlede: %s", tostring(errDep)) end
end
-- ---- GUI-handlinger ----
local function suggestionFor(fieldId)
for _, action in ipairs(ADChain.last ~= nil and ADChain.last.actions or {}) do
if action.fieldId == fieldId then return action end
end
return nil
end
-- Godkend forslag: marken får kædens afgrøde som mål (kilde "chain").
function ADChain.approve(fieldId)
local action = suggestionFor(fieldId)
if action == nil then return false end
local ok, err = pcall(function()
-- et auto-skift må aldrig overskrive en afgrøde spilleren har sat siden; et forslag godkender spilleren selv
if action.mode == "auto" then
local plan = ADFieldJobs.getField(fieldId).plan or {}
if plan.targetSource == "user" then error("spilleren har selv valgt afgrøde") end
end
ADFieldJobs.setTarget(fieldId, action.to, "chain")
end)
if not ok then
Logging.warning(ADChain.LOG_PREFIX .. "F%s: forslag kunne ikke godkendes: %s", tostring(fieldId), tostring(err))
return false
end
Logging.info(ADChain.LOG_PREFIX .. "F%s: forslag godkendt: %s -> %s", tostring(fieldId), tostring(action.from), tostring(action.to))
return true
end
-- Afvis forslag: vises ikke igen, før manglen for afgrøden ændrer sig > 25 %.
function ADChain.dismiss(fieldId)
local action = suggestionFor(fieldId)
if action == nil then return false end
local missingHa = action.hectares or 0
for _, shortage in ipairs(ADChain.last.shortages or {}) do
if shortage.fruit == action.to then missingHa = shortage.missingHa end
end
ADChain.state.dismissed[fieldId] = {fruit = action.to, missingHa = missingHa}
Logging.info(ADChain.LOG_PREFIX .. "F%s: forslag %s afvist", tostring(fieldId), tostring(action.to))
return true
end
function ADChain.setPaused(rootKey, isPaused)
if rootKey == nil then return end
ADChain.state.paused[rootKey] = isPaused and true or nil
Logging.info(ADChain.LOG_PREFIX .. "%s %s", tostring(rootKey), isPaused and "på pause" or "genoptaget")
end
-- ---- gem / indlæs (samme sti-opslag som runs; skrives kun fra ADRunsController.save) ----
local function getPath()
local path = ADRunsStorage ~= nil and ADRunsStorage.getPath() or nil
if path == nil then return nil end
return (path:gsub(ADRunsStorage.FILE_NAME .. "$", ADChain.FILE_NAME))
end
function ADChain.save()
local path = getPath()
if path == nil then return false end
local xmlFile = XMLFile.create("ADSmartPickupChain", path, ADChain.ROOT)
if xmlFile == nil then return false end
local records = ADChain.stateToRecords(ADChain.state)
for index, key in ipairs(records.byChain) do
xmlFile:setString(string.format("%s.byChain.recipe(%d)#key", ADChain.ROOT, index - 1), key)
end
for index, key in ipairs(records.paused) do
xmlFile:setString(string.format("%s.paused.root(%d)#key", ADChain.ROOT, index - 1), key)
end
for index, key in ipairs(records.userOff) do
xmlFile:setString(string.format("%s.userOff.recipe(%d)#key", ADChain.ROOT, index - 1), key)
end
if records.roots ~= nil then
xmlFile:setBool(ADChain.ROOT .. ".roots#known", true)
for index, key in ipairs(records.roots) do
xmlFile:setString(string.format("%s.roots.recipe(%d)#key", ADChain.ROOT, index - 1), key)
end
end
for index, entry in ipairs(records.dismissed) do
local key = string.format("%s.dismissed.field(%d)", ADChain.ROOT, index - 1)
xmlFile:setInt(key .. "#id", entry.id)
xmlFile:setString(key .. "#fruit", entry.fruit)
xmlFile:setString(key .. "#missingHa", string.format("%.2f", entry.missingHa or 0))
end
xmlFile:save()
xmlFile:delete()
return true
end
function ADChain.load()
ADChain.state = ADChain.newState()
ADChain.noVehicle = {}
ADChain.prevEnabled, ADChain.afterLoad = nil, true
ADChain.problemKeys = {}
ADChain.depPrevEnabled = nil
local path = getPath()
if path == nil or not fileExists(path) then return end
local xmlFile = XMLFile.load("ADSmartPickupChain", path)
if xmlFile == nil then return end
local records = {byChain = {}, paused = {}, userOff = {}, dismissed = {}}
xmlFile:iterate(ADChain.ROOT .. ".byChain.recipe", function(_, key)
table.insert(records.byChain, xmlFile:getString(key .. "#key"))
end)
xmlFile:iterate(ADChain.ROOT .. ".paused.root", function(_, key)
table.insert(records.paused, xmlFile:getString(key .. "#key"))
end)
xmlFile:iterate(ADChain.ROOT .. ".userOff.recipe", function(_, key)
table.insert(records.userOff, xmlFile:getString(key .. "#key"))
end)
if xmlFile:getBool(ADChain.ROOT .. ".roots#known", false) then
records.roots = {}
xmlFile:iterate(ADChain.ROOT .. ".roots.recipe", function(_, key)
table.insert(records.roots, xmlFile:getString(key .. "#key"))
end)
end
xmlFile:iterate(ADChain.ROOT .. ".dismissed.field", function(_, key)
table.insert(records.dismissed, {id = xmlFile:getInt(key .. "#id"), fruit = xmlFile:getString(key .. "#fruit"),
missingHa = tonumber(xmlFile:getString(key .. "#missingHa"))})
end)
xmlFile:delete()
ADChain.state = ADChain.stateFromRecords(records)
Logging.info(ADChain.LOG_PREFIX .. "%d produktioner tændt af kæden, %d pausede kæder indlæst", #records.byChain, #records.paused)
end