-- RSWarehouse.lua -- Construction: https://cdn.domekologe.eu/gameserver/minecraft/cc/Minecolonies_RefinedStorage_Integration.png -- IMPORTANT: Entangled Block MUST be on left Side of Computer, if not, change direction variable! --[[ Changelog: - Change Domum Items, that only items from architects cutter are excluded - [FIX] pcall wrapping for all peripheral calls to prevent crashes - [FIX] exportItem return value handling (can return nil on error) - [FIX] craftItem can return job object in newer AP, handle truthy check - [FIX] Variant comparison case-insensitive ("Unknown" vs "unknown") - [FIX] drawLine right-aligned text off-by-one positioning - [FIX] Cleanup loop: collect keys first, then delete (safe iteration) - [FIX] Partial delivery now clears stale crafting state - [FIX] Monitor overflow protection - [FIX] NBT serialization in makeRequestId protected with pcall - [FIX] Equipment requests are now fully delivered from RS (exact item match) - [NEW] Equipment (tools/armor) delivery: exports the exact requested item - [NEW] Multi-item fallback: tries all r.items[] alternatives for equipment - [NEW] isItemCrafting() check before starting duplicate craft jobs - [NEW] Sorted request display by status - [NEW] Error recovery: auto-reconnect peripherals on detach - [NEW] Statistics line on monitor - [NEW] Stale crafting job timeout (5 minutes) - [NEW] Peripheral type supports both "rs_bridge"/"rsBridge" naming - [NEW] 1.21.1 Data Components support alongside legacy NBT ]]-- ------------------------------------------------------------------------------- -- CONFIGURATION ------------------------------------------------------------------------------- local time_between_runs = 15 local MAX_EXPORT = 256 local direction = "left" local CRAFT_TIMEOUT_MS = 5 * 60 * 1000 -- 5 min stale craft timeout ------------------------------------------------------------------------------- -- INIT ------------------------------------------------------------------------------- local monitor, bridge, colony local function findPeripherals() monitor = peripheral.find("monitor") if not monitor then error("Monitor not found") end monitor.setTextScale(0.5) -- AP 0.7+/0.8+ for 1.21.1 may register as "rsBridge" instead of "rs_bridge" bridge = peripheral.find("rsBridge") or peripheral.find("rs_bridge") if not bridge then error("RS Bridge not found") end -- Colony Integrator: "colonyIntegrator" or "colony_integrator" colony = peripheral.find("colonyIntegrator") or peripheral.find("colony_integrator") if not colony then error("Colony Integrator not found") end end findPeripherals() local deliveredRequests = {} local craftingRequests = {} local stats = { total = 0, delivered = 0, crafting = 0, missing = 0, partial = 0 } ------------------------------------------------------------------------------- -- ARCHITECTS CUTTER ITEMS (Domum Ornamentum) ------------------------------------------------------------------------------- local ARCHITECTS_CUTTER_ITEMS = { ["domum_ornamentum:panel"] = true, ["domum_ornamentum:plain"] = true, ["domum_ornamentum:post"] = true, ["domum_ornamentum:fancy_door"] = true, ["domum_ornamentum:fancy_light"] = true, ["domum_ornamentum:fancy_trapdoor"] = true, ["domum_ornamentum:blockpaperwall"] = true, ["domum_ornamentum:blockpillar"] = true, ["domum_ornamentum:blocktiledpaperwall"] = true, ["domum_ornamentum:blockypillar"] = true, ["domum_ornamentum:centerlight"] = true, ["domum_ornamentum:shingle"] = true, ["domum_ornamentum:shingle_slab"] = true, ["domum_ornamentum:shingle_stairs"] = true, ["domum_ornamentum:vanilla_fence_gate_compat"] = true, ["domum_ornamentum:vanilla_fence_compat"] = true, ["domum_ornamentum:vanilla_wall_compat"] = true, ["domum_ornamentum:vanilla_stairs_compat"] = true, ["domum_ornamentum:vanilla_slab_compat"] = true, ["domum_ornamentum:vanilla_trapdoors_compat"] = true, ["domum_ornamentum:framed"] = true, ["domum_ornamentum:framed_light"] = true, ["domum_ornamentum:four_light"] = true, ["domum_ornamentum:extra"] = true, ["domum_ornamentum:extra_slab"] = true, ["domum_ornamentum:extra_stairs"] = true, } ------------------------------------------------------------------------------- -- HELPERS ------------------------------------------------------------------------------- --- Safe peripheral call - prevents script crash on peripheral errors local function safeCall(fn, ...) local args = { ... } local ok, result = pcall(function() return fn(table.unpack(args)) end) if not ok then print("[ERROR] Peripheral call failed: " .. tostring(result)) return nil end return result end local function isBuilder(colonistName) if not colonistName then return false end return string.find(colonistName, "Builder") ~= nil end --- Equipment = tool/armor requests from colonists (desc contains "level") local function isEquipmentRequest(desc) if not desc then return false end return string.find(desc, "level") ~= nil end --- Build unique request ID from request data local function makeRequestId(data) -- Safely serialize nbt/components local dataStr = "" if data.item.nbt then local ok, s = pcall(textutils.serialize, data.item.nbt) if ok then dataStr = s end end if data.item.components then local ok, s = pcall(textutils.serialize, data.item.components) if ok then dataStr = dataStr .. s end end return string.format( "%s|%s|%s|%d|%s", data.item.name or "?", dataStr, data.colonist or "?", data.needed or 0, data.building or "" ) end --- Extract variant name from Domum Ornamentum data --- Supports both legacy NBT and 1.21.1 Data Components local function getDomumVariant(itemData) if not itemData then return "Unknown" end -- 1.21.1 Data Components path local comp = itemData.components if comp and type(comp) == "table" then for _, key in ipairs({ "domum_ornamentum:variant", "domum_ornamentum:block", "domum_ornamentum:material", "domum_ornamentum:texture", }) do if comp[key] then return tostring(comp[key]) end end end -- Legacy NBT path local nbt = itemData.nbt or itemData if type(nbt) == "table" then for _, field in ipairs({ "variant", "block", "material", "texture", "state" }) do if nbt[field] then return tostring(nbt[field]) end end end return "Unspecified. Check Requester" end local function isArchitectCutterItem(item) if not item or not item.name then return false end return ARCHITECTS_CUTTER_ITEMS[item.name] == true end --- Check if item has meaningful NBT or Components (1.21.1+) local function hasItemData(item) if not item then return false end if type(item.nbt) == "table" and next(item.nbt) ~= nil then return true end if type(item.components) == "table" and next(item.components) ~= nil then return true end if type(item.nbt) == "string" and #item.nbt > 0 then return true end return false end ------------------------------------------------------------------------------- -- MONITOR UI ------------------------------------------------------------------------------- local monWidth, monHeight = 0, 0 local function drawHeader(mon, remaining) monWidth, monHeight = mon.getSize() local now = os.time() local cycle = "day" local color = colors.lightBlue if now >= 18.5 or now < 5 then cycle = "night" color = colors.red elseif now >= 18 then cycle = "sunset" color = colors.orange elseif now < 6 then cycle = "sunrise" color = colors.yellow end mon.setCursorPos(1, 1) mon.setTextColor(color) mon.write("Time: " .. textutils.formatTime(now, false) .. " [" .. cycle .. "]") local remainText = string.format("Next: %02ds", remaining) mon.setCursorPos(monWidth - #remainText + 1, 1) mon.setTextColor(colors.green) mon.write(remainText) end local function drawStats(mon, row) if row > monHeight then return row + 1 end local text = string.format( "Total: %d | Done: %d | Craft: %d | Partial: %d | Miss: %d", stats.total, stats.delivered, stats.crafting, stats.partial, stats.missing ) mon.setCursorPos(1, row) mon.setTextColor(colors.lightGray) if #text > monWidth then text = text:sub(1, monWidth) end mon.write(text) return row + 1 end local function drawSectionTitle(mon, row, title) if row > monHeight then return row + 1 end mon.setCursorPos(math.floor((monWidth - #title) / 2) + 1, row) mon.setTextColor(colors.white) mon.write(title) return row + 1 end local function drawLine(mon, row, left, right, color) if row > monHeight then return row + 1 end local maxLeft = monWidth - #right - 1 if #left > maxLeft and maxLeft > 3 then left = left:sub(1, maxLeft - 2) .. ".." end mon.setCursorPos(1, row) mon.setTextColor(color) mon.write(left) if #right > 0 then mon.setCursorPos(monWidth - #right + 1, row) mon.write(right) end return row + 1 end ------------------------------------------------------------------------------- -- REQUEST FETCH ------------------------------------------------------------------------------- local function getRequests() local rawRequests = safeCall(colony.getRequests) if not rawRequests then print("[WARN] Failed to fetch colony requests") return {} end local list = {} for _, r in pairs(rawRequests) do if r.items and r.items[1] and r.items[1].name then local itemCount = r.count or 1 table.insert(list, { name = r.name or "Unknown", desc = r.desc, needed = itemCount, -- Primary item item = { name = r.items[1].name, count = math.min(itemCount, MAX_EXPORT), nbt = r.items[1].nbt, components = r.items[1].components, }, -- Keep ALL alternative items for equipment fallback allItems = r.items, colonist = r.target or "Unknown", building = r.building or r.target or "Unknown", }) end end return list end ------------------------------------------------------------------------------- -- EQUIPMENT DELIVERY -- Colonists request specific tools/armor (e.g. "Iron Pickaxe with level 2"). -- The request's items[] list contains ALL acceptable alternatives. -- We try each alternative and export the first one RS has in stock. ------------------------------------------------------------------------------- local function tryExportEquipment(data) -- Try every acceptable item alternative from the request local items = data.allItems or { data.item } for _, candidate in ipairs(items) do if candidate and candidate.name then local exportFilter = { name = candidate.name, count = math.min(data.needed, MAX_EXPORT), } -- Include NBT/components if present (for enchanted or specific items) if candidate.nbt then exportFilter.nbt = candidate.nbt end if candidate.components then exportFilter.components = candidate.components end local result = safeCall(bridge.exportItem, exportFilter, direction) if result and result > 0 then return result, candidate.name end end end return 0, (data.item.name or "?") end ------------------------------------------------------------------------------- -- MAIN SCAN ------------------------------------------------------------------------------- local function scan(mon, remaining) local builderEntries = {} local nonBuilderEntries = {} local equipmentEntries = {} local activeRequests = {} local missingDomum = {} stats = { total = 0, delivered = 0, crafting = 0, missing = 0, partial = 0 } local requests = getRequests() for _, data in ipairs(requests) do local requestId = makeRequestId(data) activeRequests[requestId] = true stats.total = stats.total + 1 local provided = 0 local color = colors.blue local isDomum = isArchitectCutterItem(data.item) local hasData = hasItemData(data.item) local isEquip = isEquipmentRequest(data.desc) local usedItem = data.item.name -- track which item was actually exported -- ====================================================== -- DELIVERY -- ====================================================== if isEquip then -- EQUIPMENT: try all acceptable alternatives from the request provided, usedItem = tryExportEquipment(data) elseif not isDomum or hasData then -- Normal items OR Domum with valid data local result = safeCall(bridge.exportItem, data.item, direction) provided = result or 0 end -- Domum without data -> provided stays 0 -- ====================================================== -- DETERMINE STATUS -- ====================================================== if provided >= data.needed then -- Fully delivered deliveredRequests[requestId] = true craftingRequests[requestId] = nil color = colors.green stats.delivered = stats.delivered + 1 elseif provided > 0 then -- Partially delivered craftingRequests[requestId] = nil color = colors.yellow stats.partial = stats.partial + 1 else -- Nothing delivered if isDomum and not hasData then color = colors.blue stats.missing = stats.missing + 1 else -- Check if already crafting (prefer isItemCrafting if available) local alreadyCrafting = false if bridge.isItemCrafting then alreadyCrafting = safeCall(bridge.isItemCrafting, { name = data.item.name, nbt = data.item.nbt, }) or false end if alreadyCrafting or craftingRequests[requestId] then color = colors.pink stats.crafting = stats.crafting + 1 -- Timeout stale craft jobs if craftingRequests[requestId] then local elapsed = os.epoch("utc") - (craftingRequests[requestId].started or 0) if elapsed > CRAFT_TIMEOUT_MS then print("[TIMEOUT] Stale craft: " .. data.item.name) craftingRequests[requestId] = nil color = colors.blue stats.crafting = stats.crafting - 1 stats.missing = stats.missing + 1 end end else -- Try to start crafting (not for equipment - those typically can't be auto-crafted) if not isEquip then local craftResult = safeCall(bridge.craftItem, { name = data.item.name, count = data.needed, nbt = data.item.nbt, }) if craftResult then craftingRequests[requestId] = { started = os.epoch("utc") } color = colors.pink stats.crafting = stats.crafting + 1 else color = colors.blue stats.missing = stats.missing + 1 end else -- Equipment not in RS and can't be auto-crafted color = colors.blue stats.missing = stats.missing + 1 end end end end -- ====================================================== -- Domum Ornamentum Missing Items -- ====================================================== if isDomum and color == colors.blue then local variant = getDomumVariant(data.item) local key = data.item.name .. "|" .. variant missingDomum[key] = { name = data.name:gsub("^[%d%-]+%s*", ""), variant = variant, } end -- ====================================================== -- UI ENTRY -- ====================================================== local variantStr = "" if isDomum then local v = getDomumVariant(data.item) if v:lower() ~= "unknown" then variantStr = " [" .. v .. "]" end end local cleanName = data.name:gsub("^[%d%-]+%s*", "") local entry = { left = string.format( "%d/%d %s%s", provided, data.needed, cleanName, variantStr ), provided = provided, needed = data.needed, color = color, } if isEquip then entry.right = data.colonist table.insert(equipmentEntries, entry) elseif isBuilder(data.colonist) then entry.right = data.building or "Builder" table.insert(builderEntries, entry) else entry.right = data.colonist table.insert(nonBuilderEntries, entry) end -- ====================================================== -- TERMINAL OUTPUT -- ====================================================== local target = entry.right or "?" if color == colors.green then print(string.format("[DONE] %s%s x%d -> %s", usedItem, variantStr, data.needed, target)) elseif color == colors.pink then print(string.format("[CRAFTING] %s%s x%d -> %s", data.item.name, variantStr, data.needed, target)) elseif color == colors.yellow then print(string.format("[PARTIAL] %s%s %d/%d -> %s", usedItem, variantStr, provided, data.needed, target)) elseif isDomum and not hasData then print(string.format("[SKIP] %s x%d -> %s (no component data)", data.item.name, data.needed, target)) elseif isEquip then print(string.format("[EQUIP] %s x%d -> %s (not in RS)", cleanName, data.needed, target)) else print(string.format("[MISSING] %s x%d -> %s (no pattern)", data.item.name, data.needed, target)) end end -- ====================================================== -- CLEANUP: remove state for vanished requests -- FIX: collect keys first, then delete (safe Lua iteration) -- ====================================================== local toRemove = {} for requestId, _ in pairs(deliveredRequests) do if not activeRequests[requestId] then table.insert(toRemove, requestId) end end for _, rid in ipairs(toRemove) do deliveredRequests[rid] = nil craftingRequests[rid] = nil end toRemove = {} for requestId, _ in pairs(craftingRequests) do if not activeRequests[requestId] then table.insert(toRemove, requestId) end end for _, rid in ipairs(toRemove) do craftingRequests[rid] = nil end -- ====================================================== -- Sort: missing first, done last -- ====================================================== local function sortByStatus(a, b) local order = { [colors.blue] = 1, [colors.pink] = 2, [colors.yellow] = 3, [colors.green] = 4 } return (order[a.color] or 0) < (order[b.color] or 0) end table.sort(builderEntries, sortByStatus) table.sort(nonBuilderEntries, sortByStatus) table.sort(equipmentEntries, sortByStatus) -- ====================================================== -- MONITOR UI -- ====================================================== mon.clear() drawHeader(mon, remaining) local row = 3 row = drawStats(mon, row) row = row + 1 if #builderEntries > 0 then row = drawSectionTitle(mon, row, "=== Builder Requests ===") for _, r in ipairs(builderEntries) do row = drawLine(mon, row, r.left, r.right, r.color) end row = row + 1 end if #nonBuilderEntries > 0 then row = drawSectionTitle(mon, row, "=== Non-Builder Requests ===") for _, r in ipairs(nonBuilderEntries) do row = drawLine(mon, row, r.left, r.right, r.color) end row = row + 1 end if #equipmentEntries > 0 then row = drawSectionTitle(mon, row, "=== Equipment Requests ===") for _, r in ipairs(equipmentEntries) do row = drawLine(mon, row, r.left, r.right, r.color) end row = row + 1 end -- ====================================================== -- MISSING DOMUM ORNAMENTUM SUMMARY -- ====================================================== if next(missingDomum) ~= nil then row = drawSectionTitle(mon, row, "=== Missing Domum Ornamentum ===") for _, entry in pairs(missingDomum) do local text = "- " .. entry.name .. " [" .. entry.variant .. "]" row = drawLine(mon, row, text, "", colors.blue) end end -- Overflow warning if row > monHeight then mon.setCursorPos(1, monHeight) mon.setTextColor(colors.orange) mon.write(string.format(".. +%d lines (expand monitor)", row - monHeight)) end end ------------------------------------------------------------------------------- -- MAIN LOOP (with error recovery) ------------------------------------------------------------------------------- local counter = time_between_runs local ok, err = pcall(scan, monitor, counter) if not ok then print("[ERROR] Initial scan failed: " .. tostring(err)) print("[INFO] Retrying in " .. time_between_runs .. "s...") end local timer = os.startTimer(1) while true do local e = { os.pullEvent() } if e[1] == "timer" and e[2] == timer then counter = counter - 1 if counter <= 0 then ok, err = pcall(scan, monitor, time_between_runs) if not ok then print("[ERROR] Scan failed: " .. tostring(err)) pcall(findPeripherals) end counter = time_between_runs else pcall(drawHeader, monitor, counter) end timer = os.startTimer(1) elseif e[1] == "peripheral" or e[1] == "peripheral_detach" then print("[INFO] Peripheral change detected, re-scanning...") sleep(1) pcall(findPeripherals) pcall(scan, monitor, time_between_runs) counter = time_between_runs timer = os.startTimer(1) end end