Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- Robot Turtle for Spatial Storage Management
- Version 1.1
- Handles 'load' and 'unload' commands for spatial dimensions via chat.
- Interacts with a System Connector (back) and IO Port (front).
- Requires a Chat Box peripheral.
- Uses global textutils for JSON operations.
- --]]
- -- textutils is globally available in CC:Tweaked for JSON functions.
- -- Configuration
- local NAME_FILE = "name.json"
- local ACTIVE_DIM_FILE = "active_dim.json"
- local CHAT_MODULE_NAME = "chatBox"
- -- Turtle State
- local turtle_id = nil -- Canonical name, e.g., "ab12"
- local chat_box = nil
- local current_dim_id_in_port = "" -- User-defined name of the dimension currently in the IO Port
- local current_dim_nbt_in_port = nil -- NBT of the dimension currently in the IO Port
- -- Error State Flag
- local in_error_state = false
- --#region Helper Functions
- function error_state(reason)
- term.setTextColor(colors.red)
- print("ERROR: " .. reason)
- term.setTextColor(colors.white)
- print("Turtle is now locked down.")
- in_error_state = true
- -- Lock down: no more reactions or interactions
- while true do
- os.sleep(3600) -- Sleep for a long time
- end
- end
- local function file_exists(path)
- return fs.exists(path)
- end
- local function read_json_file(path)
- if not file_exists(path) then
- return nil
- end
- local handle = fs.open(path, "r")
- if not handle then
- print("Warning: Could not open " .. path .. " for reading.")
- return nil
- end
- local content = handle.readAll()
- handle.close()
- -- Use pcall to safely attempt to unserialise JSON, as malformed JSON will error
- local success, data = pcall(textutils.unserialiseJSON, content)
- if success then
- return data
- else
- print("Warning: Could not parse JSON from " .. path .. ". Error: " .. tostring(data))
- return nil
- end
- end
- local function write_json_file(path, data)
- local success, serialized_data = pcall(textutils.serialiseJSON, data, {compact = false})
- if not success or not serialized_data then
- error_state("Failed to serialize data for " .. path .. ". Error: " .. tostring(serialized_data)) -- serialized_data will be error msg on fail
- return false
- end
- local handle = fs.open(path, "w")
- if not handle then
- error_state("Could not open " .. path .. " for writing.")
- return false
- end
- handle.write(serialized_data)
- handle.close()
- return true
- end
- local function validate_name_structure(name_str)
- if type(name_str) ~= "string" then return false end
- local len = string.len(name_str)
- if len < 2 or len > 4 then return false end
- local letters = name_str:gsub("[^a-zA-Z]", "")
- local numbers = name_str:gsub("[^0-9]", "")
- if string.len(letters) + string.len(numbers) ~= len then return false end -- Contains other chars
- if not (string.len(letters) >= 1 and string.len(letters) <= 2) then return false end
- if not (string.len(numbers) >= 1 and string.len(numbers) <= 2) then return false end
- return true
- end
- local function canonicalize_name(name_str)
- if type(name_str) ~= "string" then return "" end
- local lower_name = name_str:lower()
- local letters_part = {}
- for char in lower_name:gmatch("[a-z]") do
- table.insert(letters_part, char)
- end
- table.sort(letters_part)
- local numbers_part = {}
- for char in lower_name:gmatch("[0-9]") do
- table.insert(numbers_part, char)
- end
- table.sort(numbers_part)
- return table.concat(letters_part) .. table.concat(numbers_part)
- end
- local function send_chat_message(message)
- if chat_box then
- local prefix = turtle_id or "Turtle"
- chat_box.sendMessage(message, prefix, "[]", "&e") -- Yellow brackets for turtle name
- os.sleep(1) -- Cooldown to prevent spam
- else
- print("Chat: [" .. (turtle_id or "Turtle") .. "] " .. message)
- end
- end
- -- Deep NBT comparison by serializing to JSON strings
- local function compare_nbt_data(nbt1, nbt2)
- if nbt1 == nil and nbt2 == nil then return true end
- if type(nbt1) ~= type(nbt2) then return false end
- if type(nbt1) == "table" then
- -- Use pcall for serialization as NBT could theoretically be non-serializable (though unlikely for valid item NBT)
- local success1, s_nbt1 = pcall(textutils.serialiseJSON, nbt1)
- local success2, s_nbt2 = pcall(textutils.serialiseJSON, nbt2)
- if success1 and success2 then
- return s_nbt1 == s_nbt2
- else
- print("Warning: Could not serialize NBT data for comparison.")
- return false -- If serialization fails, consider them not equal
- end
- end
- return nbt1 == nbt2
- end
- --#endregion Helper Functions
- --#region Initialization
- local function initialize_name()
- local name_data = read_json_file(NAME_FILE)
- if name_data and name_data.id and validate_name_structure(name_data.id) then
- turtle_id = canonicalize_name(name_data.id)
- print("Name loaded: " .. turtle_id)
- else
- print("No valid name found. Please set a name for this turtle.")
- local input_name
- repeat
- term.write("Enter name (1-2 letters, 1-2 numbers, e.g., n1, ab12): ")
- input_name = read()
- if not validate_name_structure(input_name) then
- print("Invalid name format. Must be 1-2 letters and 1-2 numbers (e.g., 'a1', 'xy12').")
- input_name = nil
- end
- until input_name
- turtle_id = canonicalize_name(input_name)
- if not write_json_file(NAME_FILE, {id = turtle_id}) then
- error_state("Failed to save name to " .. NAME_FILE)
- end
- print("Name set to: " .. turtle_id)
- end
- end
- local function initialize_peripherals_and_state()
- chat_box = peripheral.find(CHAT_MODULE_NAME)
- if not chat_box then
- error_state("Chat Box peripheral ('" .. CHAT_MODULE_NAME .. "') not found.")
- end
- print("Chat Box found.")
- for i = 1, 16 do
- if turtle.getItemCount(i) > 0 then
- error_state("Turtle inventory is not empty. Slot " .. i .. " contains items. Please clear inventory.")
- end
- end
- print("Inventory is empty.")
- local active_dim_data = read_json_file(ACTIVE_DIM_FILE)
- 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
- print("Warning: " .. ACTIVE_DIM_FILE .. " missing or corrupt. Initializing fresh.")
- active_dim_data = {name = "", nbt = nil}
- if not write_json_file(ACTIVE_DIM_FILE, active_dim_data) then
- error_state("Failed to create/initialize " .. ACTIVE_DIM_FILE)
- end
- end
- current_dim_id_in_port = active_dim_data.name
- current_dim_nbt_in_port = active_dim_data.nbt
- if current_dim_id_in_port ~= "" and current_dim_id_in_port ~= nil then
- print("Remembering loaded dimension: " .. current_dim_id_in_port)
- else
- print("No dimension currently loaded in IO Port.")
- end
- end
- --#endregion Initialization
- --#region Command Processing
- local function do_unload_procedure(target_slot_for_unloaded_disk)
- print("Starting unload procedure...")
- if current_dim_id_in_port == "" or current_dim_id_in_port == nil then
- send_chat_message("I don't have any dimension loaded to unload.")
- return false
- end
- local original_selected_slot = turtle.getSelectedSlot()
- if target_slot_for_unloaded_disk then
- turtle.select(target_slot_for_unloaded_disk)
- if turtle.getItemCount(target_slot_for_unloaded_disk) > 0 then
- error_state("Target slot " .. target_slot_for_unloaded_disk .. " for unload is not empty.")
- end
- else
- turtle.select(1)
- if turtle.getItemCount(1) > 0 then
- error_state("Slot 1 for unload is not empty.")
- end
- end
- print("Powering system connector (back)...")
- rs.setOutput("back", true)
- os.sleep(0.5)
- print("Pulsing IO Port (front) to eject disk...")
- local disk_retrieved = false
- for i = 1, 3 do
- rs.setOutput("front", true)
- os.sleep(0.1)
- rs.setOutput("front", false)
- os.sleep(0.1)
- if turtle.suck() then
- disk_retrieved = true
- print("Disk retrieved into slot " .. turtle.getSelectedSlot())
- break
- end
- print("Failed to suck disk, attempt " .. i .. "/3. Retrying...")
- if i < 3 then os.sleep(0.5) end
- end
- if not disk_retrieved then
- rs.setOutput("back", false)
- error_state("Failed to retrieve disk from IO Port after 3 attempts during unload.")
- end
- local item_detail = turtle.getItemDetail()
- if not item_detail then
- rs.setOutput("back", false)
- error_state("Failed to get details of retrieved disk from IO Port.")
- end
- print("Comparing NBT of retrieved disk...")
- if not compare_nbt_data(item_detail.nbt, current_dim_nbt_in_port) then
- rs.setOutput("back", false)
- error_state("NBT mismatch! Retrieved disk does not match expected NBT for '" .. current_dim_id_in_port .. "'.")
- end
- print("NBT matches.")
- if not target_slot_for_unloaded_disk then
- print("Updating active_dim.json to empty state.")
- local temp_unloaded_id = current_dim_id_in_port -- Cache for message
- current_dim_id_in_port = ""
- current_dim_nbt_in_port = nil
- if not write_json_file(ACTIVE_DIM_FILE, {name = "", nbt = nil}) then
- rs.setOutput("back", false)
- error_state("Failed to update " .. ACTIVE_DIM_FILE .. " after unload.")
- end
- -- send_chat_message("Dimension '" .. temp_unloaded_id .. "' successfully unloaded.") -- Moved to process_unload
- end
- print("Sending disk to chest above...")
- if not turtle.dropUp() then
- rs.setOutput("back", false)
- error_state("Failed to drop disk up into chest after unload.")
- end
- turtle.select(original_selected_slot)
- return true
- end
- local function process_unload()
- if current_dim_id_in_port == "" or current_dim_id_in_port == nil then
- send_chat_message("I don't have any dimension loaded to unload.")
- return
- end
- local unloaded_dim_name_cache = current_dim_id_in_port
- if do_unload_procedure(nil) then
- send_chat_message("Dimension '" .. unloaded_dim_name_cache .. "' has been unloaded.")
- else
- -- error_state would have been called, or message sent by do_unload_procedure
- print("Unload procedure reported failure or no action for '" .. unloaded_dim_name_cache .. "'.")
- end
- rs.setOutput("back", false)
- end
- local function process_load(dim_to_load_user_name)
- print("Processing load command for dimension: " .. dim_to_load_user_name)
- local was_dim_loaded_in_port = (current_dim_id_in_port ~= "" and current_dim_id_in_port ~= nil)
- local old_dim_name_cache = current_dim_id_in_port
- print("Attempting to retrieve new disk '" .. dim_to_load_user_name .. "' from chest above...")
- turtle.select(1)
- if turtle.getItemCount(1) > 0 then
- error_state("Slot 1 is not empty before attempting to suckUp new disk.")
- end
- local new_disk_retrieved = false
- for i = 1, 20 do
- if turtle.suckUp() then
- if turtle.getItemCount(1) > 0 then
- new_disk_retrieved = true
- print("New disk retrieved into slot 1.")
- break
- end
- end
- if i < 20 then os.sleep(0.1) end
- end
- if not new_disk_retrieved then
- send_chat_message("Could not find item for dimension '" .. dim_to_load_user_name .. "' in the chest above after 20 tries.")
- os.sleep(3)
- return
- end
- print("Powering system connector (back)...")
- rs.setOutput("back", true)
- os.sleep(0.5)
- if was_dim_loaded_in_port then
- print("A dimension ('" .. old_dim_name_cache .. "') is already loaded. Unloading it first...")
- if not do_unload_procedure(16) then
- rs.setOutput("back", false)
- print("Failed to unload the existing dimension '" .. old_dim_name_cache .. "'. Aborting load of '" .. dim_to_load_user_name .. "'.")
- if turtle.getItemCount(1) > 0 then
- print("Returning unused new disk from slot 1 to chest above.")
- if not turtle.dropUp() then
- print("Warning: Failed to return new disk to chest above.")
- end
- end
- return
- end
- print("Old dimension '" .. old_dim_name_cache .. "' unloaded and sent to system (from slot 16).")
- end
- turtle.select(1)
- local new_item_detail = turtle.getItemDetail(1)
- if not new_item_detail then
- rs.setOutput("back", false)
- error_state("Failed to get details of the new disk in slot 1 for '".. dim_to_load_user_name .."'.")
- end
- print("Updating active_dim.json with new dimension info: " .. dim_to_load_user_name)
- current_dim_id_in_port = dim_to_load_user_name
- current_dim_nbt_in_port = new_item_detail.nbt
- if not write_json_file(ACTIVE_DIM_FILE, {name = current_dim_id_in_port, nbt = current_dim_nbt_in_port}) then
- rs.setOutput("back", false)
- error_state("Failed to update " .. ACTIVE_DIM_FILE .. " for new dimension '" .. dim_to_load_user_name .. "'.")
- end
- print("Dropping new disk into IO Port (front)...")
- if not turtle.drop() then
- rs.setOutput("back", false)
- error_state("Failed to drop new disk '" .. dim_to_load_user_name .. "' into IO Port.")
- end
- print("Pulsing IO Port to load dimension '" .. dim_to_load_user_name .. "'...")
- rs.setOutput("front", true)
- os.sleep(0.1)
- rs.setOutput("front", false)
- os.sleep(0.1)
- print("Cycling disk in IO Port (suck then drop)...")
- turtle.select(1)
- if turtle.getItemCount(1) > 0 then
- error_state("Slot 1 not empty before IO disk cycle suck for '" .. dim_to_load_user_name .. "'.")
- end
- if not turtle.suck() then
- rs.setOutput("back", false)
- error_state("Failed to suck disk from IO Port after load signal for '" .. dim_to_load_user_name .. "'.")
- end
- if not turtle.drop() then
- rs.setOutput("back", false)
- error_state("Failed to drop disk back into IO Port after suck-back for '" .. dim_to_load_user_name .. "'.")
- end
- rs.setOutput("back", false)
- send_chat_message("Dimension '" .. dim_to_load_user_name .. "' loaded successfully.")
- if was_dim_loaded_in_port then
- send_chat_message("(Replaced previously loaded dimension: '" .. old_dim_name_cache .. "')")
- end
- end
- local function handle_chat_message(username, message)
- print("Chat from " .. username .. ": " .. message)
- local lower_message = message:lower()
- local parts = {}
- for part in lower_message:gmatch("%S+") do -- %S+ matches one or more non-space characters
- table.insert(parts, part)
- end
- if #parts < 1 then return end -- Ignore empty messages
- local trigger = parts[1]
- if not (trigger == "@spatial" or trigger == "@spat") then
- return -- Not for us
- end
- if #parts < 3 then
- send_chat_message("Command too short. Usage: @" .. (trigger == "@spat" and "spat" or "spatial") .. " <load|unload> <your_name> [dim_id_for_load]")
- return
- end
- local command = parts[2]
- local target_name_str = parts[3]
- local dim_name_for_load -- Will be parts[4] if command is "load"
- local target_canonical = canonicalize_name(target_name_str)
- if target_canonical ~= turtle_id then
- print("Command was for turtle '" .. target_name_str .. "' (parsed as " .. target_canonical .. "), not me (" .. turtle_id .. "). Sleeping.")
- os.sleep(5)
- return
- end
- print("Command is for me. Command: " .. command)
- if command == "load" then
- if #parts < 4 then
- send_chat_message("Load command requires a dimension name. Usage: @" .. (trigger == "@spat" and "spat" or "spatial") .. " load " .. target_name_str .. " <DimensionName>")
- return
- end
- dim_name_for_load = parts[4] -- The actual dimension ID string, can contain mixed case or special chars
- process_load(dim_name_for_load)
- elseif command == "unload" then
- process_unload()
- else
- send_chat_message("Unknown command '" .. command .. "'. Available: load, unload.")
- end
- end
- --#endregion Command Processing
- --#region Main Program
- local function main()
- print("Spatial Storage Turtle Initializing...")
- rs.setOutput("front", false)
- rs.setOutput("back", false)
- print("Redstone outputs initialized to off.")
- initialize_name()
- if in_error_state then return end
- initialize_peripherals_and_state()
- if in_error_state then return end
- print("Initialization complete. Turtle '" .. turtle_id .. "' is operational.")
- send_chat_message("Turtle '" .. turtle_id .. "' online and ready.")
- while not in_error_state do
- local event_data = {os.pullEvent("chat")} -- Wait indefinitely for a chat event
- local event_type = event_data[1]
- if event_type == "chat" then
- local username = event_data[2]
- local message_content = event_data[3]
- local success, err = pcall(handle_chat_message, username, message_content)
- if not success then
- local error_message_log = "Runtime error during command processing: " .. tostring(err)
- print(error_message_log)
- term.setTextColor(colors.red)
- -- Attempt to print stack trace if available (err might contain it)
- if type(err) == "string" and err:match(":%d+:") then -- Basic check for stack trace like string
- print(err)
- else
- -- If no obvious stack trace, print a generic part of the error.
- -- This part is tricky as CC error objects can vary.
- debug.traceback("Error in handle_chat_message", 2) -- Print current stack trace
- end
- term.setTextColor(colors.white)
- send_chat_message("An internal error occurred processing a command. Check my console. I might need a reset.")
- -- Decide if this should go to full error_state() or try to continue.
- -- For now, it logs and continues listening for more chat commands.
- -- Consider error_state(error_message_log) for critical failures.
- end
- end
- -- Loop continues if not in_error_state
- end
- print("Main loop exited due to error state.")
- end
- -- Execute main function
- main()
- if not in_error_state then
- print("Turtle script finished unexpectedly without entering error state.")
- end
- --#endregion Main Program
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement