diff --git a/README.md b/README.md index baf810a..5364d34 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,12 @@ 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. +V1.5 ergaenzt Ore-/Tag-Substitution fuer vereinheitlichte Materialien. Ingots, +Nuggets, Plates, Dusts, Gears, Rods und Clumps werden ueber ihre gemeinsamen +`c:`-Tags erkannt. Dadurch sind beispielsweise `create:brass_ingot` und +`alltheores:brass_ingot` fuer Lagerzaehlung, Create-Versand und ME-Export +gleichwertig. Eindeutige Create-Bauteile verwenden weiterhin exakte IDs. + ## Create-Fabrik: Peripherie erkennen Das rein lesende Diagnoseprogramm herunterladen und starten: diff --git a/create_factory_scheduler.lua b/create_factory_scheduler.lua index 5437ec9..efdeef3 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.4.1" +local VERSION = "1.5.0" local REFRESH_SECONDS = 1 local CONFIRM_SECONDS = 6 local DRAIN_SECONDS = 60 @@ -218,6 +218,51 @@ local statusText, statusColor = "Scheduler bereit", colors.lime local errorPage = 1 local actionPage = 1 +-- ATM10/Almost Unified uses the common NeoForge `c:` material tags. Resolve +-- ordinary metal forms by tag so e.g. create:brass_ingot and +-- alltheores:brass_ingot behave as the same ingredient everywhere. +local MATERIAL_FORMS = { + ingot = "ingots", nugget = "nuggets", plate = "plates", dust = "dusts", + gear = "gears", rod = "rods", clump = "clumps", +} + +local function materialTag(item) + if type(item) ~= "string" then return nil end + local path = item:match("^[^:]+:(.+)$") + if not path then return nil end + for singular, plural in pairs(MATERIAL_FORMS) do + local material = path:match("^(.+)_" .. singular .. "$") + if material then return "#c:" .. plural .. "/" .. material end + end +end + +local function equivalentItem(left, right) + if left == right then return true end + local leftTag, rightTag = materialTag(left), materialTag(right) + return leftTag ~= nil and leftTag == rightTag +end + +local function stockCount(item) + local total = 0 + for storedItem, count in pairs(stock) do + if equivalentItem(item, storedItem) then total = total + (tonumber(count) or 0) end + end + return total +end + +local function stockCandidates(item) + local candidates, alternatives = {}, {} + if (stock[item] or 0) > 0 then candidates[#candidates + 1] = item end + for storedItem, count in pairs(stock) do + if storedItem ~= item and (tonumber(count) or 0) > 0 and equivalentItem(item, storedItem) then + alternatives[#alternatives + 1] = storedItem + end + end + table.sort(alternatives) + for _, storedItem in ipairs(alternatives) do candidates[#candidates + 1] = storedItem end + return candidates +end + local function formatNumber(value) local text = tostring(math.floor(tonumber(value) or 0)) local sign, digits = text:match("^([%-]?)(%d+)$") @@ -390,21 +435,31 @@ end local function setBaseline(job, item) if job.baseline[item] == nil then - job.baseline[item], job.lastObserved[item] = stock[item] or 0, stock[item] or 0 + job.baseline[item], job.lastObserved[item] = stockCount(item), stockCount(item) job.arrived = job.arrived or {} job.arrived[item] = job.arrived[item] or 0 end end local function request(address, item, count) - local ok, result = pcall(peripheral.call, tickerName, "requestFiltered", address, { - name = item, _requestCount = math.max(1, math.floor(count)), - }) - if not ok then return 0, tostring(result) end - local sent = tonumber(result) or 0 - -- Prevent two jobs in the same scheduler tick from reserving the same input. - if sent > 0 then stock[item] = math.max(0, (stock[item] or 0) - sent) end - return sent + local remaining, sentTotal, lastError = math.max(1, math.floor(count)), 0, nil + for _, candidate in ipairs(stockCandidates(item)) do + if remaining <= 0 then break end + local wanted = math.min(remaining, stock[candidate] or 0) + local ok, result = pcall(peripheral.call, tickerName, "requestFiltered", address, { + name = candidate, _requestCount = wanted, + }) + if ok then + local sent = tonumber(result) or 0 + if sent > 0 then + stock[candidate] = math.max(0, (stock[candidate] or 0) - sent) + sentTotal, remaining = sentTotal + sent, remaining - sent + end + else + lastError = tostring(result) + end + end + return sentTotal, lastError end local clearMEWait @@ -432,7 +487,7 @@ local function observeItems(job, items) local changed = false job.arrived = job.arrived or {} for _, item in ipairs(items) do - local current = stock[item] or 0 + local current = stockCount(item) local previous = job.lastObserved[item] if previous == nil then previous = current end if current > previous then @@ -457,7 +512,7 @@ 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 + local createAvailable = stockCount(item) if createAvailable >= required then clearMEWait(job) return true @@ -484,17 +539,22 @@ local function waitForME(job, item, required) 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 + createAvailable = stockCount(item) 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 meFilter = materialTag(item) or item + local okItems, meItems = pcall(peripheral.call, meBridgeName, "getItems", { name = meFilter }) local meAvailable = 0 - if okItem and type(meItem) == "table" then - meAvailable = tonumber(meItem.amount or meItem.count) or 0 + if okItems and type(meItems) == "table" then + for _, meItem in pairs(meItems) do + if type(meItem) == "table" then + meAvailable = meAvailable + (tonumber(meItem.amount or meItem.count) or 0) + end + end end if meAvailable <= 0 then failJob(job, "Zu wenig Input: " .. prettyItemName(item) @@ -505,7 +565,7 @@ local function waitForME(job, item, required) local wanted = math.min(missing, meAvailable) local okExport, result = pcall(peripheral.call, meBridgeName, "exportItem", - { name = item, count = wanted }, ME_BUFFER_NAME) + { name = meFilter, count = wanted }, ME_BUFFER_NAME) local moved = okExport and tonumber(result) or 0 if moved <= 0 then failJob(job, "ME-Export fehlgeschlagen: " .. prettyItemName(item) @@ -568,7 +628,7 @@ local function waitForDependency(job, item, required) job.dependencyJobId = nil end - local missing = math.max(1, required - (stock[item] or 0)) + local missing = math.max(1, required - stockCount(item)) local child = findJobByRecipe(producerId) if not child then child = newJob(producerId, missing, "DEPENDENCY", job.id) end job.status, job.activeStation = "WAITING_INPUT", nil @@ -610,7 +670,7 @@ local function dispatchNormal(job, missingOutput) -- Requirements are kept in the logistics network and are not sent with the -- main package. Precision Mechanisms use this for their deployer buffers. for _, requirement in ipairs(recipe.requires or {}) do - if (stock[requirement.item] or 0) < requirement.count then + if stockCount(requirement.item) < requirement.count then return waitForDependency(job, requirement.item, requirement.count) end end @@ -618,7 +678,7 @@ local function dispatchNormal(job, missingOutput) -- Check the complete recipe before sending its first package. This prevents -- a mixer basin from receiving only one half of a planned batch. for _, ingredient in ipairs(inputPlan) do - if ingredient.count <= 0 or (stock[ingredient.item] or 0) < ingredient.count then + if ingredient.count <= 0 or stockCount(ingredient.item) < ingredient.count then return waitForDependency(job, ingredient.item, ingredient.count) end end @@ -695,7 +755,7 @@ end local function dispatchMetalCrushing(job, remaining, plan) if stationBusy("crushing", job) then return end local required = math.max(1, math.min(64, math.ceil(remaining / 3))) - local wanted = math.min(stock[plan.source] or 0, required) + local wanted = math.min(stockCount(plan.source), required) if wanted <= 0 then waitForME(job, plan.source, required) return @@ -709,7 +769,7 @@ end local function dispatchMetalWashing(job, remaining, plan) if stationBusy("fan:washing", job) then return end - local wanted = math.min(stock[plan.clump] or 0, math.ceil(remaining / 9)) + local wanted = math.min(stockCount(plan.clump), 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 @@ -741,7 +801,7 @@ local function updateMetal(job) -- 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[plan.clump] or 0) * 9 >= remaining then + if job.phase == "CRUSHING" and stockCount(plan.clump) * 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() @@ -805,7 +865,7 @@ 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 + local current = stockCount(rule.item) if current < rule.target and not findJobByRecipe(rule.recipeId) then local missing = rule.target - current newJob(rule.recipeId, missing, "AUTO") @@ -872,7 +932,7 @@ local function progressText(job) 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(stockCount(job.meWaitingFor or "")) .. "/" .. formatNumber(job.meRequired or 0) end if recipe and recipe.completion == "activity" then