m7md

RSWarehouse.lua -- Test 3.0

Apr 2nd, 2026
22
0
Never
Not a member of GistPad yet? Sign Up, it unlocks many cool features!
Lua 107.47 KB | None | 0 0
  1. -- RSWarehouse.lua
  2. -- Construction: https://cdn.domekologe.eu/gameserver/minecraft/cc/Minecolonies_RefinedStorage_Integration.png
  3. -- IMPORTANT: Entangled Block MUST be on left Side of Computer, if not, change direction variable!
  4. --[[
  5. Changelog:
  6. - Change Domum Items, that only items from architects cutter are excluded
  7. - [FIX] pcall wrapping for all peripheral calls to prevent crashes
  8. - [FIX] exportItem return value handling (can return nil on error)
  9. - [FIX] craftItem can return job object in newer AP, handle truthy check
  10. - [FIX] Variant comparison case-insensitive ("Unknown" vs "unknown")
  11. - [FIX] drawLine right-aligned text off-by-one positioning
  12. - [FIX] Cleanup loop: collect keys first, then delete (safe iteration)
  13. - [FIX] Partial delivery now clears stale crafting state
  14. - [FIX] Monitor overflow protection
  15. - [FIX] NBT serialization in makeRequestId protected with pcall
  16. - [FIX] Equipment requests are now fully delivered from RS (exact item match)
  17. - [NEW] Equipment (tools/armor) delivery: exports the exact requested item
  18. - [NEW] Multi-item fallback: tries all r.items[] alternatives for equipment
  19. - [NEW] isItemCrafting() check before starting duplicate craft jobs
  20. - [NEW] Sorted request display by status
  21. - [NEW] Error recovery: auto-reconnect peripherals on detach
  22. - [NEW] Statistics line on monitor
  23. - [NEW] Stale crafting job timeout (5 minutes)
  24. - [NEW] Peripheral type supports both "rs_bridge"/"rsBridge" naming
  25. - [NEW] 1.21.1 Data Components support alongside legacy NBT
  26. ]]--
  27.  
  28. -------------------------------------------------------------------------------
  29. -- CONFIGURATION
  30. -------------------------------------------------------------------------------
  31.  
  32. local time_between_runs = 15
  33. local MAX_EXPORT = 256
  34. local direction = "left"
  35. local CRAFT_TIMEOUT_MS = 5 * 60 * 1000 -- 5 min stale craft timeout
  36.  
  37. -------------------------------------------------------------------------------
  38. -- INIT
  39. -------------------------------------------------------------------------------
  40.  
  41. local monitor, bridge, colony
  42.  
  43. local function findPeripherals()
  44. monitor = peripheral.find("monitor")
  45. if not monitor then error("Monitor not found") end
  46. monitor.setTextScale(0.5)
  47.  
  48. -- AP 0.7+/0.8+ for 1.21.1 may register as "rsBridge" instead of "rs_bridge"
  49. bridge = peripheral.find("rsBridge") or peripheral.find("rs_bridge")
  50. if not bridge then error("RS Bridge not found") end
  51.  
  52. -- Colony Integrator: "colonyIntegrator" or "colony_integrator"
  53. colony = peripheral.find("colonyIntegrator") or peripheral.find("colony_integrator")
  54. if not colony then error("Colony Integrator not found") end
  55. end
  56.  
  57. findPeripherals()
  58.  
  59. local deliveredRequests = {}
  60. local craftingRequests = {}
  61. local stats = { total = 0, delivered = 0, crafting = 0, missing = 0, partial = 0 }
  62.  
  63. -------------------------------------------------------------------------------
  64. -- ARCHITECTS CUTTER ITEMS (Domum Ornamentum)
  65. -------------------------------------------------------------------------------
  66.  
  67. local ARCHITECTS_CUTTER_ITEMS = {
  68. ["domum_ornamentum:panel"] = true,
  69. ["domum_ornamentum:plain"] = true,
  70. ["domum_ornamentum:post"] = true,
  71. ["domum_ornamentum:fancy_door"] = true,
  72. ["domum_ornamentum:fancy_light"] = true,
  73. ["domum_ornamentum:fancy_trapdoor"] = true,
  74. ["domum_ornamentum:blockpaperwall"] = true,
  75. ["domum_ornamentum:blockpillar"] = true,
  76. ["domum_ornamentum:blocktiledpaperwall"] = true,
  77. ["domum_ornamentum:blockypillar"] = true,
  78. ["domum_ornamentum:centerlight"] = true,
  79. ["domum_ornamentum:shingle"] = true,
  80. ["domum_ornamentum:shingle_slab"] = true,
  81. ["domum_ornamentum:shingle_stairs"] = true,
  82. ["domum_ornamentum:vanilla_fence_gate_compat"] = true,
  83. ["domum_ornamentum:vanilla_fence_compat"] = true,
  84. ["domum_ornamentum:vanilla_wall_compat"] = true,
  85. ["domum_ornamentum:vanilla_stairs_compat"] = true,
  86. ["domum_ornamentum:vanilla_slab_compat"] = true,
  87. ["domum_ornamentum:vanilla_trapdoors_compat"] = true,
  88. ["domum_ornamentum:framed"] = true,
  89. ["domum_ornamentum:framed_light"] = true,
  90. ["domum_ornamentum:four_light"] = true,
  91. ["domum_ornamentum:extra"] = true,
  92. ["domum_ornamentum:extra_slab"] = true,
  93. ["domum_ornamentum:extra_stairs"] = true,
  94. }
  95.  
  96. -------------------------------------------------------------------------------
  97. -- HELPERS
  98. -------------------------------------------------------------------------------
  99.  
  100. --- Safe peripheral call - prevents script crash on peripheral errors
  101. local function safeCall(fn, ...)
  102. local args = { ... }
  103. local ok, result = pcall(function()
  104. return fn(table.unpack(args))
  105. end)
  106. if not ok then
  107. print("[ERROR] Peripheral call failed: " .. tostring(result))
  108. return nil
  109. end
  110. return result
  111. end
  112.  
  113. local function isBuilder(colonistName)
  114. if not colonistName then return false end
  115. return string.find(colonistName, "Builder") ~= nil
  116. end
  117.  
  118. --- Equipment = tool/armor requests from colonists (desc contains "level")
  119. local function isEquipmentRequest(desc)
  120. if not desc then return false end
  121. return string.find(desc, "level") ~= nil
  122. end
  123.  
  124. --- Build unique request ID from request data
  125. local function makeRequestId(data)
  126. -- Safely serialize nbt/components
  127. local dataStr = ""
  128. if data.item.nbt then
  129. local ok, s = pcall(textutils.serialize, data.item.nbt)
  130. if ok then dataStr = s end
  131. end
  132. if data.item.components then
  133. local ok, s = pcall(textutils.serialize, data.item.components)
  134. if ok then dataStr = dataStr .. s end
  135. end
  136.  
  137. return string.format(
  138. "%s|%s|%s|%d|%s",
  139. data.item.name or "?",
  140. dataStr,
  141. data.colonist or "?",
  142. data.needed or 0,
  143. data.building or ""
  144. )
  145. end
  146.  
  147. --- Extract variant name from Domum Ornamentum data
  148. --- Supports both legacy NBT and 1.21.1 Data Components
  149. local function getDomumVariant(itemData)
  150. if not itemData then return "Unknown" end
  151.  
  152. -- 1.21.1 Data Components path
  153. local comp = itemData.components
  154. if comp and type(comp) == "table" then
  155. for _, key in ipairs({
  156. "domum_ornamentum:variant",
  157. "domum_ornamentum:block",
  158. "domum_ornamentum:material",
  159. "domum_ornamentum:texture",
  160. }) do
  161. if comp[key] then return tostring(comp[key]) end
  162. end
  163. end
  164.  
  165. -- Legacy NBT path
  166. local nbt = itemData.nbt or itemData
  167. if type(nbt) == "table" then
  168. for _, field in ipairs({ "variant", "block", "material", "texture", "state" }) do
  169. if nbt[field] then return tostring(nbt[field]) end
  170. end
  171. end
  172.  
  173. return "Unspecified. Check Requester"
  174. end
  175.  
  176. local function isArchitectCutterItem(item)
  177. if not item or not item.name then return false end
  178. return ARCHITECTS_CUTTER_ITEMS[item.name] == true
  179. end
  180.  
  181. --- Check if item has meaningful NBT or Components (1.21.1+)
  182. local function hasItemData(item)
  183. if not item then return false end
  184. if type(item.nbt) == "table" and next(item.nbt) ~= nil then return true end
  185. if type(item.components) == "table" and next(item.components) ~= nil then return true end
  186. if type(item.nbt) == "string" and #item.nbt > 0 then return true end
  187. return false
  188. end
  189.  
  190. -------------------------------------------------------------------------------
  191. -- MONITOR UI
  192. -------------------------------------------------------------------------------
  193.  
  194. local monWidth, monHeight = 0, 0
  195.  
  196. local function drawHeader(mon, remaining)
  197. monWidth, monHeight = mon.getSize()
  198. local now = os.time()
  199.  
  200. local cycle = "day"
  201. local color = colors.lightBlue
  202. if now >= 18.5 or now < 5 then
  203. cycle = "night"
  204. color = colors.red
  205. elseif now >= 18 then
  206. cycle = "sunset"
  207. color = colors.orange
  208. elseif now < 6 then
  209. cycle = "sunrise"
  210. color = colors.yellow
  211. end
  212.  
  213. mon.setCursorPos(1, 1)
  214. mon.setTextColor(color)
  215. mon.write("Time: " .. textutils.formatTime(now, false) .. " [" .. cycle .. "]")
  216.  
  217. local remainText = string.format("Next: %02ds", remaining)
  218. mon.setCursorPos(monWidth - #remainText + 1, 1)
  219. mon.setTextColor(colors.green)
  220. mon.write(remainText)
  221. end
  222.  
  223. local function drawStats(mon, row)
  224. if row > monHeight then return row + 1 end
  225. local text = string.format(
  226. "Total: %d | Done: %d | Craft: %d | Partial: %d | Miss: %d",
  227. stats.total, stats.delivered, stats.crafting, stats.partial, stats.missing
  228. )
  229. mon.setCursorPos(1, row)
  230. mon.setTextColor(colors.lightGray)
  231. if #text > monWidth then text = text:sub(1, monWidth) end
  232. mon.write(text)
  233. return row + 1
  234. end
  235.  
  236. local function drawSectionTitle(mon, row, title)
  237. if row > monHeight then return row + 1 end
  238. mon.setCursorPos(math.floor((monWidth - #title) / 2) + 1, row)
  239. mon.setTextColor(colors.white)
  240. mon.write(title)
  241. return row + 1
  242. end
  243.  
  244. local function drawLine(mon, row, left, right, color)
  245. if row > monHeight then return row + 1 end
  246. local maxLeft = monWidth - #right - 1
  247. if #left > maxLeft and maxLeft > 3 then
  248. left = left:sub(1, maxLeft - 2) .. ".."
  249. end
  250.  
  251. mon.setCursorPos(1, row)
  252. mon.setTextColor(color)
  253. mon.write(left)
  254.  
  255. if #right > 0 then
  256. mon.setCursorPos(monWidth - #right + 1, row)
  257. mon.write(right)
  258. end
  259.  
  260. return row + 1
  261. end
  262.  
  263. -------------------------------------------------------------------------------
  264. -- REQUEST FETCH
  265. -------------------------------------------------------------------------------
  266.  
  267. local function getRequests()
  268. local rawRequests = safeCall(colony.getRequests)
  269. if not rawRequests then
  270. print("[WARN] Failed to fetch colony requests")
  271. return {}
  272. end
  273.  
  274. local list = {}
  275. for _, r in pairs(rawRequests) do
  276. if r.items and r.items[1] and r.items[1].name then
  277. local itemCount = r.count or 1
  278. table.insert(list, {
  279. name = r.name or "Unknown",
  280. desc = r.desc,
  281. needed = itemCount,
  282. -- Primary item
  283. item = {
  284. name = r.items[1].name,
  285. count = math.min(itemCount, MAX_EXPORT),
  286. nbt = r.items[1].nbt,
  287. components = r.items[1].components,
  288. },
  289. -- Keep ALL alternative items for equipment fallback
  290. allItems = r.items,
  291. colonist = r.target or "Unknown",
  292. building = r.building or r.target or "Unknown",
  293. })
  294. end
  295. end
  296. return list
  297. end
  298.  
  299. -------------------------------------------------------------------------------
  300. -- EQUIPMENT DELIVERY
  301. -- Colonists request specific tools/armor (e.g. "Iron Pickaxe with level 2").
  302. -- The request's items[] list contains ALL acceptable alternatives.
  303. -- We try each alternative and export the first one RS has in stock.
  304. -------------------------------------------------------------------------------
  305.  
  306. local function tryExportEquipment(data)
  307. -- Try every acceptable item alternative from the request
  308. local items = data.allItems or { data.item }
  309. for _, candidate in ipairs(items) do
  310. if candidate and candidate.name then
  311. local exportFilter = {
  312. name = candidate.name,
  313. count = math.min(data.needed, MAX_EXPORT),
  314. }
  315. -- Include NBT/components if present (for enchanted or specific items)
  316. if candidate.nbt then exportFilter.nbt = candidate.nbt end
  317. if candidate.components then exportFilter.components = candidate.components end
  318.  
  319. local result = safeCall(bridge.exportItem, exportFilter, direction)
  320. if result and result > 0 then
  321. return result, candidate.name
  322. end
  323. end
  324. end
  325. return 0, (data.item.name or "?")
  326. end
  327.  
  328. -------------------------------------------------------------------------------
  329. -- MAIN SCAN
  330. -------------------------------------------------------------------------------
  331.  
  332. local function scan(mon, remaining)
  333. local builderEntries = {}
  334. local nonBuilderEntries = {}
  335. local equipmentEntries = {}
  336. local activeRequests = {}
  337. local missingDomum = {}
  338.  
  339. stats = { total = 0, delivered = 0, crafting = 0, missing = 0, partial = 0 }
  340.  
  341. local requests = getRequests()
  342.  
  343. for _, data in ipairs(requests) do
  344. local requestId = makeRequestId(data)
  345. activeRequests[requestId] = true
  346. stats.total = stats.total + 1
  347.  
  348. local provided = 0
  349. local color = colors.blue
  350. local isDomum = isArchitectCutterItem(data.item)
  351. local hasData = hasItemData(data.item)
  352. local isEquip = isEquipmentRequest(data.desc)
  353. local usedItem = data.item.name -- track which item was actually exported
  354.  
  355. -- ======================================================
  356. -- DELIVERY
  357. -- ======================================================
  358.  
  359. if isEquip then
  360. -- EQUIPMENT: try all acceptable alternatives from the request
  361. provided, usedItem = tryExportEquipment(data)
  362. elseif not isDomum or hasData then
  363. -- Normal items OR Domum with valid data
  364. local result = safeCall(bridge.exportItem, data.item, direction)
  365. provided = result or 0
  366. end
  367. -- Domum without data -> provided stays 0
  368.  
  369. -- ======================================================
  370. -- DETERMINE STATUS
  371. -- ======================================================
  372.  
  373. if provided >= data.needed then
  374. -- Fully delivered
  375. deliveredRequests[requestId] = true
  376. craftingRequests[requestId] = nil
  377. color = colors.green
  378. stats.delivered = stats.delivered + 1
  379.  
  380. elseif provided > 0 then
  381. -- Partially delivered
  382. craftingRequests[requestId] = nil
  383. color = colors.yellow
  384. stats.partial = stats.partial + 1
  385.  
  386. else
  387. -- Nothing delivered
  388. if isDomum and not hasData then
  389. color = colors.blue
  390. stats.missing = stats.missing + 1
  391. else
  392. -- Check if already crafting (prefer isItemCrafting if available)
  393. local alreadyCrafting = false
  394. if bridge.isItemCrafting then
  395. alreadyCrafting = safeCall(bridge.isItemCrafting, {
  396. name = data.item.name,
  397. nbt = data.item.nbt,
  398. }) or false
  399. end
  400.  
  401. if alreadyCrafting or craftingRequests[requestId] then
  402. color = colors.pink
  403. stats.crafting = stats.crafting + 1
  404.  
  405. -- Timeout stale craft jobs
  406. if craftingRequests[requestId] then
  407. local elapsed = os.epoch("utc") - (craftingRequests[requestId].started or 0)
  408. if elapsed > CRAFT_TIMEOUT_MS then
  409. print("[TIMEOUT] Stale craft: " .. data.item.name)
  410. craftingRequests[requestId] = nil
  411. color = colors.blue
  412. stats.crafting = stats.crafting - 1
  413. stats.missing = stats.missing + 1
  414. end
  415. end
  416. else
  417. -- Try to start crafting (not for equipment - those typically can't be auto-crafted)
  418. if not isEquip then
  419. local craftResult = safeCall(bridge.craftItem, {
  420. name = data.item.name,
  421. count = data.needed,
  422. nbt = data.item.nbt,
  423. })
  424.  
  425. if craftResult then
  426. craftingRequests[requestId] = { started = os.epoch("utc") }
  427. color = colors.pink
  428. stats.crafting = stats.crafting + 1
  429. else
  430. color = colors.blue
  431. stats.missing = stats.missing + 1
  432. end
  433. else
  434. -- Equipment not in RS and can't be auto-crafted
  435. color = colors.blue
  436. stats.missing = stats.missing + 1
  437. end
  438. end
  439. end
  440. end
  441.  
  442. -- ======================================================
  443. -- Domum Ornamentum Missing Items
  444. -- ======================================================
  445.  
  446. if isDomum and color == colors.blue then
  447. local variant = getDomumVariant(data.item)
  448. local key = data.item.name .. "|" .. variant
  449. missingDomum[key] = {
  450. name = data.name:gsub("^[%d%-]+%s*", ""),
  451. variant = variant,
  452. }
  453. end
  454.  
  455. -- ======================================================
  456. -- UI ENTRY
  457. -- ======================================================
  458.  
  459. local variantStr = ""
  460. if isDomum then
  461. local v = getDomumVariant(data.item)
  462. if v:lower() ~= "unknown" then
  463. variantStr = " [" .. v .. "]"
  464. end
  465. end
  466.  
  467. local cleanName = data.name:gsub("^[%d%-]+%s*", "")
  468. local entry = {
  469. left = string.format(
  470. "%d/%d %s%s",
  471. provided, data.needed, cleanName, variantStr
  472. ),
  473. provided = provided,
  474. needed = data.needed,
  475. color = color,
  476. }
  477.  
  478. if isEquip then
  479. entry.right = data.colonist
  480. table.insert(equipmentEntries, entry)
  481. elseif isBuilder(data.colonist) then
  482. entry.right = data.building or "Builder"
  483. table.insert(builderEntries, entry)
  484. else
  485. entry.right = data.colonist
  486. table.insert(nonBuilderEntries, entry)
  487. end
  488.  
  489. -- ======================================================
  490. -- TERMINAL OUTPUT
  491. -- ======================================================
  492.  
  493. local target = entry.right or "?"
  494. if color == colors.green then
  495. print(string.format("[DONE] %s%s x%d -> %s", usedItem, variantStr, data.needed, target))
  496. elseif color == colors.pink then
  497. print(string.format("[CRAFTING] %s%s x%d -> %s", data.item.name, variantStr, data.needed, target))
  498. elseif color == colors.yellow then
  499. print(string.format("[PARTIAL] %s%s %d/%d -> %s", usedItem, variantStr, provided, data.needed, target))
  500. elseif isDomum and not hasData then
  501. print(string.format("[SKIP] %s x%d -> %s (no component data)", data.item.name, data.needed, target))
  502. elseif isEquip then
  503. print(string.format("[EQUIP] %s x%d -> %s (not in RS)", cleanName, data.needed, target))
  504. else
  505. print(string.format("[MISSING] %s x%d -> %s (no pattern)", data.item.name, data.needed, target))
  506. end
  507. end
  508.  
  509. -- ======================================================
  510. -- CLEANUP: remove state for vanished requests
  511. -- FIX: collect keys first, then delete (safe Lua iteration)
  512. -- ======================================================
  513.  
  514. local toRemove = {}
  515. for requestId, _ in pairs(deliveredRequests) do
  516. if not activeRequests[requestId] then
  517. table.insert(toRemove, requestId)
  518. end
  519. end
  520. for _, rid in ipairs(toRemove) do
  521. deliveredRequests[rid] = nil
  522. craftingRequests[rid] = nil
  523. end
  524.  
  525. toRemove = {}
  526. for requestId, _ in pairs(craftingRequests) do
  527. if not activeRequests[requestId] then
  528. table.insert(toRemove, requestId)
  529. end
  530. end
  531. for _, rid in ipairs(toRemove) do
  532. craftingRequests[rid] = nil
  533. end
  534.  
  535. -- ======================================================
  536. -- Sort: missing first, done last
  537. -- ======================================================
  538.  
  539. local function sortByStatus(a, b)
  540. local order = { [colors.blue] = 1, [colors.pink] = 2, [colors.yellow] = 3, [colors.green] = 4 }
  541. return (order[a.color] or 0) < (order[b.color] or 0)
  542. end
  543. table.sort(builderEntries, sortByStatus)
  544. table.sort(nonBuilderEntries, sortByStatus)
  545. table.sort(equipmentEntries, sortByStatus)
  546.  
  547. -- ======================================================
  548. -- MONITOR UI
  549. -- ======================================================
  550.  
  551. mon.clear()
  552. drawHeader(mon, remaining)
  553.  
  554. local row = 3
  555. row = drawStats(mon, row)
  556. row = row + 1
  557.  
  558. if #builderEntries > 0 then
  559. row = drawSectionTitle(mon, row, "=== Builder Requests ===")
  560. for _, r in ipairs(builderEntries) do
  561. row = drawLine(mon, row, r.left, r.right, r.color)
  562. end
  563. row = row + 1
  564. end
  565.  
  566. if #nonBuilderEntries > 0 then
  567. row = drawSectionTitle(mon, row, "=== Non-Builder Requests ===")
  568. for _, r in ipairs(nonBuilderEntries) do
  569. row = drawLine(mon, row, r.left, r.right, r.color)
  570. end
  571. row = row + 1
  572. end
  573.  
  574. if #equipmentEntries > 0 then
  575. row = drawSectionTitle(mon, row, "=== Equipment Requests ===")
  576. for _, r in ipairs(equipmentEntries) do
  577. row = drawLine(mon, row, r.left, r.right, r.color)
  578. end
  579. row = row + 1
  580. end
  581.  
  582. -- ======================================================
  583. -- MISSING DOMUM ORNAMENTUM SUMMARY
  584. -- ======================================================
  585.  
  586. if next(missingDomum) ~= nil then
  587. row = drawSectionTitle(mon, row, "=== Missing Domum Ornamentum ===")
  588. for _, entry in pairs(missingDomum) do
  589. local text = "- " .. entry.name .. " [" .. entry.variant .. "]"
  590. row = drawLine(mon, row, text, "", colors.blue)
  591. end
  592. end
  593.  
  594. -- Overflow warning
  595. if row > monHeight then
  596. mon.setCursorPos(1, monHeight)
  597. mon.setTextColor(colors.orange)
  598. mon.write(string.format(".. +%d lines (expand monitor)", row - monHeight))
  599. end
  600. end
  601.  
  602. -------------------------------------------------------------------------------
  603. -- MAIN LOOP (with error recovery)
  604. -------------------------------------------------------------------------------
  605.  
  606. local counter = time_between_runs
  607.  
  608. local ok, err = pcall(scan, monitor, counter)
  609. if not ok then
  610. print("[ERROR] Initial scan failed: " .. tostring(err))
  611. print("[INFO] Retrying in " .. time_between_runs .. "s...")
  612. end
  613.  
  614. local timer = os.startTimer(1)
  615.  
  616. while true do
  617. local e = { os.pullEvent() }
  618.  
  619. if e[1] == "timer" and e[2] == timer then
  620. counter = counter - 1
  621. if counter <= 0 then
  622. ok, err = pcall(scan, monitor, time_between_runs)
  623. if not ok then
  624. print("[ERROR] Scan failed: " .. tostring(err))
  625. pcall(findPeripherals)
  626. end
  627. counter = time_between_runs
  628. else
  629. pcall(drawHeader, monitor, counter)
  630. end
  631. timer = os.startTimer(1)
  632.  
  633. elseif e[1] == "peripheral" or e[1] == "peripheral_detach" then
  634. print("[INFO] Peripheral change detected, re-scanning...")
  635. sleep(1)
  636. pcall(findPeripherals)
  637. pcall(scan, monitor, time_between_runs)
  638. counter = time_between_runs
  639. timer = os.startTimer(1)
  640. end
  641. end
RAW Paste Data Copied