diff --git a/autofeed.lua b/autofeed.lua new file mode 100644 index 0000000..4224cf2 --- /dev/null +++ b/autofeed.lua @@ -0,0 +1,66 @@ +-- AutoFeed module for Minit +-- Copyright (C) 2023 AlexDevs +-- This software is licensed under the MIT license. + +local module = { + name = "autofeed", +} + +local saturation = 7 +local foods = { + "golden_carrot", + "popcorn", + "cooked_beef", + "cooked_pork", + "cooked_chicken", + "baked_potato", + "cooked_cod", + "cooked_salmon", + "bread", + "melon_slice", + "cooked_rabbit", + "cooked_mutton", +} + +local neural + +local function has(arr, val) + for k, v in ipairs(arr) do + if val:find(v) then + return true + end + end + return false +end + +local function findSlot(inventory) + local list = inventory.list() + for k, v in pairs(list) do + if has(foods, v.name) then + return k + end + end + + return -1 +end + +local function feed() + local inventory = neural.getInventory() + local foodSlot = findSlot(inventory) + if foodSlot == -1 then + return + end + inventory.consume(foodSlot) +end + +function module.setup(ni) + neural = ni +end + +function module.update(meta) + if meta.food.hungry or meta.food.saturation < saturation then + feed() + end +end + +return module \ No newline at end of file diff --git a/client.lua b/client.lua new file mode 100644 index 0000000..2c25ea2 --- /dev/null +++ b/client.lua @@ -0,0 +1,68 @@ +local modules = {} + +local function getIndex(tab, val) + local index = nil + for i, v in ipairs (tab) do + if (v == val) then + index = i + end + end + return index +end + + +local kinetic = peripheral.wrap("back") +local canvas = peripheral.wrap("back").canvas() + +timer = os.startTimer(0.5) +canvas.clear() + +local group = canvas.addGroup({ 0, 0 }) +local text = group.addText({ 5, 5 }, "") + +text.setText("this is NOT a client") + +local moduleText = group.addText({ 5, 15 }, table.concat(modules, "\n")) +moduleText.setScale(0.5) +local function enableModule(module) + table.insert(modules, module) + moduleText.setText(table.concat(modules, "\n")) +end + +local function disableModule(module) + local idx = getIndex(modules, module) + table.remove(modules, idx) + moduleText.setText(table.concat(modules, "\n")) +end + +local function isModuleEnabled(module) + local idx = getIndex(modules, module) + return idx ~= nil +end + +function checkKey() + while true do + local event, key, is_held = os.pullEvent("key") + if keys.getName(key) == "g" then + if isModuleEnabled("flight") then + disableModule("flight") + else + enableModule("flight") + timer = os.startTimer(0.5) + end + end + + end +end + +function flyEvent() + while true do + local _, tid = os.pullEvent("timer") + if tid == timer and isModuleEnabled("flight") then + timer = os.startTimer(0.5) + kinetic.launch(0, -90, 4); + end + end +end + +parallel.waitForAll(checkKey, flyEvent) \ No newline at end of file diff --git a/flight.lua b/flight.lua new file mode 100644 index 0000000..d5f5968 --- /dev/null +++ b/flight.lua @@ -0,0 +1,250 @@ +-- Elytra Flight module for Minit +-- Copyright (C) 2023 AlexDevs +-- This software is licensed under the MIT license. + +local module = { + name = "flight", +} + +local locale = { + direction = "%s : %d", -- windrose : pitch + altitude = "Y: %d", + yMotion = "Y: %.2f", + speed = "%.2f m/s", +} + +local icons = { + empty = "air", + fly = "elytra", + launch = "firework_rocket", + slow = "feather", + disabled = "barrier", + propelling = "blaze_powder" +} + +-- Default variables. Use settings file to change values. +settings.define("elytra.power", { + description = "Propeller power", + type = "number", + default = 2, +}) + +settings.define("elytra.pitch", { + description = "Propeller pitch treshold for automatic mode", + type = "number", + default = 0, +}) + +settings.define("elytra.scale", { + description = "Scale of UI", + type = "number", + default = 0.6, +}) + +settings.define("elytra.sounds", { + description = "Enable sound effects", + type = "boolean", + default = true, +}) + +settings.define("elytra.manual", { + description = "Press SHIFT while flying to propel", + type = "boolean", + default = true, +}) + +settings.define("elytra.softfall.enable", { + description = "Enable softfall", + type = "boolean", + default = true, +}) + +settings.define("elytra.softfall.basepower", { + description = "Base power for soft fall", + type = "number", + default = 0.75, +}) + +settings.define("elytra.softfall.trigger", { + description = "Minimum negative Y motion required to trigger soft fall. USE NEGATIVE NUMBERS", + type = "number", + default = -1, +}) + +local neural, speaker +local canvas, container +local screen = {} +local currentY = 0 + +local function getRoseWind(degrees) + degrees = degrees + 180 + local directions = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" } + local index = math.floor((degrees / 45) + 0.5) % 8 + return directions[index + 1] +end + +local function createScreen() + canvas = neural.canvas() + screen = {} + + if container then + pcall(container.clear) + end + + container = canvas.addGroup({ 1, 1 }) + + local scale = settings.get("elytra.scale") + local x, y = 25, math.ceil(9 * scale) + + screen.icon = container.addItem({ 0, 1 }, "elytra") + screen.speed = container.addText({ x, y * 1 }, "") + screen.speed.setScale(scale) + + screen.altitude = container.addText({ x, y * 2 }, "") + screen.altitude.setScale(scale) + + screen.direction = container.addText({ x, y * 3 }, "") + screen.direction.setScale(scale) +end + +local function updateScreen(meta) + if not canvas or not container then + createScreen() + end + + local mVector = vector.new(meta.deltaPosX, meta.deltaPosY, meta.deltaPosZ) + local speed = mVector:length() * 20 + screen.speed.setText(string.format(locale.speed, speed)) + screen.direction.setText(string.format(locale.direction, getRoseWind(meta.yaw), meta.pitch)) + screen.altitude.setText(string.format(locale.altitude, currentY)) +end + +local icon = icons.empty +local function toggleScreen(show) + local alpha = show and 0xff or 0 + screen.icon.setItem(show and icon or icons.empty) + screen.speed.setAlpha(alpha) + screen.altitude.setAlpha(alpha) + screen.direction.setAlpha(alpha) +end + +local function setIcon(newIcon) + icon = newIcon + screen.icon.setItem(newIcon) +end + +local function playSound(sound, volume, pitch) + if speaker and settings.get("elytra.sounds") then + speaker.playSound(sound, volume, pitch) + end +end + +local function launch(yaw, pitch, power) + power = power or settings.get("elytra.power") + neural.launch(yaw, pitch, math.min(power, 4)) +end + +local function launchUp(power) + launch(0, -90, power) +end + +local function softFall(motionY) + motionY = motionY or 0 + launchUp(-motionY + settings.get("elytra.softfall.basepower")) + playSound("minecraft:entity.phantom.flap", 1, 1) +end + +local function canPropel(meta) + return settings.get("elytra.manual") and meta.isSneaking or not meta.isSneaking +end + +local function propel(meta, icon) + neural.launch(meta.yaw, meta.pitch, settings.get("elytra.power")) + playSound("minecraft:entity.fishing_bobber.throw", 0.4, 1) + + if icon then + setIcon(icon) + end +end + +local function gpsLocate() + while true do + local x, y, z = gps.locate() + if x then + currentY = y + end + sleep(0.5) + end +end + +function module.init(init) + init.addTask(gpsLocate) +end + +function module.setup(ni) + neural = ni + speaker = peripheral.find("speaker") + + createScreen() + toggleScreen(false) +end + +function module.update(meta) + if not meta.isElytraFlying then + if meta.isSneaking then + if meta.pitch == -90 then + setIcon(icons.launch) + launchUp(2) + else + setIcon(icons.disabled) + end + return + end + + if meta.deltaPosY < settings.get("elytra.softfall.trigger") and settings.get("elytra.softfall.enable") then + softFall(meta.motionY) + setIcon(icons.slow) + return + end + + toggleScreen(false) + return + end + + toggleScreen(true) + updateScreen(meta) + + local pitch = meta.pitch + local yaw = meta.yaw + + if not canPropel(meta) then + setIcon(icons.disabled) + return + end + + if settings.get("elytra.manual") then + if meta.isSneaking then + propel(meta, icons.propelling) + else + if meta.deltaPosY < settings.get("elytra.softfall.trigger") and settings.get("elytra.softfall.enable") then + softFall(meta.motionY) + setIcon(icons.slow) + else + setIcon(icons.fly) + end + end + else + if pitch > settings.get("elytra.pitch") then + if meta.deltaPosY < settings.get("elytra.softfall.trigger") and settings.get("elytra.softfall.enable") then + softFall(meta.motionY) + setIcon(icons.slow) + end + return + end + + setIcon(icons.fly) + propel(meta, false) + end +end + +return module \ No newline at end of file diff --git a/hopper.lua b/hopper.lua new file mode 100644 index 0000000..9e7f990 --- /dev/null +++ b/hopper.lua @@ -0,0 +1,521 @@ +-- Copyright umnikos (Alex Stefanov) 2023 +-- Licensed under MIT license +-- Version 1.2BETA + +-- TODO: take into account NBT data when stacking + +local help_message = [[ +hopper script v1.2, made by umnikos + +usage: hopper {from} {to} [{item name}/{flag}]* +example: hopper *chest* *barrel* *:pink_wool + +flags: + -once : run the script only once instead of in a loop (undo with -forever) + -quiet: print less things to the terminal (undo with -verbose) + -from_slot [slot]: restrict pulling to a single slot + -to_slot [slot]: restrict pushing to a single slot + -from_limit [num]: keep at least this many matching items in every source chest + -to_limit [num]: fill every destination chest with at most this many matching items + -sleep [num]: set the delay in seconds between each iteration (default is 1)]] + +-- further things of note: +-- - `self` is a valid peripheral name if you're running the script from a turtle connected to a wired modem +-- - you can import this file as a library with `require "hopper"` +-- - the script will prioritize taking from almost empty stacks and filling into almost full stacks + +local function noop() +end + +local print = print + +-- for debugging purposes +local function dump(o) + if type(o) == 'table' then + local s = '{ ' + for k,v in pairs(o) do + if type(k) ~= 'number' then k = '"'..k..'"' end + s = s .. '['..k..'] = ' .. dump(v) .. ',' + end + return s .. '} ' + else + return tostring(o) + end +end + +local function is_empty(t) + return next(t) == nil +end + +local function glob(p, s) + local p = "^"..string.gsub(p,"*",".*").."$" + local res = string.find(s,p) + return res ~= nil +end + +local function default_options(options) + if not options then + options = {} + end + if options.quiet == nil then + options.quiet = true + end + if options.once == nil then + options.once = true + end + if options.sleep == nil then + options.sleep = 1 + end + --IDEA: to/from slot ranges instead of singular slots + --if type(options.from_slot) == "number" then + -- options.from_slot = {options.from_slot, options.from_slot} + --end + --if type(options.to_slot) == "number" then + -- options.to_slot = {options.to_slot, options.to_slot} + --end + return options +end + +local function default_filters(filters) + if not filters then + filters = {} + end + if type(filters) == "string" then + filters = {filters} + end + return filters +end + +local function display_info(from, to, sources, destinations, filters, options) + if options.quiet then print = noop end + + print("hoppering from "..from) + if options.from_slot then + print("and only from slot "..tostring(options.from_slot)) + end + if options.from_limit then + print("keeping at least "..tostring(options.from_limit).." items in reserve per container") + end + if #sources == 0 then + print("except there's nothing matching that description!") + return false + end + print("to "..to) + if #destinations == 0 then + print("except there's nothing matching that description!") + return false + end + if options.to_slot then + print("and only to slot "..tostring(options.to_slot)) + end + if options.to_limit then + print("filling up to "..tostring(options.to_limit).." items per container") + end + + if #filters == 1 and filters[1] ~= "*" then + print("only the items matching the filter "..filters[1]) + elseif #filters > 1 then + print("only the items matching any of the "..tostring(#filters).." filters") + end + + return true +end + +local function matches_filters(filters,s) + if #filters == 0 then return true end + for _,filter in pairs(filters) do + --print(filter) + if glob(filter,s) then + return true + end + end + return false +end + +-- if the computer has storage (aka. is a turtle) +-- we'd like to be able to transfer to it +local self = nil +local function determine_self() + if not turtle then return end + for _,dir in ipairs({"top","front","bottom","back"}) do + local p = peripheral.wrap(dir) + --print(p) + if p and p.getNameLocal then + --print("FOUND SELF") + self = p.getNameLocal() + return + end + end +end + +local function transfer(from,to,from_slot,to_slot,count) + if count <= 0 then + --print("WARNING: transfering 0 or less items?!?") + return 0 + end + --print("TRANSFER! from "..from.." to "..to) + if from ~= "self" then + if to == "self" then to = self end + return peripheral.call(from,"pushItems",to,from_slot,count,to_slot) + else + if to == "self" then + turtle.select(from_slot) + turtle.transferTo(to_slot,count) + else + return peripheral.call(to,"pullItems",self,from_slot,count,to_slot) + end + end +end + +local limits_cache = {} +local function chest_list(chest) + if chest ~= "self" then + local c = peripheral.wrap(chest) + local l = c.list() + for i,item in pairs(l) do + --print(i) + if limits_cache[item.name] == nil then + limits_cache[item.name] = c.getItemLimit(i) + end + l[i].limit = limits_cache[item.name] + end + return l + else + local l = {} + for i=1,16 do + l[i] = turtle.getItemDetail(i,true) + if l[i] then + --print(i) + l[i].limit = l[i].maxCount + end + end + return l + end +end + +local function chest_size(chest) + if chest == "self" then return 16 end + return peripheral.call(chest,"size") +end + +local function hopper_step(from,to,sources,dests,filters,options) + --print("hopper step") + -- get all of the chests' contents + -- which we will be updating internally + -- in order to not have to list the chests + -- over and over again + local source_lists = {} + local dest_lists = {} + for _,source_name in ipairs(sources) do + source_lists[source_name] = chest_list(source_name) + end + for _,dest_name in ipairs(dests) do + dest_lists[dest_name] = chest_list(dest_name) + end + + -- we will be iterating over item types to be moved + -- as well as over source and destination chests + -- in order to capitalize on knowing when the destinations are full + -- and when the sources are empty + -- so we can stop hoppering early + local item_jobs = {} + + -- we will also prioritize filling items into slots + -- that already have existing partial stacks of those items + -- ideally there shouldn't be that many partial stacks + -- so this won't be horribly slow + local partial_source_slots = {} + local partial_dest_slots = {} + + -- for to/from limits we'll also need to know + -- how many items per chest we can move + -- of every item type + local chest_contains = {} + + for source_name,source_list in pairs(source_lists) do + chest_contains[source_name] = chest_contains[source_name] or {} + for i,item in pairs(source_list) do + if not (options.from_slot and options.from_slot ~= i) then + if matches_filters(filters,item.name) then + if not item_jobs[item.name] then + item_jobs[item.name] = 0 + partial_source_slots[item.name] = {} + partial_dest_slots[item.name] = {} + end + + item_jobs[item.name] = item_jobs[item.name] + item.count + chest_contains[source_name][item.name] = (chest_contains[source_name][item.name] or 0) + item.count + if item.count > 0 and item.count < item.limit then + partial_source_slots[item.name] = partial_source_slots[item.name] or {} + partial_source_slots[item.name][item.count] = partial_source_slots[item.name][item.count] or {} + table.insert(partial_source_slots[item.name][item.count], {source_name,i}) + end + end + end + end + end + + for dest_name,dest_list in pairs(dest_lists) do + chest_contains[dest_name] = chest_contains[dest_name] or {} + for i,item in pairs(dest_list) do + if not (options.to_slot and options.to_slot ~= i) then + if (item_jobs[item.name] or 0) > 0 then -- item name matches filter if so + chest_contains[dest_name][item.name] = (chest_contains[dest_name][item.name] or 0) + item.count + if item.count > 0 and item.count < item.limit then + partial_dest_slots[item.name] = partial_dest_slots[item.name] or {} + partial_dest_slots[item.name][item.count] = partial_dest_slots[item.name][item.count] or {} + table.insert(partial_dest_slots[item.name][item.count], {dest_name,i}) + end + end + end + end + end + + --print(dump(partial_source_slots)) + --print(dump(partial_dest_slots)) + + -- and now for the actual hoppering + for item_name,_ in pairs(item_jobs) do + -- we first do it for the partially filled source slots only + -- into partially filled destinations only + local s = partial_source_slots[item_name] + local source_counts = {} + for c,_ in pairs(s) do table.insert(source_counts,c) end + local d = partial_dest_slots[item_name] + local dest_counts = {} + for c,_ in pairs(d) do table.insert(dest_counts,c) end + table.sort(source_counts) + table.sort(dest_counts) + local si = 1 -- container index + local sii = nil -- slot index + local ssi = nil -- whole container index + local ssii = nil -- whole container slot + local di = #dest_counts -- container index + local dii = nil -- slot index + local ddi = nil -- whole container index + + if si > #source_counts then + ssi = #sources + ssii = chest_size(sources[ssi]) + end + + local source_name, source_i, source_amount + local dest_name, dest_i, dest_amount + local function get_source() + if ssi == nil then + if not sii then sii = #s[source_counts[si]] end + local source_name, source_i = table.unpack(s[source_counts[si]][sii]) + local source_amount = source_lists[source_name][source_i].count + return source_name, source_i, source_amount + else + while ssi > 0 do + if options.from_slot then ssii = options.from_slot end + local item_found = source_lists[sources[ssi]][ssii] + -- TODO: replace ~= with comparison operators + if item_found and item_found.count > 0 and item_found.name == item_name and chest_contains[sources[ssi]][item_name] ~= options.from_limit then + return sources[ssi], ssii, item_found.count + end + ssii = ssii - 1 + if ssii <= 0 or (options.from_slot and ssii < options.from_slot) then + ssi = ssi - 1 + if ssi <= 0 then + break + end + ssii = chest_size(sources[ssi]) + end + end + return nil, nil, nil + end + end + local function update_source(amount, transferred) -- planned vs actual transffered amount + source_lists[source_name][source_i].count = source_lists[source_name][source_i].count - amount + chest_contains[source_name][item_name] = (chest_contains[source_name][item_name] or 0) - amount + if ssi == nil then + if source_lists[source_name][source_i].count == 0 or chest_contains[source_name][item_name] == options.from_limit then + sii = sii - 1 + end + if sii <= 0 then + si = si + 1 + sii = nil + if si > #source_counts then + ssi = #sources + ssii = chest_size(sources[ssi]) + end + end + end + end + local function get_dest() + if di and di < 1 then + ddi = #dests + di = nil + dii = nil + end + if ddi == nil then + if not dii then dii = #d[dest_counts[di]] end + local dest_name, dest_i = table.unpack(d[dest_counts[di]][dii]) + local dest_amount = dest_lists[dest_name][dest_i].limit - dest_lists[dest_name][dest_i].count + return dest_name, dest_i, dest_amount + else + if options.to_slot then + -- find chest where slot is empty + while true do + if ddi < 1 then break end + if (chest_contains[dests[ddi]][item_name] or 0) ~= (options.to_limit or math.huge) then + if dest_lists[dests[ddi]][options.to_slot] == nil then break end + if dest_lists[dests[ddi]][options.to_slot].count == 0 then break end + end + ddi = ddi - 1 + end + return dests[ddi], options.to_slot, math.huge + else + -- just shove into the chest and move to the next one if 0 get moved + return dests[ddi], nil, math.huge + end + end + end + local function update_dest(amount, transferred) + chest_contains[dest_name][item_name] = (chest_contains[dest_name][item_name] or 0) + amount + if ddi == nil then + -- TODO: this needs to be a thing even if ddi is not nil, else the list becomes invalid and is not reusable + dest_lists[dest_name][dest_i].count = dest_lists[dest_name][dest_i].count + amount + if dest_lists[dest_name][dest_i].limit - dest_lists[dest_name][dest_i].count == 0 or chest_contains[dest_name][item_name] == options.to_limit then + dii = dii - 1 + end + if dii <= 0 then + di = di - 1 + dii = nil + end + else + --print(transferred == 0 and (chest_contains[source_name][item_name] or 0) ~= options.from_limit) + if transferred == 0 and (chest_contains[source_name][item_name] or 0) ~= options.from_limit then + ddi = ddi - 1 + end + --print(ddi) + end + end + + while true do + if item_jobs[item_name] <= 0 then break end + source_name, source_i, source_amount = get_source() + if source_name == nil then break end + dest_name, dest_i, dest_amount = get_dest() + if dest_name == nil then break end + --print(dump(chest_contains)) + local amount = math.min(source_amount, + dest_amount, + (options.to_limit or math.huge) - (chest_contains[dest_name][item_name] or 0), + (chest_contains[source_name][item_name] or 0) - (options.from_limit or 0) + ) + local transferred = transfer(source_name,dest_name,source_i,dest_i,amount) + + update_source(amount, transferred) + update_dest(amount, transferred) + end + + end + +end + +local function hopper(from,to,filters,options) + options = default_options(options) + filters = default_filters(filters) + + determine_self() + --print("SELF IS:") + --print(self) + + local peripherals = peripheral.getNames() + if self then + table.insert(peripherals,"self") + end + + local sources = {} + local destinations = {} + for i,per in ipairs(peripherals) do + if glob(from,per) then + -- prevent the source and the destination ever being the same + -- (if a chest matches both, it's only a destination) + if (not glob(to,per)) or (options.to_slot and options.from_slot and options.from_slot ~= options.to_slot) then + sources[#sources+1] = per + end + end + if glob(to,per) then + destinations[#destinations+1] = per + end + end + + local valid = display_info(from,to,sources,destinations,filters,options) + if not valid then return end + + while true do + hopper_step(from,to,sources,destinations,filters,options) + if options.once then + break + end + sleep(options.sleep) + end +end + + +local args = {...} + +local function main() + + if args[1] == "hopper" then + return hopper + end + + if #args < 2 then + print(help_message) + return + end + local from = args[1] + local to = args[2] + local options = {} + options.once = false + options.quiet = false + + local filters = {} + local i=3 + while i <= #args do + if glob("-*",args[i]) then + if args[i] == "-once" then + --print("(only once!)") + options.once = true + elseif args[i] == "-forever" then + options.once = false + elseif args[i] == "-quiet" then + options.quiet = true + elseif args[i] == "-verbose" then + options.quiet = false + elseif args[i] == "-from_slot" then + i = i+1 + options.from_slot = tonumber(args[i]) + elseif args[i] == "-to_slot" then + i = i+1 + options.to_slot = tonumber(args[i]) + elseif args[i] == "-from_limit" then + i = i+1 + options.from_limit = tonumber(args[i]) + elseif args[i] == "-to_limit" then + i = i+1 + options.to_limit = tonumber(args[i]) + elseif args[i] == "-sleep" then + i = i+1 + options.sleep = tonumber(args[i]) + else + print("UNKNOWN ARGUMENT: "..args[i]) + return + end + else + filters[#filters+1] = args[i] + end + + i = i+1 + end + + hopper(from,to,filters,options) +end + +return main() + diff --git a/scanner.lua b/scanner.lua new file mode 100644 index 0000000..0c48d47 --- /dev/null +++ b/scanner.lua @@ -0,0 +1,150 @@ +--- This renders a minimap showing nearby ores using the overlay glasses and block scanner. + +--- We start the program by specifying a series of configuration options. Feel free to ignore these, and use the values +--- inline. Whilst you don't strictly speaking need a delay between each iteration, it does reduce the impact on the +--- server. +local scanInterval = 0.2 +local renderInterval = 0.05 +local scannerRange = 8 +local scannerWidth = scannerRange * 2 + 1 + +--- These values aren't very exciting, they just control what the minimap looks like +local size = 0.5 +local cellSize = 16 +local offsetX = 75 +local offsetY = 75 + +--- We end our configuration section by defining the ores we're interested in and what colour we'll draw them as. We +--- define some ores as having a higher priority, so large ore veins don't mask smaller veins of more precious ores. +local ores = { + ["minecraft:ancient_debris"] = 20, + ["minecraft:diamond_ore"] = 10, + ["minecraft:emerald_ore"] = 10, + ["minecraft:gold_ore"] = 8, + ["minecraft:redstone_ore"] = 5, + ["minecraft:lapis_ore"] = 5, + ["minecraft:iron_ore"] = 2, + --["minecraft:nether_quartz_ore"] = 2, + ["minecraft:coal_ore"] = 1 +} + +local colours = { + ["minecraft:ancient_debris"] = { 255,255,255 }, + --["minecraft:nether_quartz_ore"] = { 255, 255, 255 }, + ["minecraft:coal_ore"] = { 150, 150, 150 }, + ["minecraft:iron_ore"] = { 255, 150, 50 }, + ["minecraft:lava"] = { 150, 75, 0 }, + ["minecraft:gold_ore"] = { 255, 255, 0 }, + ["minecraft:diamond_ore"] = { 0, 255, 255 }, + ["minecraft:redstone_ore"] = { 255, 0, 0 }, + ["minecraft:lapis_ore"] = { 0, 50, 255 }, + ["minecraft:emerald_ore"] = { 0, 255, 0 } +} + +--- Now let's get into the interesting stuff! Let's look for a neural interface and check we've got all the required +--- modules. +local modules = peripheral.find("neuralInterface") +if not modules then error("Must have a neural interface", 0) end +if not modules.hasModule("plethora:scanner") then error("The block scanner is missing", 0) end +if not modules.hasModule("plethora:glasses") then error("The overlay glasses are missing", 0) end + +--- Now we've got our neural interface, let's extract the canvas and ensure nothing else is on it. +local canvas = modules.canvas() +canvas.clear() + +--- We now need to set up our minimap. We create a 2D array of text objects around the player, each starting off +--- displaying an empty string. If we find an ore, we'll update their colour and text. +local block_text = {} +local blocks = {} +for x = -scannerRange, scannerRange, 1 do + block_text[x] = {} + blocks[x] = {} + + for z = -scannerRange, scannerRange, 1 do + block_text[x][z] = canvas.addText({ 0, 0 }, " ", 0xFFFFFFFF, size) + blocks[x][z] = { y = nil, block = nil } + end +end + +--- We also create a marker showing the current player's location. +canvas.addText({ offsetX, offsetY }, "^", 0xFFFFFFFF, size * 2) + +--- Our first big function is the scanner: this searches for ores near the player, finds the most important ones, and +--- updates the block table. +local function scan() + while true do + local scanned_blocks = modules.scan() + + --- For each nearby position, we search the y axis for interesting ores. We look for the one which has + --- the highest priority and update the block information + for x = -scannerRange, scannerRange do + for z = -scannerRange, scannerRange do + local best_score, best_block, best_y = -1 + for y = -scannerRange, scannerRange do + --- The block scanner returns blocks in a flat array, so we index into it with this rather scary formulae. + local scanned = scanned_blocks[scannerWidth ^ 2 * (x + scannerRange) + scannerWidth * (y + scannerRange) + (z + scannerRange) + 1] + + --- If there is a block here, and it's more interesting than our previous ores, then let's use that! + if scanned then + local new_score = ores[scanned.name] + if new_score and new_score > best_score then + best_block = scanned.name + best_score = new_score + best_y = y + end + end + end + + -- Update our block table with this information. + blocks[x][z].block = best_block + blocks[x][z].y = best_y + end + end + + --- We wait for some delay before starting again. This isn't _strictly_ needed, but helps reduce server load + sleep(scanInterval) + end +end + +--- The render function takes our block information generated in the previous function and updates the text elements. +local function render() + while true do + --- If possible, we rotate the map using the current player's look direction. If it's not available, we'll just + --- use north as up. + local meta = modules.getMetaOwner and modules.getMetaOwner() + local angle = meta and math.rad(-meta.yaw % 360) or math.rad(180) + + --- Like before, loop over every nearby block and update something. Though this time we're updating objects on + --- the overlay canvas. + for x = -scannerRange, scannerRange do + for z = -scannerRange, scannerRange do + local text = block_text[x][z] + local block = blocks[x][z] + + if block.block then + --- If we've got a block here, we update the position of our text element to account for rotation, + local px = math.cos(angle) * -x - math.sin(angle) * -z + local py = math.sin(angle) * -x + math.cos(angle) * -z + + local sx = math.floor(px * size * cellSize) + local sy = math.floor(py * size * cellSize) + text.setPosition(offsetX + sx, offsetY + sy) + + --- Then change the text and colour to match the location of the ore + text.setText(tostring(block.y)) + text.setColor(table.unpack(colours[block.block])) + else + --- Otherwise we just make sure the text is empty. We don't need to faff about with clearing the + --- colour or position, as we'll change it next iteration anyway. + text.setText(" ") + end + end + end + + sleep(renderInterval) + end +end + +--- We now run our render and scan loops in parallel, continually updating our block list and redisplaying it to the +--- wearer. +parallel.waitForAll(render, scan)