Guest User

RSWarehouse.lua - All The Mods 10 (ATM10)

a guest
Dec 19th, 2025
90
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 21.88 KB | None | 0 0
  1. -- RSWarehouse.lua
  2. -- Author(s):
  3. -- Scott Adkins <[email protected]> (Zucanthor)
  4. -- Chuck Burgess
  5. -- nobodyshome
  6. -- Updated: 2025-12-04
  7.  
  8. local logFile = "colony.log"
  9. local time_between_runs = 30
  10.  
  11. -- Initialize Monitor
  12. -- see: https://www.computercraft.info/wiki/Advanced_Monitor
  13. local monitor = peripheral.find("monitor")
  14. if not monitor then error("Monitor not found.") end
  15. monitor.setTextScale(0.5)
  16. monitor.clear()
  17. monitor.setCursorPos(1, 1)
  18. monitor.setCursorBlink(false)
  19. print("Monitor initialized.")
  20.  
  21. -- Initialize RS Bridge
  22. -- see: https://advancedperipherals.madefor.cc/peripherals/rs_bridge/
  23. local bridge = peripheral.find("rs_bridge")
  24. if not bridge then error("RS Bridge not found.") end
  25. print("RS Bridge initialized.")
  26.  
  27. -- Initialize Colony Integrator
  28. -- see: https://docs.advanced-peripherals.de/peripherals/colony_integrator/
  29. local colony = peripheral.find("colony_integrator")
  30. if not colony then error("Colony Integrator not found.") end
  31. if not colony.isInColony then error("Colony Integrator is not in a colony.") end
  32. print("Colony Integrator initialized.")
  33.  
  34. -- Establish the direction to transport the items into the Warehouse based on
  35. -- where the entnglement block is sitting in reference to the RS Bridge block. Default to empty string.
  36. -- If your barrel/chest/storage/entangled block is on top/back/right then specify change that here.
  37. local storageDirection = "Right"
  38. if not storageDirection then error("Warehouse storage not found.") end
  39. print("Warehouse storage initialized.")
  40.  
  41. -- Track active crafting jobs to avoid duplicates
  42. local craftingJobs = {}
  43.  
  44. ----------------------------------------------------------------------------
  45. -- FUNCTIONS
  46. ----------------------------------------------------------------------------
  47. --[[
  48. Table.Empty
  49. @desc check to see if a table contains any data
  50. @return boolean
  51. ]]
  52. function table.empty(self)
  53. for _, _ in pairs(self) do
  54. return false
  55. end
  56. return true
  57. end
  58.  
  59. --[[
  60. Is Item Craftable
  61. @desc Check if an item can be crafted in the RS system
  62. @return boolean
  63. ]]
  64. function isItemCraftable(bridge, item)
  65. local craftableItems = bridge.getCraftableItems({ name = item.name })
  66. return craftableItems and #craftableItems > 0
  67. end
  68.  
  69. --[[
  70. Get Available Item Count
  71. @desc Get the current amount of an item in storage
  72. @return number
  73. ]]
  74. function getAvailableItemCount(bridge, item)
  75. -- Try to match with fingerprint first for precise matching
  76. if item.fingerprint then
  77. local storageItem = bridge.getItem({ fingerprint = item.fingerprint })
  78. if storageItem then
  79. return storageItem.amount or 0
  80. end
  81. end
  82.  
  83. -- Fall back to name-based matching
  84. local storageItem = bridge.getItem({ name = item.name })
  85. if storageItem then
  86. return storageItem.amount or 0
  87. end
  88. return 0
  89. end
  90.  
  91. --[[
  92. Schedule Craft Job
  93. @desc Schedule a crafting job if one isn't already active for this item
  94. @return boolean, string (success, message)
  95. ]]
  96. function scheduleCraftJob(bridge, item, amount, requestName)
  97. -- Check if we already have a crafting job for this item
  98. local itemKey = item.name
  99. if item.nbt then
  100. itemKey = itemKey .. "_nbt"
  101. end
  102.  
  103. if craftingJobs[itemKey] then
  104. return false, "Already crafting"
  105. end
  106.  
  107. -- Create craft filter
  108. local craftFilter = {
  109. name = item.name,
  110. count = amount
  111. }
  112. if item.nbt then
  113. craftFilter.nbt = item.nbt
  114. end
  115.  
  116. -- Attempt to schedule the craft
  117. local craftJob, err = bridge.craftItem(craftFilter)
  118. if craftJob then
  119. craftingJobs[itemKey] = {
  120. job = craftJob,
  121. requestName = requestName,
  122. amount = amount,
  123. startTime = os.time()
  124. }
  125. return true, "Crafting scheduled"
  126. else
  127. return false, err or "Craft failed"
  128. end
  129. end
  130.  
  131. --[[
  132. Handle Crafting Event
  133. @desc Process rs_crafting events and update job tracking
  134. @return void, boolean (shouldRescan)
  135. ]]
  136. function handleCraftingEvent(error, id, message)
  137. local shouldRescan = false
  138. -- Find the job in our tracking table
  139. for itemKey, jobData in pairs(craftingJobs) do
  140. if jobData.job and jobData.job.id == id then
  141. if error then
  142. print(string.format("[Craft Error] %s - %s", jobData.requestName, message))
  143. craftingJobs[itemKey] = nil
  144. elseif message == "CRAFTING_STARTED" then
  145. print(string.format("[Crafting] %s (x%d)", jobData.requestName, jobData.amount))
  146. elseif message == "MISSING_ITEMS" then
  147. print(string.format("[Missing Items] Cannot craft %s", jobData.requestName))
  148. craftingJobs[itemKey] = nil
  149. elseif message == "DONE" then
  150. print(string.format("[Craft Complete] %s - waiting for items...", jobData.requestName))
  151. craftingJobs[itemKey] = nil
  152. shouldRescan = true
  153. elseif message == "CANCELED" then
  154. print(string.format("[Craft Canceled] %s", jobData.requestName))
  155. craftingJobs[itemKey] = nil
  156. shouldRescan = true
  157. end
  158. break
  159. end
  160. end
  161. return shouldRescan
  162. end
  163.  
  164. --[[
  165. Write To Log
  166. @desc Write the specified `table` to the file surrounded by the `blockTop` and `blockBottom`
  167. @return void
  168. ]]
  169. function writeToLog(data, blockTop, blockBottom)
  170. file.write("\n")
  171. file.write(blockTop)
  172. file.write("\n")
  173. file.write(textutils.serialize(data, { allow_repetitions = true }))
  174. file.write("\n")
  175. file.write(blockBottom)
  176. file.write("\n")
  177. end
  178.  
  179. --[[
  180. Process Work Request Item
  181. @desc Determine if this item can be delivered to the warehouse from the storage
  182. @return boolean
  183. ]]
  184. function processWorkRequestItem(request)
  185. if string.find(request.desc, "Tool of class") then return false end
  186. if string.find(request.name, "Hoe") then return false end
  187. if string.find(request.name, "Shovel") then return false end
  188. if string.find(request.name, "Axe") then return false end
  189. if string.find(request.name, "Pickaxe") then return false end
  190. if string.find(request.name, "Bow") and not string.find(request.name, "Bowl") then return false end
  191. if string.find(request.name, "Sword") then return false end
  192. if string.find(request.name, "Shield") then return false end
  193. if string.find(request.name, "Helmet") then return false end
  194. if string.find(request.name, "Leather Cap") then return false end
  195. if string.find(request.name, "Chestplate") then return false end
  196. if string.find(request.name, "Tunic") then return false end
  197. if string.find(request.name, "Pants") then return false end
  198. if string.find(request.name, "Leggings") then return false end
  199. if string.find(request.name, "Boots") then return false end
  200. if request.name == "Rallying Banner" then return false end --bugged in alpha versions
  201. if request.name == "Crafter" then return false end
  202. if request.name == "Compostable" then return false end
  203. if request.name == "Fertilizer" then return false end
  204. if request.name == "Flowers" then return false end
  205. if request.name == "Food" then return false end
  206. if request.name == "Fuel" then return false end
  207. if request.name == "Smeltable Ore" then return false end
  208. if request.name == "Stack List" then return false end
  209.  
  210. -- Skip decorative blocks with components (they need manual crafting with specific textures)
  211. if request.item and request.item.components and not table.empty(request.item.components) then
  212. return false
  213. end
  214.  
  215. -- you can add any new items here if they are found
  216. return true
  217. end
  218.  
  219. --[[
  220. Monitor Print Row Justified
  221. @desc Print a line of data to the in-game monitor
  222. @return void
  223. ]]
  224. function mPrintRowJustified(mon, y, pos, text, textcolor)
  225. w, h = mon.getSize()
  226. fg = colors.white
  227. bg = colors.black
  228.  
  229. if pos == "left" then x = 1 end
  230. if pos == "center" then x = math.floor((w - #text) / 2) end
  231. if pos == "right" then x = w - #text end
  232.  
  233. mon.setTextColor(textcolor)
  234. mon.setCursorPos(x, y)
  235. mon.write(text)
  236. mon.setTextColor(fg)
  237. mon.setBackgroundColor(bg)
  238. end
  239.  
  240. --[[
  241. Display Timer
  242. @desc Update the time on the monitor
  243. @return void
  244. ]]
  245. function displayTimer(mon, t)
  246. now = os.time()
  247. cycle = "day"
  248. cycle_color = colors.orange
  249. if now >= 4 and now < 6 then
  250. cycle = "sunrise"
  251. cycle_color = colors.yellow
  252. elseif now >= 6 and now < 18 then
  253. cycle = "day"
  254. cycle_color = colors.lightBlue
  255. elseif now >= 18 and now < 19.5 then
  256. cycle = "sunset"
  257. cycle_color = colors.magenta
  258. elseif now >= 19.5 or now < 5 then
  259. cycle = "night"
  260. cycle_color = colors.red
  261. end
  262.  
  263. timer_color = colors.green
  264. if t < 15 then timer_color = colors.yellow end
  265. if t < 5 then timer_color = colors.orange end
  266.  
  267. mPrintRowJustified(mon, 1, "left", string.format("Time: %s [%s] ", textutils.formatTime(now, false), cycle),
  268. cycle_color)
  269. if cycle ~= "night" then
  270. mPrintRowJustified(mon, 1, "right", string.format(" Remaining: %ss", t), timer_color)
  271. else
  272. mPrintRowJustified(mon, 1, "right", " Remaining: PAUSED", colors.red)
  273. end
  274. end
  275.  
  276. --[[
  277. Create Colonist Data
  278. @desc Build a table of Colonist making the request
  279. @return table
  280. ]]
  281. function createColonistData(colonist)
  282. title_words = {}
  283. words_in_name = 0
  284. colonist_job = ""
  285. word_count = 1
  286.  
  287. for word in colonist:gmatch("%S+") do
  288. table.insert(title_words, word)
  289. words_in_name = words_in_name + 1
  290. end
  291.  
  292. if words_in_name >= 3 then
  293. colonist_name = title_words[words_in_name - 2] .. " " .. title_words[words_in_name]
  294. else
  295. colonist_name = colonist
  296. end
  297.  
  298. repeat
  299. if colonist_job ~= "" then colonist_job = colonist_job .. " " end
  300. colonist_job = colonist_job .. title_words[word_count]
  301. word_count = word_count + 1
  302. until word_count > words_in_name - 3
  303.  
  304. return {
  305. fullName = colonist,
  306. titleWords = title_words,
  307. job = colonist_job,
  308. name = colonist_name,
  309. wordsInName =
  310. words_in_name
  311. }
  312. end
  313.  
  314. --[[
  315. Get Work Request List (from colony)
  316. @desc Build a table of the work request data from the colony
  317. @return table
  318. ]]
  319. function getWorkRequestList(colony)
  320. requestList = {}
  321. workRequests = colony.getRequests()
  322. file = fs.open(logFile, "w")
  323.  
  324. for w in pairs(workRequests) do
  325. writeToLog(workRequests[w], "--- Request start ---", "--- Request end ---");
  326. name = workRequests[w].name -- the name of the count/item being requested
  327. colonist = createColonistData(workRequests[w].target)
  328. desc = workRequests[w].desc -- the request description
  329. item = {}
  330. -- create the filter item for the transfer request through the bridge
  331. if workRequests[w].items and workRequests[w].items[1] then
  332. item = {
  333. name = workRequests[w].items[1].name,
  334. count = workRequests[w].count,
  335. displayName = workRequests[w].items[1].displayName
  336. }
  337.  
  338. -- Preserve NBT data if present
  339. if workRequests[w].items[1].nbt and not table.empty(workRequests[w].items[1].nbt) then
  340. item.nbt = workRequests[w].items[1].nbt
  341. end
  342.  
  343. -- Preserve components data if present (newer format)
  344. if workRequests[w].items[1].components and not table.empty(workRequests[w].items[1].components) then
  345. item.components = workRequests[w].items[1].components
  346. end
  347.  
  348. -- Store fingerprint for reference
  349. if workRequests[w].items[1].fingerprint then
  350. item.fingerprint = workRequests[w].items[1].fingerprint
  351. end
  352. end
  353. -- how many items are needed to fulfill this request?
  354. needed = workRequests[w].count
  355.  
  356. local newRecord = {}
  357. newRecord.name = name
  358. newRecord.desc = desc
  359. newRecord.needed = needed
  360. newRecord.item = item
  361. newRecord.colonist = colonist
  362. table.insert(requestList, newRecord)
  363. writeToLog(newRecord, "--- Record start ---", "--- Record end ---");
  364. end
  365. file.close()
  366. return requestList
  367. end
  368.  
  369. --[[
  370. Get Supporting Materials
  371. @desc Extract supporting materials from item components (for Domum Ornamentum)
  372. @return table or nil
  373. ]]
  374. function getSupportingMaterials(item)
  375. if not item or not item.components then return nil end
  376.  
  377. local materials = {}
  378. local textureData = item.components["domum_ornamentum:texture_data"]
  379.  
  380. if textureData then
  381. for _, material in pairs(textureData) do
  382. table.insert(materials, material)
  383. end
  384. end
  385.  
  386. if #materials > 0 then
  387. return materials
  388. end
  389. return nil
  390. end
  391.  
  392. --[[
  393. Display List
  394. @desc Update the monitor with the work request items currently in the system
  395. @return void
  396. ]]
  397. function displayList(mon, listName, itemList)
  398. -- show the list header first
  399. mPrintRowJustified(mon, row, "center", listName, colors.white)
  400. row = row + 1
  401. for e in pairs(itemList) do
  402. record = itemList[e]
  403. text = string.format("%d %s", record.provided, record.name)
  404. mPrintRowJustified(mon, row, "left", text, record.color)
  405. mPrintRowJustified(mon, row, "right", " " .. record.colonist, record.color)
  406. row = row + 1
  407.  
  408. if record.materials then
  409. for _, mat in pairs(record.materials) do
  410. local displayName = mat:match(":(.+)") or mat
  411. mPrintRowJustified(mon, row, "left", " + " .. displayName, colors.lightGray)
  412. row = row + 1
  413. end
  414. end
  415. end
  416. -- add a space at the end of the list
  417. row = row + 1
  418. end
  419.  
  420. -- Color References:
  421. -- RED: work order can't be satisfied by Refined Storage (lack of pattern or lack of
  422. -- required crafting ingredients).
  423. -- YELLOW: order partially filled and a crafting job was scheduled for the rest.
  424. -- GREEN: order fully filled.
  425. -- BLUE: the Player needs to manually fill the work order. This includes some equipment as well as generic requests ike Compostables, Fuel, Food, Flowers, etc.
  426. --[[
  427. Cleanup Crafting Jobs
  428. @desc Check status of active jobs and remove finished ones
  429. @return void
  430. ]]
  431. function cleanupCraftingJobs(bridge)
  432. local toRemove = {}
  433. for itemKey, jobData in pairs(craftingJobs) do
  434. local job = jobData.job
  435. -- Check if job is done or canceled
  436. -- We use pcall in case the object is disconnected or invalid
  437. local success, isDone = pcall(function() return job.isDone() end)
  438. local success2, isCanceled = pcall(function() return job.isCanceled() end)
  439.  
  440. if success and isDone then
  441. print(string.format("[Cleanup] Job %s detected as DONE", jobData.requestName))
  442. table.insert(toRemove, itemKey)
  443. elseif success2 and isCanceled then
  444. print(string.format("[Cleanup] Job %s detected as CANCELED", jobData.requestName))
  445. table.insert(toRemove, itemKey)
  446. elseif not success then
  447. -- If method call failed, try to check if job exists via bridge
  448. local freshJob = bridge.getCraftingJob(job.id)
  449. if not freshJob then
  450. print(string.format("[Cleanup] Job %s ID:%d not found (clearing)", jobData.requestName, job.id))
  451. table.insert(toRemove, itemKey)
  452. end
  453. end
  454. end
  455.  
  456. for _, key in ipairs(toRemove) do
  457. craftingJobs[key] = nil
  458. end
  459. end
  460.  
  461. --[[
  462. Scan Work Requests
  463. @desc Manages all of the open work requests in the system and attempts to fulfill them from the inventory
  464. @desc Not called at night (as determined by the server) since requests cannot be fulfilled anyway
  465. @return void
  466. ]]
  467. function scanWorkRequests(mon, bridge, direction)
  468. cleanupCraftingJobs(bridge)
  469. print("\nScan starting at", textutils.formatTime(os.time(), false) .. " (" .. os.time() .. ").")
  470. builder_list = {}
  471. nonbuilder_list = {}
  472. equipment_list = {}
  473. requestList = getWorkRequestList(colony)
  474.  
  475. for j, data in ipairs(requestList) do
  476. color = colors.blue
  477. provided = 0
  478.  
  479. -- Always attempt to export what we have (even if it's 0, to handle newly crafted items)
  480. -- This allows manual fulfillment of "Blue" items
  481. if data.item and data.item.name then
  482. local available = getAvailableItemCount(bridge, data.item)
  483. provided = bridge.exportItem(data.item, direction)
  484.  
  485. -- If nothing was exported but items are available, the warehouse might be full
  486. if provided == 0 and available > 0 then
  487. print(string.format("[Warning] Could not export %s - warehouse may be full", data.name))
  488. end
  489. end
  490.  
  491. if processWorkRequestItem(data) then
  492. -- Determine if we need to craft more
  493. local stillNeeded = data.needed - provided
  494. if stillNeeded > 0 then
  495. -- Check if item is craftable
  496. if isItemCraftable(bridge, data.item) then
  497. -- Try to schedule crafting for the remaining amount
  498. local success, msg = scheduleCraftJob(bridge, data.item, stillNeeded, data.name)
  499. if success then
  500. color = colors.yellow -- Partially filled, crafting in progress
  501. print(string.format("[Scheduling Craft] %s - Need %d more", data.name, stillNeeded))
  502. elseif msg == "Already crafting" then
  503. color = colors.orange -- Already crafting
  504. else
  505. color = colors.red -- Can't craft (missing ingredients)
  506. print(string.format("[Cannot Craft] %s - %s", data.name, msg))
  507. end
  508. else
  509. color = colors.red -- No pattern available
  510. print(string.format("[No Pattern] %s", data.name))
  511. end
  512. else
  513. color = colors.green -- Fully satisfied
  514. end
  515.  
  516. -- Override color if nothing was provided and no craft scheduled
  517. if provided == 0 and color ~= colors.yellow and color ~= colors.orange then
  518. color = colors.lightGray
  519. end
  520. else
  521. -- Manual item handling
  522. if provided >= data.needed then
  523. color = colors.green
  524. else
  525. nameString = data.name .. " [" .. data.colonist.fullName .. "]"
  526. print("[Skipped/Manual]", nameString)
  527. end
  528. end
  529. -- ---------------------------------------------------------------------
  530. -- Build the newList data
  531. -- ---------------------------------------------------------------------
  532. -- create the target text
  533. expectedList = "Builder"
  534. colonist = data.colonist.name
  535. if not string.find(data.colonist.fullName, "Builder") then
  536. expectedList = ""
  537. colonist = data.colonist.job .. " " .. data.colonist.name
  538. if data.colonist.wordsInName < 3 then
  539. colonist = data.colonist.name
  540. end
  541. end
  542.  
  543. -- create the name
  544. listName = data.name
  545. if string.find(data.desc, "level") then
  546. expectedList = "Equipment"
  547. level = "Any Level"
  548. if string.find(data.desc, "with maximal level: Leather") then level = "Leather" end
  549. if string.find(data.desc, "with maximal level: Gold") then level = "Gold" end
  550. if string.find(data.desc, "with maximal level: Chain") then level = "Chain" end
  551. if string.find(data.desc, "with maximal level: Wood or Gold") then level = "Wood or Gold" end
  552. if string.find(data.desc, "with maximal level: Stone") then level = "Stone" end
  553. if string.find(data.desc, "with maximal level: Iron") then level = "Iron" end
  554. if string.find(data.desc, "with maximal level: Diamond") then level = "Diamond" end
  555. listName = level .. " " .. data.name
  556. if level == "Any Level" then listName = data.name .. " of any level" end
  557. end
  558.  
  559. -- create the new list table defining what is inserted into a specific list
  560. local materials = nil
  561. if color == colors.blue and data.item then
  562. materials = getSupportingMaterials(data.item)
  563. end
  564. newList = {
  565. name = listName,
  566. colonist = colonist,
  567. needed = data.needed,
  568. provided = provided,
  569. color = color,
  570. materials =
  571. materials
  572. }
  573.  
  574. if expectedList == "Equipment" then
  575. table.insert(equipment_list, newList)
  576. elseif expectedList == "Builder" then
  577. table.insert(builder_list, newList)
  578. else
  579. table.insert(nonbuilder_list, newList)
  580. end
  581. -- ---------------------------------------------------------------------
  582. end
  583.  
  584. -- Show the various lists on the attached monitor.
  585. mon.clear()
  586. row = 3
  587. if not table.empty(builder_list) then displayList(mon, "Builder Requests", builder_list) end
  588. if not table.empty(nonbuilder_list) then displayList(mon, "Nonbuilder Requests", nonbuilder_list) end
  589. if not table.empty(equipment_list) then displayList(mon, "Equipment", equipment_list) end
  590.  
  591. -- no requests
  592. if row == 3 then
  593. mPrintRowJustified(mon, row, "center", "No Open Requests", colors.white)
  594. end
  595. print("Scan completed at", textutils.formatTime(os.time(), false) .. " (" .. os.time() .. ").")
  596. end
  597.  
  598. --[[
  599. MAIN
  600. @desc establish the run times and execute the work request management
  601. @return void
  602. ]]
  603. local current_run = time_between_runs
  604. scanWorkRequests(monitor, bridge, storageDirection)
  605. displayTimer(monitor, current_run)
  606. local TIMER = os.startTimer(1)
  607.  
  608. while true do
  609. local e = { os.pullEvent() }
  610. if e[1] == "timer" and e[2] == TIMER then
  611. now = os.time()
  612. if now >= 5 and now < 19.5 then
  613. current_run = current_run - 1
  614. if current_run <= 0 then
  615. scanWorkRequests(monitor, bridge, storageDirection)
  616. current_run = time_between_runs
  617. end
  618. end
  619. displayTimer(monitor, current_run)
  620. TIMER = os.startTimer(1)
  621. elseif e[1] == "monitor_touch" then
  622. os.cancelTimer(TIMER)
  623. scanWorkRequests(monitor, bridge, storageDirection)
  624. current_run = time_between_runs
  625. displayTimer(monitor, current_run)
  626. TIMER = os.startTimer(1)
  627. elseif e[1] == "rs_crafting" then
  628. -- Handle crafting events: rs_crafting returns (error, id, message)
  629. local error = e[2]
  630. local id = e[3]
  631. local message = e[4]
  632. local shouldRescan = handleCraftingEvent(error, id, message)
  633. -- Trigger a rescan when crafting completes (with small delay for items to appear)
  634. if shouldRescan then
  635. print("[Info] Waiting 2 seconds for items to appear in storage...")
  636. sleep(2)
  637. os.cancelTimer(TIMER)
  638. scanWorkRequests(monitor, bridge, storageDirection)
  639. current_run = time_between_runs
  640. displayTimer(monitor, current_run)
  641. TIMER = os.startTimer(1)
  642. end
  643. end
  644. end
  645.  
Advertisement
Add Comment
Please, Sign In to add comment