Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- --[[
- Spatial Helper Turtle
- Version 1.7
- --]]
- -- Configuration
- local DISK_DATA_FILE = "disks.json"
- local HELPER_NAME_FILE = "helper_name.json"
- local CHAT_MODULE_NAME = "chatBox"
- local TEMP_SLOT_FOR_STAGING_AND_SUCKING = 16 -- Utility slot for sucking and staging new disks
- local MAX_REGISTERED_DISKS = 15
- -- Chat Appearance
- local HELPER_CHAT_BRACKET_COLOR = "&9"
- local HELPER_CHAT_ERROR_BRACKET_COLOR = "&c"
- -- Turtle State
- local disk_inventory_data = {}
- local chat_box = nil
- local helper_turtle_name = "" -- e.g., "Cube123"
- local in_error_state = false
- -- pending_registration_info stores context between 'start' and 'end' commands for new disk registration.
- -- Structure: { staging_slot = number, target_slot_if_direct = number_or_nil, existing_reg_at_staging_slot = table_or_nil }
- local pending_registration_info = nil
- --#region Helper Functions (Core Utilities)
- local function get_helper_chat_tag_text()
- -- Generates the "[CubeXXX]" part of the chat messages.
- if helper_turtle_name and helper_turtle_name ~= "" then
- return "[" .. helper_turtle_name .. "]"
- else
- return "[Helper]" -- Fallback if name isn't set (should only be during initial prompt)
- end
- end
- function error_state(reason)
- -- Centralized error handling: logs, sends chat (if possible), and halts turtle.
- term.setTextColor(colors.red)
- print("ERROR: " .. reason)
- term.setTextColor(colors.white)
- print("Helper Turtle (" .. (helper_turtle_name or "Unnamed") .. ") is now locked down.")
- in_error_state = true
- if chat_box then
- pcall(chat_box.sendMessage, "CRITICAL ERROR: " .. reason .. " Halting operations.", get_helper_chat_tag_text(), "[]", HELPER_CHAT_ERROR_BRACKET_COLOR)
- end
- while true do os.sleep(3600) end -- Effectively halts the turtle
- end
- local function tonumber_strict(val, context_msg)
- -- Converts a value to a number, calling error_state if conversion fails or value is nil.
- local num = tonumber(val)
- if num == nil then
- error_state("Type error: Expected a number for " .. (context_msg or "value") .. ", got " .. type(val) .. " (" .. tostring(val) .. ")")
- end
- return num
- end
- local function canonicalize_name(name_str) -- For standardizing operator turtle names
- 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_body, is_error_level)
- -- Sends a formatted message to chat via the chatBox peripheral.
- if chat_box then
- local bracket_color_to_use = is_error_level and HELPER_CHAT_ERROR_BRACKET_COLOR or HELPER_CHAT_BRACKET_COLOR
- local success, err = pcall(chat_box.sendMessage, message_body, get_helper_chat_tag_text(), "[]", bracket_color_to_use)
- if not success then
- print("Warning: Failed to send chat message: " .. tostring(err))
- end
- os.sleep(tonumber_strict(1, "chat cooldown sleep")) -- Prevent spam
- else
- -- Fallback to console if chat_box is not available (e.g., during early init or error)
- local console_prefix = (is_error_level and HELPER_CHAT_ERROR_BRACKET_COLOR or HELPER_CHAT_BRACKET_COLOR) .. get_helper_chat_tag_text() .. "&r"
- print("Chat: " .. console_prefix .. " " .. message_body)
- end
- end
- local function file_exists(path) return fs.exists(path) end
- local function load_json_file(path, context_for_error)
- -- Generic JSON file loader with error handling.
- if not file_exists(path) then return nil end -- File not existing is not an error here, handled by caller.
- local handle = fs.open(path, "r")
- if not handle then error_state("Cannot open " .. path .. " for reading (" .. context_for_error .. ").") return nil end
- local content = handle.readAll()
- handle.close()
- local success, data = pcall(textutils.unserialiseJSON, content)
- if not success then error_state("Cannot parse JSON from " .. path .. " (" .. context_for_error .. "). Error: " .. tostring(data)) return nil end
- return data
- end
- local function save_json_file(path, data, context_for_error)
- -- Generic JSON file saver with error handling.
- local success, serialized_data = pcall(textutils.serialiseJSON, data, {compact=false}) -- Use compact=true for smaller files if preferred
- if not success or not serialized_data then error_state("Failed to serialize " .. context_for_error .. ". Error: " .. tostring(serialized_data)) return false end
- local handle = fs.open(path, "w")
- if not handle then error_state("Cannot open " .. path .. " for writing (" .. context_for_error .. ").") return false end
- handle.write(serialized_data)
- handle.close()
- print(context_for_error .. " saved to " .. path)
- return true
- end
- local function load_disk_data()
- -- Loads and validates the disk inventory from DISK_DATA_FILE.
- local data = load_json_file(DISK_DATA_FILE, "disk inventory data")
- if not data then
- disk_inventory_data = {} -- Initialize empty if file doesn't exist
- return file_exists(DISK_DATA_FILE) == false -- True if genuinely new, false if load_json_file failed on existing file
- end
- disk_inventory_data = data
- -- Validate each entry
- for i, disk_entry in ipairs(disk_inventory_data) do
- if not disk_entry.slot_number then
- error_state("Corrupt " .. DISK_DATA_FILE .. ": 'slot_number' missing for disk entry '"..tostring(disk_entry.user_given_name).."'.")
- return false
- end
- disk_entry.slot_number = tonumber_strict(disk_entry.slot_number, "disk_entry.slot_number for '"..tostring(disk_entry.user_given_name).."' in " .. DISK_DATA_FILE)
- if disk_entry.slot_number == TEMP_SLOT_FOR_STAGING_AND_SUCKING then
- error_state("Corrupt " .. DISK_DATA_FILE .. ": Disk '" .. tostring(disk_entry.user_given_name) .. "' is registered to the TEMP_SLOT (" .. TEMP_SLOT_FOR_STAGING_AND_SUCKING .. "). This slot is reserved for utility.")
- return false
- end
- if disk_entry.slot_number < 1 or disk_entry.slot_number > MAX_REGISTERED_DISKS then
- error_state("Corrupt " .. DISK_DATA_FILE .. ": Disk '" .. tostring(disk_entry.user_given_name) .. "' is registered to invalid slot " .. disk_entry.slot_number .. ". Valid slots are 1-" .. MAX_REGISTERED_DISKS ..".")
- return false
- end
- -- Further validation (e.g., for user_given_name, nbt presence) could be added here.
- end
- if #disk_inventory_data > MAX_REGISTERED_DISKS then
- error_state("Corrupt " .. DISK_DATA_FILE .. ": Contains " .. #disk_inventory_data .. " disk entries, but maximum allowed is " .. MAX_REGISTERED_DISKS .. ".")
- return false
- end
- return true
- end
- local function save_disk_data()
- -- Saves the current disk_inventory_data to DISK_DATA_FILE.
- if #disk_inventory_data > MAX_REGISTERED_DISKS then
- -- This check is crucial to prevent saving an oversized inventory.
- error_state("Attempted to save disk data with " .. #disk_inventory_data .. " entries, exceeding maximum of " .. MAX_REGISTERED_DISKS .. ". Critical error, data not saved.")
- return false
- end
- return save_json_file(DISK_DATA_FILE, disk_inventory_data, "Disk inventory data")
- end
- local function compare_nbt_data(nbt1, nbt2)
- -- Compares two NBT data tables by serializing them to JSON strings.
- if nbt1 == nil and nbt2 == nil then return true end -- Both nil means they are the same
- if type(nbt1) ~= type(nbt2) then return false end -- Different types means not the same
- if type(nbt1) == "table" then -- If they are tables, compare their JSON string representations
- local s_nbt1_ok, s_nbt1 = pcall(textutils.serialiseJSON, nbt1)
- local s_nbt2_ok, s_nbt2 = pcall(textutils.serialiseJSON, nbt2)
- if s_nbt1_ok and s_nbt2_ok then
- return s_nbt1 == s_nbt2
- else
- print("Warning: Could not serialize NBT data for comparison. Assuming not equal.")
- return false -- If serialization fails, cannot reliably compare; assume not equal.
- end
- end
- return nbt1 == nbt2 -- For non-table types (though NBT usually is table or nil)
- end
- local function find_disk_by_slot(slot_num_val) -- Searches registered storage slots (1-MAX_REGISTERED_DISKS)
- local slot_num = tonumber_strict(slot_num_val, "find_disk_by_slot argument")
- if slot_num == TEMP_SLOT_FOR_STAGING_AND_SUCKING then return nil end -- Registered disks are not in the temp slot.
- for i, disk_entry in ipairs(disk_inventory_data) do
- if disk_entry.slot_number == slot_num then
- return disk_entry, i -- Return the disk entry table and its index in disk_inventory_data
- end
- end
- return nil -- Not found
- end
- local function find_disk_by_user_name(user_name)
- -- Finds a registered disk by its user-given name (case-insensitive).
- if type(user_name) ~= "string" then return nil end
- local lower_user_name = user_name:lower()
- for i, disk_entry in ipairs(disk_inventory_data) do
- if type(disk_entry.user_given_name) == "string" and disk_entry.user_given_name:lower() == lower_user_name then
- return disk_entry, i -- Return the disk entry table and its index
- end
- end
- return nil -- Not found
- end
- local function find_disk_lent_to_operator(operator_id_canonical)
- -- Finds a disk that is currently marked as lent to a specific operator.
- for i, disk_entry in ipairs(disk_inventory_data) do
- if disk_entry.lent_to_operator_id == operator_id_canonical then
- return disk_entry, i -- Return the disk entry table and its index
- end
- end
- return nil -- Not found
- end
- local function get_next_available_storage_slot()
- -- Finds the first available (unused) slot number between 1 and MAX_REGISTERED_DISKS.
- local used_slots = {}
- for _, disk_entry in ipairs(disk_inventory_data) do
- used_slots[disk_entry.slot_number] = true
- end
- for i = 1, MAX_REGISTERED_DISKS do
- if not used_slots[i] then
- return i -- Return the first unused slot number
- end
- end
- return nil -- No storage slot available
- end
- local function start_timed_action(delay_val, action_type, data_for_action)
- -- Starts a timer for a delayed action and stores action details.
- local delay = tonumber_strict(delay_val, "start_timed_action delay")
- local timer_id = os.startTimer(delay)
- if not _G.active_timers then _G.active_timers = {} end -- Ensure the global table for timers exists
- _G.active_timers[timer_id] = {
- type = action_type,
- data = data_for_action,
- attempts_left = data_for_action.max_attempts or 20 -- Default max attempts for retriable actions
- }
- print("Timer " .. timer_id .. " started for action '" .. action_type .. "' in " .. delay .. "s.")
- return timer_id
- end
- --#endregion
- --#region Name Initialization
- local function initialize_helper_name()
- -- Loads the helper turtle's name from HELPER_NAME_FILE or prompts the user if not found/invalid.
- local name_data = load_json_file(HELPER_NAME_FILE, "helper name data")
- local valid_name_loaded = false
- if name_data and type(name_data.name) == "string" then
- -- Validate the "CubeXXX" format (1-999)
- local num_part_str = name_data.name:match("^Cube([1-9]%d*)$") -- Number must start with 1-9
- if num_part_str then
- local num = tonumber(num_part_str)
- if num and num >= 1 and num <= 999 then
- helper_turtle_name = name_data.name
- print("Helper name loaded: " .. helper_turtle_name)
- valid_name_loaded = true
- end
- end
- end
- if not valid_name_loaded then
- if name_data then print("Invalid name data found in " .. HELPER_NAME_FILE .. ". Expected format: Cube<1-999>.") else print(HELPER_NAME_FILE .. " not found.") end
- print("Please assign a number (1-999) for this Helper Cube.")
- local num_str_input, num_input
- local initial_prompt_prefix = HELPER_CHAT_BRACKET_COLOR .. "[Helper]" .. "&r" -- Static prefix for this one-time prompt
- repeat
- term.write(initial_prompt_prefix .. " Enter number (1-999): ")
- num_str_input = read()
- num_input = tonumber(num_str_input)
- if not (num_input and num_input >= 1 and num_input <= 999 and math.floor(num_input) == num_input) then
- print("Invalid input. Please enter a whole number between 1 and 999.")
- num_input = nil -- Reset to continue loop
- end
- until num_input
- helper_turtle_name = "Cube" .. num_input
- if not save_json_file(HELPER_NAME_FILE, {name = helper_turtle_name}, "Helper name data") then
- error_state("Critical: Failed to save helper name to " .. HELPER_NAME_FILE .. " after initial setup!")
- end
- print("Helper name set to: " .. helper_turtle_name)
- end
- end
- --#endregion
- --#region Startup and Inventory Audit
- local function verify_inventory_on_startup()
- -- Checks the turtle's physical inventory against the registered disk data.
- print("Verifying inventory (" .. helper_turtle_name .. ")...")
- local all_registered_slots_accounted_for = {} -- Keeps track of which storage slots have a verified registered disk
- -- Step 1: Verify all registered disks (expected in slots 1-MAX_REGISTERED_DISKS)
- for _, disk_reg_entry in ipairs(disk_inventory_data) do
- local slot_num = tonumber_strict(disk_reg_entry.slot_number, "disk_reg_entry.slot_number in verify_inventory")
- -- Double-check that no registered disk is in the TEMP_SLOT (should be caught by load_disk_data too)
- if slot_num == TEMP_SLOT_FOR_STAGING_AND_SUCKING then
- error_state("Startup Error: Disk '" .. disk_reg_entry.user_given_name .. "' is registered to the TEMP_SLOT (" .. slot_num .. "). This slot is reserved for utility purposes.")
- end
- all_registered_slots_accounted_for[slot_num] = true
- local item_details_in_slot = turtle.getItemDetail(slot_num)
- if disk_reg_entry.lent_to_operator_id and disk_reg_entry.lent_to_operator_id ~= "" then
- -- Disk is marked as lent, so the slot should be empty.
- if item_details_in_slot then
- error_state("Startup Error: Disk '" .. disk_reg_entry.user_given_name .. "' in slot " .. slot_num .. " is physically present but its registration indicates it is lent to operator '" .. disk_reg_entry.lent_to_operator_id .. "'.")
- end
- else
- -- Disk should be present in its registered slot.
- if not item_details_in_slot then
- error_state("Startup Error: Registered disk '" .. disk_reg_entry.user_given_name .. "' is missing from its designated slot " .. slot_num .. ".")
- end
- -- Verify NBT data if disk is present.
- if not compare_nbt_data(item_details_in_slot.nbt, disk_reg_entry.nbt) then
- error_state("Startup Error: NBT data mismatch for registered disk '" .. disk_reg_entry.user_given_name .. "' in slot " .. slot_num .. ".")
- end
- end
- end
- -- Step 2: Check all physical storage slots (1-MAX_REGISTERED_DISKS) for any unregistered items.
- for i = 1, MAX_REGISTERED_DISKS do
- local item_details_in_slot = turtle.getItemDetail(i)
- if item_details_in_slot and not all_registered_slots_accounted_for[i] then
- -- An item exists in a storage slot that has no corresponding registration.
- error_state("Startup Error: Unregistered item '" .. item_details_in_slot.name .. "' found in storage slot " .. i .. ". All items in storage slots 1-" .. MAX_REGISTERED_DISKS .. " must be registered.")
- end
- end
- -- Step 3: Check the TEMP_SLOT_FOR_STAGING_AND_SUCKING (slot 16).
- -- This slot should ideally be empty on startup. If not, it might be leftover from a crash.
- local item_in_temp_slot = turtle.getItemDetail(TEMP_SLOT_FOR_STAGING_AND_SUCKING)
- if item_in_temp_slot then
- print("Warning: Item '" .. item_in_temp_slot.name .. "' found in TEMP_SLOT (" .. TEMP_SLOT_FOR_STAGING_AND_SUCKING .. ") on startup. This slot should ideally be clear. Manual check recommended.")
- -- Future enhancement: Could attempt to identify and return unknown items from temp slot to chest.
- end
- print("Inventory verification successful for " .. helper_turtle_name .. ".")
- return true
- end
- local function initialize()
- -- Main initialization sequence for the turtle.
- print("Helper Turtle Initializing...")
- initialize_helper_name() -- Sets helper_turtle_name, crucial for logging and chat.
- _G.active_timers = {} -- Initialize global table for managing active timers.
- 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 peripheral found for " .. helper_turtle_name .. ".")
- if not load_disk_data() then
- -- load_disk_data calls error_state on critical failure or returns false.
- -- If it returns false without error_state, it means file didn't exist and was initialized empty (which is fine).
- -- If error_state was called, script would halt there. This check ensures we don't proceed if load truly failed.
- if file_exists(DISK_DATA_FILE) then -- If file existed but load failed
- error_state("Failed to correctly load existing disk data. Please check " .. DISK_DATA_FILE)
- end
- -- If we are here, and load_disk_data returned false, it means disk_inventory_data is empty.
- end
- print("Disk data loaded for " .. helper_turtle_name .. ". " .. #disk_inventory_data .. " of " .. MAX_REGISTERED_DISKS .. " maximum disks registered.")
- if not verify_inventory_on_startup() then
- -- verify_inventory_on_startup calls error_state on failure.
- return false
- end
- print("Initialization complete. " .. helper_turtle_name .. " is operational.")
- send_chat_message("Online and ready.")
- return true
- end
- --#endregion
- --#region Disk Interaction Logic (Providing and Retrieving Disks)
- local function attempt_suck_and_verify(expected_disk_entry, operation_context)
- -- This function attempts to suck a disk from the chest (front),
- -- verifies its NBT against expected_disk_entry.nbt,
- -- and if it matches, moves it from the TEMP_SLOT to the disk's registered storage slot.
- print(helper_turtle_name .. ": Attempting to suck disk for '" .. operation_context .. "' (expecting '" .. expected_disk_entry.user_given_name .. "') into temp slot " .. TEMP_SLOT_FOR_STAGING_AND_SUCKING)
- local original_selected_slot = tonumber_strict(turtle.getSelectedSlot(), "original_selected_slot in attempt_suck")
- turtle.select(tonumber_strict(TEMP_SLOT_FOR_STAGING_AND_SUCKING, "suck attempt select temp slot"))
- if turtle.getItemCount(TEMP_SLOT_FOR_STAGING_AND_SUCKING) > 0 then
- print("Warning (" .. helper_turtle_name .. "): TEMP_SLOT ("..TEMP_SLOT_FOR_STAGING_AND_SUCKING..") is not empty before sucking. Current item will be overwritten if suck is successful.")
- end
- if turtle.suck() then -- Suck from front into the selected TEMP_SLOT
- local sucked_item_detail = turtle.getItemDetail(TEMP_SLOT_FOR_STAGING_AND_SUCKING)
- if not sucked_item_detail then
- print(helper_turtle_name .. ": Sucked something but failed to get its details (from temp slot " .. TEMP_SLOT_FOR_STAGING_AND_SUCKING .. "). No action taken with sucked item.")
- turtle.select(original_selected_slot)
- return false -- Can't verify if we can't get details
- end
- -- NBT Verification
- if compare_nbt_data(sucked_item_detail.nbt, expected_disk_entry.nbt) then
- local target_storage_slot = tonumber_strict(expected_disk_entry.slot_number, "target_storage_slot for retrieved disk")
- -- Sanity check: Registered disks should NEVER have TEMP_SLOT as their target_storage_slot.
- if target_storage_slot == TEMP_SLOT_FOR_STAGING_AND_SUCKING then
- error_state("CRITICAL LOGIC ERROR (" .. helper_turtle_name .. "): Retrieved disk '"..expected_disk_entry.user_given_name.."' NBT matches, but its target storage slot IS the TEMP_SLOT. Registered disks cannot be stored in TEMP_SLOT.")
- end
- print(helper_turtle_name .. ": NBT MATCH for '" .. expected_disk_entry.user_given_name .. "'. Moving from temp slot " .. TEMP_SLOT_FOR_STAGING_AND_SUCKING .. " to target slot " .. target_storage_slot)
- -- Ensure target storage slot is empty before transferring
- turtle.select(target_storage_slot)
- if turtle.getItemCount(target_storage_slot) > 0 then
- local item_in_target_details = turtle.getItemDetail(target_storage_slot)
- error_state("CRITICAL (" .. helper_turtle_name .. "): Target storage slot " .. target_storage_slot .. " for '" .. expected_disk_entry.user_given_name ..
- "' is ALREADY OCCUPIED by '" .. (item_in_target_details and item_in_target_details.name or "UNKNOWN ITEM") .. "' before transfer from temp slot!")
- end
- -- Perform the transfer from TEMP_SLOT to target_storage_slot
- turtle.select(tonumber_strict(TEMP_SLOT_FOR_STAGING_AND_SUCKING, "transfer select temp slot"))
- if not turtle.transferTo(target_storage_slot, sucked_item_detail.count) then
- error_state("CRITICAL (" .. helper_turtle_name .. "): Failed to transfer retrieved disk '" .. expected_disk_entry.user_given_name .. "' from temp slot " .. TEMP_SLOT_FOR_STAGING_AND_SUCKING .. " to target slot " .. target_storage_slot)
- end
- -- Update disk registration: no longer lent out
- expected_disk_entry.lent_to_operator_id = nil
- if not save_disk_data() then
- error_state("CRITICAL (" .. helper_turtle_name .. "): Failed to save disk data after retrieving disk '"..expected_disk_entry.user_given_name .."'.")
- end
- send_chat_message("Successfully retrieved and stored disk '" .. expected_disk_entry.user_given_name .. "' (" .. operation_context .. ").")
- turtle.select(original_selected_slot)
- return true
- else
- -- NBT Mismatch: Sucked item is not the expected disk
- send_chat_message("WARNING: Retrieved an item into temp slot " .. TEMP_SLOT_FOR_STAGING_AND_SUCKING .. " while expecting '" .. expected_disk_entry.user_given_name ..
- "', but NBT MISMATCH (" .. operation_context .. "). Returning item to chest.", true)
- print(helper_turtle_name .. ": NBT Mismatch. Expected NBT for '" .. expected_disk_entry.user_given_name .. "'. Got item: '" .. sucked_item_detail.name .. "' with NBT: " .. textutils.serialize(sucked_item_detail.nbt or {}))
- -- Return the incorrect item to the chest
- turtle.select(tonumber_strict(TEMP_SLOT_FOR_STAGING_AND_SUCKING, "NBT mismatch return select temp slot"))
- turtle.drop()
- turtle.select(original_selected_slot)
- return false
- end
- end
- -- turtle.suck() failed, nothing was picked up from the chest
- print(helper_turtle_name .. ": Suck attempt failed for '".. operation_context .."'; chest might be empty or item not accessible.")
- turtle.select(original_selected_slot)
- return false
- end
- local function handle_provide_disk(disk_to_provide_entry, operator_id_canonical, operator_name_str)
- -- This function takes a registered disk entry, drops it from its storage slot into the chest (front),
- -- and marks it as lent to the specified operator.
- if disk_to_provide_entry.lent_to_operator_id and disk_to_provide_entry.lent_to_operator_id ~= "" then
- send_chat_message("Disk '" .. disk_to_provide_entry.user_given_name .. "' is already lent to '" .. disk_to_provide_entry.lent_to_operator_id .. "'. Cannot provide to '"..operator_name_str.."'.")
- return
- end
- local storage_slot_to_use = tonumber_strict(disk_to_provide_entry.slot_number, "storage_slot_to_use in handle_provide_disk")
- -- Sanity check: Registered disks should not be in the TEMP_SLOT for providing.
- if storage_slot_to_use == TEMP_SLOT_FOR_STAGING_AND_SUCKING then
- error_state("LOGIC ERROR (" .. helper_turtle_name .. "): Attempted to provide disk from TEMP_SLOT. Registered disks are stored in slots 1-" .. MAX_REGISTERED_DISKS .. ".")
- end
- local original_selected_slot = tonumber_strict(turtle.getSelectedSlot(), "original_selected_slot in handle_provide_disk")
- turtle.select(storage_slot_to_use)
- if turtle.getItemCount(storage_slot_to_use) == 0 then
- error_state("CRITICAL (" .. helper_turtle_name .. "): Storage slot " .. storage_slot_to_use .. " for disk '" .. disk_to_provide_entry.user_given_name .. "' is empty, but it should be here for providing.")
- end
- print(helper_turtle_name .. ": Providing disk '" .. disk_to_provide_entry.user_given_name .. "' (from slot " .. storage_slot_to_use .. ") for Operator '" .. operator_name_str .. "' into chest (front).")
- if turtle.drop() then -- Drop from selected slot (storage_slot_to_use) into the chest in front
- disk_to_provide_entry.lent_to_operator_id = operator_id_canonical
- if not save_disk_data() then
- error_state("CRITICAL (" .. helper_turtle_name .. "): Failed to save disk data after marking disk '"..disk_to_provide_entry.user_given_name .."' as lent.")
- end
- print(helper_turtle_name .. ": Disk '" .. disk_to_provide_entry.user_given_name .. "' placed in chest for Operator '" .. operator_name_str .. "'. Marked as lent.")
- -- Schedule a reclaim check in case the operator doesn't pick it up
- start_timed_action(7, "reclaim_provided_disk", { disk_user_name = disk_to_provide_entry.user_given_name, operator_id = operator_id_canonical, max_attempts = 1 })
- else
- send_chat_message("ERROR: Failed to drop disk '" .. disk_to_provide_entry.user_given_name .. "' into chest for Operator '" .. operator_name_str .. "'.", true)
- end
- turtle.select(original_selected_slot)
- end
- --#endregion
- --#region Chat Command Processing (Operator and New Disk Commands)
- -- Handles @spatial load/unload commands from operator turtles
- local function process_operator_load_command(operator_name_str, requested_disk_user_name)
- local operator_id_canonical = canonicalize_name(operator_name_str)
- print(helper_turtle_name .. ": Processing Operator Load: Operator='" .. operator_name_str .. "' (" .. operator_id_canonical .. "), Requested Disk='" .. requested_disk_user_name .. "'")
- -- Check if this operator was borrowing another disk that should now be returned
- local disk_operator_should_return, _ = find_disk_lent_to_operator(operator_id_canonical)
- if disk_operator_should_return then
- send_chat_message("Awaiting return of disk '" .. disk_operator_should_return.user_given_name .. "' from Operator " .. operator_name_str .. ".")
- start_timed_action(3, "expect_return_from_operator", {
- disk_user_name = disk_operator_should_return.user_given_name,
- operator_id = operator_id_canonical,
- reason = "Operator " .. operator_name_str .. " is loading a new disk",
- max_attempts = 20, -- Will try to suck for ~2 seconds (20 * 0.1s interval)
- suck_interval = 0.1
- })
- end
- -- Check if we have the disk the operator is requesting to load
- local disk_to_provide, _ = find_disk_by_user_name(requested_disk_user_name)
- if disk_to_provide then
- handle_provide_disk(disk_to_provide, operator_id_canonical, operator_name_str)
- else
- -- Not an error, another helper might have it. Just log locally.
- print(helper_turtle_name .. ": We do not have the requested disk '" .. requested_disk_user_name .. "' for Operator " .. operator_name_str .. ".")
- end
- end
- local function process_operator_unload_command(operator_name_str)
- local operator_id_canonical = canonicalize_name(operator_name_str)
- print(helper_turtle_name .. ": Processing Operator Unload: Operator='" .. operator_name_str .. "' (" .. operator_id_canonical .. ")")
- -- Check if the unloading operator was borrowing a disk from us
- local disk_being_unloaded, _ = find_disk_lent_to_operator(operator_id_canonical)
- if disk_being_unloaded then
- send_chat_message("Awaiting return of disk '" .. disk_being_unloaded.user_given_name .. "' from Operator " .. operator_name_str .. ".")
- start_timed_action(3, "expect_return_from_operator", {
- disk_user_name = disk_being_unloaded.user_given_name,
- operator_id = operator_id_canonical,
- reason = "Operator " .. operator_name_str .. " is unloading a disk",
- is_critical_return = true, -- NBT mismatch on this return is more serious
- max_attempts = 20,
- suck_interval = 0.1
- })
- else
- -- Not an error, the operator might have unloaded a disk they got from elsewhere or had nothing.
- print(helper_turtle_name .. ": Operator " .. operator_name_str .. " unloaded, but they were not borrowing any disk from us.")
- end
- end
- -- Handles the '@newDisk register start [slot_num]' command
- local function handle_new_disk_register_start(optional_slot_num_str)
- if pending_registration_info then
- send_chat_message("A disk registration process is already active. Please complete it with the 'end' command or restart the turtle to cancel.", true)
- return
- end
- local specified_storage_slot = nil
- if optional_slot_num_str and optional_slot_num_str ~= "" then
- specified_storage_slot = tonumber(optional_slot_num_str)
- if not (specified_storage_slot and specified_storage_slot >= 1 and specified_storage_slot <= MAX_REGISTERED_DISKS) then
- send_chat_message("Invalid slot number '" .. optional_slot_num_str .. "' provided for 'start'. Must be a storage slot between 1 and " .. MAX_REGISTERED_DISKS .. ".", true)
- return
- end
- end
- if specified_storage_slot then
- -- User specified a storage slot (1-MAX_REGISTERED_DISKS) to check for coal.
- print(helper_turtle_name .. ": Checking specified slot " .. specified_storage_slot .. ".")
- local item_in_spec_slot = turtle.getItemDetail(specified_storage_slot)
- if item_in_spec_slot and item_in_spec_slot.name == "minecraft:coal_block" then
- local existing_reg_at_spec_slot, _ = find_disk_by_slot(specified_storage_slot)
- if existing_reg_at_spec_slot then
- send_chat_message("Slot " .. specified_storage_slot .. " is already registered to disk '" .. existing_reg_at_spec_slot.user_given_name .. "'. To overwrite this slot's registration with a new disk use force'.")
- pending_registration_info = {
- staging_slot = specified_storage_slot, -- The new disk will replace coal here.
- target_slot_if_direct = specified_storage_slot, -- This slot is the final destination for the new disk.
- existing_reg_at_staging_slot = existing_reg_at_spec_slot -- Store info about what's being overwritten.
- }
- else -- Coal is in a specified, available (empty and unregistered) storage slot.
- send_chat_message("Slot " .. specified_storage_slot .. " is ready.")
- pending_registration_info = {
- staging_slot = specified_storage_slot,
- target_slot_if_direct = specified_storage_slot,
- existing_reg_at_staging_slot = nil
- }
- end
- end
- else
- -- No specific slot provided by user, search for coal block, prioritizing utility slot.
- print(helper_turtle_name .. ": No slot specified")
- local item_in_utility_slot = turtle.getItemDetail(TEMP_SLOT_FOR_STAGING_AND_SUCKING)
- if item_in_utility_slot and item_in_utility_slot.name == "minecraft:coal_block" then
- send_chat_message("Utility slot " .. TEMP_SLOT_FOR_STAGING_AND_SUCKING .. " is ready.")
- pending_registration_info = {
- staging_slot = TEMP_SLOT_FOR_STAGING_AND_SUCKING,
- target_slot_if_direct = nil, -- Target will be the next available storage slot, not the utility slot.
- existing_reg_at_staging_slot = nil
- }
- else
- -- If not in utility slot, search storage slots 1-MAX_REGISTERED_DISKS.
- local found_coal_in_storage_slot_num = nil
- for i = 1, MAX_REGISTERED_DISKS do
- local item_in_scan_slot = turtle.getItemDetail(i)
- if item_in_scan_slot and item_in_scan_slot.name == "minecraft:coal_block" then
- found_coal_in_storage_slot_num = i
- break
- end
- end
- if found_coal_in_storage_slot_num then
- local existing_reg_at_found_slot, _ = find_disk_by_slot(found_coal_in_storage_slot_num)
- if existing_reg_at_found_slot then
- send_chat_message("Storage slot " .. found_coal_in_storage_slot_num .. " is currently registered to disk '" .. existing_reg_at_found_slot.user_given_name .. "'. To overwrite this slot's registration use force.")
- pending_registration_info = {
- staging_slot = found_coal_in_storage_slot_num,
- target_slot_if_direct = found_coal_in_storage_slot_num,
- existing_reg_at_staging_slot = existing_reg_at_found_slot
- }
- else -- Coal found in an available (empty, unregistered) storage slot.
- send_chat_message("Storage slot " .. found_coal_in_storage_slot_num .. "is ready.")
- pending_registration_info = {
- staging_slot = found_coal_in_storage_slot_num,
- target_slot_if_direct = found_coal_in_storage_slot_num,
- existing_reg_at_staging_slot = nil
- }
- end
- end
- end
- end
- end
- -- Handles the '@newDisk register end <NewDiskName> [force]' command
- local function handle_new_disk_register_end(new_disk_name_argument, force_str_argument)
- -- Step 0: Basic Validations
- if not pending_registration_info then
- return
- end
- local force_override_active = (type(force_str_argument) == "string" and force_str_argument:lower() == "force")
- local staging_slot_info = pending_registration_info -- Keep a reference
- pending_registration_info = nil -- Crucial: Consume the pending state immediately to prevent re-entry
- local item_in_staging_slot = turtle.getItemDetail(staging_slot_info.staging_slot)
- if not item_in_staging_slot then
- error_state("Disk Registration End Error: The staging slot " .. staging_slot_info.staging_slot .. " is now empty. Expected the new disk to be here.")
- return
- end
- if item_in_staging_slot.name == "minecraft:coal_block" then
- error_state("Disk Registration End Error: Staging slot " .. staging_slot_info.staging_slot .. " not empty.")
- return
- end
- local new_disk_nbt_from_item_in_staging = item_in_staging_slot.nbt
- -- Step 1: Determine the type of registration and the final target storage slot.
- local final_target_storage_slot = nil
- local existing_registration_to_update_idx = nil -- If we are overwriting an existing registration by its name.
- local old_registration_to_remove_idx = nil -- If we are overwriting a slot that had a *different* named disk.
- local old_disk_name_being_replaced = nil -- Name of the disk being physically replaced/unregistered.
- -- Scenario A: Does the <NewDiskName> conflict with an existing registered disk name?
- local existing_reg_if_name_matches, idx_if_name_matches = find_disk_by_user_name(new_disk_name_argument)
- if existing_reg_if_name_matches then
- -- The provided new_disk_name_argument is already in use.
- if force_override_active then
- send_chat_message("The name '" .. new_disk_name_argument .. "' is already registered. 'force' is active: Proceeding to overwrite the existing registration for '" .. new_disk_name_argument .. "' at its current slot " .. existing_reg_if_name_matches.slot_number .. ".", false)
- final_target_storage_slot = existing_reg_if_name_matches.slot_number -- The new disk will go into the old disk's slot.
- existing_registration_to_update_idx = idx_if_name_matches -- We will update this entry in disk_inventory_data.
- old_disk_name_being_replaced = existing_reg_if_name_matches.user_given_name -- This is the disk being replaced.
- else
- send_chat_message("The disk name '" .. new_disk_name_argument .. "' is already registered for a disk at slot " .. existing_reg_if_name_matches.slot_number .. ". If you intend to replace it, use the 'force' argument. Registration aborted. Returning the disk from staging slot " .. staging_slot_info.staging_slot .. " to the chest.", true)
- turtle.select(staging_slot_info.staging_slot) turtle.drop()
- return
- end
- -- Scenario B: Was the staging_slot (where coal was, now new disk is) already registered to a *different* disk?
- elseif staging_slot_info.existing_reg_at_staging_slot then
- -- staging_slot_info.existing_reg_at_staging_slot contains the data of the disk that *was* registered to staging_slot_info.staging_slot
- if force_override_active then
- -- User wants to overwrite the disk that was in staging_slot_info.staging_slot with the new disk (and new_disk_name_argument).
- send_chat_message("The staging slot " .. staging_slot_info.staging_slot .. " was previously registered to disk '" .. staging_slot_info.existing_reg_at_staging_slot.user_given_name .. "'. 'force' is active: Overwriting this slot with the new disk named '" .. new_disk_name_argument .. "'.", false)
- final_target_storage_slot = staging_slot_info.staging_slot -- The new disk stays/goes here.
- old_disk_name_being_replaced = staging_slot_info.existing_reg_at_staging_slot.user_given_name
- -- We need to find the index of this old registration to remove it.
- local _, old_idx = find_disk_by_user_name(old_disk_name_being_replaced)
- if old_idx then registration_to_remove_due_to_slot_overwrite_idx = old_idx
- else error_state("Logic Error: Could not find index for old disk '"..old_disk_name_being_replaced.."' during slot overwrite.") return end -- Should not happen
- else
- send_chat_message("The staging slot " .. staging_slot_info.staging_slot .. " is already registered to disk '" .. staging_slot_info.existing_reg_at_staging_slot.user_given_name .. "'. To overwrite this slot, use the 'force' argument. Registration aborted. Returning the disk from staging slot " .. staging_slot_info.staging_slot .. " to the chest.", true)
- turtle.select(staging_slot_info.staging_slot) turtle.drop()
- return
- end
- -- Scenario C: This is a completely new disk registration.
- else
- if #disk_inventory_data >= MAX_REGISTERED_DISKS then
- send_chat_message("Cannot register new disk '" .. new_disk_name_argument .. "': Maximum capacity of " .. MAX_REGISTERED_DISKS .. " disks has been reached. Returning the disk from staging slot " .. staging_slot_info.staging_slot .. " to the chest.", true)
- turtle.select(staging_slot_info.staging_slot) turtle.drop()
- return
- end
- if staging_slot_info.target_slot_if_direct then
- -- The user placed coal in an empty, unregistered storage slot (1-MAX_REGISTERED_DISKS).
- final_target_storage_slot = staging_slot_info.target_slot_if_direct
- else
- -- The user placed coal in the utility slot (16), or no direct target was set.
- -- Find the next available storage slot (1-MAX_REGISTERED_DISKS).
- final_target_storage_slot = get_next_available_storage_slot()
- if not final_target_storage_slot then
- -- This should ideally be caught by the #disk_inventory_data check above.
- error_state("Logic Error: Disk capacity not full, but no available storage slot was found for the new disk '" .. new_disk_name_argument .. "'.")
- return
- end
- end
- end
- -- Step 2: Perform physical disk operations.
- print(helper_turtle_name .. ": Finalizing registration for '" .. new_disk_name_argument .. "'. Staging Slot: " .. staging_slot_info.staging_slot .. ", Final Target Storage Slot: " .. final_target_storage_slot .. (old_disk_name_being_replaced and ", Overwriting registration of: '"..old_disk_name_being_replaced.."'" or ""))
- -- If final_target_storage_slot currently holds an old disk (because we are overwriting), clear that physical disk.
- if old_disk_name_being_replaced then
- turtle.select(final_target_storage_slot)
- if turtle.getItemCount(final_target_storage_slot) > 0 then
- -- Ensure the item in the target slot is indeed the one we think we're replacing (optional, good for safety)
- -- local item_details = turtle.getItemDetail(final_target_storage_slot)
- -- if item_details and item_details.name ... (compare with old_disk_name_being_replaced's expected item type if known)
- print(helper_turtle_name .. ": Clearing old disk '" .. old_disk_name_being_replaced .. "' from its slot " .. final_target_storage_slot .. " to make way for the new/updated disk.")
- if not turtle.dropUp() then -- Attempt to dispose of it upwards (e.g., into an "obsolete" chest)
- if not turtle.drop() then -- Otherwise, just drop it in front
- send_chat_message("WARNING: Failed to automatically clear the old disk from slot " .. final_target_storage_slot .. " during overwrite. Manual cleanup of that slot may be needed before the new disk can be placed if the turtle failed to move it.", true)
- -- Note: This doesn't stop the registration update, but the physical state might be imperfect.
- end
- end
- end
- elseif turtle.getItemCount(final_target_storage_slot) > 0 then
- -- This case is for a NEW disk registration where the target slot (determined as free) is somehow not.
- local item_in_final_target_details = turtle.getItemDetail(final_target_storage_slot)
- error_state("Disk Registration End Error (" .. helper_turtle_name .. "): The target storage slot " .. final_target_storage_slot .. " for the new disk '"..new_disk_name_argument.."' is unexpectedly occupied by '"..(item_in_final_target_details and item_in_final_target_details.name or "an unknown item").."'. This slot should have been free.")
- return
- end
- -- Move the new disk from the staging_slot to the final_target_storage_slot, if they are different.
- if staging_slot_info.staging_slot ~= final_target_storage_slot then
- turtle.select(tonumber_strict(staging_slot_info.staging_slot, "select staging slot for transfer"))
- if not turtle.transferTo(final_target_storage_slot, item_in_staging_slot.count) then -- item_in_staging_slot was fetched at the start
- error_state("Disk Registration End Error (" .. helper_turtle_name .. "): Failed to move the new disk from staging slot " .. staging_slot_info.staging_slot .. " to its final target storage slot " .. final_target_storage_slot .. ".")
- return
- end
- end
- -- If staging_slot_info.staging_slot IS final_target_storage_slot, the new disk (already verified) is correctly in place.
- -- Step 3: Update the disk_inventory_data array.
- local new_disk_registration_data = {
- user_given_name = new_disk_name_argument,
- nbt = new_disk_nbt_from_item_in_staging, -- NBT from the actual disk item in the staging slot
- slot_number = final_target_storage_slot,
- lent_to_operator_id = nil -- New/overwritten disks are not initially lent
- }
- if existing_registration_to_update_idx then
- -- This means we are overwriting an existing registration identified by its name.
- disk_inventory_data[existing_registration_to_update_idx] = new_disk_registration_data
- send_chat_message("Successfully updated registration for disk '" .. new_disk_name_argument .. "' in slot " .. final_target_storage_slot .. ".")
- elseif registration_to_remove_due_to_slot_overwrite_idx then
- -- This means we are overwriting a slot that was previously registered to a *different* disk name.
- -- The old registration needs to be removed, and the new one added.
- table.remove(disk_inventory_data, registration_to_remove_due_to_slot_overwrite_idx)
- table.insert(disk_inventory_data, new_disk_registration_data) -- Could also re-sort disk_inventory_data by slot_number if desired.
- send_chat_message("Successfully overwrote slot " .. final_target_storage_slot .. " (which was registered to '" .. old_disk_name_being_replaced .. "') with the new disk '" .. new_disk_name_argument .. "'.")
- else
- -- This is a brand new registration for a new disk name in a new slot.
- table.insert(disk_inventory_data, new_disk_registration_data)
- send_chat_message("Successfully registered new disk '" .. new_disk_name_argument .. "' in slot " .. final_target_storage_slot .. ".")
- end
- -- Final step: Save the updated disk inventory data.
- if not save_disk_data() then
- -- save_disk_data would call error_state on failure, but good to have a message.
- send_chat_message("ERROR: Critical failure while saving disk data after registering/updating '" .. new_disk_name_argument .. "'. Check console logs immediately.", true)
- end
- end
- -- Main chat handler (calls the sub-handlers)
- local function handle_chat_message(username, message_content)
- print(helper_turtle_name .. ": Chat from " .. username .. ": " .. message_content)
- local lower_message = message_content:lower()
- local parts = {}
- for part in lower_message:gmatch("%S+") do table.insert(parts, part) end
- if #parts == 0 then return end -- Ignore empty messages
- local trigger_command = parts[1]
- if trigger_command == "@spatial" or trigger_command == "@spat" then
- if #parts < 3 then return end -- Needs at least @spatial <action> <op_name>
- local action = parts[2]
- local operator_name_str = parts[3]
- if action == "load" and #parts >= 4 then
- -- Reconstruct disk name which might have spaces
- local disk_name_parts = {}
- for i = 4, #parts do table.insert(disk_name_parts, parts[i]) end
- local requested_disk_user_name = table.concat(disk_name_parts, " ")
- if requested_disk_user_name ~= "" then
- process_operator_load_command(operator_name_str, requested_disk_user_name)
- else
- send_chat_message("Load command is missing the disk name.", true)
- end
- elseif action == "unload" then
- process_operator_unload_command(operator_name_str)
- else
- send_chat_message("Unknown '@spatial' action: " .. action .. ". Use 'load' or 'unload'.", true)
- end
- elseif trigger_command == "@newdisk" and #parts >= 2 then
- local action = parts[2]
- if action == "register" and #parts >= 3 then
- local sub_action = parts[3]
- if sub_action == "start" then
- local start_arg_slot_num_str = parts[4] or "" -- Optional slot number for 'start'
- handle_new_disk_register_start(start_arg_slot_num_str)
- elseif sub_action == "end" and #parts >= 4 then
- -- Disk name can have spaces. The last argument might be "force".
- local force_keyword_present = false
- local disk_name_argument_parts = {}
- if parts[#parts]:lower() == "force" then
- force_keyword_present = true
- for i = 4, #parts - 1 do table.insert(disk_name_argument_parts, parts[i]) end
- else
- for i = 4, #parts do table.insert(disk_name_argument_parts, parts[i]) end
- end
- local disk_name_argument = table.concat(disk_name_argument_parts, " ")
- local force_str_argument = force_keyword_present and "force" or ""
- if disk_name_argument ~= "" then
- handle_new_disk_register_end(disk_name_argument, force_str_argument)
- else
- send_chat_message("The 'end' command for disk registration requires a disk name.", true)
- end
- else
- send_chat_message("Invalid '@newDisk register' command. Usage: 'start [slot_num]' or 'end <DiskName> [force]'", true)
- end
- else
- send_chat_message("Unknown '@newDisk' action: " .. action .. ". Did you mean 'register'?", true)
- end
- else
- -- Not a command for this turtle or an unrecognised command prefix.
- -- print(helper_turtle_name .. ": Ignoring unrecognised message.")
- end
- end
- --#endregion
- --#region Timer Event Processing & Main Loop
- local function handle_timer(timer_id_val)
- local timer_id = tonumber_strict(timer_id_val, "timer_id in handle_timer")
- if not _G.active_timers then _G.active_timers = {} return end -- Should have been initialized
- local action_details = _G.active_timers[timer_id]
- _G.active_timers[timer_id] = nil -- Consume the timer immediately
- if not action_details then
- print(helper_turtle_name .. ": Warning - Received timer ID " .. timer_id .. " which has no associated action or was already processed.")
- return
- end
- print(helper_turtle_name .. ": Timer " .. timer_id .. " fired for action: " .. action_details.type)
- local disk_entry_for_action, _ = find_disk_by_user_name(action_details.data.disk_user_name)
- if not disk_entry_for_action and (action_details.type == "expect_return_from_operator" or action_details.type == "reclaim_provided_disk") then
- print(helper_turtle_name .. ": Warning - Disk '" .. tostring(action_details.data.disk_user_name) .. "' for timed action type '" .. action_details.type .. "' is no longer registered. Action cancelled.")
- return
- end
- if action_details.type == "expect_return_from_operator" then
- if not disk_entry_for_action.lent_to_operator_id or disk_entry_for_action.lent_to_operator_id == "" then
- print(helper_turtle_name .. ": Disk '" .. action_details.data.disk_user_name .. "' is no longer marked as lent. Return action for operator '"..tostring(action_details.data.operator_id).."' cancelled.")
- return
- end
- local retrieved_successfully = attempt_suck_and_verify(disk_entry_for_action, action_details.data.reason)
- if not retrieved_successfully then
- action_details.attempts_left = action_details.attempts_left - 1
- if action_details.attempts_left > 0 then
- local next_try_delay = action_details.data.suck_interval or 0.1
- local new_timer_id_for_retry = os.startTimer(tonumber_strict(next_try_delay, "timer retry delay"))
- _G.active_timers[new_timer_id_for_retry] = action_details -- Re-queue the action with remaining attempts
- print(helper_turtle_name .. ": Failed to retrieve '"..disk_entry_for_action.user_given_name.."' for '"..action_details.data.reason.."'. Retrying (new timer "..new_timer_id_for_retry..") in "..next_try_delay.."s. Attempts left: "..action_details.attempts_left)
- else
- send_chat_message("Failed to retrieve disk '" .. disk_entry_for_action.user_given_name .. "' after multiple attempts (" .. action_details.data.reason .. "). The disk remains marked as lent to '"..tostring(disk_entry_for_action.lent_to_operator_id).."'.", true)
- -- Consider if is_critical_return should trigger error_state if NBT mismatch was the cause,
- -- but attempt_suck_and_verify handles returning bad NBT items. This is for total failure to retrieve.
- end
- end
- elseif action_details.type == "reclaim_provided_disk" then
- if disk_entry_for_action.lent_to_operator_id == action_details.data.operator_id then
- print(helper_turtle_name .. ": Attempting to reclaim disk '" .. disk_entry_for_action.user_given_name .. "' (as it might not have been picked up by operator '" .. action_details.data.operator_id .. "').")
- if attempt_suck_and_verify(disk_entry_for_action, "reclaim disk not picked up by operator") then
- send_chat_message("Successfully reclaimed disk '" .. disk_entry_for_action.user_given_name .. "' that was likely not picked up by the operator.")
- else
- -- Silent failure on console as per earlier request (it was likely picked up by operator)
- print(helper_turtle_name .. ": Failed to reclaim disk '" .. disk_entry_for_action.user_given_name .. "'. It was likely picked up by the operator or is not in the chest.")
- end
- else
- -- Disk is no longer lent to the operator we expected, or not lent at all.
- print(helper_turtle_name .. ": Reclaim action for disk '" .. disk_entry_for_action.user_given_name .. "' (from operator " .. action_details.data.operator_id .. ") cancelled. Disk's lending status has changed (now lent to: " .. tostring(disk_entry_for_action.lent_to_operator_id) .. ").")
- end
- end
- end
- local function main()
- if not initialize() then
- -- initialize() calls error_state on critical failure.
- print(helper_turtle_name .. ": Initialization failed. Turtle will not operate.")
- return
- end
- while not in_error_state do
- local event_data = {os.pullEvent()} -- Wait for any event
- local event_type = event_data[1]
- -- Wrap event handling in pcall to catch runtime errors within handlers and prevent full script crash.
- local success, err_msg_or_obj = pcall(function()
- if event_type == "chat" then
- handle_chat_message(event_data[2], event_data[3])
- elseif event_type == "timer" then
- handle_timer(event_data[2]) -- Timer ID is event_data[2]
- elseif event_type == "terminate" then
- print(helper_turtle_name .. ": Terminate event received. Shutting down.")
- in_error_state = true -- Set flag to exit the main loop
- end
- end)
- if not success then
- local error_log_message = "Runtime error in main loop (" .. event_type .. " event): " .. tostring(err_msg_or_obj)
- print(error_log_message)
- term.setTextColor(colors.red)
- -- Attempt to print a stack trace if the error object contains it (typical for Lua errors)
- if type(err_msg_or_obj) == "string" and err_msg_or_obj:match(":%d+:") then
- print(err_msg_or_obj)
- else
- -- For other types of errors or if no clear stack trace, use debug.traceback
- debug.traceback("Error occurred in " .. event_type .. " event handler", 2) -- '2' to skip traceback of pcall and this func
- end
- term.setTextColor(colors.white)
- send_chat_message("An internal error occurred. Please check the turtle's console. Operations may be unstable.", true)
- -- Depending on severity, could call error_state here to fully halt.
- -- For now, it logs and attempts to continue, which might be risky.
- end
- end
- print(helper_turtle_name .. " main operating loop has exited.")
- end
- -- Start the turtle's main execution
- main()
- -- Fallback message if main() exits unexpectedly without in_error_state being true.
- if not in_error_state then
- print(helper_turtle_name .. " script finished unexpectedly without entering a formal error state.")
- end
- --#endregion
Add Comment
Please, Sign In to add comment