-- 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