CCCPaste

Updated RSWarehouse

-- RSWarehouse.lua
-- Orignal Author: Chuck Burgess
-- updated by: Mathew Biltonen
-- Updated: 2025-10-17

local logFile = "RSWarehouse.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. Default to empty string.
local storage = peripheral.find("entangled:tile")
if not storage then error("Warehouse storage not found.") end
local direction = ""
local names = peripheral.getNames()
for _, pos in ipairs(names) do
  if peripheral.getType(pos) == "entangled:tile" then
    direction = pos
    end
end
print("Warehouse storage initialized.")

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

--[[
    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") 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
  -- 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
          if not workRequests[w].items[1].nbt or table.empty(workRequests[w].items[1].nbt) then
            item = { name = workRequests[w].items[1].name, count =  workRequests[w].count, displayName = workRequests[w].items[1].displayName}
          else
            item = { name = workRequests[w].items[1].name, count = workRequests[w].count, displayName = workRequests[w].items[1].displayName, nbt =  workRequests[w].items[1].nbt}
          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

--[[
    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
  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.
--[[
    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)
    
    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

        if processWorkRequestItem(data) then
            provided = bridge.exportItem(data.item, direction)
            color = colors.lightGray
            if provided >= data.needed then
              color = colors.green
            end
            -- only handle the Non-NBT data items or empty nbt table item records
            if provided < data.needed then
                if bridge.isCrafting(data.item) then
                    color = colors.yellow
                    print("[Crafting]", data.name)
                else
                    if bridge.craftItem(data.item) then
                      color = colors.yellow
                      print("[Scheduled]", data.item.count, "x", data.name)
                    else
                      color = colors.red
                      print("[Failed]", data.name)
                    end
                end
            end
        else 
           nameString = data.name .. " [" .. data.colonist.fullName .. "]"
           print("[Skipped]", nameString)
        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
        newList = { name=listName, colonist=colonist, needed=data.needed, provided=provided, color=color}
        
        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, direction)
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, direction)
        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, direction)
    current_run = time_between_runs
    displayTimer(monitor, current_run)
    TIMER = os.startTimer(1)
  end
end