diff --git a/README.md b/README.md index 8493337..baf810a 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,12 @@ wird erst freigegeben, nachdem der Vorauftrag seine Ankunft registriert hat. So kann ein nachfolgender Auftrag den neuen Bestand nicht mehr im selben Tick wegnehmen und dadurch einen falschen Retry oder Fehler ausloesen. +V1.4 bindet die Advanced-Peripherals-ME-Bridge als geschuetzten Fallback ein. +Vorhandener Create-Bestand und Create-eigene Produktionsketten haben weiterhin +Vorrang. Nur externe, im Create-Netz fehlende Rohstoffe werden in exakt der +fehlenden Menge aus dem ME in `sophisticatedstorage:chest_0` exportiert. Diese +erste Stufe startet bewusst noch kein ME-Autocrafting. + ## Create-Fabrik: Peripherie erkennen Das rein lesende Diagnoseprogramm herunterladen und starten: diff --git a/create_factory_scheduler.lua b/create_factory_scheduler.lua index 7368a3b..c0cc082 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.3.2" +local VERSION = "1.4.0" local REFRESH_SECONDS = 1 local CONFIRM_SECONDS = 6 local DRAIN_SECONDS = 60 @@ -12,6 +12,8 @@ local STATE_TEMP_FILE = STATE_FILE .. ".tmp" local EVENT_LOG_FILE = "factory_scheduler_log.txt" local MAX_EVENTS = 5 local MAX_ERRORS = 50 +local ME_BUFFER_NAME = "sophisticatedstorage:chest_0" +local ME_TRANSFER_TIMEOUT = 15 -- Add ordinary modules/recipes here. Multi-step chains use a named planner. local RECIPES = { @@ -200,6 +202,7 @@ end local tickerName = findPeripheral("Create_StockTicker") local monitorName = findPeripheral("monitor") +local meBridgeName = findPeripheral("me_bridge") if not tickerName then error("Kein Create Stock Ticker gefunden", 0) end if not monitorName then error("Kein Monitor gefunden", 0) end local monitor = peripheral.wrap(monitorName) @@ -404,7 +407,10 @@ local function request(address, item, count) return sent end +local clearMEWait + local function finishJob(job) + clearMEWait(job) job.status, job.activeStation, job.finishedAt = "DONE", nil, epoch() statusText, statusColor = "FERTIG: " .. RECIPES[job.recipeId].label, colors.lime logEvent("#" .. job.id .. " fertig: " .. RECIPES[job.recipeId].label, colors.lime) @@ -412,6 +418,7 @@ local function finishJob(job) end local function failJob(job, reason) + clearMEWait(job) job.status, job.activeStation, job.finishedAt = "FAILED", nil, epoch() statusText, statusColor = "FEHLER #" .. job.id .. ": " .. reason, colors.red logEvent(statusText, colors.red) addError(job, "FAILED", reason) saveState() @@ -443,6 +450,78 @@ end local newJob +clearMEWait = function(job) + job.meWaitingFor, job.meRequired, job.meRequestedAt, job.meMoved = nil, nil, nil, nil +end + +local function waitForME(job, item, required) + -- Create always has priority. Only the difference between the complete + -- requirement and the currently visible Create stock may leave the ME. + local createAvailable = stock[item] or 0 + if createAvailable >= required then + clearMEWait(job) + return true + end + + if not meBridgeName or not peripheral.isPresent(meBridgeName) then + failJob(job, "Zu wenig Input: " .. prettyItemName(item) + .. " (Create " .. formatNumber(createAvailable) .. "/" .. formatNumber(required) + .. ", ME Bridge fehlt)") + return false + end + if not peripheral.isPresent(ME_BUFFER_NAME) then + failJob(job, "ME-Puffer " .. ME_BUFFER_NAME .. " nicht erreichbar") + return false + end + + local okOnline, online = pcall(peripheral.call, meBridgeName, "isConnected") + if not okOnline or not online then + failJob(job, "ME Bridge offline fuer " .. prettyItemName(item)) + return false + end + + if job.meWaitingFor == item then + job.status, job.activeStation, job.waitingFor = "WAITING_ME", nil, item + if epoch() - (job.meRequestedAt or epoch()) < ME_TRANSFER_TIMEOUT then return false end + clearMEWait(job) + createAvailable = stock[item] or 0 + if createAvailable >= required then return true end + elseif job.meWaitingFor then + clearMEWait(job) + end + + local missing = math.max(1, required - createAvailable) + local okItem, meItem = pcall(peripheral.call, meBridgeName, "getItem", { name = item }) + local meAvailable = 0 + if okItem and type(meItem) == "table" then + meAvailable = tonumber(meItem.amount or meItem.count) or 0 + end + if meAvailable <= 0 then + failJob(job, "Zu wenig Input: " .. prettyItemName(item) + .. " (Create " .. formatNumber(createAvailable) .. "/" .. formatNumber(required) + .. ", ME 0)") + return false + end + + local wanted = math.min(missing, meAvailable) + local okExport, result = pcall(peripheral.call, meBridgeName, "exportItem", + { name = item, count = wanted }, ME_BUFFER_NAME) + local moved = okExport and tonumber(result) or 0 + if moved <= 0 then + failJob(job, "ME-Export fehlgeschlagen: " .. prettyItemName(item) + .. (okExport and " (Puffer voll oder nicht erreichbar)" or (" - " .. tostring(result)))) + return false + end + + job.status, job.activeStation, job.waitingFor = "WAITING_ME", nil, item + job.meWaitingFor, job.meRequired = item, required + job.meRequestedAt, job.meMoved = epoch(), moved + logEvent("#" .. job.id .. " ME-Fallback: " .. formatNumber(moved) .. " " + .. prettyItemName(item) .. " (Create hatte " .. formatNumber(createAvailable) .. ")", colors.purple) + saveState() + return false +end + local function dependencyReleased(job) if not job.dependencyJobId then return true end local dependency = findJobById(job.dependencyJobId) @@ -473,9 +552,7 @@ end local function waitForDependency(job, item, required) local producerId = PRODUCER_BY_ITEM[item] if not producerId or producerId == job.recipeId then - failJob(job, "Zu wenig Input: " .. prettyItemName(item) - .. " (" .. formatNumber(stock[item] or 0) .. "/" .. formatNumber(required) .. ")") - return false + return waitForME(job, item, required) end if job.dependencyJobId then @@ -508,6 +585,9 @@ end local function dispatchNormal(job, missingOutput) local recipe = RECIPES[job.recipeId] if not dependencyReleased(job) then return false end + if job.meWaitingFor then + if not waitForME(job, job.meWaitingFor, job.meRequired or 1) then return false end + end local selectedStation = chooseStation(recipe, job) if not selectedStation then job.status = "WAITING" return false end @@ -560,6 +640,7 @@ local function dispatchNormal(job, missingOutput) job.lastBatchInput = nil job.activeStation, job.status = selectedStation, "RUNNING" job.waitingFor, job.dependencyJobId, job.lastDependencyLog = nil, nil, nil + clearMEWait(job) job.phaseStartedAt, job.lastProgressAt, job.warned = epoch(), epoch(), false local inputTotal = 0 for _, count in pairs(sentInputs) do inputTotal = inputTotal + count end @@ -581,7 +662,7 @@ local function updateNormal(job) local watched = recipe.outputs or { recipe.output } observeItems(job, watched) if recipe.completion == "activity" then - if job.status == "WAITING" or job.status == "WAITING_INPUT" then + if job.status == "WAITING" or job.status == "WAITING_INPUT" or job.status == "WAITING_ME" then dispatchNormal(job, 1) return end @@ -601,7 +682,7 @@ local function updateNormal(job) if epoch() - job.lastProgressAt >= DRAIN_SECONDS then finishJob(job) end return end - if job.status == "WAITING" or job.status == "WAITING_INPUT" then dispatchNormal(job, missing) + if job.status == "WAITING" or job.status == "WAITING_INPUT" or job.status == "WAITING_ME" then dispatchNormal(job, missing) elseif job.status == "DRAINING" and epoch() - job.lastProgressAt >= DRAIN_SECONDS then retryOrFail(job, missing, "Nachlauf beendet, Output unvollstaendig") elseif job.status == "RUNNING" then @@ -613,8 +694,12 @@ end local function dispatchMetalCrushing(job, remaining, plan) if stationBusy("crushing", job) then 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 required = math.max(1, math.min(64, math.ceil(remaining / 3))) + local wanted = math.min(stock[plan.source] or 0, required) + if wanted <= 0 then + waitForME(job, plan.source, required) + 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" @@ -646,7 +731,12 @@ local function updateMetal(job) if epoch() - job.lastProgressAt >= DRAIN_SECONDS then finishJob(job) end return end - if job.status == "WAITING" then job.activeStation = nil dispatchMetalWashing(job, remaining, plan) return end + if job.status == "WAITING" or job.status == "WAITING_ME" then + job.activeStation = nil + if job.meWaitingFor and not waitForME(job, job.meWaitingFor, job.meRequired or 1) then return end + 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. @@ -726,6 +816,7 @@ local function maintainStock() end local function cancelJob(job) + clearMEWait(job) job.status, job.activeStation, job.finishedAt = "CANCELLED", nil, epoch() logEvent("#" .. job.id .. " manuell abgebrochen", colors.red) addError(job, "CANCELLED", "Manuell abgebrochen; bereits versendete Pakete laufen aus") @@ -779,6 +870,11 @@ local function progressText(job) return "WARTE: " .. prettyItemName(job.waitingFor or "Input") .. (job.dependencyJobId and (" (#" .. job.dependencyJobId .. ")") or "") end + if job.status == "WAITING_ME" then + return "ME: " .. prettyItemName(job.meWaitingFor or job.waitingFor or "Input") + .. " " .. formatNumber(stock[job.meWaitingFor or ""] or 0) + .. "/" .. formatNumber(job.meRequired or 0) + end if recipe and recipe.completion == "activity" then return formatNumber(job.progress or 0) .. " ANGEKOMMEN " .. tostring(job.phase) .. " " .. idle .. "s" end