diff --git a/README.md b/README.md index 4631044..9c42e55 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,11 @@ create_factory_scheduler Neue normale Module und Rezepte werden oben in der Tabelle `RECIPES` registriert. Die Scheduler-Logik muss dafuer nicht veraendert werden. Mehrstufige oder zufaellige -Produktionsketten erhalten einen benannten Planner; V1 enthaelt den Planner fuer -Asurine -> Zinc Clumps/Nuggets -> gewaschene Zinc Nuggets. +Produktionsketten erhalten einen benannten Planner. V1.2 nutzt einen gemeinsamen +Metall-Planner fuer Asurine -> Zinc und Crimsite -> Iron. Im AUTO-Modus haelt der +erste Keep-Stock-Regler kleine Cogwheels, grosse Cogwheels und Iron Nuggets auf +jeweils 64 Stueck. Mixer-, Casing- und Precision-Rezepte sind ueber blätterbare +Rezeptseiten erreichbar. ## Create-Fabrik: Peripherie erkennen diff --git a/create_factory_scheduler.lua b/create_factory_scheduler.lua index 3482c3d..4d9bb36 100644 --- a/create_factory_scheduler.lua +++ b/create_factory_scheduler.lua @@ -1,7 +1,7 @@ -- ATM10 Create factory scheduler V1 -- Persistent jobs, dependency planning, progress-aware retries and diagnostics. -local VERSION = "1.1.0" +local VERSION = "1.2.0" local REFRESH_SECONDS = 1 local CONFIRM_SECONDS = 6 local DRAIN_SECONDS = 60 @@ -20,6 +20,11 @@ local RECIPES = { input = "create:asurine", inputCount = 64, outputs = { "alltheores:zinc_clump", "alltheores:zinc_nugget" }, completion = "activity", }, + crush_crimsite = { + label = "64 CRIMSITE CRUSHEN", station = "crushing", address = "crushing", + input = "create:crimsite", inputCount = 64, + outputs = { "alltheores:iron_clump", "minecraft:iron_nugget" }, completion = "activity", + }, strip_oak = { label = "64 STRIPPED OAK", station = "saw:2", address = "saw:2", input = "minecraft:oak_log", inputCount = 64, @@ -43,14 +48,91 @@ local RECIPES = { }, output = "create:andesite_alloy", outputCount = 64, targetCount = 64, }, + mixer_cogwheel = { + label = "64 SMALL COGWHEELS", station = "mixer:1", address = "mixer:1", + inputs = { + { item = "minecraft:oak_planks", count = 64 }, + { item = "create:shaft", count = 64 }, + }, + output = "create:cogwheel", outputCount = 64, targetCount = 64, + }, + mixer_large_cogwheel = { + label = "64 LARGE COGWHEELS", station = "mixer:2", address = "mixer:2", + inputs = { + { item = "minecraft:oak_planks", count = 128 }, + { item = "create:shaft", count = 64 }, + }, + output = "create:large_cogwheel", outputCount = 64, targetCount = 64, + }, + casing_andesite = { + label = "64 ANDESITE CASINGS", station = "casing", address = "casing", + inputs = { + { item = "minecraft:stripped_oak_log", count = 64 }, + { item = "create:andesite_alloy", count = 64 }, + }, + output = "create:andesite_casing", outputCount = 64, targetCount = 64, + }, + casing_copper = { + label = "64 COPPER CASINGS", station = "casing", address = "casing", + inputs = { + { item = "minecraft:stripped_oak_log", count = 64 }, + { item = "minecraft:copper_ingot", count = 64 }, + }, + output = "create:copper_casing", outputCount = 64, targetCount = 64, + }, + casing_brass = { + label = "64 BRASS CASINGS", station = "casing", address = "casing", + inputs = { + { item = "minecraft:stripped_oak_log", count = 64 }, + { item = "create:brass_ingot", count = 64 }, + }, + output = "create:brass_casing", outputCount = 64, targetCount = 64, + }, + casing_railway = { + label = "64 RAILWAY CASINGS", station = "casing", address = "casing", + inputs = { + { item = "create:brass_casing", count = 64 }, + { item = "create:sturdy_sheet", count = 64 }, + }, + output = "create:railway_casing", outputCount = 64, targetCount = 64, + }, + precision_mechanism = { + label = "16 GOLD SHEETS -> PRECISION", station = "precisionmechanism", address = "precisionmechanism", + input = "create:golden_sheet", inputCount = 16, + outputs = { "create:precision_mechanism" }, completion = "activity", + }, zinc_nuggets = { label = "576 ZINC NUGGETS", planner = "zinc_nuggets", output = "alltheores:zinc_nugget", targetCount = 576, }, + iron_nuggets = { + label = "64 IRON NUGGETS", planner = "iron_nuggets", + output = "minecraft:iron_nugget", targetCount = 64, + }, } local ACTION_ORDER = { - "crush_asurine", "strip_oak", "planks_oak", "shafts", "zinc_nuggets", "mixer_alloy", + "crush_asurine", "crush_crimsite", "strip_oak", "planks_oak", "shafts", "zinc_nuggets", + "iron_nuggets", + "mixer_alloy", "mixer_cogwheel", "mixer_large_cogwheel", + "casing_andesite", "casing_copper", "casing_brass", "casing_railway", "precision_mechanism", +} + +local KEEP_STOCK = { + { recipeId = "mixer_cogwheel", item = "create:cogwheel", target = 64 }, + { recipeId = "mixer_large_cogwheel", item = "create:large_cogwheel", target = 64 }, + { recipeId = "iron_nuggets", item = "minecraft:iron_nugget", target = 64 }, +} + +local METAL_PLANS = { + zinc_nuggets = { + name = "Zinc", source = "create:asurine", sourceName = "Asurine", + clump = "alltheores:zinc_clump", nugget = "alltheores:zinc_nugget", + }, + iron_nuggets = { + name = "Iron", source = "create:crimsite", sourceName = "Crimsite", + clump = "alltheores:iron_clump", nugget = "minecraft:iron_nugget", + }, } local function epoch() @@ -73,12 +155,13 @@ monitor.setTextScale(0.5) local stock, stockEntries, vaults = {}, {}, {} local stockPage, stockPageCount = 1, 1 -local stockPageButtons, buttons, modeButtons, errorPageButtons = {}, {}, {}, {} +local stockPageButtons, buttons, modeButtons, errorPageButtons, actionPageButtons = {}, {}, {}, {}, {} local events = {} local state = { version = 1, nextJobId = 1, mode = "MANUAL", jobs = {}, errors = {} } local armed, armedUntil = nil, 0 local statusText, statusColor = "Scheduler bereit", colors.lime local errorPage = 1 +local actionPage = 1 local function formatNumber(value) local text = tostring(math.floor(tonumber(value) or 0)) @@ -362,7 +445,7 @@ local function updateNormal(job) job.progress = total if total > 0 and job.status ~= "DRAINING" then beginDrain(job) end if job.status == "DRAINING" and epoch() - job.lastProgressAt >= DRAIN_SECONDS then finishJob(job) - elseif job.status == "RUNNING" and epoch() - job.lastProgressAt >= DRAIN_SECONDS then retryOrFail(job, 1, "Kein Crushing-Output") end + elseif job.status == "RUNNING" and epoch() - job.lastProgressAt >= DRAIN_SECONDS then retryOrFail(job, 1, "Kein Output angekommen") end return end local current = produced(job, recipe.output) @@ -383,33 +466,34 @@ local function updateNormal(job) end end -local function dispatchZincCrushing(job, remaining) +local function dispatchMetalCrushing(job, remaining, plan) if stationBusy("crushing", job) then return end - local wanted = math.min(stock["create:asurine"] or 0, math.max(1, math.min(64, math.ceil(remaining / 3)))) - if wanted <= 0 then failJob(job, "Zu wenig Asurine fuer fehlende Zinc Nuggets") return end - local sent, err = request("crushing", "create:asurine", wanted) - if sent <= 0 then failJob(job, "Asurine-Versand fehlgeschlagen: " .. tostring(err or "unbekannt")) return end + local wanted = math.min(stock[plan.source] or 0, math.max(1, math.min(64, math.ceil(remaining / 3)))) + if wanted <= 0 then failJob(job, "Zu wenig " .. plan.sourceName .. " fuer fehlende " .. plan.name .. " Nuggets") return end + local sent, err = request("crushing", plan.source, wanted) + if sent <= 0 then failJob(job, plan.sourceName .. "-Versand fehlgeschlagen: " .. tostring(err or "unbekannt")) return end job.activeStation, job.phase, job.status = "crushing", "CRUSHING", "RUNNING" job.lastBatchInput, job.phaseStartedAt, job.lastProgressAt, job.warned = sent, epoch(), epoch(), false - logEvent("#" .. job.id .. " Teilauftrag: " .. sent .. " Asurine crushen", colors.orange) saveState() + logEvent("#" .. job.id .. " Teilauftrag: " .. sent .. " " .. plan.sourceName .. " crushen", colors.orange) saveState() end -local function dispatchZincWashing(job, remaining) +local function dispatchMetalWashing(job, remaining, plan) if stationBusy("fan:washing", job) then return end - local wanted = math.min(stock["alltheores:zinc_clump"] or 0, math.ceil(remaining / 9)) - if wanted <= 0 then dispatchZincCrushing(job, remaining) return end - local sent, err = request("fan:washing", "alltheores:zinc_clump", wanted) - if sent <= 0 then failJob(job, "Zinc-Clump-Versand fehlgeschlagen: " .. tostring(err or "unbekannt")) return end + local wanted = math.min(stock[plan.clump] or 0, math.ceil(remaining / 9)) + if wanted <= 0 then dispatchMetalCrushing(job, remaining, plan) return end + local sent, err = request("fan:washing", plan.clump, wanted) + if sent <= 0 then failJob(job, plan.name .. "-Clump-Versand fehlgeschlagen: " .. tostring(err or "unbekannt")) return end job.activeStation, job.phase, job.status = "fan:washing", "WASHING", "RUNNING" job.lastBatchInput, job.phaseStartedAt, job.lastProgressAt, job.warned = sent, epoch(), epoch(), false - job.phaseOutputBaseline = produced(job, "alltheores:zinc_nugget") + job.phaseOutputBaseline = produced(job, plan.nugget) job.phaseExpected = sent * 9 logEvent("#" .. job.id .. " Teilauftrag: " .. sent .. " Clumps waschen", colors.orange) saveState() end -local function updateZinc(job) - observeItems(job, { "alltheores:zinc_nugget", "alltheores:zinc_clump" }) - local nuggets = produced(job, "alltheores:zinc_nugget") +local function updateMetal(job) + local plan = METAL_PLANS[job.recipeId] + observeItems(job, { plan.nugget, plan.clump }) + local nuggets = produced(job, plan.nugget) job.progress = math.min(job.targetCount, nuggets) local remaining = math.max(0, job.targetCount - nuggets) if remaining <= 0 then @@ -417,16 +501,16 @@ local function updateZinc(job) if epoch() - job.lastProgressAt >= DRAIN_SECONDS then finishJob(job) end return end - if job.status == "WAITING" then job.activeStation = nil dispatchZincWashing(job, remaining) return end + if job.status == "WAITING" then job.activeStation = nil dispatchMetalWashing(job, remaining, plan) return end -- Do not wait for the complete 60-second crushing drain when the material -- which has already reached storage can satisfy the remaining parent job. -- Late crushing returns remain valid surplus and are still counted. - if job.phase == "CRUSHING" and (stock["alltheores:zinc_clump"] or 0) * 9 >= remaining then + if job.phase == "CRUSHING" and (stock[plan.clump] or 0) * 9 >= remaining then job.activeStation, job.status, job.phase = nil, "WAITING", "PLAN" logEvent("#" .. job.id .. " genug Clumps angekommen - sofort weiter zum Washing", colors.lime) saveState() - dispatchZincWashing(job, remaining) + dispatchMetalWashing(job, remaining, plan) return end @@ -439,7 +523,7 @@ local function updateZinc(job) if job.phase == "CRUSHING" then job.status, job.phase, job.lastProgressAt = "WAITING", "PLAN", epoch() saveState() elseif job.phase == "WASHING" then - local phaseGain = math.max(0, produced(job, "alltheores:zinc_nugget") - (job.phaseOutputBaseline or 0)) + local phaseGain = math.max(0, produced(job, plan.nugget) - (job.phaseOutputBaseline or 0)) if phaseGain >= (job.phaseExpected or 0) then -- Batch arrived completely, but the parent still needs more: replan. job.status, job.phase, job.lastProgressAt = "WAITING", "PLAN", epoch() @@ -447,37 +531,54 @@ local function updateZinc(job) else retryOrFail(job, remaining, "Washing-Nachlauf beendet") end - elseif idle >= DRAIN_SECONDS then retryOrFail(job, remaining, "Zinkauftrag ohne Fortschritt") end + elseif idle >= DRAIN_SECONDS then retryOrFail(job, remaining, plan.name .. "-Auftrag ohne Fortschritt") end end +local newJob + local function updateJobs() if state.mode == "OFF" then return end for _, job in ipairs(state.jobs) do if not terminal(job) then local recipe = RECIPES[job.recipeId] if not recipe then failJob(job, "Rezept nicht mehr vorhanden") - elseif recipe.planner == "zinc_nuggets" then updateZinc(job) + elseif METAL_PLANS[recipe.planner] then updateMetal(job) else updateNormal(job) end end end end -local function newJob(recipeId) +newJob = function(recipeId, targetCount, origin) local recipe = RECIPES[recipeId] local job = { id = state.nextJobId, recipeId = recipeId, status = "WAITING", phase = recipe.planner and "PLAN" or "NORMAL", - targetCount = recipe.targetCount or recipe.outputCount or 1, + targetCount = targetCount or recipe.targetCount or recipe.outputCount or 1, + origin = origin or "MANUAL", baseline = {}, lastObserved = {}, arrived = {}, progress = 0, retries = 0, createdAt = epoch(), lastProgressAt = epoch(), phaseStartedAt = epoch(), } state.nextJobId = state.nextJobId + 1 if recipe.output then setBaseline(job, recipe.output) end for _, output in ipairs(recipe.outputs or {}) do setBaseline(job, output) end - if recipe.planner == "zinc_nuggets" then setBaseline(job, "alltheores:zinc_clump") end + local metalPlan = METAL_PLANS[recipe.planner] + if metalPlan then setBaseline(job, metalPlan.clump) end state.jobs[#state.jobs + 1] = job statusText, statusColor = "EINGEREIHT #" .. job.id .. ": " .. recipe.label, colors.orange - logEvent(statusText, colors.orange) saveState() return job + logEvent(statusText .. (job.origin == "AUTO" and " [AUTO]" or ""), colors.orange) saveState() return job +end + +local function maintainStock() + if state.mode ~= "AUTO" then return end + for _, rule in ipairs(KEEP_STOCK) do + local current = stock[rule.item] or 0 + if current < rule.target and not findJobByRecipe(rule.recipeId) then + local missing = rule.target - current + newJob(rule.recipeId, missing, "AUTO") + logEvent("KEEP STOCK: " .. prettyItemName(rule.item) .. " " + .. formatNumber(current) .. "/" .. formatNumber(rule.target), colors.cyan) + end + end end local function cancelJob(job) @@ -565,6 +666,18 @@ local function drawModes(width) end end +local function drawActionPage(width) + local perPage = 6 + local pages = math.max(1, math.ceil(#ACTION_ORDER / perPage)) + actionPage = math.max(1, math.min(actionPage, pages)) + center(10, "< REZEPTE " .. actionPage .. "/" .. pages .. " >", colors.yellow) + actionPageButtons = { + { direction = -1, x1 = 1, x2 = math.floor(width / 2), y1 = 10, y2 = 10, enabled = actionPage > 1 }, + { direction = 1, x1 = math.floor(width / 2) + 1, x2 = width, y1 = 10, y2 = 10, enabled = actionPage < pages }, + } + return (actionPage - 1) * perPage + 1, math.min(#ACTION_ORDER, actionPage * perPage) +end + local function drawLogs(width, height, top) local half = math.floor(width / 2) writeAt(2, top, "LIVE-LOG", colors.yellow) @@ -622,11 +735,13 @@ local function render() local top, gap = 11, 2 local buttonWidth = math.floor((width - 6 - gap) / 2) local left, right = 3, 3 + buttonWidth + gap - for index, recipeId in ipairs(ACTION_ORDER) do + local firstAction, lastAction = drawActionPage(width) + for absoluteIndex = firstAction, lastAction do + local index, recipeId = absoluteIndex - firstAction + 1, ACTION_ORDER[absoluteIndex] drawAction(recipeId, index % 2 == 1 and left or right, top + math.floor((index - 1) / 2) * 2, (index % 2 == 1 and left or right) + buttonWidth - 1, top + math.floor((index - 1) / 2) * 2) end - drawLogs(width, height, top + math.ceil(#ACTION_ORDER / 2) * 2) + drawLogs(width, height, 17) drawVaults(width, height) writeAt(2, height, "Bestellen/Abbrechen/OFF: doppelt beruehren | Nachlauf: " .. DRAIN_SECONDS .. "s", colors.gray) end @@ -670,6 +785,9 @@ local function handleTouch(x, y) for _, button in ipairs(errorPageButtons) do if inside(x, y, button) then if button.enabled then errorPage = errorPage + button.direction end return end end + for _, button in ipairs(actionPageButtons) do + if inside(x, y, button) then if button.enabled then actionPage = actionPage + button.direction end return end + end for _, button in ipairs(modeButtons) do if inside(x, y, button) then if button.mode == "OFF" then @@ -707,7 +825,7 @@ while true do if armed and epoch() > armedUntil then armed, armedUntil = nil, 0 statusText, statusColor = "Bestaetigung abgelaufen", colors.gray end - if refreshStock() then updateJobs() end + if refreshStock() then maintainStock() updateJobs() end refreshVaults() render() refreshTimer = os.startTimer(REFRESH_SECONDS) elseif event == "peripheral" or event == "peripheral_detach" then refreshStock() refreshVaults() render() end