Advertisement
Myros27

spatialLoaderBot

May 30th, 2025 (edited)
373
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Lua 18.94 KB | None | 0 0
  1. --[[
  2. Robot Turtle for Spatial Storage Management
  3. Version 1.1
  4.  
  5. Handles 'load' and 'unload' commands for spatial dimensions via chat.
  6. Interacts with a System Connector (back) and IO Port (front).
  7. Requires a Chat Box peripheral.
  8. Uses global textutils for JSON operations.
  9. --]]
  10.  
  11. -- textutils is globally available in CC:Tweaked for JSON functions.
  12.  
  13. -- Configuration
  14. local NAME_FILE = "name.json"
  15. local ACTIVE_DIM_FILE = "active_dim.json"
  16. local CHAT_MODULE_NAME = "chatBox"
  17.  
  18. -- Turtle State
  19. local turtle_id = nil -- Canonical name, e.g., "ab12"
  20. local chat_box = nil
  21. local current_dim_id_in_port = "" -- User-defined name of the dimension currently in the IO Port
  22. local current_dim_nbt_in_port = nil -- NBT of the dimension currently in the IO Port
  23.  
  24. -- Error State Flag
  25. local in_error_state = false
  26.  
  27. --#region Helper Functions
  28.  
  29. function error_state(reason)
  30.     term.setTextColor(colors.red)
  31.     print("ERROR: " .. reason)
  32.     term.setTextColor(colors.white)
  33.     print("Turtle is now locked down.")
  34.     in_error_state = true
  35.     -- Lock down: no more reactions or interactions
  36.     while true do
  37.         os.sleep(3600) -- Sleep for a long time
  38.     end
  39. end
  40.  
  41. local function file_exists(path)
  42.     return fs.exists(path)
  43. end
  44.  
  45. local function read_json_file(path)
  46.     if not file_exists(path) then
  47.         return nil
  48.     end
  49.     local handle = fs.open(path, "r")
  50.     if not handle then
  51.         print("Warning: Could not open " .. path .. " for reading.")
  52.         return nil
  53.     end
  54.     local content = handle.readAll()
  55.     handle.close()
  56.     -- Use pcall to safely attempt to unserialise JSON, as malformed JSON will error
  57.     local success, data = pcall(textutils.unserialiseJSON, content)
  58.     if success then
  59.         return data
  60.     else
  61.         print("Warning: Could not parse JSON from " .. path .. ". Error: " .. tostring(data))
  62.         return nil
  63.     end
  64. end
  65.  
  66. local function write_json_file(path, data)
  67.     local success, serialized_data = pcall(textutils.serialiseJSON, data, {compact = false})
  68.     if not success or not serialized_data then
  69.         error_state("Failed to serialize data for " .. path .. ". Error: " .. tostring(serialized_data)) -- serialized_data will be error msg on fail
  70.         return false
  71.     end
  72.     local handle = fs.open(path, "w")
  73.     if not handle then
  74.         error_state("Could not open " .. path .. " for writing.")
  75.         return false
  76.     end
  77.     handle.write(serialized_data)
  78.     handle.close()
  79.     return true
  80. end
  81.  
  82. local function validate_name_structure(name_str)
  83.     if type(name_str) ~= "string" then return false end
  84.     local len = string.len(name_str)
  85.     if len < 2 or len > 4 then return false end
  86.  
  87.     local letters = name_str:gsub("[^a-zA-Z]", "")
  88.     local numbers = name_str:gsub("[^0-9]", "")
  89.  
  90.     if string.len(letters) + string.len(numbers) ~= len then return false end -- Contains other chars
  91.  
  92.     if not (string.len(letters) >= 1 and string.len(letters) <= 2) then return false end
  93.     if not (string.len(numbers) >= 1 and string.len(numbers) <= 2) then return false end
  94.  
  95.     return true
  96. end
  97.  
  98. local function canonicalize_name(name_str)
  99.     if type(name_str) ~= "string" then return "" end
  100.     local lower_name = name_str:lower()
  101.     local letters_part = {}
  102.     for char in lower_name:gmatch("[a-z]") do
  103.         table.insert(letters_part, char)
  104.     end
  105.     table.sort(letters_part)
  106.  
  107.     local numbers_part = {}
  108.     for char in lower_name:gmatch("[0-9]") do
  109.         table.insert(numbers_part, char)
  110.     end
  111.     table.sort(numbers_part)
  112.  
  113.     return table.concat(letters_part) .. table.concat(numbers_part)
  114. end
  115.  
  116. local function send_chat_message(message)
  117.     if chat_box then
  118.         local prefix = turtle_id or "Turtle"
  119.         chat_box.sendMessage(message, prefix, "[]", "&e") -- Yellow brackets for turtle name
  120.         os.sleep(1) -- Cooldown to prevent spam
  121.     else
  122.         print("Chat: [" .. (turtle_id or "Turtle") .. "] " .. message)
  123.     end
  124. end
  125.  
  126. -- Deep NBT comparison by serializing to JSON strings
  127. local function compare_nbt_data(nbt1, nbt2)
  128.     if nbt1 == nil and nbt2 == nil then return true end
  129.     if type(nbt1) ~= type(nbt2) then return false end
  130.  
  131.     if type(nbt1) == "table" then
  132.         -- Use pcall for serialization as NBT could theoretically be non-serializable (though unlikely for valid item NBT)
  133.         local success1, s_nbt1 = pcall(textutils.serialiseJSON, nbt1)
  134.         local success2, s_nbt2 = pcall(textutils.serialiseJSON, nbt2)
  135.         if success1 and success2 then
  136.             return s_nbt1 == s_nbt2
  137.         else
  138.             print("Warning: Could not serialize NBT data for comparison.")
  139.             return false -- If serialization fails, consider them not equal
  140.         end
  141.     end
  142.     return nbt1 == nbt2
  143. end
  144.  
  145. --#endregion Helper Functions
  146.  
  147. --#region Initialization
  148.  
  149. local function initialize_name()
  150.     local name_data = read_json_file(NAME_FILE)
  151.     if name_data and name_data.id and validate_name_structure(name_data.id) then
  152.         turtle_id = canonicalize_name(name_data.id)
  153.         print("Name loaded: " .. turtle_id)
  154.     else
  155.         print("No valid name found. Please set a name for this turtle.")
  156.         local input_name
  157.         repeat
  158.             term.write("Enter name (1-2 letters, 1-2 numbers, e.g., n1, ab12): ")
  159.             input_name = read()
  160.             if not validate_name_structure(input_name) then
  161.                 print("Invalid name format. Must be 1-2 letters and 1-2 numbers (e.g., 'a1', 'xy12').")
  162.                 input_name = nil
  163.             end
  164.         until input_name
  165.         turtle_id = canonicalize_name(input_name)
  166.         if not write_json_file(NAME_FILE, {id = turtle_id}) then
  167.             error_state("Failed to save name to " .. NAME_FILE)
  168.         end
  169.         print("Name set to: " .. turtle_id)
  170.     end
  171. end
  172.  
  173. local function initialize_peripherals_and_state()
  174.     chat_box = peripheral.find(CHAT_MODULE_NAME)
  175.     if not chat_box then
  176.         error_state("Chat Box peripheral ('" .. CHAT_MODULE_NAME .. "') not found.")
  177.     end
  178.     print("Chat Box found.")
  179.  
  180.     for i = 1, 16 do
  181.         if turtle.getItemCount(i) > 0 then
  182.             error_state("Turtle inventory is not empty. Slot " .. i .. " contains items. Please clear inventory.")
  183.         end
  184.     end
  185.     print("Inventory is empty.")
  186.  
  187.     local active_dim_data = read_json_file(ACTIVE_DIM_FILE)
  188.     if not active_dim_data or type(active_dim_data.name) ~= "string" or (active_dim_data.nbt ~= nil and type(active_dim_data.nbt) ~= "table") then
  189.         print("Warning: " .. ACTIVE_DIM_FILE .. " missing or corrupt. Initializing fresh.")
  190.         active_dim_data = {name = "", nbt = nil}
  191.         if not write_json_file(ACTIVE_DIM_FILE, active_dim_data) then
  192.             error_state("Failed to create/initialize " .. ACTIVE_DIM_FILE)
  193.         end
  194.     end
  195.     current_dim_id_in_port = active_dim_data.name
  196.     current_dim_nbt_in_port = active_dim_data.nbt
  197.     if current_dim_id_in_port ~= "" and current_dim_id_in_port ~= nil then
  198.         print("Remembering loaded dimension: " .. current_dim_id_in_port)
  199.     else
  200.         print("No dimension currently loaded in IO Port.")
  201.     end
  202. end
  203.  
  204. --#endregion Initialization
  205.  
  206. --#region Command Processing
  207.  
  208. local function do_unload_procedure(target_slot_for_unloaded_disk)
  209.     print("Starting unload procedure...")
  210.     if current_dim_id_in_port == "" or current_dim_id_in_port == nil then
  211.         send_chat_message("I don't have any dimension loaded to unload.")
  212.         return false
  213.     end
  214.  
  215.     local original_selected_slot = turtle.getSelectedSlot()
  216.     if target_slot_for_unloaded_disk then
  217.         turtle.select(target_slot_for_unloaded_disk)
  218.         if turtle.getItemCount(target_slot_for_unloaded_disk) > 0 then
  219.              error_state("Target slot " .. target_slot_for_unloaded_disk .. " for unload is not empty.")
  220.         end
  221.     else
  222.         turtle.select(1)
  223.         if turtle.getItemCount(1) > 0 then
  224.             error_state("Slot 1 for unload is not empty.")
  225.         end
  226.     end
  227.  
  228.     print("Powering system connector (back)...")
  229.     rs.setOutput("back", true)
  230.     os.sleep(0.5)
  231.  
  232.     print("Pulsing IO Port (front) to eject disk...")
  233.     local disk_retrieved = false
  234.     for i = 1, 3 do
  235.         rs.setOutput("front", true)
  236.         os.sleep(0.1)
  237.         rs.setOutput("front", false)
  238.         os.sleep(0.1)
  239.  
  240.         if turtle.suck() then
  241.             disk_retrieved = true
  242.             print("Disk retrieved into slot " .. turtle.getSelectedSlot())
  243.             break
  244.         end
  245.         print("Failed to suck disk, attempt " .. i .. "/3. Retrying...")
  246.         if i < 3 then os.sleep(0.5) end
  247.     end
  248.  
  249.     if not disk_retrieved then
  250.         rs.setOutput("back", false)
  251.         error_state("Failed to retrieve disk from IO Port after 3 attempts during unload.")
  252.     end
  253.  
  254.     local item_detail = turtle.getItemDetail()
  255.     if not item_detail then
  256.         rs.setOutput("back", false)
  257.         error_state("Failed to get details of retrieved disk from IO Port.")
  258.     end
  259.  
  260.     print("Comparing NBT of retrieved disk...")
  261.     if not compare_nbt_data(item_detail.nbt, current_dim_nbt_in_port) then
  262.         rs.setOutput("back", false)
  263.         error_state("NBT mismatch! Retrieved disk does not match expected NBT for '" .. current_dim_id_in_port .. "'.")
  264.     end
  265.     print("NBT matches.")
  266.  
  267.     if not target_slot_for_unloaded_disk then
  268.         print("Updating active_dim.json to empty state.")
  269.         local temp_unloaded_id = current_dim_id_in_port -- Cache for message
  270.         current_dim_id_in_port = ""
  271.         current_dim_nbt_in_port = nil
  272.         if not write_json_file(ACTIVE_DIM_FILE, {name = "", nbt = nil}) then
  273.             rs.setOutput("back", false)
  274.             error_state("Failed to update " .. ACTIVE_DIM_FILE .. " after unload.")
  275.         end
  276.          -- send_chat_message("Dimension '" .. temp_unloaded_id .. "' successfully unloaded.") -- Moved to process_unload
  277.     end
  278.  
  279.     print("Sending disk to chest above...")
  280.     if not turtle.dropUp() then
  281.         rs.setOutput("back", false)
  282.         error_state("Failed to drop disk up into chest after unload.")
  283.     end
  284.    
  285.     turtle.select(original_selected_slot)
  286.     return true
  287. end
  288.  
  289. local function process_unload()
  290.     if current_dim_id_in_port == "" or current_dim_id_in_port == nil then
  291.         send_chat_message("I don't have any dimension loaded to unload.")
  292.         return
  293.     end
  294.  
  295.     local unloaded_dim_name_cache = current_dim_id_in_port
  296.     if do_unload_procedure(nil) then
  297.         send_chat_message("Dimension '" .. unloaded_dim_name_cache .. "' has been unloaded.")
  298.     else
  299.         -- error_state would have been called, or message sent by do_unload_procedure
  300.         print("Unload procedure reported failure or no action for '" .. unloaded_dim_name_cache .. "'.")
  301.     end
  302.     rs.setOutput("back", false)
  303. end
  304.  
  305.  
  306. local function process_load(dim_to_load_user_name)
  307.     print("Processing load command for dimension: " .. dim_to_load_user_name)
  308.  
  309.     local was_dim_loaded_in_port = (current_dim_id_in_port ~= "" and current_dim_id_in_port ~= nil)
  310.     local old_dim_name_cache = current_dim_id_in_port
  311.  
  312.     print("Attempting to retrieve new disk '" .. dim_to_load_user_name .. "' from chest above...")
  313.     turtle.select(1)
  314.  
  315.     if turtle.getItemCount(1) > 0 then
  316.         error_state("Slot 1 is not empty before attempting to suckUp new disk.")
  317.     end
  318.  
  319.     local new_disk_retrieved = false
  320.     for i = 1, 20 do
  321.         if turtle.suckUp() then
  322.             if turtle.getItemCount(1) > 0 then
  323.                 new_disk_retrieved = true
  324.                 print("New disk retrieved into slot 1.")
  325.                 break
  326.             end
  327.         end
  328.         if i < 20 then os.sleep(0.1) end
  329.     end
  330.  
  331.     if not new_disk_retrieved then
  332.         send_chat_message("Could not find item for dimension '" .. dim_to_load_user_name .. "' in the chest above after 20 tries.")
  333.         os.sleep(3)
  334.         return
  335.     end
  336.  
  337.     print("Powering system connector (back)...")
  338.     rs.setOutput("back", true)
  339.     os.sleep(0.5)
  340.  
  341.     if was_dim_loaded_in_port then
  342.         print("A dimension ('" .. old_dim_name_cache .. "') is already loaded. Unloading it first...")
  343.         if not do_unload_procedure(16) then
  344.             rs.setOutput("back", false)
  345.             print("Failed to unload the existing dimension '" .. old_dim_name_cache .. "'. Aborting load of '" .. dim_to_load_user_name .. "'.")
  346.             if turtle.getItemCount(1) > 0 then
  347.                 print("Returning unused new disk from slot 1 to chest above.")
  348.                 if not turtle.dropUp() then
  349.                     print("Warning: Failed to return new disk to chest above.")
  350.                 end
  351.             end
  352.             return
  353.         end
  354.         print("Old dimension '" .. old_dim_name_cache .. "' unloaded and sent to system (from slot 16).")
  355.     end
  356.  
  357.     turtle.select(1)
  358.     local new_item_detail = turtle.getItemDetail(1)
  359.     if not new_item_detail then
  360.         rs.setOutput("back", false)
  361.         error_state("Failed to get details of the new disk in slot 1 for '".. dim_to_load_user_name .."'.")
  362.     end
  363.  
  364.     print("Updating active_dim.json with new dimension info: " .. dim_to_load_user_name)
  365.     current_dim_id_in_port = dim_to_load_user_name
  366.     current_dim_nbt_in_port = new_item_detail.nbt
  367.     if not write_json_file(ACTIVE_DIM_FILE, {name = current_dim_id_in_port, nbt = current_dim_nbt_in_port}) then
  368.         rs.setOutput("back", false)
  369.         error_state("Failed to update " .. ACTIVE_DIM_FILE .. " for new dimension '" .. dim_to_load_user_name .. "'.")
  370.     end
  371.  
  372.     print("Dropping new disk into IO Port (front)...")
  373.     if not turtle.drop() then
  374.         rs.setOutput("back", false)
  375.         error_state("Failed to drop new disk '" .. dim_to_load_user_name .. "' into IO Port.")
  376.     end
  377.  
  378.     print("Pulsing IO Port to load dimension '" .. dim_to_load_user_name .. "'...")
  379.     rs.setOutput("front", true)
  380.     os.sleep(0.1)
  381.     rs.setOutput("front", false)
  382.     os.sleep(0.1)
  383.  
  384.     print("Cycling disk in IO Port (suck then drop)...")
  385.     turtle.select(1)
  386.     if turtle.getItemCount(1) > 0 then
  387.         error_state("Slot 1 not empty before IO disk cycle suck for '" .. dim_to_load_user_name .. "'.")
  388.     end
  389.     if not turtle.suck() then
  390.         rs.setOutput("back", false)
  391.         error_state("Failed to suck disk from IO Port after load signal for '" .. dim_to_load_user_name .. "'.")
  392.     end
  393.     if not turtle.drop() then
  394.         rs.setOutput("back", false)
  395.         error_state("Failed to drop disk back into IO Port after suck-back for '" .. dim_to_load_user_name .. "'.")
  396.     end
  397.  
  398.     rs.setOutput("back", false)
  399.     send_chat_message("Dimension '" .. dim_to_load_user_name .. "' loaded successfully.")
  400.     if was_dim_loaded_in_port then
  401.         send_chat_message("(Replaced previously loaded dimension: '" .. old_dim_name_cache .. "')")
  402.     end
  403. end
  404.  
  405.  
  406. local function handle_chat_message(username, message)
  407.     print("Chat from " .. username .. ": " .. message)
  408.  
  409.     local lower_message = message:lower()
  410.     local parts = {}
  411.     for part in lower_message:gmatch("%S+") do -- %S+ matches one or more non-space characters
  412.         table.insert(parts, part)
  413.     end
  414.  
  415.     if #parts < 1 then return end -- Ignore empty messages
  416.  
  417.     local trigger = parts[1]
  418.     if not (trigger == "@spatial" or trigger == "@spat") then
  419.         return -- Not for us
  420.     end
  421.  
  422.     if #parts < 3 then
  423.         send_chat_message("Command too short. Usage: @" .. (trigger == "@spat" and "spat" or "spatial") .. " <load|unload> <your_name> [dim_id_for_load]")
  424.         return
  425.     end
  426.  
  427.     local command = parts[2]
  428.     local target_name_str = parts[3]
  429.     local dim_name_for_load -- Will be parts[4] if command is "load"
  430.  
  431.     local target_canonical = canonicalize_name(target_name_str)
  432.     if target_canonical ~= turtle_id then
  433.         print("Command was for turtle '" .. target_name_str .. "' (parsed as " .. target_canonical .. "), not me (" .. turtle_id .. "). Sleeping.")
  434.         os.sleep(5)
  435.         return
  436.     end
  437.  
  438.     print("Command is for me. Command: " .. command)
  439.  
  440.     if command == "load" then
  441.         if #parts < 4 then
  442.             send_chat_message("Load command requires a dimension name. Usage: @" .. (trigger == "@spat" and "spat" or "spatial") .. " load " .. target_name_str .. " <DimensionName>")
  443.             return
  444.         end
  445.         dim_name_for_load = parts[4] -- The actual dimension ID string, can contain mixed case or special chars
  446.         process_load(dim_name_for_load)
  447.     elseif command == "unload" then
  448.         process_unload()
  449.     else
  450.         send_chat_message("Unknown command '" .. command .. "'. Available: load, unload.")
  451.     end
  452. end
  453.  
  454. --#endregion Command Processing
  455.  
  456. --#region Main Program
  457. local function main()
  458.     print("Spatial Storage Turtle Initializing...")
  459.     rs.setOutput("front", false)
  460.     rs.setOutput("back", false)
  461.     print("Redstone outputs initialized to off.")
  462.  
  463.     initialize_name()
  464.     if in_error_state then return end
  465.  
  466.     initialize_peripherals_and_state()
  467.     if in_error_state then return end
  468.  
  469.     print("Initialization complete. Turtle '" .. turtle_id .. "' is operational.")
  470.     send_chat_message("Turtle '" .. turtle_id .. "' online and ready.")
  471.  
  472.     while not in_error_state do
  473.         local event_data = {os.pullEvent("chat")} -- Wait indefinitely for a chat event
  474.         local event_type = event_data[1]
  475.         if event_type == "chat" then
  476.             local username = event_data[2]
  477.             local message_content = event_data[3]
  478.            
  479.             local success, err = pcall(handle_chat_message, username, message_content)
  480.             if not success then
  481.                 local error_message_log = "Runtime error during command processing: " .. tostring(err)
  482.                 print(error_message_log)
  483.                 term.setTextColor(colors.red)
  484.                 -- Attempt to print stack trace if available (err might contain it)
  485.                 if type(err) == "string" and err:match(":%d+:") then -- Basic check for stack trace like string
  486.                     print(err)
  487.                 else
  488.                     -- If no obvious stack trace, print a generic part of the error.
  489.                     -- This part is tricky as CC error objects can vary.
  490.                     debug.traceback("Error in handle_chat_message", 2) -- Print current stack trace
  491.                 end
  492.                 term.setTextColor(colors.white)
  493.                 send_chat_message("An internal error occurred processing a command. Check my console. I might need a reset.")
  494.                 -- Decide if this should go to full error_state() or try to continue.
  495.                 -- For now, it logs and continues listening for more chat commands.
  496.                 -- Consider error_state(error_message_log) for critical failures.
  497.             end
  498.         end
  499.         -- Loop continues if not in_error_state
  500.     end
  501.    
  502.     print("Main loop exited due to error state.")
  503. end
  504.  
  505. -- Execute main function
  506. main()
  507.  
  508. if not in_error_state then
  509.     print("Turtle script finished unexpectedly without entering error state.")
  510. end
  511.  
  512. --#endregion Main Program
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement