commit 7b7addb054b2c17cb43921401ef0ef0bded2b4e7 Author: osmarks Date: Thu Jul 26 10:28:37 2018 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6994671 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +mnt \ No newline at end of file diff --git a/backend-chests.lua b/backend-chests.lua new file mode 100644 index 0000000..7df91ee --- /dev/null +++ b/backend-chests.lua @@ -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() \ No newline at end of file diff --git a/dev.sh b/dev.sh new file mode 100755 index 0000000..3f13135 --- /dev/null +++ b/dev.sh @@ -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 \ No newline at end of file diff --git a/lib.lua b/lib.lua new file mode 100644 index 0000000..df06ef5 --- /dev/null +++ b/lib.lua @@ -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 } \ No newline at end of file diff --git a/luadash.lua b/luadash.lua new file mode 100644 index 0000000..3e6ff0d --- /dev/null +++ b/luadash.lua @@ -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 _ diff --git a/mnt/backend-chests.lua b/mnt/backend-chests.lua new file mode 100644 index 0000000..7df91ee --- /dev/null +++ b/mnt/backend-chests.lua @@ -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() \ No newline at end of file diff --git a/mnt/lib.lua b/mnt/lib.lua new file mode 100644 index 0000000..d597594 --- /dev/null +++ b/mnt/lib.lua @@ -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 } \ No newline at end of file diff --git a/mnt/luadash.lua b/mnt/luadash.lua new file mode 100644 index 0000000..3e6ff0d --- /dev/null +++ b/mnt/luadash.lua @@ -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 _