Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- -- 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 = '<ExpFarm>', 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()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement