wyvern/lib.lua

276 lines
10 KiB
Lua
Raw Normal View History

2018-07-26 09:28:37 +00:00
--[[
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)
2018-07-26 15:32:48 +00:00
general functions of utility
Plethora helpers
2018-07-26 09:28:37 +00:00
]]
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.
2018-07-26 11:47:35 +00:00
NOSPACE = 6, -- No data required. Should be returned if there is no available storage space.
2018-07-26 09:28:37 +00:00
make = function(e, d)
return { type = "error", error = e, data = d }
end
}
-- Converts an error into human-readable format
errors.format = function(e)
2018-07-26 11:47:35 +00:00
if not (e.type and e.type == "error" and e.data and e.error) then return "Provided error is not an error object." end
2018-07-26 09:28:37 +00:00
if e.error == errors.INTERNAL then
return "Internal error - provided info: " .. textutils.serialise(e.data) .. "."
elseif e.error == errors.INVALID then
return "Request invalid."
2018-08-13 07:56:30 +00:00
elseif e.error == errors.NOPATTERN then
2018-07-26 09:28:37 +00:00
return "Missing pattern " .. textutils.serialise(e.data) .. "."
2018-08-13 07:56:30 +00:00
elseif e.error == errors.NOITEMS then
2018-07-26 09:28:37 +00:00
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."
2018-08-13 07:56:30 +00:00
elseif e.error == errors.NORESPONSE then
2018-07-26 09:28:37 +00:00
local text = "No response"
if e.data then text = text .. " from " .. textutils.serialise(e.data) end
return text .. "."
2018-08-13 07:56:30 +00:00
elseif e.error == errors.NOMATCHINGNODE then
2018-07-26 09:28:37 +00:00
if e.data then
return "No " .. textutils.serialise(e.data) .. " node found."
else
return "No node of desired type found."
end
2018-08-13 07:56:30 +00:00
elseif e.error == errors.NOSPACE then
2018-07-26 11:47:35 +00:00
return "No available storage space."
2018-07-26 09:28:37 +00:00
else
return "Error is invalid. Someone broke it."
end
end
-- NETWORKING
local protocol = "wyvern"
local lookup_cache = {}
local function cached_lookup(protocol)
if lookup_cache[protocol] then return lookup_cache[protocol]
else
local ID = rednet.lookup(protocol)
lookup_cache[protocol] = ID
return ID
end
end
2018-07-26 17:13:05 +00:00
local function init_screen(scr, bg, fg)
scr.setCursorPos(1, 1)
2018-07-26 20:54:05 +00:00
scr.setBackgroundColor(bg)
scr.setTextColor(fg)
2018-07-26 17:21:50 +00:00
scr.clear()
2018-07-26 17:13:05 +00:00
end
2018-07-26 09:28:37 +00:00
-- 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.
2018-07-26 17:13:05 +00:00
-- Also displays a nice informational UI
local function serve(fn, node_type)
local w, h = term.getSize()
local titlebar = window.create(term.current(), 1, 1, w, 1)
local main_screen = window.create(term.current(), 1, 2, w, h - 1)
2018-07-26 17:13:05 +00:00
2018-07-26 20:55:21 +00:00
init_screen(titlebar, colors.lightGray, colors.black)
2018-07-26 17:13:05 +00:00
titlebar.write("Wyvern " .. node_type)
2018-07-26 20:55:21 +00:00
init_screen(main_screen, colors.white, colors.black)
2018-07-26 17:13:05 +00:00
term.redirect(main_screen)
2018-07-26 17:16:41 +00:00
titlebar.redraw()
main_screen.redraw()
2018-07-26 17:13:05 +00:00
rednet.host(protocol .. "/" .. node_type, node_type .. "/" .. tostring(os.getComputerID()))
2018-07-26 09:28:37 +00:00
while true do
local sender, message = rednet.receive(protocol)
-- As a default response, send an "invalid request" error
local response = errors.make(errors.INVALID)
2018-07-26 17:13:05 +00:00
local start_time = os.clock()
print(tostring(sender) .. " > " .. tostring(os.getComputerID())) -- show sender and recipient
2018-07-26 09:28:37 +00:00
-- 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
2018-07-26 17:13:05 +00:00
print("Request:", textutils.serialise(message.request))
2018-07-27 08:45:41 +00:00
local ok, result = pcall(fn, message.request)
2018-07-26 17:13:05 +00:00
if not ok then
2018-08-13 10:25:18 +00:00
if type(result) ~= "table" or not result.error then response = errors.make(errors.INTERNAL, result)
else response = result end
2018-07-26 17:13:05 +00:00
print("Error:", textutils.serialise(result)) -- show error
else
local end_time = os.clock()
print("Response:", textutils.serialise(result))
2018-07-27 11:03:50 +00:00
print("Time:", string.format("%.1f", end_time - start_time))
response = { type = "OK", value = result }
2018-07-26 17:13:05 +00:00
end
else
print("Request Invalid")
2018-07-26 09:28:37 +00:00
end
2018-07-26 17:16:41 +00:00
main_screen.redraw()
2018-07-26 09:28:37 +00:00
rednet.send(sender, response, protocol)
end
end
-- Attempts to send "request" to "ID", with the maximum number of allowable tries being "tries"
2018-07-27 08:43:03 +00:00
local function query_by_ID(ID, request, max_tries)
local max_tries = max_tries or 3
2018-07-26 09:28:37 +00:00
local request_object = { type = "request", request = request }
2018-07-27 08:43:03 +00:00
local result = nil
local tries = 0
2018-07-26 09:28:37 +00:00
repeat
2018-07-27 08:41:13 +00:00
rednet.send(ID, request_object, protocol)
2018-07-26 09:28:37 +00:00
_, result = rednet.receive(protocol, 1)
2018-07-27 08:43:03 +00:00
tries = tries + 1
2018-07-26 09:28:37 +00:00
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 = cached_lookup(protocol .. "/" .. type)
2018-07-26 09:28:37 +00:00
if not ID then return errors.make(errors.NOMATCHINGNODE, type) end
return query_by_ID(ID, request, tries)
end
2018-07-26 15:32:48 +00:00
-- PLETHORA HELPERS
2018-08-13 07:33:32 +00:00
-- Converts a plethora item (as in a slot) to a Wyvern item
local function to_wyvern_item(item)
return { NBT_hash = item.NBT_hash or item.nbtHash, ID = item.ID or item.name, meta = item.meta or item.damage, display_name = item.display_name or item.displayName, count = item.count }
end
2018-07-26 15:32:48 +00:00
-- Gets the internal identifier of an item - unique (hopefully) per type of item, as defined by NBT, metadata/damage and ID/name
local function get_internal_identifier(item)
2018-08-13 07:59:10 +00:00
local n = item.ID
if item.meta then n = n .. ":" .. item.meta end
if item.NBT_hash then n = n .. "#" .. item.NBT_hash end
2018-07-26 15:32:48 +00:00
return n
end
2018-07-26 09:28:37 +00:00
-- GENERAL STUFF
2018-07-27 12:59:32 +00:00
-- Converts a table of the form {"x", "x", "y"} into {x = 2, y = 1}
local function collate(items)
local ret = {}
for _, i in pairs(items) do
ret[i] = (ret[i] or 0) + 1
end
return ret
end
2018-07-27 15:18:33 +00:00
-- Functions like "collate" but on itemstacks (adds their counts)
local function collate_stacks(s)
local out = {}
2018-07-27 16:01:07 +00:00
for _, stack in pairs(s) do
2018-07-28 12:34:27 +00:00
local i = get_internal_identifier(stack)
2018-07-27 15:18:33 +00:00
if out[i] then out[i].count = out[i].count + stack.count
else out[i] = stack end
end
return out
end
2018-07-27 12:59:32 +00:00
-- Checks whether "needs"'s (a collate-formatted table) values are all greater than those of "has"
local function satisfied(needs, has)
local good = true
for k, qty in pairs(needs) do
if qty > (has[k] or 0) then good = false end
end
return good
end
2018-07-26 09:28:37 +00:00
-- 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)
2018-07-26 11:47:35 +00:00
local required_data = required_data or {}
local defaults = defaults or {}
2018-07-26 09:28:37 +00:00
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, from)
2018-07-26 09:28:37 +00:00
local matching = {}
local list
if from then
list = peripheral.call(from, "getNamesRemote")
else
list = peripheral.getNames()
end
for k, name in pairs(list) do
2018-07-26 09:28:37 +00:00
local wrapped = peripheral.wrap(name)
local type = peripheral.getType(name)
2018-07-26 21:07:27 +00:00
if predicate(type, name, wrapped) then table.insert(matching, { wrapped = wrapped, name = name} ) end
2018-07-26 09:28:37 +00:00
end
return matching
end
-- Set up stuff for running this library's features (currently, modem initialization)
local function init()
2018-07-26 21:07:27 +00:00
d.map(find_peripherals(function(type, name, wrapped) return type == "modem" end), function(p) rednet.open(p.name) end)
2018-07-26 09:28:37 +00:00
end
-- Rust-style unwrap. If x is an OK table, will take out its contents and return them - if error, will crash and print it, with msg if provided
local function unwrap(x, msg)
if not x or type(x) ~= "table" or not x.type then x = errors.make(errors.INTERNAL, "Error/response object is invalid. This is probably a problem with the node being contacted.") end
if x.type == "error" then
local text = "An error occured"
2018-08-12 16:13:15 +00:00
if msg then text = text .. " " .. msg
else text = text .. "!" end
2018-08-13 07:56:30 +00:00
text = text .. ".\nDetails: " .. errors.format(x)
error(text)
elseif x.type == "OK" then
return x.value
end
end
-- Wrap x in an OK result
local function make_OK(x)
return { type = "OK", value = x }
end
-- TODO: Not do this
return { errors = errors, serve = serve, query_by_ID = query_by_ID, query_by_type = query_by_type, unwrap = unwrap, to_wyvern_item = to_wyvern_item, get_internal_identifier = get_internal_identifier, load_config = load_config, find_peripherals = find_peripherals, init = init, collate = collate, satisfied = satisfied, collate_stacks = collate_stacks, make_error = errors.make, make_OK = make_OK }