This commit is contained in:
Soph :3 2025-07-09 12:58:47 +00:00
parent 89a15d277f
commit f7bb6c8137
5 changed files with 1055 additions and 0 deletions

66
autofeed.lua Normal file
View file

@ -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

68
client.lua Normal file
View file

@ -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)

250
flight.lua Normal file
View file

@ -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

521
hopper.lua Normal file
View file

@ -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()

150
scanner.lua Normal file
View file

@ -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)