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.
567 lines
27 KiB
Lua
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
|