Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- RSWarehouse.lua
- -- Author(s):
- -- Scott Adkins <[email protected]> (Zucanthor)
- -- Chuck Burgess
- -- nobodyshome
- -- Updated: 2025-12-04
- local logFile = "colony.log"
- local time_between_runs = 30
- -- Initialize Monitor
- -- see: https://www.computercraft.info/wiki/Advanced_Monitor
- local monitor = peripheral.find("monitor")
- if not monitor then error("Monitor not found.") end
- monitor.setTextScale(0.5)
- monitor.clear()
- monitor.setCursorPos(1, 1)
- monitor.setCursorBlink(false)
- print("Monitor initialized.")
- -- Initialize RS Bridge
- -- see: https://advancedperipherals.madefor.cc/peripherals/rs_bridge/
- local bridge = peripheral.find("rs_bridge")
- if not bridge then error("RS Bridge not found.") end
- print("RS Bridge initialized.")
- -- Initialize Colony Integrator
- -- see: https://docs.advanced-peripherals.de/peripherals/colony_integrator/
- local colony = peripheral.find("colony_integrator")
- if not colony then error("Colony Integrator not found.") end
- if not colony.isInColony then error("Colony Integrator is not in a colony.") end
- print("Colony Integrator initialized.")
- -- Establish the direction to transport the items into the Warehouse based on
- -- where the entnglement block is sitting in reference to the RS Bridge block. Default to empty string.
- -- If your barrel/chest/storage/entangled block is on top/back/right then specify change that here.
- local storageDirection = "Right"
- if not storageDirection then error("Warehouse storage not found.") end
- print("Warehouse storage initialized.")
- -- Track active crafting jobs to avoid duplicates
- local craftingJobs = {}
- ----------------------------------------------------------------------------
- -- FUNCTIONS
- ----------------------------------------------------------------------------
- --[[
- Table.Empty
- @desc check to see if a table contains any data
- @return boolean
- ]]
- function table.empty(self)
- for _, _ in pairs(self) do
- return false
- end
- return true
- end
- --[[
- Is Item Craftable
- @desc Check if an item can be crafted in the RS system
- @return boolean
- ]]
- function isItemCraftable(bridge, item)
- local craftableItems = bridge.getCraftableItems({ name = item.name })
- return craftableItems and #craftableItems > 0
- end
- --[[
- Get Available Item Count
- @desc Get the current amount of an item in storage
- @return number
- ]]
- function getAvailableItemCount(bridge, item)
- -- Try to match with fingerprint first for precise matching
- if item.fingerprint then
- local storageItem = bridge.getItem({ fingerprint = item.fingerprint })
- if storageItem then
- return storageItem.amount or 0
- end
- end
- -- Fall back to name-based matching
- local storageItem = bridge.getItem({ name = item.name })
- if storageItem then
- return storageItem.amount or 0
- end
- return 0
- end
- --[[
- Schedule Craft Job
- @desc Schedule a crafting job if one isn't already active for this item
- @return boolean, string (success, message)
- ]]
- function scheduleCraftJob(bridge, item, amount, requestName)
- -- Check if we already have a crafting job for this item
- local itemKey = item.name
- if item.nbt then
- itemKey = itemKey .. "_nbt"
- end
- if craftingJobs[itemKey] then
- return false, "Already crafting"
- end
- -- Create craft filter
- local craftFilter = {
- name = item.name,
- count = amount
- }
- if item.nbt then
- craftFilter.nbt = item.nbt
- end
- -- Attempt to schedule the craft
- local craftJob, err = bridge.craftItem(craftFilter)
- if craftJob then
- craftingJobs[itemKey] = {
- job = craftJob,
- requestName = requestName,
- amount = amount,
- startTime = os.time()
- }
- return true, "Crafting scheduled"
- else
- return false, err or "Craft failed"
- end
- end
- --[[
- Handle Crafting Event
- @desc Process rs_crafting events and update job tracking
- @return void, boolean (shouldRescan)
- ]]
- function handleCraftingEvent(error, id, message)
- local shouldRescan = false
- -- Find the job in our tracking table
- for itemKey, jobData in pairs(craftingJobs) do
- if jobData.job and jobData.job.id == id then
- if error then
- print(string.format("[Craft Error] %s - %s", jobData.requestName, message))
- craftingJobs[itemKey] = nil
- elseif message == "CRAFTING_STARTED" then
- print(string.format("[Crafting] %s (x%d)", jobData.requestName, jobData.amount))
- elseif message == "MISSING_ITEMS" then
- print(string.format("[Missing Items] Cannot craft %s", jobData.requestName))
- craftingJobs[itemKey] = nil
- elseif message == "DONE" then
- print(string.format("[Craft Complete] %s - waiting for items...", jobData.requestName))
- craftingJobs[itemKey] = nil
- shouldRescan = true
- elseif message == "CANCELED" then
- print(string.format("[Craft Canceled] %s", jobData.requestName))
- craftingJobs[itemKey] = nil
- shouldRescan = true
- end
- break
- end
- end
- return shouldRescan
- end
- --[[
- Write To Log
- @desc Write the specified `table` to the file surrounded by the `blockTop` and `blockBottom`
- @return void
- ]]
- function writeToLog(data, blockTop, blockBottom)
- file.write("\n")
- file.write(blockTop)
- file.write("\n")
- file.write(textutils.serialize(data, { allow_repetitions = true }))
- file.write("\n")
- file.write(blockBottom)
- file.write("\n")
- end
- --[[
- Process Work Request Item
- @desc Determine if this item can be delivered to the warehouse from the storage
- @return boolean
- ]]
- function processWorkRequestItem(request)
- if string.find(request.desc, "Tool of class") then return false end
- if string.find(request.name, "Hoe") then return false end
- if string.find(request.name, "Shovel") then return false end
- if string.find(request.name, "Axe") then return false end
- if string.find(request.name, "Pickaxe") then return false end
- if string.find(request.name, "Bow") and not string.find(request.name, "Bowl") then return false end
- if string.find(request.name, "Sword") then return false end
- if string.find(request.name, "Shield") then return false end
- if string.find(request.name, "Helmet") then return false end
- if string.find(request.name, "Leather Cap") then return false end
- if string.find(request.name, "Chestplate") then return false end
- if string.find(request.name, "Tunic") then return false end
- if string.find(request.name, "Pants") then return false end
- if string.find(request.name, "Leggings") then return false end
- if string.find(request.name, "Boots") then return false end
- if request.name == "Rallying Banner" then return false end --bugged in alpha versions
- if request.name == "Crafter" then return false end
- if request.name == "Compostable" then return false end
- if request.name == "Fertilizer" then return false end
- if request.name == "Flowers" then return false end
- if request.name == "Food" then return false end
- if request.name == "Fuel" then return false end
- if request.name == "Smeltable Ore" then return false end
- if request.name == "Stack List" then return false end
- -- Skip decorative blocks with components (they need manual crafting with specific textures)
- if request.item and request.item.components and not table.empty(request.item.components) then
- return false
- end
- -- you can add any new items here if they are found
- return true
- end
- --[[
- Monitor Print Row Justified
- @desc Print a line of data to the in-game monitor
- @return void
- ]]
- function mPrintRowJustified(mon, y, pos, text, textcolor)
- w, h = mon.getSize()
- fg = colors.white
- bg = colors.black
- if pos == "left" then x = 1 end
- if pos == "center" then x = math.floor((w - #text) / 2) end
- if pos == "right" then x = w - #text end
- mon.setTextColor(textcolor)
- mon.setCursorPos(x, y)
- mon.write(text)
- mon.setTextColor(fg)
- mon.setBackgroundColor(bg)
- end
- --[[
- Display Timer
- @desc Update the time on the monitor
- @return void
- ]]
- function displayTimer(mon, t)
- now = os.time()
- cycle = "day"
- cycle_color = colors.orange
- if now >= 4 and now < 6 then
- cycle = "sunrise"
- cycle_color = colors.yellow
- elseif now >= 6 and now < 18 then
- cycle = "day"
- cycle_color = colors.lightBlue
- elseif now >= 18 and now < 19.5 then
- cycle = "sunset"
- cycle_color = colors.magenta
- elseif now >= 19.5 or now < 5 then
- cycle = "night"
- cycle_color = colors.red
- end
- timer_color = colors.green
- if t < 15 then timer_color = colors.yellow end
- if t < 5 then timer_color = colors.orange end
- mPrintRowJustified(mon, 1, "left", string.format("Time: %s [%s] ", textutils.formatTime(now, false), cycle),
- cycle_color)
- if cycle ~= "night" then
- mPrintRowJustified(mon, 1, "right", string.format(" Remaining: %ss", t), timer_color)
- else
- mPrintRowJustified(mon, 1, "right", " Remaining: PAUSED", colors.red)
- end
- end
- --[[
- Create Colonist Data
- @desc Build a table of Colonist making the request
- @return table
- ]]
- function createColonistData(colonist)
- title_words = {}
- words_in_name = 0
- colonist_job = ""
- word_count = 1
- for word in colonist:gmatch("%S+") do
- table.insert(title_words, word)
- words_in_name = words_in_name + 1
- end
- if words_in_name >= 3 then
- colonist_name = title_words[words_in_name - 2] .. " " .. title_words[words_in_name]
- else
- colonist_name = colonist
- end
- repeat
- if colonist_job ~= "" then colonist_job = colonist_job .. " " end
- colonist_job = colonist_job .. title_words[word_count]
- word_count = word_count + 1
- until word_count > words_in_name - 3
- return {
- fullName = colonist,
- titleWords = title_words,
- job = colonist_job,
- name = colonist_name,
- wordsInName =
- words_in_name
- }
- end
- --[[
- Get Work Request List (from colony)
- @desc Build a table of the work request data from the colony
- @return table
- ]]
- function getWorkRequestList(colony)
- requestList = {}
- workRequests = colony.getRequests()
- file = fs.open(logFile, "w")
- for w in pairs(workRequests) do
- writeToLog(workRequests[w], "--- Request start ---", "--- Request end ---");
- name = workRequests[w].name -- the name of the count/item being requested
- colonist = createColonistData(workRequests[w].target)
- desc = workRequests[w].desc -- the request description
- item = {}
- -- create the filter item for the transfer request through the bridge
- if workRequests[w].items and workRequests[w].items[1] then
- item = {
- name = workRequests[w].items[1].name,
- count = workRequests[w].count,
- displayName = workRequests[w].items[1].displayName
- }
- -- Preserve NBT data if present
- if workRequests[w].items[1].nbt and not table.empty(workRequests[w].items[1].nbt) then
- item.nbt = workRequests[w].items[1].nbt
- end
- -- Preserve components data if present (newer format)
- if workRequests[w].items[1].components and not table.empty(workRequests[w].items[1].components) then
- item.components = workRequests[w].items[1].components
- end
- -- Store fingerprint for reference
- if workRequests[w].items[1].fingerprint then
- item.fingerprint = workRequests[w].items[1].fingerprint
- end
- end
- -- how many items are needed to fulfill this request?
- needed = workRequests[w].count
- local newRecord = {}
- newRecord.name = name
- newRecord.desc = desc
- newRecord.needed = needed
- newRecord.item = item
- newRecord.colonist = colonist
- table.insert(requestList, newRecord)
- writeToLog(newRecord, "--- Record start ---", "--- Record end ---");
- end
- file.close()
- return requestList
- end
- --[[
- Get Supporting Materials
- @desc Extract supporting materials from item components (for Domum Ornamentum)
- @return table or nil
- ]]
- function getSupportingMaterials(item)
- if not item or not item.components then return nil end
- local materials = {}
- local textureData = item.components["domum_ornamentum:texture_data"]
- if textureData then
- for _, material in pairs(textureData) do
- table.insert(materials, material)
- end
- end
- if #materials > 0 then
- return materials
- end
- return nil
- end
- --[[
- Display List
- @desc Update the monitor with the work request items currently in the system
- @return void
- ]]
- function displayList(mon, listName, itemList)
- -- show the list header first
- mPrintRowJustified(mon, row, "center", listName, colors.white)
- row = row + 1
- for e in pairs(itemList) do
- record = itemList[e]
- text = string.format("%d %s", record.provided, record.name)
- mPrintRowJustified(mon, row, "left", text, record.color)
- mPrintRowJustified(mon, row, "right", " " .. record.colonist, record.color)
- row = row + 1
- if record.materials then
- for _, mat in pairs(record.materials) do
- local displayName = mat:match(":(.+)") or mat
- mPrintRowJustified(mon, row, "left", " + " .. displayName, colors.lightGray)
- row = row + 1
- end
- end
- end
- -- add a space at the end of the list
- row = row + 1
- end
- -- Color References:
- -- RED: work order can't be satisfied by Refined Storage (lack of pattern or lack of
- -- required crafting ingredients).
- -- YELLOW: order partially filled and a crafting job was scheduled for the rest.
- -- GREEN: order fully filled.
- -- 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.
- --[[
- Cleanup Crafting Jobs
- @desc Check status of active jobs and remove finished ones
- @return void
- ]]
- function cleanupCraftingJobs(bridge)
- local toRemove = {}
- for itemKey, jobData in pairs(craftingJobs) do
- local job = jobData.job
- -- Check if job is done or canceled
- -- We use pcall in case the object is disconnected or invalid
- local success, isDone = pcall(function() return job.isDone() end)
- local success2, isCanceled = pcall(function() return job.isCanceled() end)
- if success and isDone then
- print(string.format("[Cleanup] Job %s detected as DONE", jobData.requestName))
- table.insert(toRemove, itemKey)
- elseif success2 and isCanceled then
- print(string.format("[Cleanup] Job %s detected as CANCELED", jobData.requestName))
- table.insert(toRemove, itemKey)
- elseif not success then
- -- If method call failed, try to check if job exists via bridge
- local freshJob = bridge.getCraftingJob(job.id)
- if not freshJob then
- print(string.format("[Cleanup] Job %s ID:%d not found (clearing)", jobData.requestName, job.id))
- table.insert(toRemove, itemKey)
- end
- end
- end
- for _, key in ipairs(toRemove) do
- craftingJobs[key] = nil
- end
- end
- --[[
- Scan Work Requests
- @desc Manages all of the open work requests in the system and attempts to fulfill them from the inventory
- @desc Not called at night (as determined by the server) since requests cannot be fulfilled anyway
- @return void
- ]]
- function scanWorkRequests(mon, bridge, direction)
- cleanupCraftingJobs(bridge)
- print("\nScan starting at", textutils.formatTime(os.time(), false) .. " (" .. os.time() .. ").")
- builder_list = {}
- nonbuilder_list = {}
- equipment_list = {}
- requestList = getWorkRequestList(colony)
- for j, data in ipairs(requestList) do
- color = colors.blue
- provided = 0
- -- Always attempt to export what we have (even if it's 0, to handle newly crafted items)
- -- This allows manual fulfillment of "Blue" items
- if data.item and data.item.name then
- local available = getAvailableItemCount(bridge, data.item)
- provided = bridge.exportItem(data.item, direction)
- -- If nothing was exported but items are available, the warehouse might be full
- if provided == 0 and available > 0 then
- print(string.format("[Warning] Could not export %s - warehouse may be full", data.name))
- end
- end
- if processWorkRequestItem(data) then
- -- Determine if we need to craft more
- local stillNeeded = data.needed - provided
- if stillNeeded > 0 then
- -- Check if item is craftable
- if isItemCraftable(bridge, data.item) then
- -- Try to schedule crafting for the remaining amount
- local success, msg = scheduleCraftJob(bridge, data.item, stillNeeded, data.name)
- if success then
- color = colors.yellow -- Partially filled, crafting in progress
- print(string.format("[Scheduling Craft] %s - Need %d more", data.name, stillNeeded))
- elseif msg == "Already crafting" then
- color = colors.orange -- Already crafting
- else
- color = colors.red -- Can't craft (missing ingredients)
- print(string.format("[Cannot Craft] %s - %s", data.name, msg))
- end
- else
- color = colors.red -- No pattern available
- print(string.format("[No Pattern] %s", data.name))
- end
- else
- color = colors.green -- Fully satisfied
- end
- -- Override color if nothing was provided and no craft scheduled
- if provided == 0 and color ~= colors.yellow and color ~= colors.orange then
- color = colors.lightGray
- end
- else
- -- Manual item handling
- if provided >= data.needed then
- color = colors.green
- else
- nameString = data.name .. " [" .. data.colonist.fullName .. "]"
- print("[Skipped/Manual]", nameString)
- end
- end
- -- ---------------------------------------------------------------------
- -- Build the newList data
- -- ---------------------------------------------------------------------
- -- create the target text
- expectedList = "Builder"
- colonist = data.colonist.name
- if not string.find(data.colonist.fullName, "Builder") then
- expectedList = ""
- colonist = data.colonist.job .. " " .. data.colonist.name
- if data.colonist.wordsInName < 3 then
- colonist = data.colonist.name
- end
- end
- -- create the name
- listName = data.name
- if string.find(data.desc, "level") then
- expectedList = "Equipment"
- level = "Any Level"
- if string.find(data.desc, "with maximal level: Leather") then level = "Leather" end
- if string.find(data.desc, "with maximal level: Gold") then level = "Gold" end
- if string.find(data.desc, "with maximal level: Chain") then level = "Chain" end
- if string.find(data.desc, "with maximal level: Wood or Gold") then level = "Wood or Gold" end
- if string.find(data.desc, "with maximal level: Stone") then level = "Stone" end
- if string.find(data.desc, "with maximal level: Iron") then level = "Iron" end
- if string.find(data.desc, "with maximal level: Diamond") then level = "Diamond" end
- listName = level .. " " .. data.name
- if level == "Any Level" then listName = data.name .. " of any level" end
- end
- -- create the new list table defining what is inserted into a specific list
- local materials = nil
- if color == colors.blue and data.item then
- materials = getSupportingMaterials(data.item)
- end
- newList = {
- name = listName,
- colonist = colonist,
- needed = data.needed,
- provided = provided,
- color = color,
- materials =
- materials
- }
- if expectedList == "Equipment" then
- table.insert(equipment_list, newList)
- elseif expectedList == "Builder" then
- table.insert(builder_list, newList)
- else
- table.insert(nonbuilder_list, newList)
- end
- -- ---------------------------------------------------------------------
- end
- -- Show the various lists on the attached monitor.
- mon.clear()
- row = 3
- if not table.empty(builder_list) then displayList(mon, "Builder Requests", builder_list) end
- if not table.empty(nonbuilder_list) then displayList(mon, "Nonbuilder Requests", nonbuilder_list) end
- if not table.empty(equipment_list) then displayList(mon, "Equipment", equipment_list) end
- -- no requests
- if row == 3 then
- mPrintRowJustified(mon, row, "center", "No Open Requests", colors.white)
- end
- print("Scan completed at", textutils.formatTime(os.time(), false) .. " (" .. os.time() .. ").")
- end
- --[[
- MAIN
- @desc establish the run times and execute the work request management
- @return void
- ]]
- local current_run = time_between_runs
- scanWorkRequests(monitor, bridge, storageDirection)
- displayTimer(monitor, current_run)
- local TIMER = os.startTimer(1)
- while true do
- local e = { os.pullEvent() }
- if e[1] == "timer" and e[2] == TIMER then
- now = os.time()
- if now >= 5 and now < 19.5 then
- current_run = current_run - 1
- if current_run <= 0 then
- scanWorkRequests(monitor, bridge, storageDirection)
- current_run = time_between_runs
- end
- end
- displayTimer(monitor, current_run)
- TIMER = os.startTimer(1)
- elseif e[1] == "monitor_touch" then
- os.cancelTimer(TIMER)
- scanWorkRequests(monitor, bridge, storageDirection)
- current_run = time_between_runs
- displayTimer(monitor, current_run)
- TIMER = os.startTimer(1)
- elseif e[1] == "rs_crafting" then
- -- Handle crafting events: rs_crafting returns (error, id, message)
- local error = e[2]
- local id = e[3]
- local message = e[4]
- local shouldRescan = handleCraftingEvent(error, id, message)
- -- Trigger a rescan when crafting completes (with small delay for items to appear)
- if shouldRescan then
- print("[Info] Waiting 2 seconds for items to appear in storage...")
- sleep(2)
- os.cancelTimer(TIMER)
- scanWorkRequests(monitor, bridge, storageDirection)
- current_run = time_between_runs
- displayTimer(monitor, current_run)
- TIMER = os.startTimer(1)
- end
- end
- end
Advertisement
Add Comment
Please, Sign In to add comment