diff --git a/FS25_ADSmartPickup/adChain.lua b/FS25_ADSmartPickup/adChain.lua index 93c1517..86710bd 100644 --- a/FS25_ADSmartPickup/adChain.lua +++ b/FS25_ADSmartPickup/adChain.lua @@ -15,7 +15,7 @@ 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 = {}} +ADChain.state = {byChain = {}, paused = {}, dismissed = {}, userOff = {}} ADChain.last = nil -- run-id -> true: flows kæden oprettede stoppet, fordi der ikke var traktor/vogn (kun i hukommelsen) ADChain.noVehicle = {} @@ -38,12 +38,13 @@ end -- ---- rene hjælpere ---- +-- userOff = {[recipeKey] = true}: produktioner spilleren slukkede selv — kæden tænder dem ikke igen. function ADChain.newState() - return {byChain = {}, paused = {}, dismissed = {}} + return {byChain = {}, paused = {}, dismissed = {}, userOff = {}} end function ADChain.stateToRecords(state) - local records = {byChain = sortedKeys(state.byChain), paused = sortedKeys(state.paused), dismissed = {}} + local records = {byChain = sortedKeys(state.byChain), paused = sortedKeys(state.paused), userOff = sortedKeys(state.userOff), dismissed = {}} local ids = {} for id in pairs(state.dismissed or {}) do table.insert(ids, id) end table.sort(ids) @@ -59,6 +60,7 @@ function ADChain.stateFromRecords(records) 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 for _, entry in ipairs(records.dismissed or {}) do local id = tonumber(entry.id) if id ~= nil and entry.fruit ~= nil and entry.fruit ~= "" then @@ -109,6 +111,25 @@ function ADChain.recipesWithPaused(recipes, paused) 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 + 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) + end + end + table.sort(added) + return {userOff = result, added = added} +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 = {} @@ -242,7 +263,18 @@ end function ADChain.approve(fieldId) local action = suggestionFor(fieldId) if action == nil then return false end - ADFieldJobs.setTarget(fieldId, action.to, "chain") + 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 @@ -251,7 +283,7 @@ end function ADChain.dismiss(fieldId) local action = suggestionFor(fieldId) if action == nil then return false end - local missingHa = 0 + 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 @@ -286,6 +318,9 @@ function ADChain.save() 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 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) @@ -304,13 +339,16 @@ function ADChain.load() 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 = {}, dismissed = {}} + 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) 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/adChainData.lua b/FS25_ADSmartPickup/adChainData.lua index c361297..a4b5789 100644 --- a/FS25_ADSmartPickup/adChainData.lua +++ b/FS25_ADSmartPickup/adChainData.lua @@ -30,6 +30,13 @@ function ADChainData.warnOnce(key, fmt, ...) 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. +local function failed(data, key, fmt, ...) + data.partial = true + ADChainData.warnOnce(key, fmt, ...) +end + -- ---- hjælpere ---- -- Navn for et fillType-indeks; husker indeks og titel til GUI og vognvalg. @@ -128,7 +135,7 @@ local function readPlaceables(data, farmId, byChain, nameOf) end) if not ok then local key = tostring(placeable ~= nil and placeable.uniqueId or index) - ADChainData.warnOnce("placeable:" .. key, "bygning %s kunne ikke læses: %s", key, tostring(err)) + 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) @@ -147,7 +154,7 @@ local function readCrops(data, nameOf) local quote = fruit.fillType.index ~= nil and ADSources.getMarketQuote(fruit.fillType.index) or nil data.prices[fruit.name] = quote ~= nil and quote.price or 0 end) - if not ok then ADChainData.warnOnce("fruit:" .. tostring(fruit.name), "afgrøde %s kunne ikke læses: %s", tostring(fruit.name), tostring(err)) 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 @@ -159,7 +166,7 @@ local function readStock(data, adEnv, farmId, nameOf) if fruit ~= nil then data.stock[fruit] = (data.stock[fruit] or 0) + liters end end end) - if not ok then ADChainData.warnOnce("stock", "lager kunne ikke læses: %s", tostring(err)) end + if not ok then failed(data, "stock", "lager 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. @@ -177,7 +184,7 @@ local function readUsedFruits(data, adEnv, farmId, nameOf) if fruit ~= nil and (rate.consumption or 0) > 0 then data.usedFruits[fruit] = true end end end) - if not ok then ADChainData.warnOnce("rates", "staldforbrug kunne ikke læses: %s", tostring(err)) end + if not ok then failed(data, "rates", "staldforbrug kunne ikke læses: %s", tostring(err)) end end local function readFields(data) @@ -193,7 +200,7 @@ local function readFields(data) if ok then table.insert(data.fields, entry) else - ADChainData.warnOnce("field:" .. tostring(info.id), "mark %s kunne ikke læses: %s", tostring(info.id), tostring(entry)) + failed(data, "field:" .. tostring(info.id), "mark %s kunne ikke læses: %s", tostring(info.id), tostring(entry)) end end end @@ -201,6 +208,7 @@ 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. function ADChainData.collect(adEnv, farmId, byChain) local data = {recipes = {}, husbandry = {}, crops = {}, stock = {}, fields = {}, yields = {}, prices = {}, usedFruits = {}, points = {}, fillTitles = {}, fillIndex = {}} diff --git a/FS25_ADSmartPickup/adChainRun.lua b/FS25_ADSmartPickup/adChainRun.lua index 5e28adc..bad6245 100644 --- a/FS25_ADSmartPickup/adChainRun.lua +++ b/FS25_ADSmartPickup/adChainRun.lua @@ -8,17 +8,32 @@ ADChainRun = {} local function log(fmt, ...) Logging.info(ADChain.LOG_PREFIX .. fmt, ...) end local function warn(fmt, ...) Logging.warning(ADChain.LOG_PREFIX .. fmt, ...) end --- Første puljevogn der kan bære varen + den bedst egnede ledige traktor (maskinpuljens regler: hk og --- kategori via ADPool.bestFit). used = {[id] = true}: maskiner der allerede er brugt i dette tick. -function ADChainRun.choosePair(implements, tractors, fillTypeIndex, used) +local UNLOADER = "unloader" + +-- Samme rolletjek som ADPool.pairAll("unloader", …) (hasRole er lokal i adPool). +local function isUnloader(implement) + return implement.role == UNLOADER or (implement.roles ~= nil and implement.roles[UNLOADER] == true) +end + +-- Traktor i et flow (også et STOPPET): ADPool.collect regner den ledig til korte lån, men ADRuns.assignVehicle +-- ville flytte den permanent ud af spillerens flow — kæden tager den aldrig. +local function belongsToFlow(tractor) + return tractor.vehicle ~= nil and ADRunsController.getRunForVehicle(tractor.vehicle) ~= nil +end + +-- Første puljetømmevogn der kan bære varen + den bedst egnede ledige traktor (maskinpuljens regler: hk og +-- kategori via ADPool.bestFit). used = {[id] = true}: brugt i dette tick; blocked = ADPool.blocked(nu). +function ADChainRun.choosePair(implements, tractors, fillTypeIndex, used, blocked) if fillTypeIndex == nil then return nil end - used = used or {} + used, blocked = used or {}, blocked or {} local free = {} for _, tractor in ipairs(tractors or {}) do - if not used[tractor.id] and not tractor.offNetwork then table.insert(free, tractor) end + if not used[tractor.id] and not blocked[tractor.id] and not tractor.offNetwork and not belongsToFlow(tractor) then + table.insert(free, tractor) + end end for _, implement in ipairs(implements or {}) do - if not used[implement.id] and ADPool.canCarry(implement, fillTypeIndex) then + if not used[implement.id] and not blocked[implement.id] and isUnloader(implement) and ADPool.canCarry(implement, fillTypeIndex) then local tractor = ADPool.bestFit(free, implement, "unloader") if tractor ~= nil then return {implement = implement, tractor = tractor} end end @@ -49,7 +64,7 @@ function ADChainRun.switchFields(data, actions) local text = string.format("F%s: %s -> %s (%.1f ha) til %s", tostring(action.fieldId), tostring(action.from), tostring(action.to), action.hectares or 0, tostring(action.reason)) log("%s", text) - ADFieldJobs.notify(text, false) + pcall(ADFieldJobs.notify, text, false) else warn("F%s kunne ikke skiftes: %s", tostring(action.fieldId), tostring(err)) end @@ -70,14 +85,23 @@ local function setProduction(data, key, isOn) return ok end -function ADChainRun.toggleProductions(data, prod) - local byChain = ADChain.state.byChain +-- 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) + 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 setProduction(data, key, true) then + if not state.userOff[key] and setProduction(data, key, true) then byChain[key] = true log("tænder %s (mellemled i kæden)", key) end end + if data.partial then return end for _, key in ipairs(prod.disable or {}) do if setProduction(data, key, false) then byChain[key] = nil @@ -119,7 +143,8 @@ local function createFlow(adEnv, data, edge, pool, used) return end if pool.implements == nil then pool.implements, pool.tractors = collectPool(adEnv) end - local pair = ADChainRun.choosePair(pool.implements, pool.tractors, fillIndex, used) + if pool.blocked == nil then pool.blocked = ADPool.blocked(ADFieldJobs.now or 0) end + local pair = ADChainRun.choosePair(pool.implements, pool.tractors, fillIndex, used, pool.blocked) if pair == nil then createStopped(run, "ingen ledig traktor/vogn til %s", tostring(title)) return @@ -159,12 +184,16 @@ function ADChainRun.stopFlows(adEnv, chain, edges) if rootKey ~= nil then ADRunsController.updateRun(run.id, {chain = rootKey}) log("flow '%s' hører nu til kæden %s", tostring(run.name), tostring(rootKey)) + changed = changed + 1 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)) - else warn("flow '%s' kunne ikke stoppes: %s", tostring(run.name), tostring(err)) end + if ok then + log("flow '%s' stoppet: 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)) + end end - changed = changed + 1 end return changed end @@ -173,12 +202,16 @@ 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) + local okProd, errProd = pcall(ADChainRun.toggleProductions, data, result.prod, result.chain.needRecipes) 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 - local okStop, stopped = pcall(ADChainRun.stopFlows, adEnv, result.chain, result.edges) - if not okStop then warn("stop af flows fejlede: %s", tostring(stopped)) end + -- delvise data: en rod der ikke kunne læses, må ikke få sine flows stoppet + local okStop, stopped = true, 0 + if not data.partial then + okStop, stopped = pcall(ADChainRun.stopFlows, adEnv, result.chain, result.edges) + if not okStop then warn("stop af flows fejlede: %s", tostring(stopped)) end + end if (okNew and created > 0) or (okStop and stopped > 0) then local okSave, errSave = pcall(ADRunsController.save) if not okSave then warn("flows kunne ikke gemmes: %s", tostring(errSave)) end