Initial commit
This commit is contained in:
commit
7b7addb054
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
mnt
|
146
backend-chests.lua
Normal file
146
backend-chests.lua
Normal file
@ -0,0 +1,146 @@
|
||||
local util = require "util"
|
||||
local conf = util.conf
|
||||
|
||||
rednet.open(conf.modem)
|
||||
|
||||
-- Find all chests or shulker boxes
|
||||
local inventories = {}
|
||||
for _, n in pairs(peripheral.getNames()) do
|
||||
local p = peripheral.wrap(n)
|
||||
if
|
||||
string.find(n, "chest") or
|
||||
string.find(n, "shulker") then
|
||||
inventories[n] = p
|
||||
end
|
||||
end
|
||||
|
||||
local nameCache = {}
|
||||
|
||||
-- Gets the display name of the given item (in the given chest peripheral & slot)
|
||||
-- If its name is not cached, cache it.
|
||||
-- If it is, just return the cached name
|
||||
function cache(item, chest, slot)
|
||||
local idx = item.name .. ":" .. item.damage
|
||||
|
||||
if nameCache[idx] then
|
||||
return nameCache[idx]
|
||||
else
|
||||
local n = chest.getItemMeta(slot).displayName
|
||||
nameCache[idx] = n
|
||||
return n
|
||||
end
|
||||
end
|
||||
|
||||
local index = {}
|
||||
function updateIndexFor(name)
|
||||
local inv = inventories[name]
|
||||
local data = inv.list()
|
||||
|
||||
for slot, item in pairs(data) do
|
||||
data[slot].displayName = cache(item, inv, slot)
|
||||
end
|
||||
|
||||
index[name] = data
|
||||
end
|
||||
|
||||
function updateIndex()
|
||||
for n in pairs(inventories) do
|
||||
updateIndexFor(n)
|
||||
sleep()
|
||||
end
|
||||
print "Indexing complete."
|
||||
end
|
||||
|
||||
-- Finds all items matching a certain predicate
|
||||
function find(predicate)
|
||||
for name, items in pairs(index) do
|
||||
for slot, item in pairs(items) do
|
||||
if predicate(item) then
|
||||
return name, slot, item
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Finds space in the chest system
|
||||
function findSpace()
|
||||
for name, items in pairs(index) do
|
||||
if #items < inventories[name].size() then
|
||||
return name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function search(msg)
|
||||
return find(function(item)
|
||||
return
|
||||
(not msg.meta or item.damage == msg.meta) and
|
||||
(not msg.name or item.name == msg.name) and
|
||||
(not msg.dname or string.find(item.displayName:lower(), msg.dname:lower()))
|
||||
end)
|
||||
end
|
||||
|
||||
function processRequest(msg)
|
||||
print(textutils.serialise(msg))
|
||||
|
||||
-- Extract an item. If meta and name are supplied, each supplied value must match exactly.
|
||||
-- Applies a fuzzy search to display names
|
||||
-- Extracted items are either deposited in buffer or directly in target inventory.
|
||||
if msg.cmd == "extract" then
|
||||
local inv, slot, item = search(msg)
|
||||
|
||||
local qty = msg.qty or 64
|
||||
|
||||
updateIndexFor(inv)
|
||||
|
||||
local moved = peripheral.call(conf.bufferOutInternal, "pullItems", inv, slot, qty, 1)
|
||||
|
||||
if msg.destInv then
|
||||
moved = peripheral.call(conf.bufferOutExternal, "pushItems", msg.destInv, 1, 64, msg.destSlot)
|
||||
end
|
||||
|
||||
return {moved, item}
|
||||
-- Pulls items from an external inventory into storage.
|
||||
elseif msg.cmd == "insert" then
|
||||
if msg.fromInv and msg.fromSlot then
|
||||
peripheral.call(conf.bufferInExternal, "pullItems", msg.fromInv, msg.fromSlot, msg.qty or 64, 1)
|
||||
end
|
||||
|
||||
local toInv = findSpace()
|
||||
if not toInv then return "ERROR" end
|
||||
|
||||
peripheral.call(conf.bufferInInternal, "pushItems", toInv, 1)
|
||||
|
||||
updateIndexFor(toInv) -- I don't know a good way to figure out where exactly the items went
|
||||
|
||||
return "OK"
|
||||
-- Just return the external network names of the buffers
|
||||
elseif msg.cmd == "buffers" then
|
||||
return { conf.bufferInExternal, conf.bufferOutExternal }
|
||||
-- Reindexes system
|
||||
elseif msg.cmd == "reindex" then
|
||||
updateIndex()
|
||||
return "OK"
|
||||
-- Returns entire index
|
||||
elseif msg.cmd == "list" then
|
||||
return util.collate(index)
|
||||
-- Looks up supplied name in the cache.
|
||||
elseif msg.cmd == "name" then
|
||||
msg.meta = msg.meta or 0
|
||||
return msg.name and msg.meta and nameCache[msg.name .. ":" .. msg.meta]
|
||||
end
|
||||
end
|
||||
|
||||
function processRequests()
|
||||
while true do
|
||||
util.processMessage(function(msg)
|
||||
local ok, r = pcall(processRequest, msg)
|
||||
if not ok then r = "ERROR" end
|
||||
|
||||
return true, r
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
updateIndex()
|
||||
processRequests()
|
11
dev.sh
Executable file
11
dev.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
function watch {
|
||||
while ls *.lua | inotifywait -e modify --fromfile -; do cp -v *.lua mnt; done
|
||||
}
|
||||
|
||||
function mount {
|
||||
ccfuse -m mnt -h ws://switchcraft.pw:4533 -c wyverndev
|
||||
}
|
||||
|
||||
mount & watch
|
147
lib.lua
Normal file
147
lib.lua
Normal file
@ -0,0 +1,147 @@
|
||||
--[[
|
||||
Wyvern utility/API library
|
||||
Contains:
|
||||
error handling (a set of usable errors and human-readable printing for them),
|
||||
networking (a simple node-based system on top of rednet)
|
||||
configuration (basically just loading serialized tables from a file)
|
||||
]]
|
||||
|
||||
local d = require "luadash"
|
||||
|
||||
-- ERRORS
|
||||
|
||||
local errors = {
|
||||
INTERNAL = 0, -- Internal error data can be in any format or not exist
|
||||
INVALID = 1, -- Invalid message errors don't require data at all
|
||||
NOPATTERN = 2, -- No pattern errors should contain a human-readable pattern name in their data
|
||||
NOITEMS = 3, -- No item errors should either provide a table of { type = "human-readable name if available or internal ID if not", quantity = number of items missing } or a human-readable string
|
||||
NORESPONSE = 4, -- No response errors (should only be produced by query_ functions may contain a description of which node the error is caused by in their data
|
||||
NOMATCHINGNODE = 5, -- No matching node errors (should only be prodcuced by query_ functions) may contain a description of which type of node cannot be found.
|
||||
make = function(e, d)
|
||||
return { type = "error", error = e, data = d }
|
||||
end
|
||||
}
|
||||
|
||||
-- Converts an error into human-readable format
|
||||
errors.format = function(e)
|
||||
if not (e.type and e.type == "error" and e.data and e.error) then return "Not actually an error." end
|
||||
if e.error == errors.INTERNAL then
|
||||
return "Internal error - provided info: " .. textutils.serialise(e.data) .. "."
|
||||
elseif e.error == errors.INVALID then
|
||||
return "Request invalid."
|
||||
elseif e.errors == errors.NOPATTERN then
|
||||
return "Missing pattern " .. textutils.serialise(e.data) .. "."
|
||||
elseif e.errors == errors.NOITEMS then
|
||||
local thing_missing = "???"
|
||||
if type(e.data) == "table" and e.data.type and e.data.quantity then
|
||||
thing_missing = tostring(e.data.quantity) .. " " .. e.data.type
|
||||
elseif type(e.data) == "string" then
|
||||
thing_missing = e.data
|
||||
end
|
||||
|
||||
return "Missing " .. thing_missing .. " to fulfil request."
|
||||
elseif e.errors == errors.NORESPONSE then
|
||||
local text = "No response"
|
||||
if e.data then text = text .. " from " .. textutils.serialise(e.data) end
|
||||
return text .. "."
|
||||
elseif e.errors == errors.NOMATCHINGNODE then
|
||||
if e.data then
|
||||
return "No " .. textutils.serialise(e.data) .. " node found."
|
||||
else
|
||||
return "No node of desired type found."
|
||||
end
|
||||
else
|
||||
return "Error is invalid. Someone broke it."
|
||||
end
|
||||
end
|
||||
|
||||
-- NETWORKING
|
||||
|
||||
local protocol = "wyvern"
|
||||
|
||||
-- Runs a Wyvern node server.
|
||||
-- First argument is a function to be run for requests. It will be provided the request data and must return the value to respond with.
|
||||
-- If it errors, an internal error will be returned.
|
||||
-- Second argument is the type of node to host as. Other nodes may attempt to use this to discover other local-network nodes.
|
||||
local function serve(fn, nodeType)
|
||||
rednet.host(protocol .. "/" .. nodeType, nodeType)
|
||||
|
||||
while true do
|
||||
local sender, message = rednet.receive(protocol)
|
||||
|
||||
-- As a default response, send an "invalid request" error
|
||||
local response = errors.make(errors.INVALID)
|
||||
|
||||
-- If the message actually is a compliant Wyvern request (is a table, containing a message ID, request, and a type saying "request") then run
|
||||
-- the provided server function, and package successful results into a response type
|
||||
if type(message) == "table" and message.type and message.type == "request" and message.request then
|
||||
local ok, result = pcall(fn, request)
|
||||
if not ok then response = errors.make(errors.INTERNAL, result) end
|
||||
else response = { type = "response", response = result }
|
||||
end
|
||||
|
||||
rednet.send(sender, response, protocol)
|
||||
end
|
||||
end
|
||||
|
||||
-- Attempts to send "request" to "ID", with the maximum number of allowable tries being "tries"
|
||||
local function query_by_ID(ID, request, tries)
|
||||
local max_tries = tries or 3
|
||||
local request_object = { type = "request", request = request }
|
||||
local result, tries
|
||||
|
||||
repeat
|
||||
rednet.send(id, request_object, protocol)
|
||||
_, result = rednet.receive(protocol, 1)
|
||||
sleep(1)
|
||||
until result ~= nil or tries >= max_tries
|
||||
|
||||
if result == nil then result = errors.make(errors.NORESPONSE, ID) end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function query_by_type(type, request, tries)
|
||||
local ID = rednet.lookup(protocol .. "/" .. type)
|
||||
if not ID then return errors.make(errors.NOMATCHINGNODE, type) end
|
||||
return query_by_ID(ID, request, tries)
|
||||
end
|
||||
|
||||
-- GENERAL STUFF
|
||||
|
||||
-- Loads a config file (in serialized-table format) from "filename" or wyvern_config.tbl
|
||||
-- "required_data" is a list of keys which must be in the config file's data
|
||||
-- "defaults" is a map of keys and default values for them, which will be used if there is no matching key in the data
|
||||
local function load_config(required_data, defaults, filename)
|
||||
local filename = filename or "wyvern_config.tbl"
|
||||
local f = fs.open(filename, "r")
|
||||
local data = textutils.unserialise(f.readAll())
|
||||
f.close()
|
||||
|
||||
for k, required_key in pairs(required_data) do
|
||||
if not data[required_key] then
|
||||
if defaults[required_key] then data[required_key] = defaults[required_key]
|
||||
else error({"Missing config key!", required_key, data}) end
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
-- Returns a list of peripheral objects whose type, name and object satisfy the given predicate
|
||||
local function find_peripherals(predicate)
|
||||
local matching = {}
|
||||
for k, name in pairs(peripheral.getNames()) do
|
||||
local wrapped = peripheral.wrap(name)
|
||||
local type = peripheral.getType(name)
|
||||
if predicate(type, name, wrapped) then table.insert(matching, wrapped) end
|
||||
end
|
||||
return matching
|
||||
end
|
||||
|
||||
-- Set up stuff for running this library's features (currently, modem initialization)
|
||||
local function init()
|
||||
d.map(find_peripherals(function(type, name, wrapped) return type == "modem" end), rednet.open)
|
||||
end
|
||||
|
||||
return { errors, serve, query_by_ID, query_by_type, load_config, find_peripherals, init }
|
358
luadash.lua
Normal file
358
luadash.lua
Normal file
@ -0,0 +1,358 @@
|
||||
-- luadash, from https://github.com/tmpim/luadash, not actually mine
|
||||
-- It's bundled with the Wyvern source code here because laziness
|
||||
-- I also added the Levenstein (probably spelt that wrong) distance implementation to it, as it's useful.
|
||||
|
||||
local _mt, _ = {}, {}
|
||||
setmetatable(_, _mt)
|
||||
|
||||
local function skip1(f)
|
||||
return function(x, _, ...)
|
||||
return f(x, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function _.expect(n, arg, t, v)
|
||||
if t == 'value' then
|
||||
if v == nil then
|
||||
return error(('%s: bad argument #%d (got nil)'):format(n, arg))
|
||||
end
|
||||
elseif type(v) ~= t then
|
||||
return error(('%s: bad argument #%d (expected %s, got %s)'):format(n, arg, t, type(v)))
|
||||
end
|
||||
end
|
||||
|
||||
function _.partial(f, ...)
|
||||
_.expect('partial', 1, 'function', f)
|
||||
local args = table.pack(...)
|
||||
return function(...)
|
||||
local args2, actual = table.pack(...), { }
|
||||
for i = 1, args.n do
|
||||
actual[i] = args[i]
|
||||
end
|
||||
for i = 1, args2.n do
|
||||
actual[args.n + i] = args2[i]
|
||||
end
|
||||
return f(unpack(actual, 1, args.n + args2.n))
|
||||
end
|
||||
end
|
||||
|
||||
function _.map_with_key(tab, f)
|
||||
_.expect('map_with_key', 1, 'table', tab)
|
||||
_.expect('map_with_key', 2, 'function', f)
|
||||
local out = {}
|
||||
for k, v in pairs(tab) do
|
||||
local k, v = f(k, v)
|
||||
out[k] = v
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.reduce_with_index(tab, f, z)
|
||||
_.expect('reduce_with_index', 1, 'table', tab)
|
||||
_.expect('reduce_with_index', 2, 'function', f)
|
||||
_.expect('reduce_with_index', 3, 'value', z)
|
||||
local out = z
|
||||
for i = 1, #tab do
|
||||
out = f(out, i, tab[i])
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.reduce(tab, f, z)
|
||||
return _.reduce_with_index(tab, skip1(f), z)
|
||||
end
|
||||
|
||||
function _.apply(f, t)
|
||||
_.expect('apply', 1, 'function', f)
|
||||
_.expect('apply', 2, 'table', t)
|
||||
return f(unpack(t, 1, #t))
|
||||
end
|
||||
|
||||
function _.map(t1, f, ...)
|
||||
_.expect('map', 1, 'table', t1)
|
||||
_.expect('map', 2, 'function', f)
|
||||
return _.flat_map(t1, function(...) return { (f(...)) } end, ...)
|
||||
end
|
||||
|
||||
function _.zip(...)
|
||||
local args = table.pack(...)
|
||||
for i = 1, args.n do
|
||||
_.expect('zip', 1, 'table', args[i])
|
||||
end
|
||||
return _.map(function(...) return {...} end, ...)
|
||||
end
|
||||
|
||||
function _.push(t, ...)
|
||||
_.expect('push', 1, 'table', t)
|
||||
local args = table.pack(...)
|
||||
for i = 1, args.n do
|
||||
table.insert(t, args[i])
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function _.intersperse(t, x)
|
||||
_.expect('intersperse', 1, 'table', t)
|
||||
local out = {}
|
||||
for i = 1, #t, 1 do
|
||||
_.push(out, t[i], x)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.flatten(t)
|
||||
_.expect('flatten', 1, 'table', t)
|
||||
local out, li = {}, 1
|
||||
for i = 1, #t do
|
||||
if type(t[i]) == 'table' then
|
||||
for j = 1, #t[i] do
|
||||
out[li] = t[i][j]
|
||||
li = li + 1
|
||||
end
|
||||
else
|
||||
out[li] = t[i]
|
||||
li = li + 1
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.flat_map(t1, f, ...)
|
||||
_.expect('flat_map', 1, 'table', t1)
|
||||
_.expect('flat_map', 2, 'function', f)
|
||||
local args, n = table.pack(t1, ...), 0
|
||||
for i = 1, args.n do
|
||||
_.expect('flat_map', 1 + i, 'table', args[i])
|
||||
n = math.max(n, #args[i])
|
||||
end
|
||||
local out, li = {}, 0
|
||||
for i = 1, n do
|
||||
local these = {}
|
||||
for j = 1, args.n do
|
||||
these[j] = args[j][i]
|
||||
end
|
||||
local r = _.apply(f, these)
|
||||
if type(r) == 'table' then
|
||||
for i = 1, #r do
|
||||
out[li + i] = r[i]
|
||||
end
|
||||
li = li + #r
|
||||
else
|
||||
out[li + 1] = r
|
||||
li = li + 1
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.filter(t, p)
|
||||
_.expect('filter', 1, 'table', t)
|
||||
_.expect('filter', 2, 'function', p)
|
||||
local out, li = {}, 1
|
||||
for i = 1, #t do
|
||||
if p(t[i]) then
|
||||
out[li] = t[i]
|
||||
li = li + 1
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.id(v)
|
||||
_.expect('id', 1, 'value', v)
|
||||
return v
|
||||
end
|
||||
|
||||
function _.sort_by(t, f)
|
||||
_.expect('sort_by', 1, 'table', t)
|
||||
_.expect('sort_by', 2, 'function', f)
|
||||
local nt = _.map(t, _.id)
|
||||
|
||||
table.sort(nt, function(a, b) return f(a) < f(b) end)
|
||||
return nt
|
||||
end
|
||||
|
||||
function _.sort(t)
|
||||
_.expect('sort', 1, 'table', t)
|
||||
|
||||
return _.sort_by(t, _.id)
|
||||
end
|
||||
|
||||
function _.sample_size(t, n)
|
||||
_.expect('sample_size', 1, 'table', t)
|
||||
_.expect('sample_size', 2, 'number', n)
|
||||
|
||||
if #t <= n then
|
||||
return t
|
||||
end
|
||||
|
||||
local src = _.keys(t)
|
||||
local out = {}
|
||||
for i = 1, n do
|
||||
local k = _.sample(src)
|
||||
out[i] = t[k]
|
||||
|
||||
src[k] = src[#src]
|
||||
src[#src] = nil
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.sample(t)
|
||||
_.expect('sample', 1, 'table', t)
|
||||
return t[math.random(1, #t)]
|
||||
end
|
||||
|
||||
function _.head(t)
|
||||
_.expect('head', 1, 'table', t)
|
||||
return x[1]
|
||||
end
|
||||
|
||||
function _.tail(t)
|
||||
_.expect('tail', 1, 'table', t)
|
||||
local out = {}
|
||||
for i = 2, #t do
|
||||
out[i - 1] = t[i]
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.every(t, p)
|
||||
_.expect('every', 1, 'table', t)
|
||||
_.expect('every', 1, 'function', p)
|
||||
for i = 1, #t do
|
||||
if not p(t[i]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function _.some(t, p)
|
||||
_.expect('some', 1, 'table', t)
|
||||
_.expect('some', 1, 'function', p)
|
||||
for i = 1, #t do
|
||||
if p(t[i]) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function _.initial(t)
|
||||
_.expect('initial', 1, 'table', t)
|
||||
local out = {}
|
||||
for i = 1, #t - 1 do
|
||||
out[i] = t[i]
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.last(t)
|
||||
_.expect('last', 1, 'table', t)
|
||||
return t[#t]
|
||||
end
|
||||
|
||||
function _.nth(t, i)
|
||||
_.expect('nth', 1, 'table', t)
|
||||
_.expect('nth', 2, 'value', i)
|
||||
return t[i]
|
||||
end
|
||||
|
||||
function _.keys(t)
|
||||
_.expect('keys', 1, 'table', t)
|
||||
local out, i = {}, 1
|
||||
for k, v in pairs(t) do
|
||||
out[i] = k
|
||||
i = i + 1
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.values(t)
|
||||
_.expect('values', 1, 'table', t)
|
||||
local out, i = {}, 1
|
||||
for k, v in pairs(t) do
|
||||
out[i] = v
|
||||
i = i + 1
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _mt.__call(_, x)
|
||||
local function wrap(f)
|
||||
return function(...)
|
||||
return _(f(...))
|
||||
end
|
||||
end
|
||||
if type(x) == 'table' then
|
||||
return setmetatable(x,
|
||||
{ __index = function(t, k)
|
||||
return wrap(_[k])
|
||||
end })
|
||||
else
|
||||
return x
|
||||
end
|
||||
end
|
||||
|
||||
function _.distance(str1, str2)
|
||||
local v0 = {}
|
||||
local v1 = {}
|
||||
|
||||
for i = 0, #str2 do
|
||||
v0[i] = i
|
||||
end
|
||||
|
||||
for i = 0, #str1 - 1 do
|
||||
v1[0] = i + 1
|
||||
|
||||
for j = 0, #str2 - 1 do
|
||||
local delCost = v0[j + 1] + 1
|
||||
local insertCost = v1[j] + 1
|
||||
local subCost
|
||||
|
||||
if str1:sub(i + 1, i + 1) == str2:sub(j + 1, j + 1) then
|
||||
subCost = v0[j]
|
||||
else
|
||||
subCost = v0[j] + 1
|
||||
end
|
||||
|
||||
v1[j + 1] = math.min(delCost, insertCost, subCost)
|
||||
end
|
||||
|
||||
local t = v0
|
||||
v0 = v1
|
||||
v1 = t
|
||||
end
|
||||
|
||||
return v0[#str2]
|
||||
end
|
||||
|
||||
_.ops = {
|
||||
plus = function(a, b) return a + b end,
|
||||
minus = function(a, b) return a - b end,
|
||||
times = function(a, b) return a * b end,
|
||||
over = function(a, b) return a / b end,
|
||||
power = function(a, b) return a ^ b end,
|
||||
modulo = function(a, b) return a % b end,
|
||||
remainder = function(a, b) return a % b end,
|
||||
rem = function(a, b) return a % b end,
|
||||
mod = function(a, b) return a % b end,
|
||||
conj = function(a, b) return a and b end,
|
||||
disj = function(a, b) return a or b end,
|
||||
equals = function(a, b) return a == b end,
|
||||
divisible_by = function(a, b)
|
||||
return b % a == 0
|
||||
end,
|
||||
['>'] = function(a, b) return a > b end,
|
||||
['>='] = function(a, b) return a >= b end,
|
||||
['<'] = function(a, b) return a < b end,
|
||||
['<='] = function(a, b) return a <= b end,
|
||||
}
|
||||
|
||||
function string.starts_with(self, s)
|
||||
_.expect('starts_with', 1, 'string', s)
|
||||
return self:find('^' .. s) ~= nil
|
||||
end
|
||||
|
||||
return _
|
146
mnt/backend-chests.lua
Normal file
146
mnt/backend-chests.lua
Normal file
@ -0,0 +1,146 @@
|
||||
local util = require "util"
|
||||
local conf = util.conf
|
||||
|
||||
rednet.open(conf.modem)
|
||||
|
||||
-- Find all chests or shulker boxes
|
||||
local inventories = {}
|
||||
for _, n in pairs(peripheral.getNames()) do
|
||||
local p = peripheral.wrap(n)
|
||||
if
|
||||
string.find(n, "chest") or
|
||||
string.find(n, "shulker") then
|
||||
inventories[n] = p
|
||||
end
|
||||
end
|
||||
|
||||
local nameCache = {}
|
||||
|
||||
-- Gets the display name of the given item (in the given chest peripheral & slot)
|
||||
-- If its name is not cached, cache it.
|
||||
-- If it is, just return the cached name
|
||||
function cache(item, chest, slot)
|
||||
local idx = item.name .. ":" .. item.damage
|
||||
|
||||
if nameCache[idx] then
|
||||
return nameCache[idx]
|
||||
else
|
||||
local n = chest.getItemMeta(slot).displayName
|
||||
nameCache[idx] = n
|
||||
return n
|
||||
end
|
||||
end
|
||||
|
||||
local index = {}
|
||||
function updateIndexFor(name)
|
||||
local inv = inventories[name]
|
||||
local data = inv.list()
|
||||
|
||||
for slot, item in pairs(data) do
|
||||
data[slot].displayName = cache(item, inv, slot)
|
||||
end
|
||||
|
||||
index[name] = data
|
||||
end
|
||||
|
||||
function updateIndex()
|
||||
for n in pairs(inventories) do
|
||||
updateIndexFor(n)
|
||||
sleep()
|
||||
end
|
||||
print "Indexing complete."
|
||||
end
|
||||
|
||||
-- Finds all items matching a certain predicate
|
||||
function find(predicate)
|
||||
for name, items in pairs(index) do
|
||||
for slot, item in pairs(items) do
|
||||
if predicate(item) then
|
||||
return name, slot, item
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Finds space in the chest system
|
||||
function findSpace()
|
||||
for name, items in pairs(index) do
|
||||
if #items < inventories[name].size() then
|
||||
return name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function search(msg)
|
||||
return find(function(item)
|
||||
return
|
||||
(not msg.meta or item.damage == msg.meta) and
|
||||
(not msg.name or item.name == msg.name) and
|
||||
(not msg.dname or string.find(item.displayName:lower(), msg.dname:lower()))
|
||||
end)
|
||||
end
|
||||
|
||||
function processRequest(msg)
|
||||
print(textutils.serialise(msg))
|
||||
|
||||
-- Extract an item. If meta and name are supplied, each supplied value must match exactly.
|
||||
-- Applies a fuzzy search to display names
|
||||
-- Extracted items are either deposited in buffer or directly in target inventory.
|
||||
if msg.cmd == "extract" then
|
||||
local inv, slot, item = search(msg)
|
||||
|
||||
local qty = msg.qty or 64
|
||||
|
||||
updateIndexFor(inv)
|
||||
|
||||
local moved = peripheral.call(conf.bufferOutInternal, "pullItems", inv, slot, qty, 1)
|
||||
|
||||
if msg.destInv then
|
||||
moved = peripheral.call(conf.bufferOutExternal, "pushItems", msg.destInv, 1, 64, msg.destSlot)
|
||||
end
|
||||
|
||||
return {moved, item}
|
||||
-- Pulls items from an external inventory into storage.
|
||||
elseif msg.cmd == "insert" then
|
||||
if msg.fromInv and msg.fromSlot then
|
||||
peripheral.call(conf.bufferInExternal, "pullItems", msg.fromInv, msg.fromSlot, msg.qty or 64, 1)
|
||||
end
|
||||
|
||||
local toInv = findSpace()
|
||||
if not toInv then return "ERROR" end
|
||||
|
||||
peripheral.call(conf.bufferInInternal, "pushItems", toInv, 1)
|
||||
|
||||
updateIndexFor(toInv) -- I don't know a good way to figure out where exactly the items went
|
||||
|
||||
return "OK"
|
||||
-- Just return the external network names of the buffers
|
||||
elseif msg.cmd == "buffers" then
|
||||
return { conf.bufferInExternal, conf.bufferOutExternal }
|
||||
-- Reindexes system
|
||||
elseif msg.cmd == "reindex" then
|
||||
updateIndex()
|
||||
return "OK"
|
||||
-- Returns entire index
|
||||
elseif msg.cmd == "list" then
|
||||
return util.collate(index)
|
||||
-- Looks up supplied name in the cache.
|
||||
elseif msg.cmd == "name" then
|
||||
msg.meta = msg.meta or 0
|
||||
return msg.name and msg.meta and nameCache[msg.name .. ":" .. msg.meta]
|
||||
end
|
||||
end
|
||||
|
||||
function processRequests()
|
||||
while true do
|
||||
util.processMessage(function(msg)
|
||||
local ok, r = pcall(processRequest, msg)
|
||||
if not ok then r = "ERROR" end
|
||||
|
||||
return true, r
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
updateIndex()
|
||||
processRequests()
|
139
mnt/lib.lua
Normal file
139
mnt/lib.lua
Normal file
@ -0,0 +1,139 @@
|
||||
--[[
|
||||
Wyvern utility/API library
|
||||
Contains:
|
||||
error handling (a set of usable errors and human-readable printing for them),
|
||||
networking (a simple node-based system on top of rednet)
|
||||
configuration (basically just loading serialized tables from a file)
|
||||
]]
|
||||
|
||||
local d = require "luadash"
|
||||
|
||||
-- ERRORS
|
||||
|
||||
local errors = {
|
||||
INTERNAL = 0, -- Internal error data can be in any format or not exist
|
||||
INVALID = 1, -- Invalid message errors don't require data at all
|
||||
NOPATTERN = 2, -- No pattern errors should contain a human-readable pattern name in their data
|
||||
NOITEMS = 3, -- No item errors should either provide a table of { type = "human-readable name if available or internal ID if not", quantity = number of items missing } or a human-readable string
|
||||
NORESPONSE = 4, -- No response errors (should only be produced by query_ functions may contain a description of which node the error is caused by in their data
|
||||
NOMATCHINGNODE = 5, -- No matching node errors (should only be prodcuced by query_ functions) may contain a description of which type of node cannot be found.
|
||||
make = function(e, d)
|
||||
return { type = "error", error = e, data = d }
|
||||
end
|
||||
}
|
||||
|
||||
-- Converts an error into human-readable format
|
||||
errors.format = function(e)
|
||||
if not (e.type and e.type == "error" and e.data and e.error) then return "Not actually an error." end
|
||||
if e.error == errors.INTERNAL then
|
||||
return "Internal error - provided info: " .. textutils.serialise(e.data) .. "."
|
||||
elseif e.error == errors.INVALID then
|
||||
return "Request invalid."
|
||||
elseif e.errors == errors.NOPATTERN then
|
||||
return "Missing pattern " .. textutils.serialise(e.data) .. "."
|
||||
elseif e.errors == errors.NOITEMS then
|
||||
local thing_missing = "???"
|
||||
if type(e.data) == "table" and e.data.type and e.data.quantity then
|
||||
thing_missing = tostring(e.data.quantity) .. " " .. e.data.type
|
||||
elseif type(e.data) == "string" then
|
||||
thing_missing = e.data
|
||||
end
|
||||
|
||||
return "Missing " .. thing_missing .. " to fulfil request."
|
||||
elseif e.errors == errors.NORESPONSE then
|
||||
local text = "No response"
|
||||
if e.data then text = text .. " from " .. textutils.serialise(e.data) end
|
||||
return text .. "."
|
||||
elseif e.errors == errors.NOMATCHINGNODE then
|
||||
if e.data then
|
||||
return "No " .. textutils.serialise(e.data) .. " node found."
|
||||
else
|
||||
return "No node of desired type found."
|
||||
end
|
||||
else
|
||||
return "Error is invalid. Someone broke it."
|
||||
end
|
||||
end
|
||||
|
||||
-- NETWORKING
|
||||
|
||||
local protocol = "wyvern"
|
||||
|
||||
-- Runs a Wyvern node server.
|
||||
-- First argument is a function to be run for requests. It will be provided the request data and must return the value to respond with.
|
||||
-- If it errors, an internal error will be returned.
|
||||
-- Second argument is the type of node to host as. Other nodes may attempt to use this to discover other local-network nodes.
|
||||
local function serve(fn, nodeType)
|
||||
rednet.host(protocol .. "/" .. nodeType, nodeType)
|
||||
|
||||
while true do
|
||||
local sender, message = rednet.receive(protocol)
|
||||
|
||||
-- As a default response, send an "invalid request" error
|
||||
local response = errors.make(errors.INVALID)
|
||||
|
||||
-- If the message actually is a compliant Wyvern request (is a table, containing a message ID, request, and a type saying "request") then run
|
||||
-- the provided server function, and package successful results into a response type
|
||||
if type(message) == "table" and message.type and message.type == "request" and message.request then
|
||||
local ok, result = pcall(fn, request)
|
||||
if not ok then response = errors.make(errors.INTERNAL, result) end
|
||||
else response = { type = "response", response = result }
|
||||
end
|
||||
|
||||
rednet.send(sender, response, protocol)
|
||||
end
|
||||
end
|
||||
|
||||
-- Attempts to send "request" to "ID", with the maximum number of allowable tries being "tries"
|
||||
local function query_by_ID(ID, request, tries)
|
||||
local max_tries = tries or 3
|
||||
local request_object = { type = "request", request = request }
|
||||
local result, tries
|
||||
|
||||
repeat
|
||||
rednet.send(id, request_object, protocol)
|
||||
_, result = rednet.receive(protocol, 1)
|
||||
sleep(1)
|
||||
until result ~= nil or tries >= max_tries
|
||||
|
||||
if result == nil then result = errors.make(errors.NORESPONSE, ID) end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function query_by_type(type, request, tries)
|
||||
local ID = rednet.lookup(protocol .. "/" .. type)
|
||||
if not ID then return errors.make(errors.NOMATCHINGNODE, type) end
|
||||
return query_by_ID(ID, request, tries)
|
||||
end
|
||||
|
||||
-- GENERAL STUFF
|
||||
|
||||
-- Loads a config file (in serialized-table format) from "filename" or wyvern_config.tbl
|
||||
-- "required_data" is a list of keys which must be in the config file's data
|
||||
-- "defaults" is a map of keys and default values for them, which will be used if there is no matching key in the data
|
||||
local function load_config(required_data, defaults, filename)
|
||||
local filename = filename or "wyvern_config.tbl"
|
||||
local f = fs.open(filename, "r")
|
||||
local data = textutils.unserialise(f.readAll())
|
||||
f.close()
|
||||
|
||||
for k, required_key in pairs(required_data) do
|
||||
if not data[required_key] then
|
||||
if defaults[required_key] then data[required_key] = defaults[required_key]
|
||||
else error({"Missing config key!", required_key, data}) end
|
||||
end
|
||||
end
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
local function find_peripherals(predicate)
|
||||
return d.map(d.filter(d.map(peripheral.getNames(), function(name) return { name, functions = peripheral.wrap(name) } end), predicate), function(obj) return obj.functions)
|
||||
end
|
||||
|
||||
local function init(config)
|
||||
d.map(find_peripherals(function(p) return p.functions.open and p.functions.close end)
|
||||
end
|
||||
|
||||
return { errors, serve, query_by_ID, query_by_type, load_config, find_peripherals, init }
|
358
mnt/luadash.lua
Normal file
358
mnt/luadash.lua
Normal file
@ -0,0 +1,358 @@
|
||||
-- luadash, from https://github.com/tmpim/luadash, not actually mine
|
||||
-- It's bundled with the Wyvern source code here because laziness
|
||||
-- I also added the Levenstein (probably spelt that wrong) distance implementation to it, as it's useful.
|
||||
|
||||
local _mt, _ = {}, {}
|
||||
setmetatable(_, _mt)
|
||||
|
||||
local function skip1(f)
|
||||
return function(x, _, ...)
|
||||
return f(x, ...)
|
||||
end
|
||||
end
|
||||
|
||||
function _.expect(n, arg, t, v)
|
||||
if t == 'value' then
|
||||
if v == nil then
|
||||
return error(('%s: bad argument #%d (got nil)'):format(n, arg))
|
||||
end
|
||||
elseif type(v) ~= t then
|
||||
return error(('%s: bad argument #%d (expected %s, got %s)'):format(n, arg, t, type(v)))
|
||||
end
|
||||
end
|
||||
|
||||
function _.partial(f, ...)
|
||||
_.expect('partial', 1, 'function', f)
|
||||
local args = table.pack(...)
|
||||
return function(...)
|
||||
local args2, actual = table.pack(...), { }
|
||||
for i = 1, args.n do
|
||||
actual[i] = args[i]
|
||||
end
|
||||
for i = 1, args2.n do
|
||||
actual[args.n + i] = args2[i]
|
||||
end
|
||||
return f(unpack(actual, 1, args.n + args2.n))
|
||||
end
|
||||
end
|
||||
|
||||
function _.map_with_key(tab, f)
|
||||
_.expect('map_with_key', 1, 'table', tab)
|
||||
_.expect('map_with_key', 2, 'function', f)
|
||||
local out = {}
|
||||
for k, v in pairs(tab) do
|
||||
local k, v = f(k, v)
|
||||
out[k] = v
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.reduce_with_index(tab, f, z)
|
||||
_.expect('reduce_with_index', 1, 'table', tab)
|
||||
_.expect('reduce_with_index', 2, 'function', f)
|
||||
_.expect('reduce_with_index', 3, 'value', z)
|
||||
local out = z
|
||||
for i = 1, #tab do
|
||||
out = f(out, i, tab[i])
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.reduce(tab, f, z)
|
||||
return _.reduce_with_index(tab, skip1(f), z)
|
||||
end
|
||||
|
||||
function _.apply(f, t)
|
||||
_.expect('apply', 1, 'function', f)
|
||||
_.expect('apply', 2, 'table', t)
|
||||
return f(unpack(t, 1, #t))
|
||||
end
|
||||
|
||||
function _.map(t1, f, ...)
|
||||
_.expect('map', 1, 'table', t1)
|
||||
_.expect('map', 2, 'function', f)
|
||||
return _.flat_map(t1, function(...) return { (f(...)) } end, ...)
|
||||
end
|
||||
|
||||
function _.zip(...)
|
||||
local args = table.pack(...)
|
||||
for i = 1, args.n do
|
||||
_.expect('zip', 1, 'table', args[i])
|
||||
end
|
||||
return _.map(function(...) return {...} end, ...)
|
||||
end
|
||||
|
||||
function _.push(t, ...)
|
||||
_.expect('push', 1, 'table', t)
|
||||
local args = table.pack(...)
|
||||
for i = 1, args.n do
|
||||
table.insert(t, args[i])
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function _.intersperse(t, x)
|
||||
_.expect('intersperse', 1, 'table', t)
|
||||
local out = {}
|
||||
for i = 1, #t, 1 do
|
||||
_.push(out, t[i], x)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.flatten(t)
|
||||
_.expect('flatten', 1, 'table', t)
|
||||
local out, li = {}, 1
|
||||
for i = 1, #t do
|
||||
if type(t[i]) == 'table' then
|
||||
for j = 1, #t[i] do
|
||||
out[li] = t[i][j]
|
||||
li = li + 1
|
||||
end
|
||||
else
|
||||
out[li] = t[i]
|
||||
li = li + 1
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.flat_map(t1, f, ...)
|
||||
_.expect('flat_map', 1, 'table', t1)
|
||||
_.expect('flat_map', 2, 'function', f)
|
||||
local args, n = table.pack(t1, ...), 0
|
||||
for i = 1, args.n do
|
||||
_.expect('flat_map', 1 + i, 'table', args[i])
|
||||
n = math.max(n, #args[i])
|
||||
end
|
||||
local out, li = {}, 0
|
||||
for i = 1, n do
|
||||
local these = {}
|
||||
for j = 1, args.n do
|
||||
these[j] = args[j][i]
|
||||
end
|
||||
local r = _.apply(f, these)
|
||||
if type(r) == 'table' then
|
||||
for i = 1, #r do
|
||||
out[li + i] = r[i]
|
||||
end
|
||||
li = li + #r
|
||||
else
|
||||
out[li + 1] = r
|
||||
li = li + 1
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.filter(t, p)
|
||||
_.expect('filter', 1, 'table', t)
|
||||
_.expect('filter', 2, 'function', p)
|
||||
local out, li = {}, 1
|
||||
for i = 1, #t do
|
||||
if p(t[i]) then
|
||||
out[li] = t[i]
|
||||
li = li + 1
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.id(v)
|
||||
_.expect('id', 1, 'value', v)
|
||||
return v
|
||||
end
|
||||
|
||||
function _.sort_by(t, f)
|
||||
_.expect('sort_by', 1, 'table', t)
|
||||
_.expect('sort_by', 2, 'function', f)
|
||||
local nt = _.map(t, _.id)
|
||||
|
||||
table.sort(nt, function(a, b) return f(a) < f(b) end)
|
||||
return nt
|
||||
end
|
||||
|
||||
function _.sort(t)
|
||||
_.expect('sort', 1, 'table', t)
|
||||
|
||||
return _.sort_by(t, _.id)
|
||||
end
|
||||
|
||||
function _.sample_size(t, n)
|
||||
_.expect('sample_size', 1, 'table', t)
|
||||
_.expect('sample_size', 2, 'number', n)
|
||||
|
||||
if #t <= n then
|
||||
return t
|
||||
end
|
||||
|
||||
local src = _.keys(t)
|
||||
local out = {}
|
||||
for i = 1, n do
|
||||
local k = _.sample(src)
|
||||
out[i] = t[k]
|
||||
|
||||
src[k] = src[#src]
|
||||
src[#src] = nil
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.sample(t)
|
||||
_.expect('sample', 1, 'table', t)
|
||||
return t[math.random(1, #t)]
|
||||
end
|
||||
|
||||
function _.head(t)
|
||||
_.expect('head', 1, 'table', t)
|
||||
return x[1]
|
||||
end
|
||||
|
||||
function _.tail(t)
|
||||
_.expect('tail', 1, 'table', t)
|
||||
local out = {}
|
||||
for i = 2, #t do
|
||||
out[i - 1] = t[i]
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.every(t, p)
|
||||
_.expect('every', 1, 'table', t)
|
||||
_.expect('every', 1, 'function', p)
|
||||
for i = 1, #t do
|
||||
if not p(t[i]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function _.some(t, p)
|
||||
_.expect('some', 1, 'table', t)
|
||||
_.expect('some', 1, 'function', p)
|
||||
for i = 1, #t do
|
||||
if p(t[i]) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function _.initial(t)
|
||||
_.expect('initial', 1, 'table', t)
|
||||
local out = {}
|
||||
for i = 1, #t - 1 do
|
||||
out[i] = t[i]
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.last(t)
|
||||
_.expect('last', 1, 'table', t)
|
||||
return t[#t]
|
||||
end
|
||||
|
||||
function _.nth(t, i)
|
||||
_.expect('nth', 1, 'table', t)
|
||||
_.expect('nth', 2, 'value', i)
|
||||
return t[i]
|
||||
end
|
||||
|
||||
function _.keys(t)
|
||||
_.expect('keys', 1, 'table', t)
|
||||
local out, i = {}, 1
|
||||
for k, v in pairs(t) do
|
||||
out[i] = k
|
||||
i = i + 1
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _.values(t)
|
||||
_.expect('values', 1, 'table', t)
|
||||
local out, i = {}, 1
|
||||
for k, v in pairs(t) do
|
||||
out[i] = v
|
||||
i = i + 1
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function _mt.__call(_, x)
|
||||
local function wrap(f)
|
||||
return function(...)
|
||||
return _(f(...))
|
||||
end
|
||||
end
|
||||
if type(x) == 'table' then
|
||||
return setmetatable(x,
|
||||
{ __index = function(t, k)
|
||||
return wrap(_[k])
|
||||
end })
|
||||
else
|
||||
return x
|
||||
end
|
||||
end
|
||||
|
||||
function _.distance(str1, str2)
|
||||
local v0 = {}
|
||||
local v1 = {}
|
||||
|
||||
for i = 0, #str2 do
|
||||
v0[i] = i
|
||||
end
|
||||
|
||||
for i = 0, #str1 - 1 do
|
||||
v1[0] = i + 1
|
||||
|
||||
for j = 0, #str2 - 1 do
|
||||
local delCost = v0[j + 1] + 1
|
||||
local insertCost = v1[j] + 1
|
||||
local subCost
|
||||
|
||||
if str1:sub(i + 1, i + 1) == str2:sub(j + 1, j + 1) then
|
||||
subCost = v0[j]
|
||||
else
|
||||
subCost = v0[j] + 1
|
||||
end
|
||||
|
||||
v1[j + 1] = math.min(delCost, insertCost, subCost)
|
||||
end
|
||||
|
||||
local t = v0
|
||||
v0 = v1
|
||||
v1 = t
|
||||
end
|
||||
|
||||
return v0[#str2]
|
||||
end
|
||||
|
||||
_.ops = {
|
||||
plus = function(a, b) return a + b end,
|
||||
minus = function(a, b) return a - b end,
|
||||
times = function(a, b) return a * b end,
|
||||
over = function(a, b) return a / b end,
|
||||
power = function(a, b) return a ^ b end,
|
||||
modulo = function(a, b) return a % b end,
|
||||
remainder = function(a, b) return a % b end,
|
||||
rem = function(a, b) return a % b end,
|
||||
mod = function(a, b) return a % b end,
|
||||
conj = function(a, b) return a and b end,
|
||||
disj = function(a, b) return a or b end,
|
||||
equals = function(a, b) return a == b end,
|
||||
divisible_by = function(a, b)
|
||||
return b % a == 0
|
||||
end,
|
||||
['>'] = function(a, b) return a > b end,
|
||||
['>='] = function(a, b) return a >= b end,
|
||||
['<'] = function(a, b) return a < b end,
|
||||
['<='] = function(a, b) return a <= b end,
|
||||
}
|
||||
|
||||
function string.starts_with(self, s)
|
||||
_.expect('starts_with', 1, 'string', s)
|
||||
return self:find('^' .. s) ~= nil
|
||||
end
|
||||
|
||||
return _
|
Loading…
Reference in New Issue
Block a user