From 605c4e8d457b53e6898334825123588676e106a6 Mon Sep 17 00:00:00 2001 From: masterdraco Date: Wed, 30 Sep 2026 14:24:16 +0200 Subject: [PATCH] =?UTF-8?q?fix(k=C3=A6de):=20spillerens=20slukninger=20hus?= =?UTF-8?q?kes,=20pause=20beskytter=20kun=20sin=20egen=20k=C3=A6de,=20stop?= =?UTF-8?q?pede=20flows=20slippes=20og=20for=C3=A6ldede=20n=C3=B8gler=20gl?= =?UTF-8?q?emmes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FS25_ADSmartPickup/adChain.lua | 140 +++++++++++++++++++++----- FS25_ADSmartPickup/adChainActions.lua | 6 +- FS25_ADSmartPickup/adChainModel.lua | 3 +- FS25_ADSmartPickup/adChainRun.lua | 17 ++-- FS25_ADSmartPickup/adChainView.lua | 2 +- 5 files changed, 131 insertions(+), 37 deletions(-) diff --git a/FS25_ADSmartPickup/adChain.lua b/FS25_ADSmartPickup/adChain.lua index 86710bd..84be85d 100644 --- a/FS25_ADSmartPickup/adChain.lua +++ b/FS25_ADSmartPickup/adChain.lua @@ -1,8 +1,9 @@ -- 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. --- Resultatet gemmes i ADChain.last til Kæder-fanen. Tilstanden (byChain, pausede rødder, afviste forslag) --- gemmes i ADSmartPickup_chain.xml under spillets gem. Spiladgang kun inde i funktioner (filen dofile's i test). +-- 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" @@ -17,6 +18,10 @@ 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 -> true: flows kæden oprettede stoppet, fordi der ikke var traktor/vogn (kun i hukommelsen) ADChain.noVehicle = {} ADChain.switchCount = 0 @@ -39,12 +44,14 @@ 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 = {}} + 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 = {}} + 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) @@ -61,6 +68,10 @@ function ADChain.stateFromRecords(records) 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 @@ -99,35 +110,81 @@ function ADChain.unionFruits(usedFruits, fieldDemand) return result end --- Kopi af opskrifterne hvor pausede rødder er slukket (så de ikke skaber nye behov). -function ADChain.recipesWithPaused(recipes, paused) +-- 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.enabled = false 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 --- Spillerens eget valg: en produktion kæden tændte (byChain) som er slukket, selv om kæden stadig har brug for --- den, er slukket af spilleren (eller tændingen virkede ikke) -> userOff. En nøgle ryddes fra userOff, når kæden --- ikke længere har brug for den (men ikke på delvise data). -> {userOff = ny tabel, added = {nye nøgler}} -function ADChain.playerOverrides(recipes, needRecipes, userOff, isPartial) - local result, added = {}, {} - for key in pairs(userOff or {}) do - if isPartial or ((needRecipes or {})[key] or 0) > 0 then result[key] = true end - 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 - local needed = ((needRecipes or {})[recipe.key] or 0) > 0 - if recipe.byChain and not recipe.enabled and needed and not result[recipe.key] then - result[recipe.key] = true - table.insert(added, recipe.key) + 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 - table.sort(added) - return {userOff = result, added = added} + 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). @@ -205,17 +262,37 @@ local function farmId() end local function plan(data, state) - local recipes = ADChain.recipesWithPaused(data.recipes, state.paused) + 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) local shortages = ADCropPlan.shortages(chain.fieldDemand, data.stock, data.fields, data.yields) state.dismissed = ADChain.pruneDismissed(state.dismissed, shortages) local actions = ADCropPlan.choose(shortages, data.fields, used, data.yields, data.prices, ADChain.dismissedFruits(state.dismissed)) -- 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, next(state.paused) ~= nil) + 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 + -- act = true: udfør handlingerne (tick). Uden act kun en frisk visning (GUI). function ADChain.evaluate(adEnv, act) local state = ADChain.state @@ -226,6 +303,10 @@ function ADChain.evaluate(adEnv, act) 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)) @@ -321,6 +402,12 @@ function ADChain.save() 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) @@ -335,6 +422,7 @@ end function ADChain.load() ADChain.state = ADChain.newState() ADChain.noVehicle = {} + ADChain.prevEnabled, ADChain.afterLoad = nil, true local path = getPath() if path == nil or not fileExists(path) then return end local xmlFile = XMLFile.load("ADSmartPickupChain", path) @@ -349,6 +437,12 @@ function ADChain.load() 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"))}) diff --git a/FS25_ADSmartPickup/adChainActions.lua b/FS25_ADSmartPickup/adChainActions.lua index 03977c7..a353bae 100644 --- a/FS25_ADSmartPickup/adChainActions.lua +++ b/FS25_ADSmartPickup/adChainActions.lua @@ -2,12 +2,14 @@ ADChainActions = {} -function ADChainActions.productions(recipes, needRecipes, anyPaused) +-- protected = {[nøgle] = cyklusser}: behov fra pausede rødder — slukkes ikke, men tændes heller ikke. +function ADChainActions.productions(recipes, needRecipes, protected) local result = {enable = {}, disable = {}} for _, recipe in ipairs(recipes or {}) do local needed = ((needRecipes or {})[recipe.key] or 0) > 0 + local kept = ((protected or {})[recipe.key] or 0) > 0 if needed and not recipe.enabled then table.insert(result.enable, recipe.key) end - if not needed and recipe.enabled and recipe.byChain and not anyPaused then table.insert(result.disable, recipe.key) end + if not needed and not kept and recipe.enabled and recipe.byChain then table.insert(result.disable, recipe.key) end end table.sort(result.enable); table.sort(result.disable) return result diff --git a/FS25_ADSmartPickup/adChainModel.lua b/FS25_ADSmartPickup/adChainModel.lua index 7948dc0..dc081d4 100644 --- a/FS25_ADSmartPickup/adChainModel.lua +++ b/FS25_ADSmartPickup/adChainModel.lua @@ -52,11 +52,12 @@ function ADChainModel.build(recipes, husbandry, crops) local producers, herds = indexProducers(recipes), indexHerds(husbandry) -- Rødderne kører for sig selv med fuld fart: deres egne input kræves af deres eget træ. Som leverandør til en -- anden rod har de derfor fuld kapacitet, men deres input regnes ikke med igen (standalone = ingen rekursion). + -- En pauset rod (recipe.paused) kører også for sig selv, men er ikke rod: kæden skaber intet nyt for den. local capacity, roots, standalone = {}, {}, {} for _, recipe in ipairs(recipes or {}) do capacity[recipe.key] = recipe.cyclesPerMonth or 0 if recipe.enabled and not recipe.byChain then - table.insert(roots, recipe) + if not recipe.paused then table.insert(roots, recipe) end standalone[recipe.key] = true end end diff --git a/FS25_ADSmartPickup/adChainRun.lua b/FS25_ADSmartPickup/adChainRun.lua index bad6245..b6adb61 100644 --- a/FS25_ADSmartPickup/adChainRun.lua +++ b/FS25_ADSmartPickup/adChainRun.lua @@ -85,16 +85,11 @@ local function setProduction(data, key, isOn) return ok end +-- Spillerens slukninger er allerede lagt i state.userOff (ADChain.reconcile før planen): de tændes ikke igen. -- Delvise data: intet slukkes (en rod der ikke kunne læses, ligner en rod der er slået fra). -function ADChainRun.toggleProductions(data, prod, needRecipes) +function ADChainRun.toggleProductions(data, prod) local state = ADChain.state local byChain = state.byChain - local overrides = ADChain.playerOverrides(data.recipes, needRecipes, state.userOff, data.partial == true) - state.userOff = overrides.userOff - for _, key in ipairs(overrides.added) do - byChain[key] = nil - log("spilleren slukkede %s — kæden tænder den ikke igen", key) - end for _, key in ipairs(prod.enable or {}) do if not state.userOff[key] and setProduction(data, key, true) then byChain[key] = true @@ -175,7 +170,8 @@ function ADChainRun.createFlows(adEnv, data, edges) return count end --- Flows for rødder der er slået fra: flyt til en anden rod der stadig bruger kanten, ellers stop (slettes ikke). +-- Flows for rødder der er slået fra: flyt til en anden rod der stadig bruger kanten, ellers stop og slip (chain +-- ryddes, så spilleren kan starte flowet igen uden at kæden stopper det næste minut; det slettes ikke). function ADChainRun.stopFlows(adEnv, chain, edges) local changed = 0 local active = ADChain.activeRoots(chain, ADChain.state.paused) @@ -188,7 +184,8 @@ function ADChainRun.stopFlows(adEnv, chain, edges) else local ok, err = pcall(ADRunsController.stopRun, adEnv, run.id) if ok then - log("flow '%s' stoppet: kæden %s er slået fra", tostring(run.name), tostring(run.chain)) + ADRunsController.updateRun(run.id, {chain = ""}) + log("flow '%s' stoppet og sluppet af kæden: kæden %s er slået fra", tostring(run.name), tostring(run.chain)) changed = changed + 1 else warn("flow '%s' kunne ikke stoppes: %s", tostring(run.name), tostring(err)) @@ -202,7 +199,7 @@ end function ADChainRun.apply(adEnv, data, result) local okFields, errFields = pcall(ADChainRun.switchFields, data, result.actions) if not okFields then warn("markskift fejlede: %s", tostring(errFields)) end - local okProd, errProd = pcall(ADChainRun.toggleProductions, data, result.prod, result.chain.needRecipes) + local okProd, errProd = pcall(ADChainRun.toggleProductions, data, result.prod) if not okProd then warn("mellemled fejlede: %s", tostring(errProd)) end local okNew, created = pcall(ADChainRun.createFlows, adEnv, data, result.edges) if not okNew then warn("nye flows fejlede: %s", tostring(created)) end diff --git a/FS25_ADSmartPickup/adChainView.lua b/FS25_ADSmartPickup/adChainView.lua index efbd20f..51d94b7 100644 --- a/FS25_ADSmartPickup/adChainView.lua +++ b/FS25_ADSmartPickup/adChainView.lua @@ -239,7 +239,7 @@ local function rootRows(rows, root, last, ctx, isExpanded) end) end --- Pausede rødder: recipesWithPaused slukker dem, så modellen ser dem ikke som rødder -> grå linje med [Genoptag]. +-- Pausede rødder: modelRecipes markerer dem paused, så modellen ser dem ikke som rødder -> grå linje med [Genoptag]. local function pausedRows(rows, last, ctx) local present, keys = {}, {} for _, root in ipairs(last.chain ~= nil and last.chain.roots or {}) do present[root.key] = true end