-- print(textutils.serialize(x)) local function findPeripheralByType(typeName) for _, name in ipairs(peripheral.getNames()) do if peripheral.getType(name) == typeName then return name, name end end return nil, nil end function levenshtein(s1, s2, limit) local len1, len2 = #s1, #s2 if len1 > len2 then s1, s2, len1, len2 = s2, s1, len2, len1 end if len2 - len1 >= limit then return limit end local v0 = {} for i = 1, len2 + 1 do v0[i] = i - 1 end for i = 1, len1 do local v1 = { [1] = i } local min_in_row = i for j = 1, len2 do local cost = s1:sub(i, i) == s2:sub(j, j) and 0 or 1 v1[j + 1] = math.min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost) if v1[j + 1] < min_in_row then min_in_row = v1[j + 1] end end if min_in_row >= limit then return limit end v0 = v1 end return math.min(v0[len2 + 1], limit) end function normalize(s) s = string.lower(s) s = string.gsub(s, " ", "") s = string.gsub(s, ",", "") s = string.gsub(s, "\"", "") s = string.gsub(s, "%*", "") s = string.gsub(s, "%(", "") s = string.gsub(s, "%)", "") s = string.gsub(s, "%[", "") s = string.gsub(s, "%]", "") s = string.gsub(s, "{", "") s = string.gsub(s, "}", "") s = string.gsub(s, "%.", "") s = string.gsub(s, "%-", "") s = string.gsub(s, "'", "") return s end function compareStrings(str1, str2) local norm_str1 = normalize(str1) local norm_str2 = normalize(str2) return levenshtein(norm_str1, norm_str2, 3) end --print("Match (0):", compareStrings("Test String!", "teststring")) --print("Distance 1 (1):", compareStrings("My Test [Disk]", "my test diskk")) --print("Distance 2 (2):", compareStrings("My Test (Disk)", "my best diskk")) --print("Distance 3+ (3):", compareStrings("My Test {Disk}", "your best diskk")) function getDiskName() local oppositeDirection = { top = "down", bottom = "up" } local bridgeName, bridgeLocation = findPeripheralByType("meBridge") if not bridgeName then return nil, "ME Bridge peripheral not found." end local direction = oppositeDirection[bridgeLocation] if not direction then return nil, "ME Bridge must be on top or bottom." end local bridge = peripheral.wrap(bridgeName) local item_details = turtle.getItemDetail(1) if not item_details or not item_details.nbt then return nil, "Item in slot 1 has no NBT data." end local import_filter = { name = item_details.name, nbt = item_details.nbt } local items_before = {} for _, item in ipairs(bridge.listItems()) do items_before[item.fingerprint] = item.count end if bridge.importItem(import_filter, direction, 1) < 1 then return nil, "Failed to import item into ME system." end local items_after = bridge.listItems() local found_item_data = nil for _, item_after in ipairs(items_after) do local count_before = items_before[item_after.fingerprint] or 0 if item_after.count > count_before then found_item_data = item_after break end end if not found_item_data then bridge.exportItem(import_filter, direction) return nil, "CRITICAL: Item imported but vanished inside the ME system." end local disk_name = found_item_data.displayName if bridge.exportItem(found_item_data, direction) < 1 then return nil, "CRITICAL: Failed to export item back. Item is stuck in ME system." end disk_name = string.gsub(disk_name, "[%[%]]", "") disk_name = string.gsub(disk_name, "^%s*(.-)%s*$", "%1") return disk_name, nil end --local name, err = getDiskName() --if name then -- print("Success! Disk name is: '" .. name .. "'") --else -- print("Failed! Reason: " .. err) --end function returnDisk() local oppositeDirection = { top = "down", bottom = "up" } local bridgeName, bridgeLocation = findPeripheralByType("meBridge") if not bridgeName then return nil, "ME Bridge peripheral not found." end local direction = oppositeDirection[bridgeLocation] if not direction then return nil, "ME Bridge must be on top or bottom." end local bridge = peripheral.wrap(bridgeName) local item_details = turtle.getItemDetail(1) if not item_details or not item_details.nbt then return nil, "Item in slot 1 has no NBT data." end local import_filter = { name = item_details.name, nbt = item_details.nbt } if bridge.importItem(import_filter, direction, 1) < 1 then return nil, "Failed to import item into ME system." end end --- Lists all storage disks in the ME system, providing their name and fingerprint. -- @return ({ {name=string, fingerprint=string}, ... }, nil) A table of disk data on success. -- @return (nil, string) An error message on any failure. function listMeDiskNames() local bridgeName = findPeripheralByType("meBridge") if not bridgeName then return nil, "ME Bridge peripheral not found." end local bridge = peripheral.wrap(bridgeName) local all_items = bridge.listItems() local disk_data = {} if not all_items then return nil, "Could not access ME system contents (is it powered and connected?)." end for _, item in ipairs(all_items) do if item.name and string.find(item.name, "storage_cell") then local cleaned_name = string.gsub(item.displayName, "[%[%]]", "") cleaned_name = string.gsub(cleaned_name, "^%s*(.-)%s*$", "%1") table.insert(disk_data, { name = cleaned_name, fingerprint = item.fingerprint }) end end return disk_data, nil end --- Finds a disk by name in the ME system and exports it to the turtle's inventory. -- @param diskNameToFind The name of the disk to search for. -- @return (true, nil) On successful export. -- @return (nil, string) An error message on failure. function getMeDiskByName(diskNameToFind) local oppositeDirection = { top = "down", bottom = "up" } local bridgeName, bridgeLocation = findPeripheralByType("meBridge") if not bridgeName then return nil, "ME Bridge not found." end local direction = oppositeDirection[bridgeLocation] if not direction then return nil, "ME Bridge must be on top or bottom." end local bridge = peripheral.wrap(bridgeName) local disks, err = listMeDiskNames() if err then return nil, err end local found_fingerprint = nil for _, disk in ipairs(disks) do if compareStrings(disk.name, diskNameToFind) == 0 then found_fingerprint = disk.fingerprint break end end if not found_fingerprint then return nil, "Disk with name '" .. diskNameToFind .. "' not found in ME system." end local export_filter = { fingerprint = found_fingerprint } if bridge.exportItem(export_filter, direction, 1) < 1 then return nil, "Found disk but failed to export it from the ME system." end return true, nil end --- A utility function to get a simple, comma-separated string of all disk names. -- @return string A list of names, or an error message. function getDiskNamesAsString() local disks, err = listMeDiskNames() if err then return "Error: " .. err end if #disks == 0 then return "No disks found in the ME system." end local names_only = {} for _, disk in ipairs(disks) do table.insert(names_only, disk.name) end return table.concat(names_only, ", ") end --print("Available disks: " .. getDiskNamesAsString()) function set(filename, content) local file, err = fs.open(filename, "w") if not file then printError("Failed to open file for setting: " .. tostring(err)) return end file.write(content) file.close() end function retrieve(filename) if not fs.exists(filename) then return "" end local file, err = fs.open(filename, "r") if not file then printError("Failed to open file for retrieving: " .. tostring(err)) return "" end local content = file.readAll() file.close() return content end function clear(filename) set(filename, "") end --local test_file = "my_data" --fs.delete(test_file) --print("Initial state: '" .. retrieve(test_file) .. "' (Should be empty)") --set(test_file, "This is the first test.") --print("After set: '" .. retrieve(test_file) .. "'") --clear(test_file) --print("After clear: '" .. retrieve(test_file) .. "' (Should be empty)") --fs.delete(test_file) function isPortReady() local success, data = turtle.inspect() if not success then return false end if data and data.state and data.state.powered then return true end return false end --print("Function isPortReady() returned: " .. tostring(isPortReady())) local function splitString(inputString) local args = {} for word in string.gmatch(inputString, "[^%s]+") do table.insert(args, word) end return args end --- Sends a message with support for multiple, advanced, inline formatted components. -- -- Use { key = 'value', ... } to format text. -- - For clickable links, provide: msg, value, clickAction. -- - For formatted text, provide: msg, and optionally color, underlined. -- -- @param message The template message string. -- @param options (optional) A table of global options for the message. function notifyUser(message, options) local chatBox = peripheral.find("chatBox") if not chatBox then printError("Chat Box not found.") return end -- 1. SET UP GLOBAL DEFAULTS options = options or {} local prefix = options.prefix or "Turtle" local brackets = options.brackets or "[]" local bracketColor = options.bracketColor or "&7" local targetUser = options.user -- 2. PARSE THE MESSAGE TEMPLATE local messageBody = {} local lastPos = 1 while true do local start, finish = string.find(message, "{(.-)}", lastPos) if not start then break end -- Add the plain text before this block, explicitly setting color to white. local beforeText = message:sub(lastPos, start - 1) if #beforeText > 0 then table.insert(messageBody, { text = beforeText, color = "white", underlined = false }) end -- Extract and parse the content inside the {} local inside = message:sub(start + 1, finish - 1) local inlineOpts = {} for key, value in string.gmatch(inside, "(%w+)%s*=%s*'([^']*)'") do inlineOpts[string.gsub(key, "%s", "")] = value end -- Construct the component. if inlineOpts.msg then local component = { text = inlineOpts.msg, color = inlineOpts.color or "aqua", -- Default to aqua if color isn't specified underlined = (inlineOpts.underlined == 'true') } -- **THE FIX**: Only add a clickEvent if both action and value are present. if inlineOpts.clickAction and inlineOpts.value then component.clickEvent = { action = inlineOpts.clickAction, value = inlineOpts.value } end table.insert(messageBody, component) else -- If malformed, treat as plain text. table.insert(messageBody, { text = "{" .. inside .. "}", color = "white", underlined = false }) end lastPos = finish + 1 end -- Add any remaining text after the last block, explicitly setting color to white. local remainingText = message:sub(lastPos) if #remainingText > 0 then table.insert(messageBody, { text = remainingText, color = "white", underlined = false }) end -- 3. SERIALIZE AND SEND local jsonMessage = textutils.serialiseJSON(messageBody) if targetUser and targetUser ~= "" then chatBox.sendFormattedMessageToPlayer(jsonMessage, targetUser, prefix, brackets, bracketColor) else chatBox.sendFormattedMessage(jsonMessage, prefix, brackets, bracketColor) end end --notifyUser("Did you mean to load { msg = '', clickAction = 'suggest_command', color = 'gold', underlined = 'true', value = '@spatial load n3 expFarm' } or was that a mistake?",{ user = "", prefix = "Confirm", brackets = "()", bracketColor = "&b" }) --notifyUser("Your { msg = 'reward', value = '/claim_reward 123' } is ready to be claimed.",{ user = "Myros27", prefix = "Quest", bracketColor = "&a" }) --notifyUser("This is just a standard broadcast.",{ prefix = "Info" }) function handleDiskStatusCommand(userName) local turtleName = retrieve("turtleName") local currentDisk = retrieve("currentDisk") if currentDisk == "" then return end local clearCommand = "@spatial clear " .. currentDisk local moveCommand = "@spatial move " .. currentDisk .. " here" notifyUser("{ msg = '"..currentDisk.."', underlined = 'true', clickAction = 'suggest_command', value = '" .. clearCommand .. "' } is loaded at { msg = '" .. retrieve("turtleName") .. "', underlined = 'true', clickAction = 'suggest_command', value = '" .. moveCommand .. "' }.", { prefix = retrieve("turtleName")}) end function getQuadrant(username) local BASE_X = 24 local BASE_Z = 232 local HEIGHT_LETTERS = { "B", "D", "F", "H", "J", "L", "N", "P", "R", "T", "V" } local player_detector = peripheral.find("playerDetector") if not player_detector then printError("Player Detector peripheral not found.") return "" end local playerData = player_detector.getPlayer(username) if not playerData then return "" end if playerData.dimension ~= "minecraft:overworld" then return "" end if playerData.y < -56 or playerData.y >= 296 then return "" end local normalized_y = playerData.y + 56 local height_index = math.floor(normalized_y / 32) + 1 local height_letter = HEIGHT_LETTERS[height_index] local quadrant_number = 0 if playerData.x >= BASE_X and playerData.z <= BASE_Z then quadrant_number = 9 -- Northeast elseif playerData.x < BASE_X and playerData.z <= BASE_Z then quadrant_number = 7 -- Northwest elseif playerData.x >= BASE_X and playerData.z > BASE_Z then quadrant_number = 3 -- Southeast else quadrant_number = 1 -- Southwest end return height_letter .. quadrant_number end --local locationCode = getQuadrant("Myros27") --if locationCode ~= "" then -- print("Myros27's location code is: " .. locationCode) --else -- print("Could not get location for Myros27 (or they are out of bounds).") --end function sanitizeQuadrant(input) if not input or type(input) ~= "string" then return "" end local s = string.lower(input) local letters = {} local digits = {} for char in string.gmatch(s, ".") do if string.match(char, "%a") then table.insert(letters, char) elseif string.match(char, "%d") then table.insert(digits, char) end end table.sort(letters) table.sort(digits) return table.concat(letters) .. table.concat(digits) end --print("'N3' -> '" .. sanitizeQuadrant("N3") .. "'") --print("'3n' -> '" .. sanitizeQuadrant("3n") .. "'") --print("'z9a1' -> '" .. sanitizeQuadrant("z9a1") .. "'") function confirmQuadrant(sanitizedInput) if not sanitizedInput or type(sanitizedInput) ~= "string" or #sanitizedInput ~= 2 then return false end local letter = sanitizedInput:sub(1, 1) local digit = sanitizedInput:sub(2, 2) local validLetters = "bdfhjlnprtv" local validDigits = "1379" if string.find(validLetters, letter, 1, true) and string.find(validDigits, digit, 1, true) then return true else return false end end --print("'n3' -> " .. tostring(confirmQuadrant("n3"))) --print("'b9x' -> " .. tostring(confirmQuadrant("b9x"))) -- Fails because length is wrong function isInfront(blockName) local success, blockData = turtle.inspect() if not success or not blockData or not blockData.name then return false end local result = compareStrings(blockName, blockData.name) return result == 0 end --print(isInfront("ae2:spatial_io_port")) --- The main function that listens for chat events and dispatches commands. local function runChatListener() print("Chat command listener is now active. Waiting for messages...") while true do local event, username, message = os.pullEvent("chat") local args = splitString(message) handleCommand(username, args[1] or "", args[2] or "", args[3] or "", args[4] or "", args[5] or "") end end function handleInit() turtle.select(1) rs.setOutput("front", false) rs.setOutput("back", false) local chatBox = peripheral.find("chatBox") if not chatBox then printError("Chat Box not found.") os.sleep(100) os.shutdown() return end local turtleName = retrieve("turtleName") if (turtleName == "") then term.write("Please enter the Name of the Turtle\n") turtleName = read() turtleName = sanitizeQuadrant(turtleName) if (confirmQuadrant(turtleName)) then set("turtleName", turtleName) else printError("Invalid Name") os.sleep(1) os.shutdown() end end if (isInfront("ae2:spatial_io_port")) then turtle.turnRight() rs.setOutput("front", true) rs.setOutput("back", true) os.sleep(0.05) rs.setOutput("front", false) rs.setOutput("back", false) turtle.turnLeft() else turtle.turnLeft() if (isInfront("ae2:spatial_io_port")) then turtle.turnRight() rs.setOutput("front", true) rs.setOutput("back", true) os.sleep(0.05) rs.setOutput("front", false) rs.setOutput("back", false) turtle.turnLeft() else notifyUser("ae2:spatial_io_port not found",{ prefix = retrieve("turtleName")}) os.sleep(1) os.shutdown() end end local player_detector = peripheral.find("playerDetector") if not player_detector then notifyUser("upgrade not found",{ prefix = retrieve("turtleName")}) os.sleep(1) os.shutdown() end local bridgeName, bridgeLocation = findPeripheralByType("meBridge") if not bridgeName then notifyUser("ME Bridge",{ prefix = retrieve("turtleName")}) os.sleep(1) os.shutdown() end local oppositeDirection = { top = "down", bottom = "up" } local direction = oppositeDirection[bridgeLocation] if not direction then notifyUser("ME Bridge must be on top or bottom.",{ prefix = retrieve("turtleName")}) os.sleep(1) os.shutdown() end end function waitForPortReady(timeout) timeout = timeout or 2 local startTime = os.clock() while true do if isPortReady() then return true end if os.clock() - startTime >= timeout then notifyUser("Port was never ready",{ prefix = retrieve("turtleName")}) return false end os.sleep(0.05) end end function waitForSuck(timeout) timeout = timeout or 2 local startTime = os.clock() while true do if turtle.suck() then return true end if os.clock() - startTime >= timeout then return false end os.sleep(0) end end function update() fs.makeDir("startup") shell.setDir("startup") fs.delete("startup/spatialBot") shell.run("pastebin get na90MBPy spatialBot") os.reboot() end function hasN(quadrant) local q = string.lower(quadrant or "") return string.find(q, "n") ~= nil end function saveTrigger() if (isPortReady()) then turtle.drop() rs.setOutput("front", true) os.sleep(0.05) if (waitForSuck(1) == false) then notifyUser("Unable to get Disk back at " .. retrieve("turtleName"),{ prefix = retrieve("turtleName")}) rs.setOutput("front", false) return false end else notifyUser("Unable to get ME Sytsm online at " .. retrieve("turtleName"),{ prefix = retrieve("turtleName")}) rs.setOutput("front", false) return false end rs.setOutput("front", false) return true end function enableMe() rs.setOutput("back", true) waitForPortReady(2) end function disableMe() rs.setOutput("back", false) end function clearSpace(userName) if (retrieve("currentDisk") == "") then notifyUser("Nothing to Clear",{ prefix = retrieve("turtleName")}) return end if (hasN(retrieve("turtleName")) and userName ~= "Myros27") then --only Myros27 may use the Level "n" notifyUser("FATAL ERROR",{ prefix = retrieve("turtleName")}) return end enableMe() if (saveTrigger()) then os.sleep(0.05) local diskName = retrieve("currentDisk") clear("currentDisk") returnDisk() disableMe() notifyUser("Returned disk " .. diskName .. " to the Base",{ prefix = retrieve("turtleName")}) return end disableMe() notifyUser("Unable to Clear",{ prefix = retrieve("turtleName")}) end function loadSpace(userName, disk, slow) if (hasN(retrieve("turtleName")) and userName ~= "Myros27") then --only Myros27 may use the Level "n" notifyUser("FATAL ERROR",{ prefix = retrieve("turtleName")}) return end if (slow) then --wait for another unload os.sleep(2) end if (retrieve("currentDisk") ~= "") then clearSpace(userName) end enableMe() local didWork,message = getMeDiskByName(disk) -- get Disk From ME System if (didWork) then if (saveTrigger()) then os.sleep(0.05) set("currentDisk", disk) notifyUser("Loaded disk " .. disk .. " at " .. retrieve("turtleName") ,{ prefix = retrieve("turtleName")}) disableMe() return else rs.setOutput("back", false) os.sleep(2)-- are we here because the disk is still in the spatial port? we should check inventory rs.setOutput("back", true) if (saveTrigger()) then -- lets try again os.sleep(0.05) set("currentDisk", disk) notifyUser("Loaded disk " .. disk .. " at " .. retrieve("turtleName") ,{ prefix = retrieve("turtleName")}) disableMe() return end end else notifyUser("Unable to find a Disk named " .. disk .. "." ,{ prefix = retrieve("turtleName")}) end disableMe() if (slow) then notifyUser("Unable to Move",{ prefix = retrieve("turtleName")}) return end notifyUser("Unable to Load",{ prefix = retrieve("turtleName")}) end function handleCommand(userName, arg1, arg2, arg3, arg4, arg5) if (userName == "Myros27" and arg1 == "@spatial" and arg2 == "update") then notifyUser("updating now",{ prefix = retrieve("turtleName")}) update() end if (arg1 == "@disks") then handleDiskStatusCommand(userName) return end local myGroupDist = compareStrings(arg1, "@spatial") if (myGroupDist > 2) then return end local loadDist = compareStrings(arg2, "load") local clearDist = compareStrings(arg2, "clear") local moveDist = compareStrings(arg2, "move") if (loadDist > 2 and clearDist > 2 and moveDist > 2) then return end local turtleName = retrieve("turtleName") local quadrant = arg3 local disk = arg4 if (quadrant == "here") then quadrant = getQuadrant(userName) end quadrant = sanitizeQuadrant(quadrant) local validQuadrant = true if not confirmQuadrant(quadrant) then disk = arg3 quadrant = arg4 if (quadrant == "here") then quadrant = getQuadrant(userName) end quadrant = sanitizeQuadrant(quadrant) if not confirmQuadrant(quadrant) then validQuadrant = false end end local currentDisk = retrieve("currentDisk") if (clearDist < 3) then local isItMyName = compareStrings(turtleName, quadrant) if (isItMyName == 0) then clearSpace(userName) return end if (validQuadrant == false) then local isItMyDisk = compareStrings(disk, currentDisk) if (isItMyDisk < 2) then clearSpace(userName) return end end return end if (loadDist < 3) then local isItMyName = compareStrings(turtleName, quadrant) if (isItMyName == 0) then loadSpace(userName, disk, false) return end return end if (moveDist < 3) then local isItMyName = compareStrings(turtleName, quadrant) if (isItMyName == 0) then loadSpace(userName, disk, true) return end local isItMyDisk = compareStrings(disk, currentDisk) if (isItMyDisk < 2) then clearSpace(userName) return end return end notifyUser("Overrun",{ prefix = retrieve("turtleName")}) end --init: handleInit() runChatListener()