move apis into rom/modules/main for shell compatibility

This commit is contained in:
kepler155c@gmail.com
2019-06-28 13:50:02 -04:00
parent c3d52c1aab
commit 343ce7fdc2
135 changed files with 297 additions and 289 deletions

55
sys/modules/opus/ansi.lua Normal file
View File

@@ -0,0 +1,55 @@
local Ansi = setmetatable({ }, {
__call = function(_, ...)
local str = '\027['
for k,v in ipairs({ ...}) do
if k == 1 then
str = str .. v
else
str = str .. ';' .. v
end
end
return str .. 'm'
end
})
Ansi.codes = {
reset = 0,
white = 1,
orange = 2,
magenta = 3,
lightBlue = 4,
yellow = 5,
lime = 6,
pink = 7,
gray = 8,
lightGray = 9,
cyan = 10,
purple = 11,
blue = 12,
brown = 13,
green = 14,
red = 15,
black = 16,
onwhite = 21,
onorange = 22,
onmagenta = 23,
onlightBlue = 24,
onyellow = 25,
onlime = 26,
onpink = 27,
ongray = 28,
onlightGray = 29,
oncyan = 30,
onpurple = 31,
onblue = 32,
onbrown = 33,
ongreen = 34,
onred = 35,
onblack = 36,
}
for k,v in pairs(Ansi.codes) do
Ansi[k] = Ansi(v)
end
return Ansi

View File

@@ -0,0 +1,13 @@
local Array = { }
function Array.filter(it, f)
local ot = { }
for _,v in pairs(it) do
if f(v) then
table.insert(ot, v)
end
end
return ot
end
return Array

View File

@@ -0,0 +1,33 @@
local Util = require('opus.util')
local parallel = _G.parallel
local BulkGet = { }
function BulkGet.download(list, callback)
local t = { }
local failed = false
for _ = 1, 5 do
table.insert(t, function()
while true do
local entry = table.remove(list)
if not entry then
break
end
local s, m = Util.download(entry.url, entry.path)
if not s then
failed = true
end
callback(entry, s, m)
if failed then
break
end
end
end)
end
parallel.waitForAll(table.unpack(t))
end
return BulkGet

View File

@@ -0,0 +1,49 @@
-- From http://lua-users.org/wiki/SimpleLuaClasses
-- (with some modifications)
-- class.lua
-- Compatible with Lua 5.1 (not 5.0).
return function(base)
local c = { } -- a new class instance
if type(base) == 'table' then
-- our new class is a shallow copy of the base class!
if base._preload then
base = base._preload(base)
end
for i,v in pairs(base) do
c[i] = v
end
c._base = base
end
-- the class will be the metatable for all its objects,
-- and they will look up their methods in it.
c.__index = c
-- expose a constructor which can be called by <classname>(<args>)
setmetatable(c, {
__call = function(class_tbl, ...)
local obj = { }
setmetatable(obj,c)
if class_tbl.init then
class_tbl.init(obj, ...)
else
-- make sure that any stuff from the base class is initialized!
if base and base.init then
base.init(obj, ...)
end
end
return obj
end
})
c.is_a =
function(self, klass)
local m = getmetatable(self)
while m do
if m == klass then return true end
m = m._base
end
return false
end
return c
end

View File

@@ -0,0 +1,50 @@
local Util = require('opus.util')
local fs = _G.fs
local shell = _ENV.shell
local Config = { }
function Config.load(fname, data)
local filename = 'usr/config/' .. fname
data = data or { }
if not fs.exists('usr/config') then
fs.makeDir('usr/config')
end
if not fs.exists(filename) then
Util.writeTable(filename, data)
else
local contents = Util.readTable(filename) or
error('Configuration file is corrupt:' .. filename)
Util.merge(data, contents)
end
return data
end
function Config.loadWithCheck(fname, data)
local filename = 'usr/config/' .. fname
if not fs.exists(filename) then
Config.load(fname, data)
print()
print('The configuration file has been created.')
print('The file name is: ' .. filename)
print()
_G.printError('Press enter to configure')
_G.read()
shell.run('edit ' .. filename)
end
return Config.load(fname, data)
end
function Config.update(fname, data)
local filename = 'usr/config/' .. fname
Util.writeTable(filename, data)
end
return Config

View File

@@ -0,0 +1,168 @@
-- Chacha20 cipher in ComputerCraft
-- By Anavrins
local sha2 = require('opus.crypto.sha2')
local util = require('opus.util')
local ROUNDS = 20 -- Adjust this for speed tradeoff
local bxor = bit32.bxor
local band = bit32.band
local blshift = bit32.lshift
local brshift = bit32.arshift
local os = _G.os
local textutils = _G.textutils
local mod = 2^32
local tau = {("expand 16-byte k"):byte(1,-1)}
local sigma = {("expand 32-byte k"):byte(1,-1)}
local function rotl(n, b)
local s = n/(2^(32-b))
local f = s%1
return (s-f) + f*mod
end
local function quarterRound(s, a, b, c, d)
s[a] = (s[a]+s[b])%mod; s[d] = rotl(bxor(s[d], s[a]), 16)
s[c] = (s[c]+s[d])%mod; s[b] = rotl(bxor(s[b], s[c]), 12)
s[a] = (s[a]+s[b])%mod; s[d] = rotl(bxor(s[d], s[a]), 8)
s[c] = (s[c]+s[d])%mod; s[b] = rotl(bxor(s[b], s[c]), 7)
return s
end
local function hashBlock(state, rnd)
local s = {unpack(state)}
for i = 1, rnd do
local r = i%2==1
s = r and quarterRound(s, 1, 5, 9, 13) or quarterRound(s, 1, 6, 11, 16)
s = r and quarterRound(s, 2, 6, 10, 14) or quarterRound(s, 2, 7, 12, 13)
s = r and quarterRound(s, 3, 7, 11, 15) or quarterRound(s, 3, 8, 9, 14)
s = r and quarterRound(s, 4, 8, 12, 16) or quarterRound(s, 4, 5, 10, 15)
end
for i = 1, 16 do s[i] = (s[i]+state[i])%mod end
return s
end
local function LE_toInt(bs, i)
return (bs[i+1] or 0)+
blshift((bs[i+2] or 0), 8)+
blshift((bs[i+3] or 0), 16)+
blshift((bs[i+4] or 0), 24)
end
local function initState(key, nonce, counter)
local isKey256 = #key == 32
local const = isKey256 and sigma or tau
local state = {}
state[ 1] = LE_toInt(const, 0)
state[ 2] = LE_toInt(const, 4)
state[ 3] = LE_toInt(const, 8)
state[ 4] = LE_toInt(const, 12)
state[ 5] = LE_toInt(key, 0)
state[ 6] = LE_toInt(key, 4)
state[ 7] = LE_toInt(key, 8)
state[ 8] = LE_toInt(key, 12)
state[ 9] = LE_toInt(key, isKey256 and 16 or 0)
state[10] = LE_toInt(key, isKey256 and 20 or 4)
state[11] = LE_toInt(key, isKey256 and 24 or 8)
state[12] = LE_toInt(key, isKey256 and 28 or 12)
state[13] = counter
state[14] = LE_toInt(nonce, 0)
state[15] = LE_toInt(nonce, 4)
state[16] = LE_toInt(nonce, 8)
return state
end
local function serialize(state)
local r = {}
for i = 1, 16 do
r[#r+1] = band(state[i], 0xFF)
r[#r+1] = band(brshift(state[i], 8), 0xFF)
r[#r+1] = band(brshift(state[i], 16), 0xFF)
r[#r+1] = band(brshift(state[i], 24), 0xFF)
end
return r
end
local mt = {
__tostring = function(a) return string.char(unpack(a)) end,
__index = {
toHex = function(self) return ("%02x"):rep(#self):format(unpack(self)) end,
isEqual = function(self, t)
if type(t) ~= "table" then return false end
if #self ~= #t then return false end
local ret = 0
for i = 1, #self do
ret = bit32.bor(ret, bxor(self[i], t[i]))
end
return ret == 0
end
}
}
local function crypt(data, key, nonce, cntr, round)
assert(type(key) == "table", "ChaCha20: Invalid key format ("..type(key).."), must be table")
assert(type(nonce) == "table", "ChaCha20: Invalid nonce format ("..type(nonce).."), must be table")
assert(#key == 16 or #key == 32, "ChaCha20: Invalid key length ("..#key.."), must be 16 or 32")
assert(#nonce == 12, "ChaCha20: Invalid nonce length ("..#nonce.."), must be 12")
data = type(data) == "table" and {unpack(data)} or {tostring(data):byte(1,-1)}
cntr = tonumber(cntr) or 1
round = tonumber(round) or 20
local out = {}
local state = initState(key, nonce, cntr)
local blockAmt = math.floor(#data/64)
for i = 0, blockAmt do
local ks = serialize(hashBlock(state, round))
state[13] = (state[13]+1) % mod
local block = {}
for j = 1, 64 do
block[j] = data[((i)*64)+j]
end
for j = 1, #block do
out[#out+1] = bxor(block[j], ks[j])
end
if i % 1000 == 0 then
os.queueEvent("")
os.pullEvent("")
end
end
return setmetatable(out, mt)
end
local function genNonce(len)
local nonce = {}
for i = 1, len do
nonce[i] = math.random(0, 0xFF)
end
return setmetatable(nonce, mt)
end
local function encrypt(data, key)
local nonce = genNonce(12)
data = textutils.serialise(data)
key = sha2.digest(key)
local ctx = crypt(data, key, nonce, 1, ROUNDS)
return { nonce:toHex(), ctx:toHex() }
end
local function decrypt(data, key)
local nonce = util.hexToByteArray(data[1])
data = util.hexToByteArray(data[2])
key = sha2.digest(key)
local ptx = crypt(data, key, nonce, 1, ROUNDS)
return textutils.unserialise(tostring(ptx))
end
return {
encrypt = encrypt,
decrypt = decrypt,
}

View File

@@ -0,0 +1,300 @@
---- Elliptic Curve Arithmetic
---- About the Curve Itself
-- Field Size: 192 bits
-- Field Modulus (p): 65533 * 2^176 + 3
-- Equation: x^2 + y^2 = 1 + 108 * x^2 * y^2
-- Parameters: Edwards Curve with c = 1, and d = 108
-- Curve Order (n): 4 * 1569203598118192102418711808268118358122924911136798015831
-- Cofactor (h): 4
-- Generator Order (q): 1569203598118192102418711808268118358122924911136798015831
---- About the Curve's Security
-- Current best attack security: 94.822 bits (Pollard's Rho)
-- Rho Security: log2(0.884 * sqrt(q)) = 94.822
-- Transfer Security? Yes: p ~= q; k > 20
-- Field Discriminant Security? Yes: t = 67602300638727286331433024168; s = 2^2; |D| = 5134296629560551493299993292204775496868940529592107064435 > 2^100
-- Rigidity? A little, the parameters are somewhat small.
-- XZ/YZ Ladder Security? No: Single coordinate ladders are insecure, so they can't be used.
-- Small Subgroup Security? Yes: Secret keys are calculated modulo 4q.
-- Invalid Curve Security? Yes: Any point to be multiplied is checked beforehand.
-- Invalid Curve Twist Security? No: The curve is not protected against single coordinate ladder attacks, so don't use them.
-- Completeness? Yes: The curve is an Edwards Curve with non-square d and square a, so the curve is complete.
-- Indistinguishability? No: The curve does not support indistinguishability maps.
local fp = require('opus.crypto.ecc.fp')
local eq = fp.eq
local mul = fp.mul
local sqr = fp.sqr
local add = fp.add
local sub = fp.sub
local shr = fp.shr
local mont = fp.mont
local invMont = fp.invMont
local sub192 = fp.sub192
local bits = 192
local pMinusTwoBinary = {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
local pMinusThreeOverFourBinary = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0}
local ZERO = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
local ONE = mont({1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
local p = mont({3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65533})
local G = {
mont({30457, 58187, 5603, 63215, 8936, 58151, 26571, 7272, 26680, 23486, 32353, 59456}),
mont({3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
mont({1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
}
local GTable = {G}
local d = mont({108, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
local function generator()
return G
end
local function expMod(a, t)
local a = {unpack(a)}
local result = {unpack(ONE)}
for i = 1, bits do
if t[i] == 1 then
result = mul(result, a)
end
a = mul(a, a)
end
return result
end
-- We're using Projective Coordinates
-- For Edwards curves
-- The identity element is represented by (0:1:1)
local function pointDouble(P1)
local X1, Y1, Z1 = unpack(P1)
local b = add(X1, Y1)
local B = sqr(b)
local C = sqr(X1)
local D = sqr(Y1)
local E = add(C, D)
local H = sqr(Z1)
local J = sub(E, add(H, H))
local X3 = mul(sub(B, E), J)
local Y3 = mul(E, sub(C, D))
local Z3 = mul(E, J)
local P3 = {X3, Y3, Z3}
return P3
end
local function pointAdd(P1, P2)
local X1, Y1, Z1 = unpack(P1)
local X2, Y2, Z2 = unpack(P2)
local A = mul(Z1, Z2)
local B = sqr(A)
local C = mul(X1, X2)
local D = mul(Y1, Y2)
local E = mul(d, mul(C, D))
local F = sub(B, E)
local G = add(B, E)
local X3 = mul(A, mul(F, sub(mul(add(X1, Y1), add(X2, Y2)), add(C, D))))
local Y3 = mul(A, mul(G, sub(D, C)))
local Z3 = mul(F, G)
local P3 = {X3, Y3, Z3}
return P3
end
local function pointNeg(P1)
local X1, Y1, Z1 = unpack(P1)
local X3 = sub(p, X1)
local Y3 = {unpack(Y1)}
local Z3 = {unpack(Z1)}
local P3 = {X3, Y3, Z3}
return P3
end
local function pointSub(P1, P2)
return pointAdd(P1, pointNeg(P2))
end
local function pointScale(P1)
local X1, Y1, Z1 = unpack(P1)
local A = expMod(Z1, pMinusTwoBinary)
local X3 = mul(X1, A)
local Y3 = mul(Y1, A)
local Z3 = {unpack(ONE)}
local P3 = {X3, Y3, Z3}
return P3
end
local function pointEq(P1, P2)
local X1, Y1, Z1 = unpack(P1)
local X2, Y2, Z2 = unpack(P2)
local A1 = mul(X1, Z2)
local B1 = mul(Y1, Z2)
local A2 = mul(X2, Z1)
local B2 = mul(Y2, Z1)
return eq(A1, A2) and eq(B1, B2)
end
local function isOnCurve(P1)
local X1, Y1, Z1 = unpack(P1)
local X12 = sqr(X1)
local Y12 = sqr(Y1)
local Z12 = sqr(Z1)
local Z14 = sqr(Z12)
local a = add(X12, Y12)
a = mul(a, Z12)
local b = mul(d, mul(X12, Y12))
b = add(Z14, b)
return eq(a, b)
end
local function mods(d)
-- w = 5
local result = d[1] % 32
if result >= 16 then
result = result - 32
end
return result
end
local function NAF(d)
local t = {}
local d = {unpack(d)}
while d[12] >= 0 and not eq(d, ZERO) do
if d[1] % 2 == 1 then
t[#t + 1] = mods(d)
d = sub192(d, {t[#t], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
else
t[#t + 1] = 0
end
d = shr(d)
end
return t
end
local function scalarMul(s, P1)
local naf = NAF(s)
local PTable = {P1}
local P2 = pointDouble(P1)
for i = 3, 31, 2 do
PTable[i] = pointAdd(PTable[i - 2], P2)
end
local Q = {{unpack(ZERO)}, {unpack(ONE)}, {unpack(ONE)}}
for i = #naf, 1, -1 do
Q = pointDouble(Q)
if naf[i] > 0 then
Q = pointAdd(Q, PTable[naf[i]])
elseif naf[i] < 0 then
Q = pointSub(Q, PTable[-naf[i]])
end
end
return Q
end
for i = 2, 196 do
GTable[i] = pointDouble(GTable[i - 1])
end
local function scalarMulG(s)
local result = {{unpack(ZERO)}, {unpack(ONE)}, {unpack(ONE)}}
local k = 1
for i = 1, 12 do
local w = s[i]
for j = 1, 16 do
if w % 2 == 1 then
result = pointAdd(result, GTable[k])
end
k = k + 1
w = w / 2
w = w - w % 1
end
end
return result
end
local function pointEncode(P1)
P1 = pointScale(P1)
local result = {}
local x, y = unpack(P1)
result[1] = x[1] % 2
for i = 1, 12 do
local m = y[i] % 256
result[2 * i] = m
result[2 * i + 1] = (y[i] - m) / 256
end
return result
end
local function pointDecode(enc)
local y = {}
for i = 1, 12 do
y[i] = enc[2 * i]
y[i] = y[i] + enc[2 * i + 1] * 256
end
local y2 = sqr(y)
local u = sub(y2, ONE)
local v = sub(mul(d, y2), ONE)
local u2 = sqr(u)
local u3 = mul(u, u2)
local u5 = mul(u3, u2)
local v3 = mul(v, sqr(v))
local w = mul(u5, v3)
local x = mul(u3, mul(v, expMod(w, pMinusThreeOverFourBinary)))
if x[1] % 2 ~= enc[1] then
x = sub(p, x)
end
local P3 = {x, y, {unpack(ONE)}}
return P3
end
return {
generator = generator,
pointDouble = pointDouble,
pointAdd = pointAdd,
pointNeg = pointNeg,
pointSub = pointSub,
pointScale = pointScale,
pointEq = pointEq,
isOnCurve = isOnCurve,
scalarMul = scalarMul,
scalarMulG = scalarMulG,
pointEncode = pointEncode,
pointDecode = pointDecode,
}

View File

@@ -0,0 +1,928 @@
-- Fp Integer Arithmetic
local n = 0xffff
local m = 0x10000
local p = {3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 65533}
local p2 = {21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 21845, 43690}
local r2 = {44014, 58358, 19452, 6484, 45852, 58974, 63348, 64806, 65292, 65454, 65508, 21512}
local function eq(a, b)
for i = 1, 12 do
if a[i] ~= b[i] then
return false
end
end
return true
end
local function reduce(a)
local r1 = a[1]
local r2 = a[2]
local r3 = a[3]
local r4 = a[4]
local r5 = a[5]
local r6 = a[6]
local r7 = a[7]
local r8 = a[8]
local r9 = a[9]
local r10 = a[10]
local r11 = a[11]
local r12 = a[12]
if r12 < 65533 or r12 == 65533 and r1 < 3 then
return {unpack(a)}
end
r1 = r1 - 3
r12 = r12 - 65533
if r1 < 0 then
r2 = r2 - 1
r1 = r1 + m
end
if r2 < 0 then
r3 = r3 - 1
r2 = r2 + m
end
if r3 < 0 then
r4 = r4 - 1
r3 = r3 + m
end
if r4 < 0 then
r5 = r5 - 1
r4 = r4 + m
end
if r5 < 0 then
r6 = r6 - 1
r5 = r5 + m
end
if r6 < 0 then
r7 = r7 - 1
r6 = r6 + m
end
if r7 < 0 then
r8 = r8 - 1
r7 = r7 + m
end
if r8 < 0 then
r9 = r9 - 1
r8 = r8 + m
end
if r9 < 0 then
r10 = r10 - 1
r9 = r9 + m
end
if r10 < 0 then
r11 = r11 - 1
r10 = r10 + m
end
if r11 < 0 then
r12 = r12 - 1
r11 = r11 + m
end
return {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}
end
local function add(a, b)
local r1 = a[1] + b[1]
local r2 = a[2] + b[2]
local r3 = a[3] + b[3]
local r4 = a[4] + b[4]
local r5 = a[5] + b[5]
local r6 = a[6] + b[6]
local r7 = a[7] + b[7]
local r8 = a[8] + b[8]
local r9 = a[9] + b[9]
local r10 = a[10] + b[10]
local r11 = a[11] + b[11]
local r12 = a[12] + b[12]
if r1 > n then
r2 = r2 + 1
r1 = r1 - m
end
if r2 > n then
r3 = r3 + 1
r2 = r2 - m
end
if r3 > n then
r4 = r4 + 1
r3 = r3 - m
end
if r4 > n then
r5 = r5 + 1
r4 = r4 - m
end
if r5 > n then
r6 = r6 + 1
r5 = r5 - m
end
if r6 > n then
r7 = r7 + 1
r6 = r6 - m
end
if r7 > n then
r8 = r8 + 1
r7 = r7 - m
end
if r8 > n then
r9 = r9 + 1
r8 = r8 - m
end
if r9 > n then
r10 = r10 + 1
r9 = r9 - m
end
if r10 > n then
r11 = r11 + 1
r10 = r10 - m
end
if r11 > n then
r12 = r12 + 1
r11 = r11 - m
end
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}
return reduce(result)
end
local function shr(a)
local r1 = a[1]
local r2 = a[2]
local r3 = a[3]
local r4 = a[4]
local r5 = a[5]
local r6 = a[6]
local r7 = a[7]
local r8 = a[8]
local r9 = a[9]
local r10 = a[10]
local r11 = a[11]
local r12 = a[12]
r1 = r1 / 2
r1 = r1 - r1 % 1
r1 = r1 + (r2 % 2) * 0x8000
r2 = r2 / 2
r2 = r2 - r2 % 1
r2 = r2 + (r3 % 2) * 0x8000
r3 = r3 / 2
r3 = r3 - r3 % 1
r3 = r3 + (r4 % 2) * 0x8000
r4 = r4 / 2
r4 = r4 - r4 % 1
r4 = r4 + (r5 % 2) * 0x8000
r5 = r5 / 2
r5 = r5 - r5 % 1
r5 = r5 + (r6 % 2) * 0x8000
r6 = r6 / 2
r6 = r6 - r6 % 1
r6 = r6 + (r7 % 2) * 0x8000
r7 = r7 / 2
r7 = r7 - r7 % 1
r7 = r7 + (r8 % 2) * 0x8000
r8 = r8 / 2
r8 = r8 - r8 % 1
r8 = r8 + (r9 % 2) * 0x8000
r9 = r9 / 2
r9 = r9 - r9 % 1
r9 = r9 + (r10 % 2) * 0x8000
r10 = r10 / 2
r10 = r10 - r10 % 1
r10 = r10 + (r11 % 2) * 0x8000
r11 = r11 / 2
r11 = r11 - r11 % 1
r11 = r11 + (r12 % 2) * 0x8000
r12 = r12 / 2
r12 = r12 - r12 % 1
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}
return result
end
local function sub192(a, b)
local r1 = a[1] - b[1]
local r2 = a[2] - b[2]
local r3 = a[3] - b[3]
local r4 = a[4] - b[4]
local r5 = a[5] - b[5]
local r6 = a[6] - b[6]
local r7 = a[7] - b[7]
local r8 = a[8] - b[8]
local r9 = a[9] - b[9]
local r10 = a[10] - b[10]
local r11 = a[11] - b[11]
local r12 = a[12] - b[12]
if r1 < 0 then
r2 = r2 - 1
r1 = r1 + m
end
if r2 < 0 then
r3 = r3 - 1
r2 = r2 + m
end
if r3 < 0 then
r4 = r4 - 1
r3 = r3 + m
end
if r4 < 0 then
r5 = r5 - 1
r4 = r4 + m
end
if r5 < 0 then
r6 = r6 - 1
r5 = r5 + m
end
if r6 < 0 then
r7 = r7 - 1
r6 = r6 + m
end
if r7 < 0 then
r8 = r8 - 1
r7 = r7 + m
end
if r8 < 0 then
r9 = r9 - 1
r8 = r8 + m
end
if r9 < 0 then
r10 = r10 - 1
r9 = r9 + m
end
if r10 < 0 then
r11 = r11 - 1
r10 = r10 + m
end
if r11 < 0 then
r12 = r12 - 1
r11 = r11 + m
end
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}
return result
end
local function sub(a, b)
local r1 = a[1] - b[1]
local r2 = a[2] - b[2]
local r3 = a[3] - b[3]
local r4 = a[4] - b[4]
local r5 = a[5] - b[5]
local r6 = a[6] - b[6]
local r7 = a[7] - b[7]
local r8 = a[8] - b[8]
local r9 = a[9] - b[9]
local r10 = a[10] - b[10]
local r11 = a[11] - b[11]
local r12 = a[12] - b[12]
if r1 < 0 then
r2 = r2 - 1
r1 = r1 + m
end
if r2 < 0 then
r3 = r3 - 1
r2 = r2 + m
end
if r3 < 0 then
r4 = r4 - 1
r3 = r3 + m
end
if r4 < 0 then
r5 = r5 - 1
r4 = r4 + m
end
if r5 < 0 then
r6 = r6 - 1
r5 = r5 + m
end
if r6 < 0 then
r7 = r7 - 1
r6 = r6 + m
end
if r7 < 0 then
r8 = r8 - 1
r7 = r7 + m
end
if r8 < 0 then
r9 = r9 - 1
r8 = r8 + m
end
if r9 < 0 then
r10 = r10 - 1
r9 = r9 + m
end
if r10 < 0 then
r11 = r11 - 1
r10 = r10 + m
end
if r11 < 0 then
r12 = r12 - 1
r11 = r11 + m
end
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}
if r12 < 0 then
result = add(result, p)
end
return result
end
local function add384(a, b)
local r1 = a[1] + b[1]
local r2 = a[2] + b[2]
local r3 = a[3] + b[3]
local r4 = a[4] + b[4]
local r5 = a[5] + b[5]
local r6 = a[6] + b[6]
local r7 = a[7] + b[7]
local r8 = a[8] + b[8]
local r9 = a[9] + b[9]
local r10 = a[10] + b[10]
local r11 = a[11] + b[11]
local r12 = a[12] + b[12]
local r13 = a[13] + b[13]
local r14 = a[14] + b[14]
local r15 = a[15] + b[15]
local r16 = a[16] + b[16]
local r17 = a[17] + b[17]
local r18 = a[18] + b[18]
local r19 = a[19] + b[19]
local r20 = a[20] + b[20]
local r21 = a[21] + b[21]
local r22 = a[22] + b[22]
local r23 = a[23] + b[23]
local r24 = a[24] + b[24]
if r1 > n then
r2 = r2 + 1
r1 = r1 - m
end
if r2 > n then
r3 = r3 + 1
r2 = r2 - m
end
if r3 > n then
r4 = r4 + 1
r3 = r3 - m
end
if r4 > n then
r5 = r5 + 1
r4 = r4 - m
end
if r5 > n then
r6 = r6 + 1
r5 = r5 - m
end
if r6 > n then
r7 = r7 + 1
r6 = r6 - m
end
if r7 > n then
r8 = r8 + 1
r7 = r7 - m
end
if r8 > n then
r9 = r9 + 1
r8 = r8 - m
end
if r9 > n then
r10 = r10 + 1
r9 = r9 - m
end
if r10 > n then
r11 = r11 + 1
r10 = r10 - m
end
if r11 > n then
r12 = r12 + 1
r11 = r11 - m
end
if r12 > n then
r13 = r13 + 1
r12 = r12 - m
end
if r13 > n then
r14 = r14 + 1
r13 = r13 - m
end
if r14 > n then
r15 = r15 + 1
r14 = r14 - m
end
if r15 > n then
r16 = r16 + 1
r15 = r15 - m
end
if r16 > n then
r17 = r17 + 1
r16 = r16 - m
end
if r17 > n then
r18 = r18 + 1
r17 = r17 - m
end
if r18 > n then
r19 = r19 + 1
r18 = r18 - m
end
if r19 > n then
r20 = r20 + 1
r19 = r19 - m
end
if r20 > n then
r21 = r21 + 1
r20 = r20 - m
end
if r21 > n then
r22 = r22 + 1
r21 = r21 - m
end
if r22 > n then
r23 = r23 + 1
r22 = r22 - m
end
if r23 > n then
r24 = r24 + 1
r23 = r23 - m
end
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24}
return result
end
local function mul384(a, b)
local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 = unpack(a)
local b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12 = unpack(b)
local r1 = a1 * b1
local r2 = a1 * b2
r2 = r2 + a2 * b1
local r3 = a1 * b3
r3 = r3 + a2 * b2
r3 = r3 + a3 * b1
local r4 = a1 * b4
r4 = r4 + a2 * b3
r4 = r4 + a3 * b2
r4 = r4 + a4 * b1
local r5 = a1 * b5
r5 = r5 + a2 * b4
r5 = r5 + a3 * b3
r5 = r5 + a4 * b2
r5 = r5 + a5 * b1
local r6 = a1 * b6
r6 = r6 + a2 * b5
r6 = r6 + a3 * b4
r6 = r6 + a4 * b3
r6 = r6 + a5 * b2
r6 = r6 + a6 * b1
local r7 = a1 * b7
r7 = r7 + a2 * b6
r7 = r7 + a3 * b5
r7 = r7 + a4 * b4
r7 = r7 + a5 * b3
r7 = r7 + a6 * b2
r7 = r7 + a7 * b1
local r8 = a1 * b8
r8 = r8 + a2 * b7
r8 = r8 + a3 * b6
r8 = r8 + a4 * b5
r8 = r8 + a5 * b4
r8 = r8 + a6 * b3
r8 = r8 + a7 * b2
r8 = r8 + a8 * b1
local r9 = a1 * b9
r9 = r9 + a2 * b8
r9 = r9 + a3 * b7
r9 = r9 + a4 * b6
r9 = r9 + a5 * b5
r9 = r9 + a6 * b4
r9 = r9 + a7 * b3
r9 = r9 + a8 * b2
r9 = r9 + a9 * b1
local r10 = a1 * b10
r10 = r10 + a2 * b9
r10 = r10 + a3 * b8
r10 = r10 + a4 * b7
r10 = r10 + a5 * b6
r10 = r10 + a6 * b5
r10 = r10 + a7 * b4
r10 = r10 + a8 * b3
r10 = r10 + a9 * b2
r10 = r10 + a10 * b1
local r11 = a1 * b11
r11 = r11 + a2 * b10
r11 = r11 + a3 * b9
r11 = r11 + a4 * b8
r11 = r11 + a5 * b7
r11 = r11 + a6 * b6
r11 = r11 + a7 * b5
r11 = r11 + a8 * b4
r11 = r11 + a9 * b3
r11 = r11 + a10 * b2
r11 = r11 + a11 * b1
local r12 = a1 * b12
r12 = r12 + a2 * b11
r12 = r12 + a3 * b10
r12 = r12 + a4 * b9
r12 = r12 + a5 * b8
r12 = r12 + a6 * b7
r12 = r12 + a7 * b6
r12 = r12 + a8 * b5
r12 = r12 + a9 * b4
r12 = r12 + a10 * b3
r12 = r12 + a11 * b2
r12 = r12 + a12 * b1
local r13 = a2 * b12
r13 = r13 + a3 * b11
r13 = r13 + a4 * b10
r13 = r13 + a5 * b9
r13 = r13 + a6 * b8
r13 = r13 + a7 * b7
r13 = r13 + a8 * b6
r13 = r13 + a9 * b5
r13 = r13 + a10 * b4
r13 = r13 + a11 * b3
r13 = r13 + a12 * b2
local r14 = a3 * b12
r14 = r14 + a4 * b11
r14 = r14 + a5 * b10
r14 = r14 + a6 * b9
r14 = r14 + a7 * b8
r14 = r14 + a8 * b7
r14 = r14 + a9 * b6
r14 = r14 + a10 * b5
r14 = r14 + a11 * b4
r14 = r14 + a12 * b3
local r15 = a4 * b12
r15 = r15 + a5 * b11
r15 = r15 + a6 * b10
r15 = r15 + a7 * b9
r15 = r15 + a8 * b8
r15 = r15 + a9 * b7
r15 = r15 + a10 * b6
r15 = r15 + a11 * b5
r15 = r15 + a12 * b4
local r16 = a5 * b12
r16 = r16 + a6 * b11
r16 = r16 + a7 * b10
r16 = r16 + a8 * b9
r16 = r16 + a9 * b8
r16 = r16 + a10 * b7
r16 = r16 + a11 * b6
r16 = r16 + a12 * b5
local r17 = a6 * b12
r17 = r17 + a7 * b11
r17 = r17 + a8 * b10
r17 = r17 + a9 * b9
r17 = r17 + a10 * b8
r17 = r17 + a11 * b7
r17 = r17 + a12 * b6
local r18 = a7 * b12
r18 = r18 + a8 * b11
r18 = r18 + a9 * b10
r18 = r18 + a10 * b9
r18 = r18 + a11 * b8
r18 = r18 + a12 * b7
local r19 = a8 * b12
r19 = r19 + a9 * b11
r19 = r19 + a10 * b10
r19 = r19 + a11 * b9
r19 = r19 + a12 * b8
local r20 = a9 * b12
r20 = r20 + a10 * b11
r20 = r20 + a11 * b10
r20 = r20 + a12 * b9
local r21 = a10 * b12
r21 = r21 + a11 * b11
r21 = r21 + a12 * b10
local r22 = a11 * b12
r22 = r22 + a12 * b11
local r23 = a12 * b12
local r24 = 0
r2 = r2 + (r1 / m)
r2 = r2 - r2 % 1
r1 = r1 % m
r3 = r3 + (r2 / m)
r3 = r3 - r3 % 1
r2 = r2 % m
r4 = r4 + (r3 / m)
r4 = r4 - r4 % 1
r3 = r3 % m
r5 = r5 + (r4 / m)
r5 = r5 - r5 % 1
r4 = r4 % m
r6 = r6 + (r5 / m)
r6 = r6 - r6 % 1
r5 = r5 % m
r7 = r7 + (r6 / m)
r7 = r7 - r7 % 1
r6 = r6 % m
r8 = r8 + (r7 / m)
r8 = r8 - r8 % 1
r7 = r7 % m
r9 = r9 + (r8 / m)
r9 = r9 - r9 % 1
r8 = r8 % m
r10 = r10 + (r9 / m)
r10 = r10 - r10 % 1
r9 = r9 % m
r11 = r11 + (r10 / m)
r11 = r11 - r11 % 1
r10 = r10 % m
r12 = r12 + (r11 / m)
r12 = r12 - r12 % 1
r11 = r11 % m
r13 = r13 + (r12 / m)
r13 = r13 - r13 % 1
r12 = r12 % m
r14 = r14 + (r13 / m)
r14 = r14 - r14 % 1
r13 = r13 % m
r15 = r15 + (r14 / m)
r15 = r15 - r15 % 1
r14 = r14 % m
r16 = r16 + (r15 / m)
r16 = r16 - r16 % 1
r15 = r15 % m
r17 = r17 + (r16 / m)
r17 = r17 - r17 % 1
r16 = r16 % m
r18 = r18 + (r17 / m)
r18 = r18 - r18 % 1
r17 = r17 % m
r19 = r19 + (r18 / m)
r19 = r19 - r19 % 1
r18 = r18 % m
r20 = r20 + (r19 / m)
r20 = r20 - r20 % 1
r19 = r19 % m
r21 = r21 + (r20 / m)
r21 = r21 - r21 % 1
r20 = r20 % m
r22 = r22 + (r21 / m)
r22 = r22 - r22 % 1
r21 = r21 % m
r23 = r23 + (r22 / m)
r23 = r23 - r23 % 1
r22 = r22 % m
r24 = r24 + (r23 / m)
r24 = r24 - r24 % 1
r23 = r23 % m
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24}
return result
end
local function REDC(T)
local m = {unpack(mul384({unpack(T, 1, 12)}, p2), 1, 12)}
local t = {unpack(add384(T, mul384(m, p)), 13, 24)}
return reduce(t)
end
local function mul(a, b)
return REDC(mul384(a, b))
end
local function sqr(a)
local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 = unpack(a)
local r1 = a1 * a1
local r2 = a1 * a2 * 2
local r3 = a1 * a3 * 2
r3 = r3 + a2 * a2
local r4 = a1 * a4 * 2
r4 = r4 + a2 * a3 * 2
local r5 = a1 * a5 * 2
r5 = r5 + a2 * a4 * 2
r5 = r5 + a3 * a3
local r6 = a1 * a6 * 2
r6 = r6 + a2 * a5 * 2
r6 = r6 + a3 * a4 * 2
local r7 = a1 * a7 * 2
r7 = r7 + a2 * a6 * 2
r7 = r7 + a3 * a5 * 2
r7 = r7 + a4 * a4
local r8 = a1 * a8 * 2
r8 = r8 + a2 * a7 * 2
r8 = r8 + a3 * a6 * 2
r8 = r8 + a4 * a5 * 2
local r9 = a1 * a9 * 2
r9 = r9 + a2 * a8 * 2
r9 = r9 + a3 * a7 * 2
r9 = r9 + a4 * a6 * 2
r9 = r9 + a5 * a5
local r10 = a1 * a10 * 2
r10 = r10 + a2 * a9 * 2
r10 = r10 + a3 * a8 * 2
r10 = r10 + a4 * a7 * 2
r10 = r10 + a5 * a6 * 2
local r11 = a1 * a11 * 2
r11 = r11 + a2 * a10 * 2
r11 = r11 + a3 * a9 * 2
r11 = r11 + a4 * a8 * 2
r11 = r11 + a5 * a7 * 2
r11 = r11 + a6 * a6
local r12 = a1 * a12 * 2
r12 = r12 + a2 * a11 * 2
r12 = r12 + a3 * a10 * 2
r12 = r12 + a4 * a9 * 2
r12 = r12 + a5 * a8 * 2
r12 = r12 + a6 * a7 * 2
local r13 = a2 * a12 * 2
r13 = r13 + a3 * a11 * 2
r13 = r13 + a4 * a10 * 2
r13 = r13 + a5 * a9 * 2
r13 = r13 + a6 * a8 * 2
r13 = r13 + a7 * a7
local r14 = a3 * a12 * 2
r14 = r14 + a4 * a11 * 2
r14 = r14 + a5 * a10 * 2
r14 = r14 + a6 * a9 * 2
r14 = r14 + a7 * a8 * 2
local r15 = a4 * a12 * 2
r15 = r15 + a5 * a11 * 2
r15 = r15 + a6 * a10 * 2
r15 = r15 + a7 * a9 * 2
r15 = r15 + a8 * a8
local r16 = a5 * a12 * 2
r16 = r16 + a6 * a11 * 2
r16 = r16 + a7 * a10 * 2
r16 = r16 + a8 * a9 * 2
local r17 = a6 * a12 * 2
r17 = r17 + a7 * a11 * 2
r17 = r17 + a8 * a10 * 2
r17 = r17 + a9 * a9
local r18 = a7 * a12 * 2
r18 = r18 + a8 * a11 * 2
r18 = r18 + a9 * a10 * 2
local r19 = a8 * a12 * 2
r19 = r19 + a9 * a11 * 2
r19 = r19 + a10 * a10
local r20 = a9 * a12 * 2
r20 = r20 + a10 * a11 * 2
local r21 = a10 * a12 * 2
r21 = r21 + a11 * a11
local r22 = a11 * a12 * 2
local r23 = a12 * a12
local r24 = 0
r2 = r2 + (r1 / m)
r2 = r2 - r2 % 1
r1 = r1 % m
r3 = r3 + (r2 / m)
r3 = r3 - r3 % 1
r2 = r2 % m
r4 = r4 + (r3 / m)
r4 = r4 - r4 % 1
r3 = r3 % m
r5 = r5 + (r4 / m)
r5 = r5 - r5 % 1
r4 = r4 % m
r6 = r6 + (r5 / m)
r6 = r6 - r6 % 1
r5 = r5 % m
r7 = r7 + (r6 / m)
r7 = r7 - r7 % 1
r6 = r6 % m
r8 = r8 + (r7 / m)
r8 = r8 - r8 % 1
r7 = r7 % m
r9 = r9 + (r8 / m)
r9 = r9 - r9 % 1
r8 = r8 % m
r10 = r10 + (r9 / m)
r10 = r10 - r10 % 1
r9 = r9 % m
r11 = r11 + (r10 / m)
r11 = r11 - r11 % 1
r10 = r10 % m
r12 = r12 + (r11 / m)
r12 = r12 - r12 % 1
r11 = r11 % m
r13 = r13 + (r12 / m)
r13 = r13 - r13 % 1
r12 = r12 % m
r14 = r14 + (r13 / m)
r14 = r14 - r14 % 1
r13 = r13 % m
r15 = r15 + (r14 / m)
r15 = r15 - r15 % 1
r14 = r14 % m
r16 = r16 + (r15 / m)
r16 = r16 - r16 % 1
r15 = r15 % m
r17 = r17 + (r16 / m)
r17 = r17 - r17 % 1
r16 = r16 % m
r18 = r18 + (r17 / m)
r18 = r18 - r18 % 1
r17 = r17 % m
r19 = r19 + (r18 / m)
r19 = r19 - r19 % 1
r18 = r18 % m
r20 = r20 + (r19 / m)
r20 = r20 - r20 % 1
r19 = r19 % m
r21 = r21 + (r20 / m)
r21 = r21 - r21 % 1
r20 = r20 % m
r22 = r22 + (r21 / m)
r22 = r22 - r22 % 1
r21 = r21 % m
r23 = r23 + (r22 / m)
r23 = r23 - r23 % 1
r22 = r22 % m
r24 = r24 + (r23 / m)
r24 = r24 - r24 % 1
r23 = r23 % m
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24}
return REDC(result)
end
local function mont(a)
return mul(a, r2)
end
local function invMont(a)
local a = {unpack(a)}
for i = 13, 24 do
a[i] = 0
end
return REDC(a)
end
return {
eq = eq,
add = add,
shr = shr,
sub192 = sub192,
sub = sub,
mul = mul,
sqr = sqr,
mont = mont,
invMont = invMont,
}

View File

@@ -0,0 +1,741 @@
-- Fq Integer Arithmetic
local n = 0xffff
local m = 0x10000
local q = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532}
local qn = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
local function eq(a, b)
for i = 1, 12 do
if a[i] ~= b[i] then
return false
end
end
return true
end
local function cmp(a, b)
for i = 12, 1, -1 do
if a[i] > b[i] then
return 1
elseif a[i] < b[i] then
return -1
end
end
return 0
end
local function cmp384(a, b)
for i = 24, 1, -1 do
if a[i] > b[i] then
return 1
elseif a[i] < b[i] then
return -1
end
end
return 0
end
local function bytes(x)
local result = {}
for i = 0, 11 do
local m = x[i + 1] % 256
result[2 * i + 1] = m
result[2 * i + 2] = (x[i + 1] - m) / 256
end
return result
end
local function fromBytes(enc)
local result = {}
for i = 0, 11 do
result[i + 1] = enc[2 * i + 1] % 256
result[i + 1] = result[i + 1] + enc[2 * i + 2] * 256
end
return result
end
local function sub192(a, b)
local r1 = a[1] - b[1]
local r2 = a[2] - b[2]
local r3 = a[3] - b[3]
local r4 = a[4] - b[4]
local r5 = a[5] - b[5]
local r6 = a[6] - b[6]
local r7 = a[7] - b[7]
local r8 = a[8] - b[8]
local r9 = a[9] - b[9]
local r10 = a[10] - b[10]
local r11 = a[11] - b[11]
local r12 = a[12] - b[12]
if r1 < 0 then
r2 = r2 - 1
r1 = r1 + m
end
if r2 < 0 then
r3 = r3 - 1
r2 = r2 + m
end
if r3 < 0 then
r4 = r4 - 1
r3 = r3 + m
end
if r4 < 0 then
r5 = r5 - 1
r4 = r4 + m
end
if r5 < 0 then
r6 = r6 - 1
r5 = r5 + m
end
if r6 < 0 then
r7 = r7 - 1
r6 = r6 + m
end
if r7 < 0 then
r8 = r8 - 1
r7 = r7 + m
end
if r8 < 0 then
r9 = r9 - 1
r8 = r8 + m
end
if r9 < 0 then
r10 = r10 - 1
r9 = r9 + m
end
if r10 < 0 then
r11 = r11 - 1
r10 = r10 + m
end
if r11 < 0 then
r12 = r12 - 1
r11 = r11 + m
end
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}
return result
end
local function reduce(a)
local result = {unpack(a)}
if cmp(result, q) >= 0 then
result = sub192(result, q)
end
return result
end
local function add(a, b)
local r1 = a[1] + b[1]
local r2 = a[2] + b[2]
local r3 = a[3] + b[3]
local r4 = a[4] + b[4]
local r5 = a[5] + b[5]
local r6 = a[6] + b[6]
local r7 = a[7] + b[7]
local r8 = a[8] + b[8]
local r9 = a[9] + b[9]
local r10 = a[10] + b[10]
local r11 = a[11] + b[11]
local r12 = a[12] + b[12]
if r1 > n then
r2 = r2 + 1
r1 = r1 - m
end
if r2 > n then
r3 = r3 + 1
r2 = r2 - m
end
if r3 > n then
r4 = r4 + 1
r3 = r3 - m
end
if r4 > n then
r5 = r5 + 1
r4 = r4 - m
end
if r5 > n then
r6 = r6 + 1
r5 = r5 - m
end
if r6 > n then
r7 = r7 + 1
r6 = r6 - m
end
if r7 > n then
r8 = r8 + 1
r7 = r7 - m
end
if r8 > n then
r9 = r9 + 1
r8 = r8 - m
end
if r9 > n then
r10 = r10 + 1
r9 = r9 - m
end
if r10 > n then
r11 = r11 + 1
r10 = r10 - m
end
if r11 > n then
r12 = r12 + 1
r11 = r11 - m
end
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}
return reduce(result)
end
local function sub(a, b)
local result = sub192(a, b)
if result[12] < 0 then
result = add(result, q)
end
return result
end
local function add384(a, b)
local r1 = a[1] + b[1]
local r2 = a[2] + b[2]
local r3 = a[3] + b[3]
local r4 = a[4] + b[4]
local r5 = a[5] + b[5]
local r6 = a[6] + b[6]
local r7 = a[7] + b[7]
local r8 = a[8] + b[8]
local r9 = a[9] + b[9]
local r10 = a[10] + b[10]
local r11 = a[11] + b[11]
local r12 = a[12] + b[12]
local r13 = a[13] + b[13]
local r14 = a[14] + b[14]
local r15 = a[15] + b[15]
local r16 = a[16] + b[16]
local r17 = a[17] + b[17]
local r18 = a[18] + b[18]
local r19 = a[19] + b[19]
local r20 = a[20] + b[20]
local r21 = a[21] + b[21]
local r22 = a[22] + b[22]
local r23 = a[23] + b[23]
local r24 = a[24] + b[24]
if r1 > n then
r2 = r2 + 1
r1 = r1 - m
end
if r2 > n then
r3 = r3 + 1
r2 = r2 - m
end
if r3 > n then
r4 = r4 + 1
r3 = r3 - m
end
if r4 > n then
r5 = r5 + 1
r4 = r4 - m
end
if r5 > n then
r6 = r6 + 1
r5 = r5 - m
end
if r6 > n then
r7 = r7 + 1
r6 = r6 - m
end
if r7 > n then
r8 = r8 + 1
r7 = r7 - m
end
if r8 > n then
r9 = r9 + 1
r8 = r8 - m
end
if r9 > n then
r10 = r10 + 1
r9 = r9 - m
end
if r10 > n then
r11 = r11 + 1
r10 = r10 - m
end
if r11 > n then
r12 = r12 + 1
r11 = r11 - m
end
if r12 > n then
r13 = r13 + 1
r12 = r12 - m
end
if r13 > n then
r14 = r14 + 1
r13 = r13 - m
end
if r14 > n then
r15 = r15 + 1
r14 = r14 - m
end
if r15 > n then
r16 = r16 + 1
r15 = r15 - m
end
if r16 > n then
r17 = r17 + 1
r16 = r16 - m
end
if r17 > n then
r18 = r18 + 1
r17 = r17 - m
end
if r18 > n then
r19 = r19 + 1
r18 = r18 - m
end
if r19 > n then
r20 = r20 + 1
r19 = r19 - m
end
if r20 > n then
r21 = r21 + 1
r20 = r20 - m
end
if r21 > n then
r22 = r22 + 1
r21 = r21 - m
end
if r22 > n then
r23 = r23 + 1
r22 = r22 - m
end
if r23 > n then
r24 = r24 + 1
r23 = r23 - m
end
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24}
return result
end
local function sub384(a, b)
local r1 = a[1] - b[1]
local r2 = a[2] - b[2]
local r3 = a[3] - b[3]
local r4 = a[4] - b[4]
local r5 = a[5] - b[5]
local r6 = a[6] - b[6]
local r7 = a[7] - b[7]
local r8 = a[8] - b[8]
local r9 = a[9] - b[9]
local r10 = a[10] - b[10]
local r11 = a[11] - b[11]
local r12 = a[12] - b[12]
local r13 = a[13] - b[13]
local r14 = a[14] - b[14]
local r15 = a[15] - b[15]
local r16 = a[16] - b[16]
local r17 = a[17] - b[17]
local r18 = a[18] - b[18]
local r19 = a[19] - b[19]
local r20 = a[20] - b[20]
local r21 = a[21] - b[21]
local r22 = a[22] - b[22]
local r23 = a[23] - b[23]
local r24 = a[24] - b[24]
if r1 < 0 then
r2 = r2 - 1
r1 = r1 + m
end
if r2 < 0 then
r3 = r3 - 1
r2 = r2 + m
end
if r3 < 0 then
r4 = r4 - 1
r3 = r3 + m
end
if r4 < 0 then
r5 = r5 - 1
r4 = r4 + m
end
if r5 < 0 then
r6 = r6 - 1
r5 = r5 + m
end
if r6 < 0 then
r7 = r7 - 1
r6 = r6 + m
end
if r7 < 0 then
r8 = r8 - 1
r7 = r7 + m
end
if r8 < 0 then
r9 = r9 - 1
r8 = r8 + m
end
if r9 < 0 then
r10 = r10 - 1
r9 = r9 + m
end
if r10 < 0 then
r11 = r11 - 1
r10 = r10 + m
end
if r11 < 0 then
r12 = r12 - 1
r11 = r11 + m
end
if r12 < 0 then
r13 = r13 - 1
r12 = r12 + m
end
if r13 < 0 then
r14 = r14 - 1
r13 = r13 + m
end
if r14 < 0 then
r15 = r15 - 1
r14 = r14 + m
end
if r15 < 0 then
r16 = r16 - 1
r15 = r15 + m
end
if r16 < 0 then
r17 = r17 - 1
r16 = r16 + m
end
if r17 < 0 then
r18 = r18 - 1
r17 = r17 + m
end
if r18 < 0 then
r19 = r19 - 1
r18 = r18 + m
end
if r19 < 0 then
r20 = r20 - 1
r19 = r19 + m
end
if r20 < 0 then
r21 = r21 - 1
r20 = r20 + m
end
if r21 < 0 then
r22 = r22 - 1
r21 = r21 + m
end
if r22 < 0 then
r23 = r23 - 1
r22 = r22 + m
end
if r23 < 0 then
r24 = r24 - 1
r23 = r23 + m
end
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24}
return result
end
local function mul384(a, b)
local a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12 = unpack(a)
local b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12 = unpack(b)
local r1 = a1 * b1
local r2 = a1 * b2
r2 = r2 + a2 * b1
local r3 = a1 * b3
r3 = r3 + a2 * b2
r3 = r3 + a3 * b1
local r4 = a1 * b4
r4 = r4 + a2 * b3
r4 = r4 + a3 * b2
r4 = r4 + a4 * b1
local r5 = a1 * b5
r5 = r5 + a2 * b4
r5 = r5 + a3 * b3
r5 = r5 + a4 * b2
r5 = r5 + a5 * b1
local r6 = a1 * b6
r6 = r6 + a2 * b5
r6 = r6 + a3 * b4
r6 = r6 + a4 * b3
r6 = r6 + a5 * b2
r6 = r6 + a6 * b1
local r7 = a1 * b7
r7 = r7 + a2 * b6
r7 = r7 + a3 * b5
r7 = r7 + a4 * b4
r7 = r7 + a5 * b3
r7 = r7 + a6 * b2
r7 = r7 + a7 * b1
local r8 = a1 * b8
r8 = r8 + a2 * b7
r8 = r8 + a3 * b6
r8 = r8 + a4 * b5
r8 = r8 + a5 * b4
r8 = r8 + a6 * b3
r8 = r8 + a7 * b2
r8 = r8 + a8 * b1
local r9 = a1 * b9
r9 = r9 + a2 * b8
r9 = r9 + a3 * b7
r9 = r9 + a4 * b6
r9 = r9 + a5 * b5
r9 = r9 + a6 * b4
r9 = r9 + a7 * b3
r9 = r9 + a8 * b2
r9 = r9 + a9 * b1
local r10 = a1 * b10
r10 = r10 + a2 * b9
r10 = r10 + a3 * b8
r10 = r10 + a4 * b7
r10 = r10 + a5 * b6
r10 = r10 + a6 * b5
r10 = r10 + a7 * b4
r10 = r10 + a8 * b3
r10 = r10 + a9 * b2
r10 = r10 + a10 * b1
local r11 = a1 * b11
r11 = r11 + a2 * b10
r11 = r11 + a3 * b9
r11 = r11 + a4 * b8
r11 = r11 + a5 * b7
r11 = r11 + a6 * b6
r11 = r11 + a7 * b5
r11 = r11 + a8 * b4
r11 = r11 + a9 * b3
r11 = r11 + a10 * b2
r11 = r11 + a11 * b1
local r12 = a1 * b12
r12 = r12 + a2 * b11
r12 = r12 + a3 * b10
r12 = r12 + a4 * b9
r12 = r12 + a5 * b8
r12 = r12 + a6 * b7
r12 = r12 + a7 * b6
r12 = r12 + a8 * b5
r12 = r12 + a9 * b4
r12 = r12 + a10 * b3
r12 = r12 + a11 * b2
r12 = r12 + a12 * b1
local r13 = a2 * b12
r13 = r13 + a3 * b11
r13 = r13 + a4 * b10
r13 = r13 + a5 * b9
r13 = r13 + a6 * b8
r13 = r13 + a7 * b7
r13 = r13 + a8 * b6
r13 = r13 + a9 * b5
r13 = r13 + a10 * b4
r13 = r13 + a11 * b3
r13 = r13 + a12 * b2
local r14 = a3 * b12
r14 = r14 + a4 * b11
r14 = r14 + a5 * b10
r14 = r14 + a6 * b9
r14 = r14 + a7 * b8
r14 = r14 + a8 * b7
r14 = r14 + a9 * b6
r14 = r14 + a10 * b5
r14 = r14 + a11 * b4
r14 = r14 + a12 * b3
local r15 = a4 * b12
r15 = r15 + a5 * b11
r15 = r15 + a6 * b10
r15 = r15 + a7 * b9
r15 = r15 + a8 * b8
r15 = r15 + a9 * b7
r15 = r15 + a10 * b6
r15 = r15 + a11 * b5
r15 = r15 + a12 * b4
local r16 = a5 * b12
r16 = r16 + a6 * b11
r16 = r16 + a7 * b10
r16 = r16 + a8 * b9
r16 = r16 + a9 * b8
r16 = r16 + a10 * b7
r16 = r16 + a11 * b6
r16 = r16 + a12 * b5
local r17 = a6 * b12
r17 = r17 + a7 * b11
r17 = r17 + a8 * b10
r17 = r17 + a9 * b9
r17 = r17 + a10 * b8
r17 = r17 + a11 * b7
r17 = r17 + a12 * b6
local r18 = a7 * b12
r18 = r18 + a8 * b11
r18 = r18 + a9 * b10
r18 = r18 + a10 * b9
r18 = r18 + a11 * b8
r18 = r18 + a12 * b7
local r19 = a8 * b12
r19 = r19 + a9 * b11
r19 = r19 + a10 * b10
r19 = r19 + a11 * b9
r19 = r19 + a12 * b8
local r20 = a9 * b12
r20 = r20 + a10 * b11
r20 = r20 + a11 * b10
r20 = r20 + a12 * b9
local r21 = a10 * b12
r21 = r21 + a11 * b11
r21 = r21 + a12 * b10
local r22 = a11 * b12
r22 = r22 + a12 * b11
local r23 = a12 * b12
local r24 = 0
r2 = r2 + (r1 / m)
r2 = r2 - r2 % 1
r1 = r1 % m
r3 = r3 + (r2 / m)
r3 = r3 - r3 % 1
r2 = r2 % m
r4 = r4 + (r3 / m)
r4 = r4 - r4 % 1
r3 = r3 % m
r5 = r5 + (r4 / m)
r5 = r5 - r5 % 1
r4 = r4 % m
r6 = r6 + (r5 / m)
r6 = r6 - r6 % 1
r5 = r5 % m
r7 = r7 + (r6 / m)
r7 = r7 - r7 % 1
r6 = r6 % m
r8 = r8 + (r7 / m)
r8 = r8 - r8 % 1
r7 = r7 % m
r9 = r9 + (r8 / m)
r9 = r9 - r9 % 1
r8 = r8 % m
r10 = r10 + (r9 / m)
r10 = r10 - r10 % 1
r9 = r9 % m
r11 = r11 + (r10 / m)
r11 = r11 - r11 % 1
r10 = r10 % m
r12 = r12 + (r11 / m)
r12 = r12 - r12 % 1
r11 = r11 % m
r13 = r13 + (r12 / m)
r13 = r13 - r13 % 1
r12 = r12 % m
r14 = r14 + (r13 / m)
r14 = r14 - r14 % 1
r13 = r13 % m
r15 = r15 + (r14 / m)
r15 = r15 - r15 % 1
r14 = r14 % m
r16 = r16 + (r15 / m)
r16 = r16 - r16 % 1
r15 = r15 % m
r17 = r17 + (r16 / m)
r17 = r17 - r17 % 1
r16 = r16 % m
r18 = r18 + (r17 / m)
r18 = r18 - r18 % 1
r17 = r17 % m
r19 = r19 + (r18 / m)
r19 = r19 - r19 % 1
r18 = r18 % m
r20 = r20 + (r19 / m)
r20 = r20 - r20 % 1
r19 = r19 % m
r21 = r21 + (r20 / m)
r21 = r21 - r21 % 1
r20 = r20 % m
r22 = r22 + (r21 / m)
r22 = r22 - r22 % 1
r21 = r21 % m
r23 = r23 + (r22 / m)
r23 = r23 - r23 % 1
r22 = r22 % m
r24 = r24 + (r23 / m)
r24 = r24 - r24 % 1
r23 = r23 % m
local result = {r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12, r13, r14, r15, r16, r17, r18, r19, r20, r21, r22, r23, r24}
return result
end
local function reduce384(a)
local result = {unpack(a)}
while cmp384(result, qn) >= 0 do
local qn = {unpack(qn)}
local qn2 = add384(qn, qn)
while cmp384(result, qn2) > 0 do
qn = qn2
qn2 = add384(qn2, qn2)
end
result = sub384(result, qn)
end
result = {unpack(result, 1, 12)}
return result
end
local function mul(a, b)
return reduce384(mul384(a, b))
end
return {
eq = eq,
cmp = cmp,
bytes = bytes,
fromBytes = fromBytes,
reduce = reduce,
add = add,
sub = sub,
mul = mul,
}

View File

@@ -0,0 +1,87 @@
local fq = require('opus.crypto.ecc.fq')
local elliptic = require('opus.crypto.ecc.elliptic')
local sha256 = require('opus.crypto.sha2')
local os = _G.os
local q = {1372, 62520, 47765, 8105, 45059, 9616, 65535, 65535, 65535, 65535, 65535, 65532}
local sLen = 24
local eLen = 24
local function hashModQ(sk)
local hash = sha256.hmac({0x00}, sk)
local x
repeat
hash = sha256.digest(hash)
x = fq.fromBytes(hash)
until fq.cmp(x, q) <= 0
return x
end
local function publicKey(sk)
local x = hashModQ(sk)
local Y = elliptic.scalarMulG(x)
local pk = elliptic.pointEncode(Y)
return pk
end
local function exchange(sk, pk)
local Y = elliptic.pointDecode(pk)
local x = hashModQ(sk)
local Z = elliptic.scalarMul(x, Y)
Z = elliptic.pointScale(Z)
local ss = fq.bytes(Z[2])
return sha256.digest(ss)
end
local function sign(sk, message)
message = type(message) == "table" and string.char(unpack(message)) or message
sk = type(sk) == "table" and string.char(unpack(sk)) or sk
local epoch = tostring(os.epoch("utc"))
local x = hashModQ(sk)
local k = hashModQ(message .. epoch .. sk)
local R = elliptic.scalarMulG(k)
R = string.char(unpack(elliptic.pointEncode(R)))
local e = hashModQ(R .. message)
local s = fq.sub(k, fq.mul(x, e))
e = fq.bytes(e)
s = fq.bytes(s)
local sig = {unpack(e)}
for i = 1, #s do
sig[#sig + 1] = s[i]
end
return sig
end
local function verify(pk, message, sig)
local Y = elliptic.pointDecode(pk)
local e = {unpack(sig, 1, eLen)}
local s = {unpack(sig, eLen + 1, eLen + sLen)}
e = fq.fromBytes(e)
s = fq.fromBytes(s)
local R = elliptic.pointAdd(elliptic.scalarMulG(s), elliptic.scalarMul(e, Y))
R = string.char(unpack(elliptic.pointEncode(R)))
local e2 = hashModQ(R .. message)
return fq.eq(e2, e)
end
return {
publicKey = publicKey,
exchange = exchange,
sign = sign,
verify = verify,
}

View File

@@ -0,0 +1,204 @@
-- SHA-256, HMAC and PBKDF2 functions in ComputerCraft
-- By Anavrins
local bit = _G.bit
local os = _G.os
local mod32 = 2^32
local band = bit32 and bit32.band or bit.band
local bnot = bit32 and bit32.bnot or bit.bnot
local bxor = bit32 and bit32.bxor or bit.bxor
local blshift = bit32 and bit32.lshift or bit.blshift
local upack = unpack
local function rrotate(n, b)
local s = n/(2^b)
local f = s%1
return (s-f) + f*mod32
end
local function brshift(int, by)
local s = int / (2^by)
return s - s%1
end
local H = {
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
}
local K = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
}
local function counter(incr)
local t1, t2 = 0, 0
if 0xFFFFFFFF - t1 < incr then
t2 = t2 + 1
t1 = incr - (0xFFFFFFFF - t1) - 1
else t1 = t1 + incr
end
return t2, t1
end
local function BE_toInt(bs, i)
return blshift((bs[i] or 0), 24) + blshift((bs[i+1] or 0), 16) + blshift((bs[i+2] or 0), 8) + (bs[i+3] or 0)
end
local function preprocess(data)
local len = #data
local proc = {}
data[#data+1] = 0x80
while #data%64~=56 do data[#data+1] = 0 end
local blocks = math.ceil(#data/64)
for i = 1, blocks do
proc[i] = {}
for j = 1, 16 do
proc[i][j] = BE_toInt(data, 1+((i-1)*64)+((j-1)*4))
end
end
proc[blocks][15], proc[blocks][16] = counter(len*8)
return proc
end
local function digestblock(w, C)
for j = 17, 64 do
-- local v = w[j-15]
local s0 = bxor(bxor(rrotate(w[j-15], 7), rrotate(w[j-15], 18)), brshift(w[j-15], 3))
local s1 = bxor(bxor(rrotate(w[j-2], 17), rrotate(w[j-2], 19)), brshift(w[j-2], 10))
w[j] = (w[j-16] + s0 + w[j-7] + s1)%mod32
end
local a, b, c, d, e, f, g, h = upack(C)
for j = 1, 64 do
local S1 = bxor(bxor(rrotate(e, 6), rrotate(e, 11)), rrotate(e, 25))
local ch = bxor(band(e, f), band(bnot(e), g))
local temp1 = (h + S1 + ch + K[j] + w[j])%mod32
local S0 = bxor(bxor(rrotate(a, 2), rrotate(a, 13)), rrotate(a, 22))
local maj = bxor(bxor(band(a, b), band(a, c)), band(b, c))
local temp2 = (S0 + maj)%mod32
h, g, f, e, d, c, b, a = g, f, e, (d+temp1)%mod32, c, b, a, (temp1+temp2)%mod32
end
C[1] = (C[1] + a)%mod32
C[2] = (C[2] + b)%mod32
C[3] = (C[3] + c)%mod32
C[4] = (C[4] + d)%mod32
C[5] = (C[5] + e)%mod32
C[6] = (C[6] + f)%mod32
C[7] = (C[7] + g)%mod32
C[8] = (C[8] + h)%mod32
return C
end
local mt = {
__tostring = function(a) return string.char(unpack(a)) end,
__index = {
toHex = function(self) return ("%02x"):rep(#self):format(unpack(self)) end,
isEqual = function(self, t)
if type(t) ~= "table" then return false end
if #self ~= #t then return false end
local ret = 0
for i = 1, #self do
ret = bit32.bor(ret, bxor(self[i], t[i]))
end
return ret == 0
end
}
}
local function toBytes(t, n)
local b = {}
for i = 1, n do
b[(i-1)*4+1] = band(brshift(t[i], 24), 0xFF)
b[(i-1)*4+2] = band(brshift(t[i], 16), 0xFF)
b[(i-1)*4+3] = band(brshift(t[i], 8), 0xFF)
b[(i-1)*4+4] = band(t[i], 0xFF)
end
return setmetatable(b, mt)
end
local function digest(data)
data = data or ""
data = type(data) == "table" and {upack(data)} or {tostring(data):byte(1,-1)}
data = preprocess(data)
local C = {upack(H)}
for i = 1, #data do C = digestblock(data[i], C) end
return toBytes(C, 8)
end
local function hmac(data, key)
data = type(data) == "table" and {upack(data)} or {tostring(data):byte(1,-1)}
key = type(key) == "table" and {upack(key)} or {tostring(key):byte(1,-1)}
local blocksize = 64
key = #key > blocksize and digest(key) or key
local ipad = {}
local opad = {}
local padded_key = {}
for i = 1, blocksize do
ipad[i] = bxor(0x36, key[i] or 0)
opad[i] = bxor(0x5C, key[i] or 0)
end
for i = 1, #data do
ipad[blocksize+i] = data[i]
end
ipad = digest(ipad)
for i = 1, blocksize do
padded_key[i] = opad[i]
padded_key[blocksize+i] = ipad[i]
end
return digest(padded_key)
end
local function pbkdf2(pass, salt, iter, dklen)
salt = type(salt) == "table" and salt or {tostring(salt):byte(1,-1)}
local hashlen = 32
dklen = dklen or 32
local block = 1
local out = {}
while dklen > 0 do
local ikey = {}
local isalt = {upack(salt)}
local clen = dklen > hashlen and hashlen or dklen
isalt[#isalt+1] = band(brshift(block, 24), 0xFF)
isalt[#isalt+1] = band(brshift(block, 16), 0xFF)
isalt[#isalt+1] = band(brshift(block, 8), 0xFF)
isalt[#isalt+1] = band(block, 0xFF)
for j = 1, iter do
isalt = hmac(isalt, pass)
for k = 1, clen do ikey[k] = bxor(isalt[k], ikey[k] or 0) end
if j % 200 == 0 then os.queueEvent("PBKDF2", j) coroutine.yield("PBKDF2") end
end
dklen = dklen - clen
block = block+1
for k = 1, clen do out[#out+1] = ikey[k] end
end
return setmetatable(out, mt)
end
local function compute(data)
return digest(data):toHex()
end
return {
digest = digest,
compute = compute,
hmac = hmac,
pbkdf2 = pbkdf2,
}

388
sys/modules/opus/entry.lua Normal file
View File

@@ -0,0 +1,388 @@
local class = require('opus.class')
local os = _G.os
local Entry = class()
function Entry:init(args)
self.pos = 0
self.scroll = 0
self.value = ''
self.width = args.width or 256
self.limit = args.limit or 1024
self.mark = { }
self.offset = args.offset or 1
end
function Entry:reset()
self.pos = 0
self.scroll = 0
self.value = ''
self.mark = { }
end
function Entry:nextWord()
return select(2, self.value:find("[%s%p]?%w[%s%p]", self.pos + 1)) or #self.value
end
function Entry:prevWord()
local x = #self.value - (self.pos - 1)
local _, n = self.value:reverse():find("[%s%p]?%w[%s%p]", x)
return n and #self.value - n + 1 or 0
end
function Entry:updateScroll()
local ps = self.scroll
if self.pos > #self.value then
self.pos = #self.value
self.scroll = 0 -- ??
end
if self.pos - self.scroll > self.width then
self.scroll = self.pos - self.width
elseif self.pos < self.scroll then
self.scroll = self.pos
end
if ps ~= self.scroll then
self.textChanged = true
end
end
function Entry:copyText(cx, ex)
return self.value:sub(cx + 1, ex)
end
function Entry:insertText(x, text)
if #self.value + #text > self.limit then
text = text:sub(1, self.limit-#self.value)
end
self.value = self.value:sub(1, x) .. text .. self.value:sub(x + 1)
self.pos = self.pos + #text
end
function Entry:deleteText(sx, ex)
local front = self.value:sub(1, sx)
local back = self.value:sub(ex + 1, #self.value)
self.value = front .. back
self.pos = sx
end
function Entry:moveLeft()
if self.pos > 0 then
self.pos = self.pos - 1
return true
end
end
function Entry:moveRight()
if self.pos < #self.value then
self.pos = self.pos + 1
return true
end
end
function Entry:moveHome()
if self.pos ~= 0 then
self.pos = 0
return true
end
end
function Entry:moveEnd()
if self.pos ~= #self.value then
self.pos = #self.value
return true
end
end
function Entry:moveTo(ie)
self.pos = math.max(0, math.min(ie.x + self.scroll - self.offset, #self.value))
end
function Entry:backspace()
if self.mark.active then
self:delete()
elseif self:moveLeft() then
self:delete()
end
end
function Entry:moveWordRight()
if self.pos < #self.value then
self.pos = self:nextWord(self.value, self.pos + 1)
return true
end
end
function Entry:moveWordLeft()
if self.pos > 0 then
self.pos = self:prevWord(self.value, self.pos - 1) or 0
return true
end
end
function Entry:delete()
if self.mark.active then
self:deleteText(self.mark.x, self.mark.ex)
elseif self.pos < #self.value then
self:deleteText(self.pos, self.pos + 1)
end
end
function Entry:cutFromStart()
if self.pos > 0 then
local text = self:copyText(1, self.pos)
self:deleteText(1, self.pos)
os.queueEvent('clipboard_copy', text)
end
end
function Entry:cutToEnd()
if self.pos < #self.value then
local text = self:copyText(self.pos, #self.value)
self:deleteText(self.pos, #self.value)
os.queueEvent('clipboard_copy', text)
end
end
function Entry:cutNextWord()
if self.pos < #self.value then
local ex = self:nextWord(self.value, self.pos)
local text = self:copyText(self.pos, ex)
self:deleteText(self.pos, ex)
os.queueEvent('clipboard_copy', text)
end
end
function Entry:cutPrevWord()
if self.pos > 0 then
local sx = self:prevWord(self.value, self.pos)
local text = self:copyText(sx, self.pos)
self:deleteText(sx, self.pos)
os.queueEvent('clipboard_copy', text)
end
end
function Entry:insertChar(ie)
if self.mark.active then
self:delete()
end
self:insertText(self.pos, ie.ch)
end
function Entry:copy()
if #self.value > 0 then
self.mark.continue = true
if self.mark.active then
self:copyMarked()
else
os.queueEvent('clipboard_copy', self.value)
end
end
end
function Entry:cut()
if self.mark.active then
self:copyMarked()
self:delete()
end
end
function Entry:copyMarked()
local text = self:copyText(self.mark.x, self.mark.ex)
os.queueEvent('clipboard_copy', text)
end
function Entry:paste(ie)
if #ie.text > 0 then
if self.mark.active then
self:delete()
end
self:insertText(self.pos, ie.text)
end
end
function Entry:clearLine()
if #self.value > 0 then
self:reset()
end
end
function Entry:markBegin()
if not self.mark.active then
self.mark.active = true
self.mark.anchor = { x = self.pos }
end
end
function Entry:markFinish()
if self.pos == self.mark.anchor.x then
self.mark.active = false
else
self.mark.x = math.min(self.mark.anchor.x, self.pos)
self.mark.ex = math.max(self.mark.anchor.x, self.pos)
end
self.textChanged = true
self.mark.continue = self.mark.active
end
function Entry:unmark()
if self.mark.active then
self.textChanged = true
self.mark.active = false
end
end
function Entry:markAnchor(ie)
self:unmark()
self:moveTo(ie)
self:markBegin()
self:markFinish()
end
function Entry:markLeft()
self:markBegin()
if self:moveLeft() then
self:markFinish()
end
end
function Entry:markRight()
self:markBegin()
if self:moveRight() then
self:markFinish()
end
end
function Entry:markWord(ie)
local index = 1
self:moveTo(ie)
while true do
local s, e = self.value:find('%w+', index)
if not s or s - 1 > self.pos then
break
end
if self.pos >= s - 1 and self.pos < e then
self.pos = s - 1
self:markBegin()
self.pos = e
self:markFinish()
self:moveTo(ie)
break
end
index = e + 1
end
end
function Entry:markNextWord()
self:markBegin()
if self:moveWordRight() then
self:markFinish()
end
end
function Entry:markPrevWord()
self:markBegin()
if self:moveWordLeft() then
self:markFinish()
end
end
function Entry:markAll()
if #self.value > 0 then
self.mark.anchor = { x = 1 }
self.mark.active = true
self.mark.continue = true
self.mark.x = 0
self.mark.ex = #self.value
self.textChanged = true
end
end
function Entry:markHome()
self:markBegin()
if self:moveHome() then
self:markFinish()
end
end
function Entry:markEnd()
self:markBegin()
if self:moveEnd() then
self:markFinish()
end
end
function Entry:markTo(ie)
self:markBegin()
self:moveTo(ie)
self:markFinish()
end
local mappings = {
[ 'left' ] = Entry.moveLeft,
[ 'control-b' ] = Entry.moveLeft,
[ 'right' ] = Entry.moveRight,
[ 'control-f' ] = Entry.moveRight,
[ 'home' ] = Entry.moveHome,
[ 'end' ] = Entry.moveEnd,
[ 'control-e' ] = Entry.moveEnd,
[ 'mouse_click' ] = Entry.moveTo,
[ 'control-right' ] = Entry.moveWordRight,
[ 'alt-f' ] = Entry.moveWordRight,
[ 'control-left' ] = Entry.moveWordLeft,
[ 'alt-b' ] = Entry.moveWordLeft,
[ 'backspace' ] = Entry.backspace,
[ 'delete' ] = Entry.delete,
[ 'char' ] = Entry.insertChar,
[ 'mouse_rightclick' ] = Entry.clearLine,
[ 'control-c' ] = Entry.copy,
[ 'control-u' ] = Entry.cutFromStart,
[ 'control-k' ] = Entry.cutToEnd,
[ 'control-w' ] = Entry.cutPrevWord,
--[ 'control-d' ] = Entry.cutNextWord,
[ 'control-x' ] = Entry.cut,
[ 'paste' ] = Entry.paste,
-- [ 'control-y' ] = Entry.paste, -- well this won't work...
[ 'mouse_doubleclick' ] = Entry.markWord,
[ 'shift-left' ] = Entry.markLeft,
[ 'shift-right' ] = Entry.markRight,
[ 'mouse_down' ] = Entry.markAnchor,
[ 'mouse_drag' ] = Entry.markTo,
[ 'shift-mouse_click' ] = Entry.markTo,
[ 'control-a' ] = Entry.markAll,
[ 'control-shift-right' ] = Entry.markNextWord,
[ 'control-shift-left' ] = Entry.markPrevWord,
[ 'shift-end' ] = Entry.markEnd,
[ 'shift-home' ] = Entry.markHome,
}
function Entry:process(ie)
local action = mappings[ie.code]
self.textChanged = false
if action then
local pos = self.pos
local line = self.value
local wasMarking = self.mark.continue
self.mark.continue = false
action(self, ie)
self.textChanged = self.textChanged or self.value ~= line
self.posChanged = pos ~= self.pos
self:updateScroll()
if not self.mark.continue and wasMarking then
self:unmark()
end
return true
end
end
return Entry

263
sys/modules/opus/event.lua Normal file
View File

@@ -0,0 +1,263 @@
local os = _G.os
local table = _G.table
local Event = {
uid = 1, -- unique id for handlers
routines = { }, -- coroutines
types = { }, -- event handlers
terminate = false,
free = { }, -- allocated unused coroutines
}
-- Use a pool of coroutines for event handlers
local function createCoroutine(h)
local co = table.remove(Event.free)
if not co then
co = coroutine.create(function(_, ...)
local args = { ... }
while true do
h.fn(table.unpack(args))
h.co = nil
table.insert(Event.free, co)
args = { coroutine.yield() }
h = table.remove(args, 1)
h.co = co
end
end)
end
h.primeCo = true -- TODO: fix...
return co
end
local Routine = { }
function Routine:isDead()
if not self.co then
return true
end
return coroutine.status(self.co) == 'dead'
end
function Routine:terminate()
if self.co then
self:resume('terminate')
end
end
function Routine:resume(event, ...)
if not self.co then
error('Cannot resume a dead routine')
end
if not self.filter or self.filter == event or event == "terminate" then
local s, m
if self.primeCo then
-- Only need self passed when using a coroutine from the pool
s, m = coroutine.resume(self.co, self, event, ...)
self.primeCo = nil
else
s, m = coroutine.resume(self.co, event, ...)
end
if self:isDead() then
self.co = nil
self.filter = nil
Event.routines[self.uid] = nil
else
self.filter = m
end
if not s and event ~= 'terminate' then
error(m or 'Error processing event', -1)
end
return s, m
end
return true, self.filter
end
local function nextUID()
Event.uid = Event.uid + 1
return Event.uid - 1
end
function Event.on(events, fn)
events = type(events) == 'table' and events or { events }
local handler = setmetatable({
uid = nextUID(),
event = events,
fn = fn,
}, { __index = Routine })
for _,event in pairs(events) do
local handlers = Event.types[event]
if not handlers then
handlers = { }
Event.types[event] = handlers
end
handlers[handler.uid] = handler
end
return handler
end
function Event.off(h)
if h and h.event then
for _,event in pairs(h.event) do
local handler = Event.types[event][h.uid]
if handler then
handler:terminate()
end
Event.types[event][h.uid] = nil
end
elseif h and h.co then
h:terminate()
end
end
function Event.onInterval(interval, fn)
local h = Event.addRoutine(function()
while true do
os.sleep(interval)
fn()
end
end)
function h.updateInterval(i)
interval = i
end
return h
end
function Event.onTimeout(timeout, fn)
local timerId = os.startTimer(timeout)
local handler
handler = Event.on('timer', function(t, id)
if timerId == id then
fn(t, id)
Event.off(handler)
end
end)
return handler
end
-- Set a handler for the terminate event. Within the function, return
-- true or false to indicate whether the event should be propagated to
-- all sub-threads
function Event.onTerminate(fn)
Event.termFn = fn
end
function Event.termFn()
Event.terminate = true
return true -- propagate
end
function Event.addRoutine(fn)
local r = setmetatable({
co = coroutine.create(fn),
uid = nextUID()
}, { __index = Routine })
Event.routines[r.uid] = r
r:resume()
return r
end
function Event.pullEvents(...)
for _, fn in ipairs({ ... }) do
Event.addRoutine(fn)
end
repeat
Event.pullEvent()
until Event.terminate
Event.terminate = false
end
function Event.exitPullEvents()
Event.terminate = true
os.sleep(0)
end
local function processHandlers(event)
local handlers = Event.types[event]
if handlers then
for _,h in pairs(handlers) do
if not h.co then
-- callbacks are single threaded (only 1 co per handler)
h.co = createCoroutine(h)
Event.routines[h.uid] = h
end
end
end
end
local function tokeys(t)
local keys = { }
for k in pairs(t) do
keys[#keys+1] = k
end
return keys
end
local function processRoutines(...)
local keys = tokeys(Event.routines)
for _,key in ipairs(keys) do
local r = Event.routines[key]
if r then
r:resume(...)
end
end
end
-- invoke the handlers registered for this event
function Event.trigger(event, ...)
local handlers = Event.types[event]
if handlers then
for _,h in pairs(handlers) do
if not h.co then
-- callbacks are single threaded (only 1 co per handler)
h.co = createCoroutine(h)
Event.routines[h.uid] = h
h:resume(event, ...)
end
end
end
end
function Event.processEvent(e)
processHandlers(e[1])
processRoutines(table.unpack(e))
end
function Event.pullEvent(eventType)
while true do
local e = { os.pullEventRaw() }
local propagate = true -- don't like this...
if e[1] == 'terminate' then
propagate = Event.termFn()
end
if propagate then
processHandlers(e[1])
processRoutines(table.unpack(e))
end
if Event.terminate then
return { 'terminate' }
end
if not eventType or e[1] == eventType then
return e
end
end
end
return Event

View File

@@ -0,0 +1,21 @@
local git = require('opus.git')
local fs = _G.fs
local gitfs = { }
function gitfs.mount(dir, repo)
if not repo then
error('gitfs syntax: repo')
end
local list = git.list(repo)
for path, entry in pairs(list) do
if not fs.exists(fs.combine(dir, path)) then
local node = fs.mount(fs.combine(dir, path), 'urlfs', entry.url)
node.size = entry.size
end
end
end
return gitfs

View File

@@ -0,0 +1,65 @@
local fs = _G.fs
local linkfs = { }
-- TODO: implement broken links
local methods = { 'exists', 'getFreeSpace', 'getSize',
'isDir', 'isReadOnly', 'list', 'listEx', 'makeDir', 'open', 'getDrive' }
for _,m in pairs(methods) do
linkfs[m] = function(node, dir, ...)
dir = dir:gsub(node.mountPoint, node.source, 1)
return fs[m](dir, ...)
end
end
function linkfs.mount(_, source)
if not source then
error('Source is required')
end
source = fs.combine(source, '')
if fs.isDir(source) then
return {
source = source,
nodes = { },
}
end
return {
source = source
}
end
function linkfs.copy(node, s, t)
s = s:gsub(node.mountPoint, node.source, 1)
t = t:gsub(node.mountPoint, node.source, 1)
return fs.copy(s, t)
end
function linkfs.delete(node, dir)
if dir == node.mountPoint then
fs.unmount(node.mountPoint)
else
dir = dir:gsub(node.mountPoint, node.source, 1)
return fs.delete(dir)
end
end
function linkfs.find(node, spec)
spec = spec:gsub(node.mountPoint, node.source, 1)
local list = fs.find(spec)
for k,f in ipairs(list) do
list[k] = f:gsub(node.source, node.mountPoint, 1)
end
return list
end
function linkfs.move(node, s, t)
s = s:gsub(node.mountPoint, node.source, 1)
t = t:gsub(node.mountPoint, node.source, 1)
return fs.move(s, t)
end
return linkfs

View File

@@ -0,0 +1,166 @@
local Socket = require('opus.socket')
local synchronized = require('opus.sync').sync
local fs = _G.fs
local netfs = { }
local function remoteCommand(node, msg)
for _ = 1, 2 do
if not node.socket then
node.socket = Socket.connect(node.id, 139)
end
if not node.socket then
error('netfs: Unable to establish connection to ' .. node.id)
fs.unmount(node.mountPoint)
return
end
local ret
synchronized(node.socket, function()
node.socket:write(msg)
ret = node.socket:read(1)
end)
if ret then
return ret.response
end
node.socket:close()
node.socket = nil
end
error('netfs: Connection failed', 2)
end
local methods = { 'delete', 'exists', 'getFreeSpace', 'makeDir', 'list', 'listEx' }
local function resolveDir(dir, node)
-- TODO: Wrong ! (does not support names with dashes)
dir = dir:gsub(node.mountPoint, '', 1)
return fs.combine(node.directory, dir)
end
for _,m in pairs(methods) do
netfs[m] = function(node, dir)
dir = resolveDir(dir, node)
return remoteCommand(node, {
fn = m,
args = { dir },
})
end
end
function netfs.mount(_, id, directory)
if not id or not tonumber(id) then
error('ramfs syntax: computerId [directory]')
end
return {
id = tonumber(id),
nodes = { },
directory = directory or '',
}
end
function netfs.getDrive()
return 'net'
end
function netfs.complete(node, partial, dir, includeFiles, includeSlash)
dir = resolveDir(dir, node)
return remoteCommand(node, {
fn = 'complete',
args = { partial, dir, includeFiles, includeSlash },
})
end
function netfs.copy(node, s, t)
s = resolveDir(s, node)
t = resolveDir(t, node)
return remoteCommand(node, {
fn = 'copy',
args = { s, t },
})
end
function netfs.isDir(node, dir)
if dir == node.mountPoint and node.directory == '' then
return true
end
return remoteCommand(node, {
fn = 'isDir',
args = { resolveDir(dir, node) },
})
end
function netfs.isReadOnly(node, dir)
if dir == node.mountPoint and node.directory == '' then
return false
end
return remoteCommand(node, {
fn = 'isReadOnly',
args = { resolveDir(dir, node) },
})
end
function netfs.getSize(node, dir)
if dir == node.mountPoint and node.directory == '' then
return 0
end
return remoteCommand(node, {
fn = 'getSize',
args = { resolveDir(dir, node) },
})
end
function netfs.find(node, spec)
spec = resolveDir(spec, node)
local list = remoteCommand(node, {
fn = 'find',
args = { spec },
})
for k,f in ipairs(list) do
list[k] = fs.combine(node.mountPoint, f)
end
return list
end
function netfs.move(node, s, t)
s = resolveDir(s, node)
t = resolveDir(t, node)
return remoteCommand(node, {
fn = 'move',
args = { s, t },
})
end
function netfs.open(node, fn, fl)
fn = resolveDir(fn, node)
local vfh = remoteCommand(node, {
fn = 'open',
args = { fn, fl },
})
if vfh then
vfh.node = node
for _,m in ipairs(vfh.methods) do
vfh[m] = function(...)
return remoteCommand(node, {
fn = 'fileOp',
args = { vfh.fileUid, m, ... },
})
end
end
end
return vfh
end
return netfs

View File

@@ -0,0 +1,147 @@
local Util = require('opus.util')
local fs = _G.fs
local ramfs = { }
function ramfs.mount(_, nodeType)
if nodeType == 'directory' then
return {
nodes = { },
size = 0,
}
elseif nodeType == 'file' then
return {
size = 0,
}
end
error('ramfs syntax: [directory, file]')
end
function ramfs.delete(node, dir)
if node.mountPoint == dir then
fs.unmount(node.mountPoint)
end
end
function ramfs.exists(node, fn)
return node.mountPoint == fn
end
function ramfs.getSize(node)
return node.size
end
function ramfs.isReadOnly()
return false
end
function ramfs.makeDir(_, dir)
fs.mount(dir, 'ramfs', 'directory')
end
function ramfs.isDir(node)
return not not node.nodes
end
function ramfs.getDrive()
return 'ram'
end
function ramfs.list(node, dir)
if node.nodes and node.mountPoint == dir then
local files = { }
for k in pairs(node.nodes) do
table.insert(files, k)
end
return files
end
error('Not a directory')
end
function ramfs.open(node, fn, fl)
if fl ~= 'r' and fl ~= 'w' and fl ~= 'rb' and fl ~= 'wb' then
error('Unsupported mode')
end
if fl == 'r' then
if node.mountPoint ~= fn then
return
end
local ctr = 0
local lines
return {
readLine = function()
if not lines then
lines = Util.split(node.contents)
end
ctr = ctr + 1
return lines[ctr]
end,
readAll = function()
return node.contents
end,
close = function()
lines = nil
end,
}
elseif fl == 'w' then
node = fs.mount(fn, 'ramfs', 'file')
local c = ''
return {
write = function(str)
c = c .. str
end,
writeLine = function(str)
c = c .. str .. '\n'
end,
flush = function()
node.contents = c
node.size = #c
end,
close = function()
node.contents = c
node.size = #c
c = nil
end,
}
elseif fl == 'rb' then
if node.mountPoint ~= fn or not node.contents then
return
end
local ctr = 0
return {
read = function()
ctr = ctr + 1
return node.contents[ctr]
end,
close = function()
end,
}
elseif fl == 'wb' then
node = fs.mount(fn, 'ramfs', 'file')
local c = { }
return {
write = function(b)
table.insert(c, b)
end,
flush = function()
node.contents = c
node.size = #c
end,
close = function()
node.contents = c
node.size = #c
c = nil
end,
}
end
end
return ramfs

View File

@@ -0,0 +1,103 @@
--local rttp = require('rttp')
local Util = require('opus.util')
local fs = _G.fs
local urlfs = { }
function urlfs.mount(_, url)
if not url then
error('URL is required')
end
return {
url = url,
}
end
function urlfs.delete(_, dir)
fs.unmount(dir)
end
function urlfs.exists()
return true
end
function urlfs.getSize(node)
return node.size or 0
end
function urlfs.isReadOnly()
return true
end
function urlfs.isDir()
return false
end
function urlfs.getDrive()
return 'url'
end
function urlfs.open(node, fn, fl)
if fl == 'w' or fl == 'wb' then
fs.delete(fn)
return fs.open(fn, fl)
end
if fl ~= 'r' and fl ~= 'rb' then
error('Unsupported mode')
end
local c = node.cache
if not c then
--[[
if node.url:match("^(rttps?:)") then
local s, response = rttp.get(node.url)
c = s and response.statusCode == 200 and response.data
else
c = Util.httpGet(node.url)
end
]]--
c = Util.httpGet(node.url)
if c then
node.cache = c
node.size = #c
end
end
if not c then
return
end
local ctr = 0
local lines
if fl == 'r' then
return {
readLine = function()
if not lines then
lines = Util.split(c)
end
ctr = ctr + 1
return lines[ctr]
end,
readAll = function()
return c
end,
close = function()
lines = nil
end,
}
end
return {
read = function()
ctr = ctr + 1
return c:sub(ctr, ctr):byte()
end,
close = function()
ctr = 0
end,
}
end
return urlfs

64
sys/modules/opus/git.lua Normal file
View File

@@ -0,0 +1,64 @@
local json = require('opus.json')
local Util = require('opus.util')
local TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1'
local FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s'
local git = { }
if _G._GIT_API_KEY then
TREE_URL = TREE_URL .. '&access_token=' .. _G._GIT_API_KEY
end
function git.list(repository)
local t = Util.split(repository, '(.-)/')
local user = table.remove(t, 1)
local repo = table.remove(t, 1)
local branch = table.remove(t, 1) or 'master'
local path
if not Util.empty(t) then
path = table.concat(t, '/') .. '/'
end
local function getContents()
local dataUrl = string.format(TREE_URL, user, repo, branch)
local contents = Util.download(dataUrl)
if contents then
return json.decode(contents)
end
end
local data = getContents() or error('Invalid repository')
if data.message and data.message:find("API rate limit exceeded") then
error("Out of API calls, try again later")
end
if data.message and data.message == "Not found" then
error("Invalid repository")
end
local list = { }
for _,v in pairs(data.tree) do
if v.type == "blob" then
v.path = v.path:gsub("%s","%%20")
if not path then
list[v.path] = {
url = string.format(FILE_URL, user, repo, branch, v.path),
size = v.size,
}
elseif Util.startsWith(v.path, path) then
local p = string.sub(v.path, #path)
list[p] = {
url = string.format(FILE_URL, user, repo, branch, path .. p),
size = v.size,
}
end
end
end
return list
end
return git

107
sys/modules/opus/gps.lua Normal file
View File

@@ -0,0 +1,107 @@
local GPS = { }
local device = _G.device
local gps = _G.gps
function GPS.locate(timeout, debug)
local pt = { }
timeout = timeout or 10
pt.x, pt.y, pt.z = gps.locate(timeout, debug)
if pt.x then
return pt
end
end
function GPS.isAvailable()
return device.wireless_modem and GPS.locate()
end
function GPS.getPoint(timeout, debug)
local pt = GPS.locate(timeout, debug)
if not pt then
return
end
pt.x = math.floor(pt.x)
pt.y = math.floor(pt.y)
pt.z = math.floor(pt.z)
if _G.pocket then
pt.y = pt.y - 1
end
return pt
end
-- from stock gps API
local function trilaterate(A, B, C)
local a2b = B.position - A.position
local a2c = C.position - A.position
if math.abs( a2b:normalize():dot( a2c:normalize() ) ) > 0.999 then
return
end
local d = a2b:length()
local ex = a2b:normalize( )
local i = ex:dot( a2c )
local ey = (a2c - (ex * i)):normalize()
local j = ey:dot( a2c )
local ez = ex:cross( ey )
local r1 = A.distance
local r2 = B.distance
local r3 = C.distance
local x = (r1*r1 - r2*r2 + d*d) / (2*d)
local y = (r1*r1 - r3*r3 - x*x + (x-i)*(x-i) + j*j) / (2*j)
local result = A.position + (ex * x) + (ey * y)
local zSquared = r1*r1 - x*x - y*y
if zSquared > 0 then
local z = math.sqrt( zSquared )
local result1 = result + (ez * z)
local result2 = result - (ez * z)
local rounded1, rounded2 = result1:round(), result2:round()
if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then
return rounded1, rounded2
else
return rounded1
end
end
return result:round()
end
local function narrow( p1, p2, fix )
local dist1 = math.abs( (p1 - fix.position):length() - fix.distance )
local dist2 = math.abs( (p2 - fix.position):length() - fix.distance )
if math.abs(dist1 - dist2) < 0.05 then
return p1, p2
elseif dist1 < dist2 then
return p1:round()
else
return p2:round()
end
end
-- end stock gps api
function GPS.trilaterate(tFixes)
local pos1, pos2 = trilaterate(tFixes[1], tFixes[2], tFixes[3])
if pos2 then
pos1, pos2 = narrow(pos1, pos2, tFixes[4])
end
if pos1 and pos2 then
print("Ambiguous position")
print("Could be "..pos1.x..","..pos1.y..","..pos1.z.." or "..pos2.x..","..pos2.y..","..pos2.z )
return
end
return pos1
end
return GPS

View File

@@ -0,0 +1,50 @@
local Util = require('opus.util')
local History = { }
local History_mt = { __index = History }
function History.load(filename, limit)
local self = setmetatable({
limit = limit,
filename = filename,
}, History_mt)
self.entries = Util.readLines(filename) or { }
self.pos = #self.entries + 1
return self
end
function History:add(line)
if line ~= self.entries[#self.entries] then
table.insert(self.entries, line)
if self.limit then
while #self.entries > self.limit do
table.remove(self.entries, 1)
end
end
Util.writeLines(self.filename, self.entries)
self.pos = #self.entries + 1
end
end
function History:reset()
self.pos = #self.entries + 1
end
function History:back()
if self.pos > 1 then
self.pos = self.pos - 1
return self.entries[self.pos]
end
end
function History:forward()
if self.pos <= #self.entries then
self.pos = self.pos + 1
return self.entries[self.pos]
end
end
return History

View File

@@ -0,0 +1,128 @@
--- Parse the pastebin code from the given code or URL
local function parseCode(paste)
local patterns = {
"^([%a%d]+)$",
"^https?://pastebin.com/([%a%d]+)$",
"^pastebin.com/([%a%d]+)$",
"^https?://pastebin.com/raw/([%a%d]+)$",
"^pastebin.com/raw/([%a%d]+)$",
}
for i = 1, #patterns do
local code = paste:match(patterns[i])
if code then
return code
end
end
return nil
end
-- Download the contents of a paste
local function download(code)
if type(code) ~= "string" then
error("bad argument #1 (expected string, got " .. type(code) .. ")", 2)
end
if not http then
return false, "Pastebin requires http API"
end
-- Add a cache buster so that spam protection is re-checked
local cacheBuster = ("%x"):format(math.random(0, 2 ^ 30))
local response, err = http.get(
"https://pastebin.com/raw/" .. textutils.urlEncode(code) .. "?cb=" .. cacheBuster
)
if not response then
return response, err
end
-- If spam protection is activated, we get redirected to /paste with Content-Type: text/html
local headers = response.getResponseHeaders()
if not headers["Content-Type"] or not headers["Content-Type"]:find("^text/plain") then
return false, "Pastebin blocked due to spam protection"
end
local contents = response.readAll()
response.close()
return contents
end
-- Upload text to pastebin
local function upload(name, text)
if not http then
return false, "Pastebin requires http API"
end
-- POST the contents to pastebin
local key = "0ec2eb25b6166c0c27a394ae118ad829"
local response = http.post(
"https://pastebin.com/api/api_post.php",
"api_option=paste&" ..
"api_dev_key=" .. key .. "&" ..
"api_paste_format=lua&" ..
"api_paste_name=" .. textutils.urlEncode(name) .. "&" ..
"api_paste_code=" .. textutils.urlEncode(text)
)
if not response then
return false, "Failed."
end
local contents = response.readAll()
response.close()
return string.match(contents, "[^/]+$")
end
-- Download the contents to a file from pastebin
local function get(code, path)
if type(code) ~= "string" then
error( "bad argument #1 (expected string, got " .. type(code) .. ")", 2)
end
if type(path) ~= "string" then
error("bad argument #2 (expected string, got " .. type(path) .. ")", 2)
end
local res, msg = download(code)
if not res then
return res, msg
end
local file = fs.open(path, "w")
file.write(res)
file.close()
return true
end
-- Upload a file to pastebin.com
local function put(path)
if type(path) ~= "string" then
error("bad argument #1 (expected string, got " .. type(path) .. ")", 2)
end
-- Determine file to upload
if not fs.exists(path) or fs.isDir(path) then
return false, "No such file"
end
-- Read in the file
local name = fs.getName(path)
local file = fs.open(path, "r")
local text = file.readAll()
file.close()
return upload(name, text)
end
return {
download = download,
upload = upload,
get = get,
put = put,
parseCode = parseCode,
}

View File

@@ -0,0 +1,205 @@
local PASTEBIN_URL = 'http://pastebin.com/raw'
local GIT_URL = 'https://raw.githubusercontent.com'
local function split(str, pattern)
local t = { }
local function helper(line) table.insert(t, line) return "" end
helper((str:gsub(pattern, helper)))
return t
end
local hasMain
local luaPaths = package and package.path and split(package.path, '(.-);') or { }
for i = 1, #luaPaths do
if luaPaths[i] == '?' or luaPaths[i] == '?.lua' then
luaPaths[i] = nil
elseif string.find(luaPaths[i], '/rom/modules/main') then
hasMain = true
end
end
table.insert(luaPaths, 1, '?.lua')
table.insert(luaPaths, 2, '?/init.lua')
table.insert(luaPaths, 3, '/usr/modules/?.lua')
table.insert(luaPaths, 4, '/usr/modules/?/init.lua')
table.insert(luaPaths, 5, '/sys/modules/?.lua')
table.insert(luaPaths, 6, '/sys/modules/?/init.lua')
local DEFAULT_PATH = table.concat(luaPaths, ';')
if not hasMain then
DEFAULT_PATH = DEFAULT_PATH .. ';/rom/modules/main/?;/rom/modules/main/?.lua;/rom/modules/main/?/init.lua'
end
local fs = _G.fs
local http = _G.http
local os = _G.os
local string = _G.string
--[[
if not http._patched then
-- fix broken http get (http.get is not coroutine safe)
local syncLocks = { }
local function sync(obj, fn)
local key = tostring(obj)
if syncLocks[key] then
local cos = tostring(coroutine.running())
table.insert(syncLocks[key], cos)
repeat
local _, co = os.pullEvent('sync_lock')
until co == cos
else
syncLocks[key] = { }
end
fn()
local co = table.remove(syncLocks[key], 1)
if co then
os.queueEvent('sync_lock', co)
else
syncLocks[key] = nil
end
end
-- todo -- completely replace http.get with function that
-- checks for success on permanent redirects (minecraft 1.75 bug)
http._patched = http.get
function http.get(url, headers)
local s, m
sync(url, function()
s, m = http._patched(url, headers)
end)
return s, m
end
end
--]]
local function loadUrl(url)
local c
local h = http.get(url)
if h then
c = h.readAll()
h.close()
end
if c and #c > 0 then
return c
end
end
-- Add require and package to the environment
return function(env)
local function preloadSearcher(modname)
if env.package.preload[modname] then
return function()
return env.package.preload[modname](modname, env)
end
end
end
local function loadedSearcher(modname)
if env.package.loaded[modname] then
return function()
return env.package.loaded[modname]
end
end
end
local sentinel = { }
local function pathSearcher(modname)
if env.package.loaded[modname] == sentinel then
error("loop or previous error loading module '" .. modname .. "'", 0)
end
env.package.loaded[modname] = sentinel
local fname = modname:gsub('%.', '/')
for pattern in string.gmatch(env.package.path, "[^;]+") do
local sPath = string.gsub(pattern, "%?", fname)
-- TODO: if there's no shell, we should not be checking relative paths below
-- as they will resolve to root directory
if sPath:match("^(https?:)") then
local c = loadUrl(sPath)
if c then
return load(c, modname, nil, env)
end
else
if env.shell and
type(env.shell.getRunningProgram) == 'function' and
sPath:sub(1, 1) ~= "/" then
sPath = fs.combine(fs.getDir(env.shell.getRunningProgram() or ''), sPath)
end
if fs.exists(sPath) and not fs.isDir(sPath) then
return loadfile(sPath, env)
end
end
end
end
-- require('BniCQPVf')
local function pastebinSearcher(modname)
if #modname == 8 and not modname:match('%W') then
local url = PASTEBIN_URL .. '/' .. modname
local c = loadUrl(url)
if c then
return load(c, modname, nil, env)
end
end
end
-- require('kepler155c.opus.master.sys.apis.util')
local function gitSearcher(modname)
local fname = modname:gsub('%.', '/') .. '.lua'
local _, count = fname:gsub("/", "")
if count >= 3 then
local url = GIT_URL .. '/' .. fname
local c = loadUrl(url)
if c then
return load(c, modname, nil, env)
end
end
end
-- place package and require function into env
env.package = {
path = env.LUA_PATH or _G.LUA_PATH or DEFAULT_PATH,
config = '/\n:\n?\n!\n-',
preload = { },
loaded = {
coroutine = coroutine,
io = io,
math = math,
os = os,
string = string,
table = table,
},
loaders = {
preloadSearcher,
loadedSearcher,
pathSearcher,
pastebinSearcher,
gitSearcher,
}
}
function env.require(modname)
for _,searcher in ipairs(env.package.loaders) do
local fn, msg = searcher(modname)
if fn then
local module, msg2 = fn(modname, env)
if not module then
error(msg2 or (modname .. ' module returned nil'), 2)
end
env.package.loaded[modname] = module
return module
end
if msg then
error(msg, 2)
end
end
error('Unable to find module ' .. modname, 2)
end
return env.require -- backwards compatible
end

206
sys/modules/opus/input.lua Normal file
View File

@@ -0,0 +1,206 @@
local Util = require('opus.util')
local keyboard = _G.device and _G.device.keyboard
local keys = _G.keys
local os = _G.os
local modifiers = Util.transpose {
keys.leftCtrl, keys.rightCtrl,
keys.leftShift, keys.rightShift,
keys.leftAlt, keys.rightAlt,
}
local input = {
state = { },
}
if not keyboard then
keyboard = { state = input.state }
end
function input:modifierPressed()
return keyboard.state[keys.leftCtrl] or
keyboard.state[keys.rightCtrl] or
keyboard.state[keys.leftAlt] or
keyboard.state[keys.rightAlt]
end
function input:toCode(ch, code)
local result = { }
if not ch and code == 1 then
ch = 'escape'
end
if keyboard.state[keys.leftCtrl] or keyboard.state[keys.rightCtrl] or
code == keys.leftCtrl or code == keys.rightCtrl then
table.insert(result, 'control')
end
-- the key-up event for alt keys is not generated if the minecraft
-- window loses focus
if keyboard.state[keys.leftAlt] or keyboard.state[keys.rightAlt] or
code == keys.leftAlt or code == keys.rightAlt then
table.insert(result, 'alt')
end
if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] or
code == keys.leftShift or code == keys.rightShift then
if code and modifiers[code] then
table.insert(result, 'shift')
elseif #ch == 1 then
table.insert(result, ch:upper())
else
table.insert(result, 'shift')
table.insert(result, ch)
end
elseif not code or not modifiers[code] then
table.insert(result, ch)
end
return table.concat(result, '-')
end
function input:reset()
self.state = { }
self.fired = nil
self.timer = nil
self.mch = nil
self.mfired = nil
end
local function isCombo()
-- allow control-alt combinations for certain keyboards
return (keyboard.state[keys.leftAlt] or keyboard.state[keys.rightAlt]) and
(keyboard.state[keys.leftCtrl] or keyboard.state[keys.rightCtrl])
end
function input:translate(event, code, p1, p2)
if event == 'key' then
if p1 then -- key is held down
if not modifiers[code] then
self.fired = true
local ch = input:toCode(keys.getName(code), code)
if #ch == 1 then
return {
code = 'char',
ch = ch,
}
end
return { code = ch }
end
elseif code then
--self.fired = true
local ch = input:toCode(keys.getName(code), code)
if #ch ~= 1 then
return { code = ch }
end
-- self.state[code] = true
end
elseif event == 'char' then
local combo = isCombo()
--if not self.fired then
if combo or not (keyboard.state[keys.leftCtrl] or keyboard.state[keys.rightCtrl]) then
self.fired = not combo
return { code = event, ch = code }
--end
-- return { code = event, ch = input:toCode(code) }
end
elseif event == 'key_upx' then
if not self.fired then
--if self.state[code] then
self.fired = true
local ch = input:toCode(keys.getName(code), code)
self.state[code] = nil
return { code = ch }
--end
end
self.state[code] = nil
elseif event == 'paste' then
self.fired = true
if keyboard.state[keys.leftShift] or keyboard.state[keys.rightShift] then
return { code = 'shift-paste', text = code }
else
return { code = 'paste', text = code }
end
elseif event == 'mouse_click' then
local buttons = { 'mouse_click', 'mouse_rightclick' }
self.mch = buttons[code]
self.mfired = nil
return {
code = input:toCode('mouse_down', 255),
button = code,
x = p1,
y = p2,
}
elseif event == 'mouse_drag' then
self.mfired = true
self.fired = true
return {
code = input:toCode('mouse_drag', 255),
button = code,
x = p1,
y = p2,
}
elseif event == 'mouse_up' then
if not self.mfired then
local clock = os.clock()
if self.timer and
p1 == self.x and p2 == self.y and
(clock - self.timer < .5) then
self.mch = 'mouse_doubleclick'
self.timer = nil
else
self.timer = os.clock()
self.x = p1
self.y = p2
end
self.mfired = input:toCode(self.mch, 255)
else
self.mch = 'mouse_up'
self.mfired = input:toCode(self.mch, 255)
end
self.fired = true
return {
code = self.mfired,
button = code,
x = p1,
y = p2,
}
elseif event == "mouse_scroll" then
local directions = {
[ -1 ] = 'scroll_up',
[ 1 ] = 'scroll_down'
}
self.fired = true
return {
code = input:toCode(directions[code], 255),
x = p1,
y = p2,
}
elseif event == 'terminate' then
return { code = 'terminate' }
end
end
function input:test()
while true do
local ch = self:translate(os.pullEvent())
if ch then
Util.print(ch)
end
end
end
return input

589
sys/modules/opus/json.lua Normal file
View File

@@ -0,0 +1,589 @@
-- Module options:
local register_global_module_table = false
local global_module_name = 'json'
--[==[
NOTE: Modified to reduce file size.
See https://github.com/LuaDist/dkjson/blob/master/dkjson.lua
for full version.
David Kolf's JSON module for Lua 5.1/5.2
Version 2.5
For the documentation see the corresponding readme.txt or visit
<http://dkolf.de/src/dkjson-lua.fsl/>.
You can contact the author by sending an e-mail to 'david' at the
domain 'dkolf.de'.
Copyright (C) 2010-2014 David Heiko Kolf
Refer to license located at https://github.com/LuaDist/dkjson/blob/master/dkjson.lua
--]==]
-- global dependencies:
local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset =
pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset
local error, require, pcall, select = error, require, pcall, select
local floor, huge = math.floor, math.huge
local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat =
string.rep, string.gsub, string.sub, string.byte, string.char,
string.find, string.len, string.format
local strmatch = string.match
local concat = table.concat
local json = { version = "dkjson 2.5" }
if register_global_module_table then
_G[global_module_name] = json
end
local _ENV = nil -- blocking globals in Lua 5.2
pcall (function()
-- Enable access to blocked metatables.
-- Don't worry, this module doesn't change anything in them.
local debmeta = require "debug".getmetatable
if debmeta then getmetatable = debmeta end
end)
json.null = setmetatable ({}, {
__tojson = function () return "null" end
})
local function isarray (tbl)
local max, n, arraylen = 0, 0, 0
for k,v in pairs (tbl) do
if k == 'n' and type(v) == 'number' then
arraylen = v
if v > max then
max = v
end
else
if type(k) ~= 'number' or k < 1 or floor(k) ~= k then
return false
end
if k > max then
max = k
end
n = n + 1
end
end
if max > 10 and max > arraylen and max > n * 2 then
return false -- don't create an array with too many holes
end
return true, max
end
local escapecodes = {
["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f",
["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t"
}
local function escapeutf8 (uchar)
local value = escapecodes[uchar]
if value then
return value
end
local a, b, c, d = strbyte (uchar, 1, 4)
a, b, c, d = a or 0, b or 0, c or 0, d or 0
if a <= 0x7f then
value = a
elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then
value = (a - 0xc0) * 0x40 + b - 0x80
elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then
value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80
elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then
value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80
else
return ""
end
if value <= 0xffff then
return strformat ("\\u%.4x", value)
elseif value <= 0x10ffff then
-- encode as UTF-16 surrogate pair
value = value - 0x10000
local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400)
return strformat ("\\u%.4x\\u%.4x", highsur, lowsur)
else
return ""
end
end
local function fsub (str, pattern, repl)
-- gsub always builds a new string in a buffer, even when no match
-- exists. First using find should be more efficient when most strings
-- don't contain the pattern.
if strfind (str, pattern) then
return gsub (str, pattern, repl)
else
return str
end
end
local function quotestring (value)
-- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js
value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8)
if strfind (value, "[\194\216\220\225\226\239]") then
value = fsub (value, "\194[\128-\159\173]", escapeutf8)
value = fsub (value, "\216[\128-\132]", escapeutf8)
value = fsub (value, "\220\143", escapeutf8)
value = fsub (value, "\225\158[\180\181]", escapeutf8)
value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8)
value = fsub (value, "\226\129[\160-\175]", escapeutf8)
value = fsub (value, "\239\187\191", escapeutf8)
value = fsub (value, "\239\191[\176-\191]", escapeutf8)
end
return "\"" .. value .. "\""
end
json.quotestring = quotestring
local function replace(str, o, n)
local i, j = strfind (str, o, 1, true)
if i then
return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1)
else
return str
end
end
-- locale independent num2str and str2num functions
local decpoint, numfilter
local function updatedecpoint ()
decpoint = strmatch(tostring(0.5), "([^05+])")
-- build a filter that can be used to remove group separators
numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+"
end
updatedecpoint()
local function num2str (num)
return replace(fsub(tostring(num), numfilter, ""), decpoint, ".")
end
local function str2num (str)
local num = tonumber(replace(str, ".", decpoint))
if not num then
updatedecpoint()
num = tonumber(replace(str, ".", decpoint))
end
return num
end
local function addnewline2 (level, buffer, buflen)
buffer[buflen+1] = "\n"
buffer[buflen+2] = strrep (" ", level)
buflen = buflen + 2
return buflen
end
function json.addnewline (state)
if state.indent then
state.bufferlen = addnewline2 (state.level or 0,
state.buffer, state.bufferlen or #(state.buffer))
end
end
local encode2 -- forward declaration
local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state)
local kt = type (key)
if kt ~= 'string' and kt ~= 'number' then
return nil, "type '" .. kt .. "' is not supported as a key by JSON."
end
if prev then
buflen = buflen + 1
buffer[buflen] = ","
end
if indent then
buflen = addnewline2 (level, buffer, buflen)
end
buffer[buflen+1] = quotestring (key)
buffer[buflen+2] = ":"
return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state)
end
local function appendcustom(res, buffer, state)
local buflen = state.bufferlen
if type (res) == 'string' then
buflen = buflen + 1
buffer[buflen] = res
end
return buflen
end
local function exception(reason, value, state, buffer, buflen, defaultmessage)
defaultmessage = defaultmessage or reason
local handler = state.exception
if not handler then
return nil, defaultmessage
else
state.bufferlen = buflen
local ret, msg = handler (reason, value, state, defaultmessage)
if not ret then return nil, msg or defaultmessage end
return appendcustom(ret, buffer, state)
end
end
function json.encodeexception(reason, value, state, defaultmessage)
return quotestring("<" .. defaultmessage .. ">")
end
encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state)
local valtype = type (value)
local valmeta = getmetatable (value)
valmeta = type (valmeta) == 'table' and valmeta -- only tables
local valtojson = valmeta and valmeta.__tojson
if valtojson then
if tables[value] then
return exception('reference cycle', value, state, buffer, buflen)
end
tables[value] = true
state.bufferlen = buflen
local ret, msg = valtojson (value, state)
if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end
tables[value] = nil
buflen = appendcustom(ret, buffer, state)
elseif value == nil then
buflen = buflen + 1
buffer[buflen] = "null"
elseif valtype == 'number' then
local s
if value ~= value or value >= huge or -value >= huge then
-- This is the behaviour of the original JSON implementation.
s = "null"
else
s = num2str (value)
end
buflen = buflen + 1
buffer[buflen] = s
elseif valtype == 'boolean' then
buflen = buflen + 1
buffer[buflen] = value and "true" or "false"
elseif valtype == 'string' then
buflen = buflen + 1
buffer[buflen] = quotestring (value)
elseif valtype == 'table' then
if tables[value] then
return exception('reference cycle', value, state, buffer, buflen)
end
tables[value] = true
level = level + 1
local isa, n = isarray (value)
if n == 0 and valmeta and valmeta.__jsontype == 'object' then
isa = false
end
local msg
if isa then -- JSON array
buflen = buflen + 1
buffer[buflen] = "["
for i = 1, n do
buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
if i < n then
buflen = buflen + 1
buffer[buflen] = ","
end
end
buflen = buflen + 1
buffer[buflen] = "]"
else -- JSON object
local prev = false
buflen = buflen + 1
buffer[buflen] = "{"
local order = valmeta and valmeta.__jsonorder or globalorder
if order then
local used = {}
n = #order
for i = 1, n do
local k = order[i]
local v = value[k]
if v then
used[k] = true
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
prev = true -- add a seperator before the next element
end
end
for k,v in pairs (value) do
if not used[k] then
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
prev = true -- add a seperator before the next element
end
end
else -- unordered
for k,v in pairs (value) do
buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state)
if not buflen then return nil, msg end
prev = true -- add a seperator before the next element
end
end
if indent then
buflen = addnewline2 (level - 1, buffer, buflen)
end
buflen = buflen + 1
buffer[buflen] = "}"
end
tables[value] = nil
else
return exception ('unsupported type', value, state, buffer, buflen,
"type '" .. valtype .. "' is not supported by JSON.")
end
return buflen
end
function json.encode (value, state)
state = state or {}
local oldbuffer = state.buffer
local buffer = oldbuffer or {}
state.buffer = buffer
updatedecpoint()
local ret, msg = encode2 (value, state.indent, state.level or 0,
buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state)
if not ret then
error (msg, 2)
elseif oldbuffer == buffer then
state.bufferlen = ret
return true
else
state.bufferlen = nil
state.buffer = nil
return concat (buffer)
end
end
local function loc (str, where)
local line, pos, linepos = 1, 1, 0
while true do
pos = strfind (str, "\n", pos, true)
if pos and pos < where then
line = line + 1
linepos = pos
pos = pos + 1
else
break
end
end
return "line " .. line .. ", column " .. (where - linepos)
end
local function unterminated (str, what, where)
return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where)
end
local function scanwhite (str, pos)
while true do
pos = strfind (str, "%S", pos)
if not pos then return nil end
local sub2 = strsub (str, pos, pos + 1)
if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then
-- UTF-8 Byte Order Mark
pos = pos + 3
elseif sub2 == "//" then
pos = strfind (str, "[\n\r]", pos + 2)
if not pos then return nil end
elseif sub2 == "/*" then
pos = strfind (str, "*/", pos + 2)
if not pos then return nil end
pos = pos + 2
else
return pos
end
end
end
local escapechars = {
["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f",
["n"] = "\n", ["r"] = "\r", ["t"] = "\t"
}
local function unichar (value)
if value < 0 then
return nil
elseif value <= 0x007f then
return strchar (value)
elseif value <= 0x07ff then
return strchar (0xc0 + floor(value/0x40),
0x80 + (floor(value) % 0x40))
elseif value <= 0xffff then
return strchar (0xe0 + floor(value/0x1000),
0x80 + (floor(value/0x40) % 0x40),
0x80 + (floor(value) % 0x40))
elseif value <= 0x10ffff then
return strchar (0xf0 + floor(value/0x40000),
0x80 + (floor(value/0x1000) % 0x40),
0x80 + (floor(value/0x40) % 0x40),
0x80 + (floor(value) % 0x40))
else
return nil
end
end
local function scanstring (str, pos)
local lastpos = pos + 1
local buffer, n = {}, 0
while true do
local nextpos = strfind (str, "[\"\\]", lastpos)
if not nextpos then
return unterminated (str, "string", pos)
end
if nextpos > lastpos then
n = n + 1
buffer[n] = strsub (str, lastpos, nextpos - 1)
end
if strsub (str, nextpos, nextpos) == "\"" then
lastpos = nextpos + 1
break
else
local escchar = strsub (str, nextpos + 1, nextpos + 1)
local value
if escchar == "u" then
value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16)
if value then
local value2
if 0xD800 <= value and value <= 0xDBff then
-- we have the high surrogate of UTF-16. Check if there is a
-- low surrogate escaped nearby to combine them.
if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then
value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16)
if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then
value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000
else
value2 = nil -- in case it was out of range for a low surrogate
end
end
end
value = value and unichar (value)
if value then
if value2 then
lastpos = nextpos + 12
else
lastpos = nextpos + 6
end
end
end
end
if not value then
value = escapechars[escchar] or escchar
lastpos = nextpos + 2
end
n = n + 1
buffer[n] = value
end
end
if n == 1 then
return buffer[1], lastpos
elseif n > 1 then
return concat (buffer), lastpos
else
return "", lastpos
end
end
local scanvalue -- forward declaration
local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta)
local len = strlen (str)
local tbl, n = {}, 0
local pos = startpos + 1
if what == 'object' then
setmetatable (tbl, objectmeta)
else
setmetatable (tbl, arraymeta)
end
while true do
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
local char = strsub (str, pos, pos)
if char == closechar then
return tbl, pos + 1
end
local val1, err
val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
if err then return nil, pos, err end
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
char = strsub (str, pos, pos)
if char == ":" then
if val1 == nil then
return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")"
end
pos = scanwhite (str, pos + 1)
if not pos then return unterminated (str, what, startpos) end
local val2
val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta)
if err then return nil, pos, err end
tbl[val1] = val2
pos = scanwhite (str, pos)
if not pos then return unterminated (str, what, startpos) end
char = strsub (str, pos, pos)
else
n = n + 1
tbl[n] = val1
end
if char == "," then
pos = pos + 1
end
end
end
scanvalue = function (str, pos, nullval, objectmeta, arraymeta)
pos = pos or 1
pos = scanwhite (str, pos)
if not pos then
return nil, strlen (str) + 1, "no valid JSON value (reached the end)"
end
local char = strsub (str, pos, pos)
if char == "{" then
return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta)
elseif char == "[" then
return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta)
elseif char == "\"" then
return scanstring (str, pos)
else
local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos)
if pstart then
local number = str2num (strsub (str, pstart, pend))
if number then
return number, pend + 1
end
end
pstart, pend = strfind (str, "^%a%w*", pos)
if pstart then
local name = strsub (str, pstart, pend)
if name == "true" then
return true, pend + 1
elseif name == "false" then
return false, pend + 1
elseif name == "null" then
return nullval, pend + 1
end
end
return nil, pos, "no valid JSON value at " .. loc (str, pos)
end
end
local function optionalmetatables(...)
if select("#", ...) > 0 then
return ...
else
return {__jsontype = 'object'}, {__jsontype = 'array'}
end
end
function json.decode (str, pos, nullval, ...)
local objectmeta, arraymeta = optionalmetatables(...)
return scanvalue (str, pos, nullval, objectmeta, arraymeta)
end
-- NOTE: added method - not in original source
function json.decodeFromFile(path)
local file = assert(fs.open(path, "r"))
local decoded = json.decode(file.readAll())
file.close()
return decoded
end
return json

View File

@@ -0,0 +1,175 @@
--- A light implementation of Binary heaps data structure.
-- While running a search, some search algorithms (Astar, Dijkstra, Jump Point Search) have to maintains
-- a list of nodes called __open list__. Retrieve from this list the lowest cost node can be quite slow,
-- as it normally requires to skim through the full set of nodes stored in this list. This becomes a real
-- problem especially when dozens of nodes are being processed (on large maps).
--
-- The current module implements a <a href="http://www.policyalmanac.org/games/binaryHeaps.htm">binary heap</a>
-- data structure, from which the search algorithm will instantiate an open list, and cache the nodes being
-- examined during a search. As such, retrieving the lower-cost node is faster and globally makes the search end
-- up quickly.
--
-- This module is internally used by the library on purpose.
-- It should normally not be used explicitely, yet it remains fully accessible.
--
--[[
Notes:
This lighter implementation of binary heaps, based on :
https://github.com/Yonaba/Binary-Heaps
--]]
if (...) then
-- Dependency
local Utils = require((...):gsub('%.bheap$','.utils'))
-- Local reference
local floor = math.floor
-- Default comparison function
local function f_min(a,b) return a < b end
-- Percolates up
local function percolate_up(heap, index)
if index == 1 then return end
local pIndex
if index <= 1 then return end
if index%2 == 0 then
pIndex = index/2
else pIndex = (index-1)/2
end
if not heap._sort(heap._heap[pIndex], heap._heap[index]) then
heap._heap[pIndex], heap._heap[index] =
heap._heap[index], heap._heap[pIndex]
percolate_up(heap, pIndex)
end
end
-- Percolates down
local function percolate_down(heap,index)
local lfIndex,rtIndex,minIndex
lfIndex = 2*index
rtIndex = lfIndex + 1
if rtIndex > heap._size then
if lfIndex > heap._size then return
else minIndex = lfIndex end
else
if heap._sort(heap._heap[lfIndex],heap._heap[rtIndex]) then
minIndex = lfIndex
else
minIndex = rtIndex
end
end
if not heap._sort(heap._heap[index],heap._heap[minIndex]) then
heap._heap[index],heap._heap[minIndex] = heap._heap[minIndex],heap._heap[index]
percolate_down(heap,minIndex)
end
end
-- Produces a new heap
local function newHeap(template,comp)
return setmetatable({_heap = {},
_sort = comp or f_min, _size = 0},
template)
end
--- The `heap` class.<br/>
-- This class is callable.
-- _Therefore,_ <code>heap(...)</code> _is used to instantiate new heaps_.
-- @type heap
local heap = setmetatable({},
{__call = function(self,...)
return newHeap(self,...)
end})
heap.__index = heap
--- Checks if a `heap` is empty
-- @class function
-- @treturn bool __true__ of no item is queued in the heap, __false__ otherwise
-- @usage
-- if myHeap:empty() then
-- print('Heap is empty!')
-- end
function heap:empty()
return (self._size==0)
end
--- Clears the `heap` (removes all items queued in the heap)
-- @class function
-- @treturn heap self (the calling `heap` itself, can be chained)
-- @usage myHeap:clear()
function heap:clear()
self._heap = {}
self._size = 0
self._sort = self._sort or f_min
return self
end
--- Adds a new item in the `heap`
-- @class function
-- @tparam value item a new value to be queued in the heap
-- @treturn heap self (the calling `heap` itself, can be chained)
-- @usage
-- myHeap:push(1)
-- -- or, with chaining
-- myHeap:push(1):push(2):push(4)
function heap:push(item)
if item then
self._size = self._size + 1
self._heap[self._size] = item
percolate_up(self, self._size)
end
return self
end
--- Pops from the `heap`.
-- Removes and returns the lowest cost item (with respect to the comparison function being used) from the `heap`.
-- @class function
-- @treturn value a value previously pushed into the heap
-- @usage
-- while not myHeap:empty() do
-- local lowestValue = myHeap:pop()
-- ...
-- end
function heap:pop()
local root
if self._size > 0 then
root = self._heap[1]
self._heap[1] = self._heap[self._size]
self._heap[self._size] = nil
self._size = self._size-1
if self._size>1 then
percolate_down(self, 1)
end
end
return root
end
--- Restores the `heap` property.
-- Reorders the `heap` with respect to the comparison function being used.
-- When given argument __item__ (a value existing in the `heap`), will sort from that very item in the `heap`.
-- Otherwise, the whole `heap` will be cheacked.
-- @class function
-- @tparam[opt] value item the modified value
-- @treturn heap self (the calling `heap` itself, can be chained)
-- @usage myHeap:heapify()
function heap:heapify(item)
if self._size == 0 then return end
if item then
local i = Utils.indexOf(self._heap,item)
if i then
percolate_down(self, i)
percolate_up(self, i)
end
return
end
for i = floor(self._size/2),1,-1 do
percolate_down(self,i)
end
return self
end
return heap
end

View File

@@ -0,0 +1,41 @@
--- The Node class.
-- The `node` represents a cell (or a tile) on a collision map. Basically, for each single cell (tile)
-- in the collision map passed-in upon initialization, a `node` object will be generated
-- and then cached within the `grid`.
--
-- In the following implementation, nodes can be compared using the `<` operator. The comparison is
-- made with regards of their `f` cost. From a given node being examined, the `pathfinder` will expand the search
-- to the next neighbouring node having the lowest `f` cost. See `core.bheap` for more details.
--
if (...) then
local Node = {}
Node.__index = Node
function Node:new(x,y,z)
return setmetatable({x = x, y = y, z = z }, Node)
end
-- Enables the use of operator '<' to compare nodes.
-- Will be used to sort a collection of nodes in a binary heap on the basis of their F-cost
function Node.__lt(A,B) return (A._f < B._f) end
function Node:getX() return self.x end
function Node:getY() return self.y end
function Node:getZ() return self.z end
--- Clears temporary cached attributes of a `node`.
-- Deletes the attributes cached within a given node after a pathfinding call.
-- This function is internally used by the search algorithms, so you should not use it explicitely.
function Node:reset()
self._g, self._h, self._f = nil, nil, nil
self._opened, self._closed, self._parent = nil, nil, nil
return self
end
return setmetatable(Node,
{__call = function(_,...)
return Node:new(...)
end}
)
end

View File

@@ -0,0 +1,67 @@
--- The Path class.
-- The `path` class is a structure which represents a path (ordered set of nodes) from a start location to a goal.
-- An instance from this class would be a result of a request addressed to `Pathfinder:getPath`.
--
-- This module is internally used by the library on purpose.
-- It should normally not be used explicitely, yet it remains fully accessible.
--
if (...) then
local t_remove = table.remove
local Path = {}
Path.__index = Path
function Path:new()
return setmetatable({_nodes = {}}, Path)
end
--- Iterates on each single `node` along a `path`. At each step of iteration,
-- returns the `node` plus a count value. Aliased as @{Path:nodes}
-- @usage
-- for node, count in p:iter() do
-- ...
-- end
function Path:nodes()
local i = 1
return function()
if self._nodes[i] then
i = i+1
return self._nodes[i-1],i-1
end
end
end
--- `Path` compression modifier. Given a `path`, eliminates useless nodes to return a lighter `path`
-- consisting of straight moves. Does the opposite of @{Path:fill}
-- @class function
-- @treturn path self (the calling `path` itself, can be chained)
-- @see Path:fill
-- @usage p:filter()
function Path:filter()
local i = 2
local xi,yi,zi,dx,dy,dz, olddx, olddy, olddz
xi,yi,zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z
dx, dy,dz = xi - self._nodes[i-1].x, yi-self._nodes[i-1].y, zi-self._nodes[i-1].z
while true do
olddx, olddy, olddz = dx, dy, dz
if self._nodes[i+1] then
i = i+1
xi, yi, zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z
dx, dy, dz = xi - self._nodes[i-1].x, yi - self._nodes[i-1].y, zi - self._nodes[i-1].z
if olddx == dx and olddy == dy and olddz == dz then
t_remove(self._nodes, i-1)
i = i - 1
end
else break end
end
return self
end
return setmetatable(Path,
{__call = function(_,...)
return Path:new(...)
end
})
end

View File

@@ -0,0 +1,57 @@
-- Various utilities for Jumper top-level modules
if (...) then
-- Dependencies
local _PATH = (...):gsub('%.utils$','')
local Path = require (_PATH .. '.path')
-- Local references
local pairs = pairs
local t_insert = table.insert
-- Raw array items count
local function arraySize(t)
local count = 0
for _ in pairs(t) do
count = count+1
end
return count
end
-- Extract a path from a given start/end position
local function traceBackPath(finder, node, startNode)
local path = Path:new()
path._grid = finder._grid
while true do
if node._parent then
t_insert(path._nodes,1,node)
node = node._parent
else
t_insert(path._nodes,1,startNode)
return path
end
end
end
-- Lookup for value in a table
local indexOf = function(t,v)
for i = 1,#t do
if t[i] == v then return i end
end
return nil
end
-- Is i out of range
local function outOfRange(i,low,up)
return (i< low or i > up)
end
return {
arraySize = arraySize,
indexOf = indexOf,
outOfRange = outOfRange,
traceBackPath = traceBackPath
}
end

View File

@@ -0,0 +1,101 @@
--- The Grid class.
-- Implementation of the `grid` class.
-- The `grid` is a implicit graph which represents the 2D
-- world map layout on which the `pathfinder` object will run.
-- During a search, the `pathfinder` object needs to save some critical values.
-- These values are cached within each `node`
-- object, and the whole set of nodes are tight inside the `grid` object itself.
if (...) then
-- Dependencies
local _PATH = (...):gsub('%.grid$','')
-- Local references
local Utils = require (_PATH .. '.core.utils')
local Node = require (_PATH .. '.core.node')
-- Local references
local setmetatable = setmetatable
-- Offsets for straights moves
local straightOffsets = {
{x = 1, y = 0, z = 0} --[[W]], {x = -1, y = 0, z = 0}, --[[E]]
{x = 0, y = 1, z = 0} --[[S]], {x = 0, y = -1, z = 0}, --[[N]]
{x = 0, y = 0, z = 1} --[[U]], {x = 0, y = -0, z = -1}, --[[D]]
}
local Grid = {}
Grid.__index = Grid
function Grid:new(dim)
local newGrid = { }
newGrid._min_x, newGrid._max_x = dim.x, dim.ex
newGrid._min_y, newGrid._max_y = dim.y, dim.ey
newGrid._min_z, newGrid._max_z = dim.z, dim.ez
newGrid._nodes = { }
newGrid._width = (newGrid._max_x-newGrid._min_x)+1
newGrid._height = (newGrid._max_y-newGrid._min_y)+1
newGrid._length = (newGrid._max_z-newGrid._min_z)+1
return setmetatable(newGrid,Grid)
end
function Grid:isWalkableAt(x, y, z)
local node = self:getNodeAt(x,y,z)
return node and node.walkable ~= 1
end
function Grid:getWidth()
return self._width
end
function Grid:getHeight()
return self._height
end
function Grid:getNodes()
return self._nodes
end
function Grid:getBounds()
return self._min_x, self._min_y, self._min_z, self._max_x, self._max_y, self._max_z
end
--- Returns neighbours. The returned value is an array of __walkable__ nodes neighbouring a given `node`.
-- @treturn {node,...} an array of nodes neighbouring a given node
function Grid:getNeighbours(node)
local neighbours = {}
for i = 1,#straightOffsets do
local n = self:getNodeAt(
node.x + straightOffsets[i].x,
node.y + straightOffsets[i].y,
node.z + straightOffsets[i].z
)
if n and self:isWalkableAt(n.x, n.y, n.z) then
neighbours[#neighbours+1] = n
end
end
return neighbours
end
function Grid:getNodeAt(x,y,z)
if not x or not y or not z then return end
if Utils.outOfRange(x,self._min_x,self._max_x) then return end
if Utils.outOfRange(y,self._min_y,self._max_y) then return end
if Utils.outOfRange(z,self._min_z,self._max_z) then return end
-- inefficient
if not self._nodes[y] then self._nodes[y] = {} end
if not self._nodes[y][x] then self._nodes[y][x] = {} end
if not self._nodes[y][x][z] then self._nodes[y][x][z] = Node:new(x,y,z) end
return self._nodes[y][x][z]
end
return setmetatable(Grid,{
__call = function(self,...)
return self:new(...)
end
})
end

View File

@@ -0,0 +1,104 @@
--[[
The following License applies to all files within the jumper directory.
Note that this is only a partial copy of the full jumper code base. Also,
the code was modified to support 3D maps.
--]]
--[[
This work is under MIT-LICENSE
Copyright (c) 2012-2013 Roland Yonaba.
-- https://opensource.org/licenses/MIT
--]]
local _VERSION = ""
local _RELEASEDATE = ""
if (...) then
-- Dependencies
local _PATH = (...):gsub('%.pathfinder$','')
local Utils = require (_PATH .. '.core.utils')
-- Internalization
local pairs = pairs
local assert = assert
local setmetatable = setmetatable
--- Finders (search algorithms implemented). Refers to the search algorithms actually implemented in Jumper.
-- <li>[A*](http://en.wikipedia.org/wiki/A*_search_algorithm)</li>
local Finders = {
['ASTAR'] = require (_PATH .. '.search.astar'),
}
-- Will keep track of all nodes expanded during the search
-- to easily reset their properties for the next pathfinding call
local toClear = {}
-- Performs a traceback from the goal node to the start node
-- Only happens when the path was found
local Pathfinder = {}
Pathfinder.__index = Pathfinder
function Pathfinder:new(heuristic)
local newPathfinder = {}
setmetatable(newPathfinder, Pathfinder)
self._finder = Finders.ASTAR
self._heuristic = heuristic
return newPathfinder
end
function Pathfinder:setGrid(grid)
self._grid = grid
return self
end
--- Calculates a `path`. Returns the `path` from start to end location
-- Both locations must exist on the collision map. The starting location can be unwalkable.
-- @treturn path a path (array of nodes) when found, otherwise nil
-- @usage local path = myFinder:getPath(1,1,5,5)
function Pathfinder:getPath(startX, startY, startZ, ih, endX, endY, endZ, oh)
self:reset()
local startNode = self._grid:getNodeAt(startX, startY, startZ)
local endNode = self._grid:getNodeAt(endX, endY, endZ)
if not startNode or not endNode then
return nil
end
startNode.heading = ih
endNode.heading = oh
assert(startNode, ('Invalid location [%d, %d, %d]'):format(startX, startY, startZ))
assert(endNode and self._grid:isWalkableAt(endX, endY, endZ),
('Invalid or unreachable location [%d, %d, %d]'):format(endX, endY, endZ))
local _endNode = self._finder(self, startNode, endNode, toClear)
if _endNode then
return Utils.traceBackPath(self, _endNode, startNode)
end
return nil
end
--- Resets the `pathfinder`. This function is called internally between
-- successive pathfinding calls, so you should not
-- use it explicitely, unless under specific circumstances.
-- @class function
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage local path, len = myFinder:getPath(1,1,5,5)
function Pathfinder:reset()
for node in pairs(toClear) do node:reset() end
toClear = {}
return self
end
-- Returns Pathfinder class
Pathfinder._VERSION = _VERSION
Pathfinder._RELEASEDATE = _RELEASEDATE
return setmetatable(Pathfinder,{
__call = function(self,...)
return self:new(...)
end
})
end

View File

@@ -0,0 +1,77 @@
-- Astar algorithm
-- This actual implementation of A-star is based on
-- [Nash A. & al. pseudocode](http://aigamedev.com/open/tutorials/theta-star-any-angle-paths/)
if (...) then
-- Internalization
local huge = math.huge
-- Dependancies
local _PATH = (...):match('(.+)%.search.astar$')
local Heap = require (_PATH.. '.core.bheap')
-- Updates G-cost
local function computeCost(node, neighbour, heuristic)
local mCost, heading = heuristic(neighbour, node) -- Heuristics.EUCLIDIAN(neighbour, node)
if node._g + mCost < neighbour._g then
neighbour._parent = node
neighbour._g = node._g + mCost
neighbour.heading = heading
end
end
-- Updates vertex node-neighbour
local function updateVertex(openList, node, neighbour, endNode, heuristic)
local oldG = neighbour._g
computeCost(node, neighbour, heuristic)
if neighbour._g < oldG then
if neighbour._opened then neighbour._opened = false end
neighbour._h = heuristic(endNode, neighbour)
neighbour._f = neighbour._g + neighbour._h
openList:push(neighbour)
neighbour._opened = true
end
end
-- Calculates a path.
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
return function (finder, startNode, endNode, toClear)
local openList = Heap()
startNode._g = 0
startNode._h = finder._heuristic(endNode, startNode)
startNode._f = startNode._g + startNode._h
openList:push(startNode)
toClear[startNode] = true
startNode._opened = true
while not openList:empty() do
local node = openList:pop()
node._closed = true
if node == endNode then return node end
local neighbours = finder._grid:getNeighbours(node)
for i = 1,#neighbours do
local neighbour = neighbours[i]
if not neighbour._closed then
toClear[neighbour] = true
if not neighbour._opened then
neighbour._g = huge
neighbour._parent = nil
end
updateVertex(openList, node, neighbour, endNode, finder._heuristic)
end
end
--[[
printf('x:%d y:%d z:%d g:%d', node.x, node.y, node.z, node._g)
for i = 1,#neighbours do
local n = neighbours[i]
printf('x:%d y:%d z:%d f:%f g:%f h:%d', n.x, n.y, n.z, n._f, n._g, n.heading or -1)
end
--]]
end
return nil
end
end

51
sys/modules/opus/map.lua Normal file
View File

@@ -0,0 +1,51 @@
-- convience functions for tables with key/value pairs
local Util = require('opus.util')
local Map = { }
-- TODO: refactor
Map.merge = Util.merge
Map.shallowCopy = Util.shallowCopy
Map.find = Util.find
Map.filter = Util.filter
function Map.removeMatches(t, values)
local function matchAll(entry)
for k, v in pairs(values) do
if entry[k] ~= v then
return
end
end
return true
end
for _, key in pairs(Util.keys(t)) do
if matchAll(t[key]) then
t[key] = nil
end
end
end
-- remove table entries if passed function returns false
function Map.prune(t, fn)
for _,k in pairs(Util.keys(t)) do
local v = t[k]
if type(v) == 'table' then
t[k] = Map.prune(v, fn)
end
if not fn(t[k]) then
t[k] = nil
end
end
return t
end
function Map.size(list)
local length = 0
for _ in pairs(list) do
length = length + 1
end
return length
end
return Map

80
sys/modules/opus/nft.lua Normal file
View File

@@ -0,0 +1,80 @@
local Util = require('opus.util')
local NFT = { }
-- largely copied from http://www.computercraft.info/forums2/index.php?/topic/5029-145-npaintpro/
local tColourLookup = { }
for n = 1, 16 do
tColourLookup[string.byte("0123456789abcdef", n, n)] = 2 ^ (n - 1)
end
local function getColourOf(hex)
return tColourLookup[hex:byte()]
end
function NFT.parse(imageText)
local image = {
fg = { },
bg = { },
text = { },
}
local num = 1
local lines = Util.split(imageText)
while #lines[#lines] == 0 do
table.remove(lines, #lines)
end
for _,sLine in ipairs(lines) do
table.insert(image.fg, { })
table.insert(image.bg, { })
table.insert(image.text, { })
--As we're no longer 1-1, we keep track of what index to write to
local writeIndex = 1
--Tells us if we've hit a 30 or 31 (BG and FG respectively)- next char specifies the curr colour
local bgNext, fgNext = false, false
--The current background and foreground colours
local currBG, currFG = nil,nil
for i = 1, #sLine do
local nextChar = string.sub(sLine, i, i)
if nextChar:byte() == 30 then
bgNext = true
elseif nextChar:byte() == 31 then
fgNext = true
elseif bgNext then
currBG = getColourOf(nextChar)
bgNext = false
elseif fgNext then
currFG = getColourOf(nextChar)
fgNext = false
else
if nextChar ~= " " and currFG == nil then
currFG = _G.colors.white
end
image.bg[num][writeIndex] = currBG
image.fg[num][writeIndex] = currFG
image.text[num][writeIndex] = nextChar
writeIndex = writeIndex + 1
end
end
image.height = num
if not image.width or writeIndex - 1 > image.width then
image.width = writeIndex - 1
end
num = num+1
end
return image
end
function NFT.load(path)
local imageText = Util.readFile(path)
if not imageText then
error('Unable to read image file')
end
return NFT.parse(imageText)
end
return NFT

View File

@@ -0,0 +1,70 @@
local Util = require('opus.util')
local fs = _G.fs
local textutils = _G.textutils
local PACKAGE_DIR = 'packages'
local Packages = { }
function Packages:installed()
local list = { }
if fs.exists(PACKAGE_DIR) then
for _, dir in pairs(fs.list(PACKAGE_DIR)) do
local path = fs.combine(fs.combine(PACKAGE_DIR, dir), '.package')
list[dir] = Util.readTable(path)
end
end
return list
end
function Packages:list()
return Util.readTable('usr/config/packages') or { }
end
function Packages:isInstalled(package)
return self:installed()[package]
end
function Packages:downloadList()
local packages = {
[ 'develop-1.8' ] = 'https://pastebin.com/raw/WhEiNGZE',
[ 'master-1.8' ] = 'https://pastebin.com/raw/pexZpAxt',
}
if packages[_G.OPUS_BRANCH] then
Util.download(packages[_G.OPUS_BRANCH], 'usr/config/packages')
end
end
function Packages:downloadManifest(package)
local list = self:list()
local url = list and list[package]
if url then
local c = Util.httpGet(url)
if c then
c = textutils.unserialize(c)
if c then
c.repository = c.repository:gsub('{{OPUS_BRANCH}}', _G.OPUS_BRANCH)
return c
end
end
end
end
function Packages:getManifest(package)
local fname = 'packages/' .. package .. '/.package'
if fs.exists(fname) then
local c = Util.readTable(fname)
if c and c.repository then
c.repository = c.repository:gsub('{{OPUS_BRANCH}}', _G.OPUS_BRANCH)
return c
end
end
return self:downloadManifest(package)
end
return Packages

View File

@@ -0,0 +1,256 @@
local Grid = require('opus.jumper.grid')
local Pathfinder = require('opus.jumper.pathfinder')
local Point = require('opus.point')
local Util = require('opus.util')
local turtle = _G.turtle
local function addBlock(grid, b, dim)
if Point.inBox(b, dim) then
local node = grid:getNodeAt(b.x, b.y, b.z)
if node then
node.walkable = 1
end
end
end
-- map shrinks/grows depending upon blocks encountered
-- the map will encompass any blocks encountered, the turtle position, and the destination
local function mapDimensions(dest, blocks, boundingBox, dests)
local box = Point.makeBox(turtle.point, turtle.point)
Point.expandBox(box, dest)
for _,d in pairs(dests) do
Point.expandBox(box, d)
end
for _,b in pairs(blocks) do
Point.expandBox(box, b)
end
-- expand one block out in all directions
if boundingBox then
box.x = math.max(box.x - 1, boundingBox.x)
box.z = math.max(box.z - 1, boundingBox.z)
box.y = math.max(box.y - 1, boundingBox.y)
box.ex = math.min(box.ex + 1, boundingBox.ex)
box.ez = math.min(box.ez + 1, boundingBox.ez)
box.ey = math.min(box.ey + 1, boundingBox.ey)
else
box.x = box.x - 1
box.z = box.z - 1
box.y = box.y - 1
box.ex = box.ex + 1
box.ez = box.ez + 1
box.ey = box.ey + 1
end
return box
end
local function nodeToPoint(node)
return { x = node.x, y = node.y, z = node.z, heading = node.heading }
end
local function heuristic(n, node)
return Point.calculateMoves(node, n)
-- { x = node.x, y = node.y, z = node.z, heading = node.heading },
-- { x = n.x, y = n.y, z = n.z, heading = n.heading })
end
local function dimsAreEqual(d1, d2)
return d1.ex == d2.ex and
d1.ey == d2.ey and
d1.ez == d2.ez and
d1.x == d2.x and
d1.y == d2.y and
d1.z == d2.z
end
-- turtle sensor returns blocks in relation to the world - not turtle orientation
-- so cannot figure out block location unless we know our orientation in the world
-- really kinda dumb since it returns the coordinates as offsets of our location
-- instead of true coordinates
local function addSensorBlocks(blocks, sblocks)
for _,b in pairs(sblocks) do
if b.type ~= 'AIR' then
local pt = { x = turtle.point.x, y = turtle.point.y + b.y, z = turtle.point.z }
pt.x = pt.x - b.x
pt.z = pt.z - b.z -- this will only work if we were originally facing west
local found = false
for _,ob in pairs(blocks) do
if pt.x == ob.x and pt.y == ob.y and pt.z == ob.z then
found = true
break
end
end
if not found then
table.insert(blocks, pt)
end
end
end
end
local function selectDestination(pts, box, grid)
while #pts > 0 do
local pt = Point.closest(turtle.point, pts)
if box and not Point.inBox(pt, box) then
Util.removeByValue(pts, pt)
else
if grid:isWalkableAt(pt.x, pt.y, pt.z) then
return pt
end
Util.removeByValue(pts, pt)
end
end
end
local function updateCanvas(path)
local t = { }
for node in path:nodes() do
table.insert(t, { x = node.x, y = node.y, z = node.z })
end
os.queueEvent('canvas', {
type = 'canvas_path',
data = t,
})
end
local function pathTo(dest, options)
local blocks = options.blocks or turtle.getState().blocks or { }
local dests = options.dest or { dest } -- support alternative destinations
local box = options.box or turtle.getState().box
local lastDim
local grid
if box then
box = Point.normalizeBox(box)
end
-- Creates a pathfinder object
local finder = Pathfinder(heuristic)
while turtle.point.x ~= dest.x or turtle.point.z ~= dest.z or turtle.point.y ~= dest.y do
-- map expands as we encounter obstacles
local dim = mapDimensions(dest, blocks, box, dests)
-- reuse map if possible
if not lastDim or not dimsAreEqual(dim, lastDim) then
-- Creates a grid object
grid = Grid(dim)
finder:setGrid(grid)
lastDim = dim
end
for _,b in pairs(blocks) do
addBlock(grid, b, dim)
end
dest = selectDestination(dests, box, grid)
if not dest then
return false, 'failed to reach destination'
end
if turtle.point.x == dest.x and turtle.point.z == dest.z and turtle.point.y == dest.y then
break
end
-- Define start and goal locations coordinates
local startPt = turtle.point
-- Calculates the path, and its length
local path = finder:getPath(
startPt.x, startPt.y, startPt.z, turtle.point.heading,
dest.x, dest.y, dest.z, dest.heading)
if not path then
Util.removeByValue(dests, dest)
else
updateCanvas(path)
path:filter()
for node in path:nodes() do
local pt = nodeToPoint(node)
if turtle.isAborted() then
return false, 'aborted'
end
--if this is the next to last node
--and we are traveling up or down, then the
--heading for this node should be the heading of the last node
--or, maybe..
--if last node is up or down (or either?)
-- use single turn method so the turtle doesn't turn around
-- when encountering obstacles
--if not turtle.gotoSingleTurn(pt.x, pt.y, pt.z, pt.heading) then
pt.heading = nil
if not turtle.go(pt) then
local bpt = Point.nearestTo(turtle.point, pt)
if turtle.getFuelLevel() == 0 then
return false, 'Out of fuel'
end
table.insert(blocks, bpt)
os.queueEvent('canvas', {
type = 'canvas_barrier',
data = { bpt },
})
-- really need to check if the block we ran into was a turtle.
-- if so, this block should be temporary (1-2 secs)
--local side = turtle.getSide(turtle.point, pt)
--if turtle.isTurtleAtSide(side) then
-- pt.timestamp = os.clock() + ?
--end
-- if dim has not changed, then need to update grid with
-- walkable = nil (after time has elapsed)
--if device.turtlesensorenvironment then
-- addSensorBlocks(blocks, device.turtlesensorenvironment.sonicScan())
--end
break
end
end
end
end
if dest.heading then
turtle.setHeading(dest.heading)
end
return dest
end
return {
pathfind = function(dest, options)
options = options or { }
--if not options.blocks and turtle.gotoPoint(dest) then
-- return dest
--end
return pathTo(dest, options)
end,
-- set a global bounding box
-- box can be overridden by passing box in pathfind options
setBox = function(box)
turtle.getState().box = box
end,
setBlocks = function(blocks)
turtle.getState().blocks = blocks
end,
addBlock = function(block)
if turtle.getState().blocks then
table.insert(turtle.getState().blocks, block)
end
end,
reset = function()
turtle.getState().box = nil
turtle.getState().blocks = nil
end,
}

View File

@@ -0,0 +1,126 @@
local Util = require('opus.util')
local Peripheral = Util.shallowCopy(_G.peripheral)
function Peripheral.getList()
if _G.device then
return _G.device
end
local deviceList = { }
for _,side in pairs(Peripheral.getNames()) do
Peripheral.addDevice(deviceList, side)
end
return deviceList
end
function Peripheral.addDevice(deviceList, side)
local name = side
pcall(function()
local ptype = Peripheral.getType(side)
local dev = Peripheral.wrap(side)
if not ptype or not dev then
return
end
if ptype == 'modem' then
if not Peripheral.call(name, 'isWireless') then
-- ptype = 'wireless_modem'
-- else
ptype = 'wired_modem'
if dev.isAccessPoint then
-- avoid open computer relays being registered
-- as 'wired_modem'
ptype = dev.getMetadata().name or 'wired_modem'
end
end
end
local sides = {
front = true,
back = true,
top = true,
bottom = true,
left = true,
right = true
}
if sides[name] then
local i = 1
local uniqueName = ptype
while deviceList[uniqueName] do
uniqueName = ptype .. '_' .. i
i = i + 1
end
name = uniqueName
end
-- this can randomly fail
if not deviceList[name] then
deviceList[name] = dev
if deviceList[name] then
Util.merge(deviceList[name], {
name = name,
type = ptype,
side = side,
})
end
end
end)
return deviceList[name]
end
function Peripheral.getBySide(side)
return Util.find(Peripheral.getList(), 'side', side)
end
function Peripheral.getByType(typeName)
return Util.find(Peripheral.getList(), 'type', typeName)
end
function Peripheral.getByMethod(method)
for _,p in pairs(Peripheral.getList()) do
if p[method] then
return p
end
end
end
-- match any of the passed arguments
function Peripheral.get(args)
if type(args) == 'string' then
args = { type = args }
end
if args.name then
return _G.device[args.name]
end
if args.type then
local p = Peripheral.getByType(args.type)
if p then
return p
end
end
if args.method then
local p = Peripheral.getByMethod(args.method)
if p then
return p
end
end
if args.side then
local p = Peripheral.getBySide(args.side)
if p then
return p
end
end
end
return Peripheral

345
sys/modules/opus/point.lua Normal file
View File

@@ -0,0 +1,345 @@
local Util = require('opus.util')
local Point = { }
Point.directions = {
[ 0 ] = { xd = 1, zd = 0, yd = 0, heading = 0, direction = 'east' },
[ 1 ] = { xd = 0, zd = 1, yd = 0, heading = 1, direction = 'south' },
[ 2 ] = { xd = -1, zd = 0, yd = 0, heading = 2, direction = 'west' },
[ 3 ] = { xd = 0, zd = -1, yd = 0, heading = 3, direction = 'north' },
[ 4 ] = { xd = 0, zd = 0, yd = 1, heading = 4, direction = 'up' },
[ 5 ] = { xd = 0, zd = 0, yd = -1, heading = 5, direction = 'down' },
}
Point.facings = {
[ 0 ] = Point.directions[0],
[ 1 ] = Point.directions[1],
[ 2 ] = Point.directions[2],
[ 3 ] = Point.directions[3],
east = Point.directions[0],
south = Point.directions[1],
west = Point.directions[2],
north = Point.directions[3],
}
Point.headings = {
[ 0 ] = Point.directions[0],
[ 1 ] = Point.directions[1],
[ 2 ] = Point.directions[2],
[ 3 ] = Point.directions[3],
[ 4 ] = Point.directions[4],
[ 5 ] = Point.directions[5],
east = Point.directions[0],
south = Point.directions[1],
west = Point.directions[2],
north = Point.directions[3],
up = Point.directions[4],
down = Point.directions[5],
}
Point.EAST = 0
Point.SOUTH = 1
Point.WEST = 2
Point.NORTH = 3
Point.UP = 4
Point.DOWN = 5
function Point.copy(pt)
return { x = pt.x, y = pt.y, z = pt.z }
end
function Point.round(pt)
pt.x = Util.round(pt.x)
pt.y = Util.round(pt.y)
pt.z = Util.round(pt.z)
return pt
end
function Point.same(pta, ptb)
return pta.x == ptb.x and
pta.y == ptb.y and
pta.z == ptb.z
end
function Point.above(pt)
return { x = pt.x, y = pt.y + 1, z = pt.z, heading = pt.heading }
end
function Point.below(pt)
return { x = pt.x, y = pt.y - 1, z = pt.z, heading = pt.heading }
end
function Point.subtract(a, b)
a.x = a.x - b.x
a.y = a.y - b.y
a.z = a.z - b.z
end
-- Euclidian distance
function Point.distance(a, b)
return math.sqrt(
math.pow(a.x - b.x, 2) +
math.pow(a.y - b.y, 2) +
math.pow(a.z - b.z, 2))
end
-- turtle distance (manhattan)
function Point.turtleDistance(a, b)
if a.y and b.y then
return math.abs(a.x - b.x) +
math.abs(a.y - b.y) +
math.abs(a.z - b.z)
else
return math.abs(a.x - b.x) +
math.abs(a.z - b.z)
end
end
function Point.calculateTurns(ih, oh)
if ih == oh then
return 0
end
if (ih % 2) == (oh % 2) then
return 2
end
return 1
end
function Point.calculateHeading(pta, ptb)
local heading
local xd, zd = pta.x - ptb.x, pta.z - ptb.z
if (pta.heading % 2) == 0 and zd ~= 0 then
heading = zd < 0 and 1 or 3
elseif (pta.heading % 2) == 1 and xd ~= 0 then
heading = xd < 0 and 0 or 2
elseif pta.heading == 0 and xd > 0 then
heading = 2
elseif pta.heading == 2 and xd < 0 then
heading = 0
elseif pta.heading == 1 and zd > 0 then
heading = 3
elseif pta.heading == 3 and zd < 0 then
heading = 1
end
return heading or pta.heading
end
-- Calculate distance to location including turns
-- also returns the resulting heading
function Point.calculateMoves(pta, ptb, distance)
local heading = pta.heading
local moves = distance or Point.turtleDistance(pta, ptb)
local weighted = moves
if (pta.heading % 2) == 0 and pta.z ~= ptb.z then
moves = moves + 1
weighted = weighted + .9
if ptb.heading and (ptb.heading % 2 == 1) then
heading = ptb.heading
elseif ptb.z > pta.z then
heading = 1
else
heading = 3
end
elseif (pta.heading % 2) == 1 and pta.x ~= ptb.x then
moves = moves + 1
weighted = weighted + .9
if ptb.heading and (ptb.heading % 2 == 0) then
heading = ptb.heading
elseif ptb.x > pta.x then
heading = 0
else
heading = 2
end
end
if not ptb.heading then
return moves, heading, weighted
end
-- need to know if we are in digging mode
-- if so, we need to face blocks -- need a no-backwards flag
-- calc turns as slightly less than moves
-- local weighted = moves
if heading ~= ptb.heading then
local turns = Point.calculateTurns(heading, ptb.heading)
moves = moves + turns
local wturns = { [0] = 0, [1] = .9, [2] = 1.8 }
weighted = weighted + wturns[turns]
heading = ptb.heading
end
return moves, heading, weighted
end
-- given a set of points, find the one taking the least moves
function Point.closest(reference, pts)
if #pts == 1 then
return pts[1]
end
local lm, lpt = math.huge
for _,pt in pairs(pts) do
local distance = Point.turtleDistance(reference, pt)
if not reference.heading then
if distance < lm then
lpt = pt
lm = distance
end
elseif distance < lm then
local _, _, m = Point.calculateMoves(reference, pt, distance)
if m < lm then
lpt = pt
lm = m
end
end
end
return lpt
end
function Point.eachClosest(spt, ipts, fn)
if not ipts then error('Point.eachClosest: invalid points', 2) end
local pts = Util.shallowCopy(ipts)
while #pts > 0 do
local pt = Point.closest(spt, pts)
local r = fn(pt)
if r then
return r
end
Util.removeByValue(pts, pt)
end
end
function Point.iterateClosest(spt, ipts)
local pts = Util.shallowCopy(ipts)
return function()
local pt = Point.closest(spt, pts)
if pt then
Util.removeByValue(pts, pt)
return pt
end
end
end
function Point.adjacentPoints(pt)
local pts = { }
for i = 0, 5 do
local hi = Point.headings[i]
table.insert(pts, { x = pt.x + hi.xd, y = pt.y + hi.yd, z = pt.z + hi.zd })
end
return pts
end
-- get the point nearest A that is in the direction of B
function Point.nearestTo(pta, ptb)
local heading
if pta.x < ptb.x then
heading = 0
elseif pta.z < ptb.z then
heading = 1
elseif pta.x > ptb.x then
heading = 2
elseif pta.z > ptb.z then
heading = 3
elseif pta.y < ptb.y then
heading = 4
elseif pta.y > ptb.y then
heading = 5
end
if heading then
return {
x = pta.x + Point.headings[heading].xd,
y = pta.y + Point.headings[heading].yd,
z = pta.z + Point.headings[heading].zd,
}
end
return pta -- error ?
end
function Point.rotate(pt, facing)
local x, z = pt.x, pt.z
if facing == 1 then
pt.x = z
pt.z = -x
elseif facing == 2 then
pt.x = -x
pt.z = -z
elseif facing == 3 then
pt.x = -z
pt.z = x
end
end
function Point.makeBox(pt1, pt2)
return {
x = pt1.x,
y = pt1.y,
z = pt1.z,
ex = pt2.x,
ey = pt2.y,
ez = pt2.z,
}
end
-- expand box to include point
function Point.expandBox(box, pt)
if pt.x < box.x then
box.x = pt.x
elseif pt.x > box.ex then
box.ex = pt.x
end
if pt.y < box.y then
box.y = pt.y
elseif pt.y > box.ey then
box.ey = pt.y
end
if pt.z < box.z then
box.z = pt.z
elseif pt.z > box.ez then
box.ez = pt.z
end
end
function Point.normalizeBox(box)
return {
x = math.min(box.x, box.ex),
y = math.min(box.y, box.ey),
z = math.min(box.z, box.ez),
ex = math.max(box.x, box.ex),
ey = math.max(box.y, box.ey),
ez = math.max(box.z, box.ez),
}
end
function Point.inBox(pt, box)
return pt.x >= box.x and
pt.y >= box.y and
pt.z >= box.z and
pt.x <= box.ex and
pt.y <= box.ey and
pt.z <= box.ez
end
function Point.closestPointInBox(pt, box)
local cpt = {
x = math.abs(pt.x - box.x) < math.abs(pt.x - box.ex) and box.x or box.ex,
y = math.abs(pt.y - box.y) < math.abs(pt.y - box.ey) and box.y or box.ey,
z = math.abs(pt.z - box.z) < math.abs(pt.z - box.ez) and box.z or box.ez,
}
cpt.x = pt.x > box.x and pt.x < box.ex and pt.x or cpt.x
cpt.y = pt.y > box.y and pt.y < box.ey and pt.y or cpt.y
cpt.z = pt.z > box.z and pt.z < box.ez and pt.z or cpt.z
return cpt
end
return Point

View File

@@ -0,0 +1,43 @@
local Config = require('opus.config')
local Util = require('opus.util')
local ECC = require('opus.crypto.ecc')
local Security = { }
function Security.verifyPassword(password)
local current = Security.getPassword()
return current and password == current
end
function Security.hasPassword()
return not not Security.getPassword()
end
function Security.getSecretKey()
local config = Config.load('os')
if not config.secretKey then
config.secretKey = ""
for _ = 1, 32 do
config.secretKey = config.secretKey .. ("%02x"):format(math.random(0, 0xFF))
end
Config.update('os', config)
end
return Util.hexToByteArray(config.secretKey)
end
function Security.getPublicKey()
local secretKey = Security.getSecretKey()
return ECC.publicKey(secretKey)
end
function Security.updatePassword(password)
local config = Config.load('os')
config.password = password
Config.update('os', config)
end
function Security.getPassword()
return Config.load('os').password
end
return Security

234
sys/modules/opus/socket.lua Normal file
View File

@@ -0,0 +1,234 @@
local Crypto = require('opus.crypto.chacha20')
local Security = require('opus.security')
local Util = require('opus.util')
local device = _G.device
local os = _G.os
local socketClass = { }
function socketClass:read(timeout)
local data, distance = _G.transport.read(self)
if data then
return data, distance
end
if not self.connected then
return
end
local timerId = os.startTimer(timeout or 5)
while true do
local e, id = os.pullEvent()
if e == 'transport_' .. self.uid then
data, distance = _G.transport.read(self)
if data then
os.cancelTimer(timerId)
return data, distance
end
if not self.connected then
break
end
elseif e == 'timer' and id == timerId then
if timeout or not self.connected then
break
end
timerId = os.startTimer(5)
self:ping()
end
end
end
function socketClass:write(data)
if self.connected then
_G.transport.write(self, {
type = 'DATA',
seq = self.wseq,
data = data,
})
return true
end
end
function socketClass:ping()
if self.connected then
_G.transport.ping(self)
return true
end
end
function socketClass:close()
if self.connected then
self.transmit(self.dport, self.dhost, {
type = 'DISC',
})
self.connected = false
end
device.wireless_modem.close(self.sport)
_G.transport.close(self)
end
local Socket = { }
local function loopback(port, sport, msg)
os.queueEvent('modem_message', 'loopback', port, sport, msg, 0)
end
local function newSocket(isLoopback)
for _ = 16384, 32767 do
local i = math.random(16384, 32767)
if not device.wireless_modem.isOpen(i) then
local socket = {
shost = os.getComputerID(),
sport = i,
transmit = device.wireless_modem.transmit,
wseq = math.random(100, 100000),
rseq = math.random(100, 100000),
timers = { },
messages = { },
}
setmetatable(socket, { __index = socketClass })
device.wireless_modem.open(socket.sport)
if isLoopback then
socket.transmit = loopback
end
return socket
end
end
error('No ports available')
end
function Socket.connect(host, port)
if not device.wireless_modem then
return false, 'Wireless modem not found', 'NOMODEM'
end
local socket = newSocket(host == os.getComputerID())
socket.dhost = tonumber(host)
socket.transmit(port, socket.sport, {
type = 'OPEN',
shost = socket.shost,
dhost = socket.dhost,
t = Crypto.encrypt({ ts = os.time(), seq = socket.seq, nts = os.epoch('utc') }, Security.getPublicKey()),
rseq = socket.wseq,
wseq = socket.rseq,
})
local timerId = os.startTimer(3)
repeat
local e, id, sport, dport, msg = os.pullEvent()
if e == 'modem_message' and
sport == socket.sport and
type(msg) == 'table' and
msg.dhost == socket.shost then
os.cancelTimer(timerId)
if msg.type == 'CONN' then
socket.dport = dport
socket.connected = true
-- Logger.log('socket', 'connection established to %d %d->%d',
-- host, socket.sport, socket.dport)
_G.transport.open(socket)
return socket
elseif msg.type == 'NOPASS' then
socket:close()
return false, 'Password not set on target', 'NOPASS'
elseif msg.type == 'REJE' then
socket:close()
return false, 'Trust not established', 'NOTRUST'
end
end
until e == 'timer' and id == timerId
socket:close()
return false, 'Connection timed out', 'TIMEOUT'
end
local function trusted(msg, port)
if port == 19 or msg.shost == os.getComputerID() then
-- no auth for trust server or loopback
return true
end
if not Security.hasPassword() then
-- no password has been set on this computer
--return true
end
local trustList = Util.readTable('usr/.known_hosts') or { }
local pubKey = trustList[msg.shost]
if pubKey and msg.t then
pubKey = Util.hexToByteArray(pubKey)
local data = Crypto.decrypt(msg.t, pubKey)
if data and data.nts then -- upgraded security
return data.nts and tonumber(data.nts) and math.abs(os.epoch('utc') - data.nts) < 1024
end
--local sharedKey = modexp(pubKey, exchange.secretKey, public.primeMod)
return data and data.ts and tonumber(data.ts) and math.abs(os.time() - data.ts) < 24
end
end
function Socket.server(port)
device.wireless_modem.open(port)
-- Logger.log('socket', 'Waiting for connections on port ' .. port)
while true do
local _, _, sport, dport, msg = os.pullEvent('modem_message')
if sport == port and
msg and
type(msg) == 'table' and
msg.dhost == os.getComputerID() and
msg.type == 'OPEN' then
local socket = newSocket(msg.shost == os.getComputerID())
socket.dport = dport
socket.dhost = msg.shost
socket.wseq = msg.wseq
socket.rseq = msg.rseq
if not Security.hasPassword() then
socket.transmit(socket.dport, socket.sport, {
type = 'NOPASS',
dhost = socket.dhost,
shost = socket.shost,
})
socket:close()
elseif trusted(msg, port) then
socket.connected = true
socket.transmit(socket.dport, socket.sport, {
type = 'CONN',
dhost = socket.dhost,
shost = socket.shost,
})
-- Logger.log('socket', 'Connection established %d->%d', socket.sport, socket.dport)
_G.transport.open(socket)
return socket
else
socket.transmit(socket.dport, socket.sport, {
type = 'REJE',
dhost = socket.dhost,
shost = socket.shost,
})
socket:close()
end
end
end
end
return Socket

View File

@@ -0,0 +1,17 @@
local peripheral = _G.peripheral
local Sound = {
_volume = 1,
}
function Sound.play(sound, vol)
peripheral.find('speaker', function(_, s)
s.playSound('minecraft:' .. sound, vol or Sound._volume)
end)
end
function Sound.setVolume(volume)
Sound._volume = math.max(0, math.min(volume, 1))
end
return Sound

61
sys/modules/opus/sync.lua Normal file
View File

@@ -0,0 +1,61 @@
local Sync = {
syncLocks = { }
}
local os = _G.os
function Sync.sync(obj, fn)
local key = tostring(obj)
if Sync.syncLocks[key] then
local cos = tostring(coroutine.running())
table.insert(Sync.syncLocks[key], cos)
repeat
local _, co = os.pullEvent('sync_lock')
until co == cos
else
Sync.syncLocks[key] = { }
end
local s, m = pcall(fn)
local co = table.remove(Sync.syncLocks[key], 1)
if co then
os.queueEvent('sync_lock', co)
else
Sync.syncLocks[key] = nil
end
if not s then
error(m)
end
end
function Sync.lock(obj)
local key = tostring(obj)
if Sync.syncLocks[key] then
local cos = tostring(coroutine.running())
table.insert(Sync.syncLocks[key], cos)
repeat
local _, co = os.pullEvent('sync_lock')
until co == cos
else
Sync.syncLocks[key] = { }
end
end
function Sync.release(obj)
local key = tostring(obj)
if not Sync.syncLocks[key] then
error('Sync.release: Lock was not obtained', 2)
end
local co = table.remove(Sync.syncLocks[key], 1)
if co then
os.queueEvent('sync_lock', co)
else
Sync.syncLocks[key] = nil
end
end
function Sync.isLocked(obj)
local key = tostring(obj)
return not not Sync.syncLocks[key]
end
return Sync

View File

@@ -0,0 +1,362 @@
local Canvas = require('opus.ui.canvas')
local colors = _G.colors
local term = _G.term
local _gsub = string.gsub
local Terminal = { }
local mapColorToGray = {
[ colors.white ] = colors.white,
[ colors.orange ] = colors.lightGray,
[ colors.magenta ] = colors.lightGray,
[ colors.lightBlue ] = colors.lightGray,
[ colors.yellow ] = colors.lightGray,
[ colors.lime ] = colors.lightGray,
[ colors.pink ] = colors.lightGray,
[ colors.gray ] = colors.gray,
[ colors.lightGray ] = colors.lightGray,
[ colors.cyan ] = colors.lightGray,
[ colors.purple ] = colors.gray,
[ colors.blue ] = colors.gray,
[ colors.brown ] = colors.gray,
[ colors.green ] = colors.lightGray,
[ colors.red ] = colors.gray,
[ colors.black ] = colors.black,
}
-- Replacement for window api with scrolling and buffering
function Terminal.window(parent, sx, sy, w, h, isVisible)
isVisible = isVisible ~= false
if not w or not h then
w, h = parent.getSize()
end
local win = { }
local maxScroll = 100
local cx, cy = 1, 1
local blink = false
local bg, fg = parent.getBackgroundColor(), parent.getTextColor()
local canvas = Canvas({
x = sx,
y = sy,
width = w,
height = h,
isColor = parent.isColor(),
offy = 0,
})
win.canvas = canvas
local function update()
if isVisible then
canvas:render(parent)
win.setCursorPos(cx, cy)
end
end
local function scrollTo(y)
y = math.max(0, y)
y = math.min(#canvas.lines - canvas.height, y)
if y ~= canvas.offy then
canvas.offy = y
canvas:dirty()
update()
end
end
function win.write(str)
str = tostring(str) or ''
canvas:write(cx, cy + canvas.offy, str, bg, fg)
win.setCursorPos(cx + #str, cy)
update()
end
function win.blit(str, fg, bg)
canvas:blit(cx, cy + canvas.offy, str, bg, fg)
win.setCursorPos(cx + #str, cy)
update()
end
function win.clear()
canvas.offy = 0
for i = #canvas.lines, canvas.height + 1, -1 do
canvas.lines[i] = nil
end
canvas:clear(bg, fg)
update()
end
function win.clearLine()
canvas:clearLine(cy + canvas.offy, bg, fg)
win.setCursorPos(cx, cy)
update()
end
function win.getCursorPos()
return cx, cy
end
function win.setCursorPos(x, y)
cx, cy = math.floor(x), math.floor(y)
if isVisible then
parent.setCursorPos(cx + canvas.x - 1, cy + canvas.y - 1)
end
end
function win.setCursorBlink(b)
blink = b
if isVisible then
parent.setCursorBlink(b)
end
end
function win.isColor()
return canvas.isColor
end
win.isColour = win.isColor
function win.setTextColor(c)
fg = c
end
win.setTextColour = win.setTextColor
function win.getPaletteColor(n)
if parent.getPaletteColor then
return parent.getPaletteColor(n)
end
return 0, 0, 0
end
win.getPaletteColour = win.getPaletteColor
function win.setPaletteColor(n, r, g, b)
if parent.setPaletteColor then
return parent.setPaletteColor(n, r, g, b)
end
end
win.setPaletteColour = win.setPaletteColor
function win.setBackgroundColor(c)
bg = c
end
win.setBackgroundColour = win.setBackgroundColor
function win.getSize()
return canvas.width, canvas.height
end
function win.scroll(n)
n = n or 1
if n > 0 then
local lines = #canvas.lines
for i = 1, n do
canvas.lines[lines + i] = { }
canvas:clearLine(lines + i, bg, fg)
end
while #canvas.lines > maxScroll do
table.remove(canvas.lines, 1)
end
scrollTo(#canvas.lines)
canvas:dirty()
update()
end
end
function win.getTextColor()
return fg
end
win.getTextColour = win.getTextColor
function win.getBackgroundColor()
return bg
end
win.getBackgroundColour = win.getBackgroundColor
function win.setVisible(visible)
if visible ~= isVisible then
isVisible = visible
if isVisible then
canvas:dirty()
update()
end
end
end
function win.redraw()
if isVisible then
canvas:dirty()
update()
end
end
function win.restoreCursor()
if isVisible then
win.setCursorPos(cx, cy)
win.setTextColor(fg)
win.setCursorBlink(blink)
end
end
function win.getPosition()
return canvas.x, canvas.y
end
function win.reposition(x, y, width, height)
canvas.x, canvas.y = x, y
canvas:resize(width or canvas.width, height or canvas.height)
end
--[[ Additional methods ]]--
function win.scrollDown()
scrollTo(canvas.offy + 1)
end
function win.scrollUp()
scrollTo(canvas.offy - 1)
end
function win.scrollTop()
scrollTo(0)
end
function win.scrollBottom()
scrollTo(#canvas.lines)
end
function win.setMaxScroll(ms)
maxScroll = ms
end
function win.getCanvas()
return canvas
end
function win.getParent()
return parent
end
canvas:clear()
return win
end
-- get windows contents
function Terminal.getContents(win, parent)
local oblit, oscp = parent.blit, parent.setCursorPos
local lines = { }
parent.blit = function(text, fg, bg)
lines[#lines + 1] = {
text = text,
fg = fg,
bg = bg,
}
end
parent.setCursorPos = function() end
win.setVisible(true)
win.redraw()
parent.blit = oblit
parent.setCursorPos = oscp
return lines
end
function Terminal.colorToGrayscale(c)
return mapColorToGray[c]
end
function Terminal.toGrayscale(ct)
local methods = { 'setBackgroundColor', 'setBackgroundColour',
'setTextColor', 'setTextColour' }
for _,v in pairs(methods) do
local fn = ct[v]
ct[v] = function(c)
fn(mapColorToGray[c])
end
end
local bcolors = {
[ '1' ] = '8',
[ '2' ] = '8',
[ '3' ] = '8',
[ '4' ] = '8',
[ '5' ] = '8',
[ '6' ] = '8',
[ '9' ] = '8',
[ 'a' ] = '7',
[ 'b' ] = '7',
[ 'c' ] = '7',
[ 'd' ] = '8',
[ 'e' ] = '7',
}
local function translate(s)
if s then
s = _gsub(s, "%w", bcolors)
end
return s
end
local fn = ct.blit
ct.blit = function(text, fg, bg)
fn(text, translate(fg), translate(bg))
end
end
function Terminal.getNullTerm(ct)
local nt = Terminal.copy(ct)
local methods = { 'blit', 'clear', 'clearLine', 'scroll',
'setCursorBlink', 'setCursorPos', 'write' }
for _,v in pairs(methods) do
nt[v] = function() end
end
return nt
end
function Terminal.copy(it, ot)
ot = ot or { }
for k,v in pairs(it) do
if type(v) == 'function' then
ot[k] = v
end
end
return ot
end
function Terminal.mirror(ct, dt)
local t = { }
for k,f in pairs(ct) do
t[k] = function(...)
local ret = { f(...) }
if dt[k] then
dt[k](...)
end
return table.unpack(ret)
end
end
return t
end
function Terminal.readPassword(prompt)
if prompt then
term.write(prompt)
end
local fn = term.current().write
term.current().write = function() end
local s
pcall(function() s = _G.read(prompt) end)
term.current().write = fn
if s == '' then
return
end
return s
end
return Terminal

110
sys/modules/opus/trace.lua Normal file
View File

@@ -0,0 +1,110 @@
-- stack trace by SquidDev (MIT License)
-- https://raw.githubusercontent.com/SquidDev-CC/mbs/master/lib/stack_trace.lua
local type = type
local debug_traceback = type(debug) == "table" and type(debug.traceback) == "function" and debug.traceback
local function traceback(x)
-- Attempt to detect error() and error("xyz", 0).
-- This probably means they're erroring the program intentionally and so we
-- shouldn't display anything.
if x == nil or (type(x) == "string" and not x:find(":%d+:")) then
return x
end
if debug_traceback then
-- The parens are important, as they prevent a tail call occuring, meaning
-- the stack level is preserved. This ensures the code behaves identically
-- on LuaJ and PUC Lua.
return (debug_traceback(tostring(x), 2))
else
local level = 3
local out = { tostring(x), "stack traceback:" }
while true do
local _, msg = pcall(error, "", level)
if msg == "" then break end
out[#out + 1] = " " .. msg
level = level + 1
end
return table.concat(out, "\n")
end
end
local function trim_traceback(target, marker)
local ttarget, tmarker = {}, {}
for line in target:gmatch("([^\n]*)\n?") do ttarget[#ttarget + 1] = line end
for line in marker:gmatch("([^\n]*)\n?") do tmarker[#tmarker + 1] = line end
-- Trim identical suffixes
local t_len, m_len = #ttarget, #tmarker
while t_len >= 3 and ttarget[t_len] == tmarker[m_len] do
table.remove(ttarget, t_len)
t_len, m_len = t_len - 1, m_len - 1
end
-- Trim elements from this file and xpcall invocations
while t_len >= 1 and ttarget[t_len]:find("^\tstack_trace%.lua:%d+:") or
ttarget[t_len] == "\t[C]: in function 'xpcall'" or ttarget[t_len] == " xpcall: " do
table.remove(ttarget, t_len)
t_len = t_len - 1
end
ttarget[#ttarget] = nil -- remove 2 calls added by the added xpcall
ttarget[#ttarget] = nil
return ttarget
end
--- Run a function with
return function (fn, ...)
-- So this is rather grim: we need to get the full traceback and current one and remove
-- the common prefix
local trace
local args = { ... }
-- xpcall in Lua 5.1 does not accept parameters
-- which is not ideal
local res = table.pack(xpcall(function()
return fn(table.unpack(args))
end, traceback))
if not res[1] then
trace = traceback("trace.lua:1:")
end
local ok, err = res[1], res[2]
if not ok and err ~= nil then
trace = trim_traceback(err, trace)
-- Find the position where the stack traceback actually starts
local trace_starts
for i = #trace, 1, -1 do
if trace[i] == "stack traceback:" then trace_starts = i; break end
end
for _, line in pairs(trace) do
_G._syslog(line)
end
-- If this traceback is more than 15 elements long, keep the first 9, last 5
-- and put an ellipsis between the rest
local max = 10
if trace_starts and #trace - trace_starts > max then
local keep_starts = trace_starts + 7
for i = #trace - trace_starts - max, 0, -1 do
table.remove(trace, keep_starts + i)
end
table.insert(trace, keep_starts, " ...")
end
for k, line in pairs(trace) do
trace[k] = line:gsub("in function", " in")
end
return false, table.remove(trace, 1), table.concat(trace, "\n")
end
return table.unpack(res, 1, res.n)
end

1205
sys/modules/opus/ui.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,414 @@
local class = require('opus.class')
local Region = require('opus.ui.region')
local Util = require('opus.util')
local _rep = string.rep
local _sub = string.sub
local _gsub = string.gsub
local colors = _G.colors
local Canvas = class()
Canvas.colorPalette = { }
Canvas.darkPalette = { }
Canvas.grayscalePalette = { }
for n = 1, 16 do
Canvas.colorPalette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n)
Canvas.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n)
Canvas.darkPalette[2 ^ (n - 1)] = _sub("8777777f77fff77f", n, n)
end
--[[
A canvas can have more lines than canvas.height in order to scroll
]]
function Canvas:init(args)
self.x = 1
self.y = 1
self.layers = { }
Util.merge(self, args)
self.ex = self.x + self.width - 1
self.ey = self.y + self.height - 1
if not self.palette then
if self.isColor then
self.palette = Canvas.colorPalette
else
self.palette = Canvas.grayscalePalette
end
end
self.lines = { }
for i = 1, self.height do
self.lines[i] = { }
end
end
function Canvas:move(x, y)
self.x, self.y = x, y
self.ex = self.x + self.width - 1
self.ey = self.y + self.height - 1
end
function Canvas:resize(w, h)
for i = #self.lines, h do
self.lines[i] = { }
self:clearLine(i)
end
while #self.lines > h do
table.remove(self.lines, #self.lines)
end
if w < self.width then
for i = 1, h do
self.lines[i].text = _sub(self.lines[i].text, 1, w)
self.lines[i].fg = _sub(self.lines[i].fg, 1, w)
self.lines[i].bg = _sub(self.lines[i].bg, 1, w)
end
elseif w > self.width then
local d = w - self.width
local text = _rep(' ', d)
local fg = _rep(self.palette[self.fg or colors.white], d)
local bg = _rep(self.palette[self.bg or colors.black], d)
for i = 1, h do
self.lines[i].text = self.lines[i].text .. text
self.lines[i].fg = self.lines[i].fg .. fg
self.lines[i].bg = self.lines[i].bg .. bg
end
end
self.ex = self.x + w - 1
self.ey = self.y + h - 1
self.width = w
self.height = h
end
function Canvas:copy()
local b = Canvas({
x = self.x,
y = self.y,
width = self.width,
height = self.height,
isColor = self.isColor,
})
for i = 1, #self.lines do
b.lines[i].text = self.lines[i].text
b.lines[i].fg = self.lines[i].fg
b.lines[i].bg = self.lines[i].bg
end
return b
end
function Canvas:addLayer(layer)
local canvas = Canvas({
x = layer.x,
y = layer.y,
width = layer.width,
height = layer.height,
isColor = self.isColor,
})
canvas.parent = self
table.insert(self.layers, canvas)
return canvas
end
function Canvas:removeLayer()
for k, layer in pairs(self.parent.layers) do
if layer == self then
self:setVisible(false)
table.remove(self.parent.layers, k)
break
end
end
end
function Canvas:setVisible(visible)
self.visible = visible
if not visible and self.parent then
self.parent:dirty()
-- TODO: set parent's lines to dirty for each line in self
end
end
-- Push a layer to the top
function Canvas:raise()
if self.parent then
local layers = self.parent.layers or { }
for k, v in pairs(layers) do
if v == self then
table.insert(layers, table.remove(layers, k))
break
end
end
end
end
function Canvas:write(x, y, text, bg, fg)
if bg then
bg = _rep(self.palette[bg], #text)
end
if fg then
fg = _rep(self.palette[fg], #text)
end
self:blit(x, y, text, bg, fg)
end
function Canvas:blit(x, y, text, bg, fg)
if y > 0 and y <= #self.lines and x <= self.width then
local width = #text
-- fix ffs
if x < 1 then
text = _sub(text, 2 - x)
if bg then
bg = _sub(bg, 2 - x)
end
if fg then
fg = _sub(fg, 2 - x)
end
width = width + x - 1
x = 1
end
if x + width - 1 > self.width then
text = _sub(text, 1, self.width - x + 1)
if bg then
bg = _sub(bg, 1, self.width - x + 1)
end
if fg then
fg = _sub(fg, 1, self.width - x + 1)
end
width = #text
end
if width > 0 then
local function replace(sstr, pos, rstr)
if pos == 1 and width == self.width then
return rstr
elseif pos == 1 then
return rstr .. _sub(sstr, pos+width)
elseif pos + width > self.width then
return _sub(sstr, 1, pos-1) .. rstr
end
return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width)
end
local line = self.lines[y]
if line then
line.dirty = true
line.text = replace(line.text, x, text, width)
if fg then
line.fg = replace(line.fg, x, fg, width)
end
if bg then
line.bg = replace(line.bg, x, bg, width)
end
end
end
end
end
function Canvas:writeLine(y, text, fg, bg)
if y > 0 and y <= #self.lines then
self.lines[y].dirty = true
self.lines[y].text = text
self.lines[y].fg = fg
self.lines[y].bg = bg
end
end
function Canvas:clearLine(y, bg, fg)
fg = _rep(self.palette[fg or colors.white], self.width)
bg = _rep(self.palette[bg or colors.black], self.width)
self:writeLine(y, _rep(' ', self.width), fg, bg)
end
function Canvas:clear(bg, fg)
local text = _rep(' ', self.width)
fg = _rep(self.palette[fg or colors.white], self.width)
bg = _rep(self.palette[bg or colors.black], self.width)
for i = 1, #self.lines do
self:writeLine(i, text, fg, bg)
end
end
function Canvas:isDirty()
for i = 1, #self.lines do
if self.lines[i].dirty then
return true
end
end
end
function Canvas:dirty()
for i = 1, #self.lines do
self.lines[i].dirty = true
end
if self.layers then
for _, canvas in pairs(self.layers) do
canvas:dirty()
end
end
end
function Canvas:clean()
for i = 1, #self.lines do
self.lines[i].dirty = nil
end
end
function Canvas:applyPalette(palette)
local lookup = { }
for n = 1, 16 do
lookup[self.palette[2 ^ (n - 1)]] = palette[2 ^ (n - 1)]
end
for _, l in pairs(self.lines) do
l.fg = _gsub(l.fg, '%w', lookup)
l.bg = _gsub(l.bg, '%w', lookup)
l.dirty = true
end
self.palette = palette
end
function Canvas:render(device)
local offset = { x = 0, y = 0 }
local parent = self.parent
while parent do
offset.x = offset.x + parent.x - 1
offset.y = offset.y + parent.y - 1
parent = parent.parent
end
if #self.layers > 0 then
self:__renderLayers(device, offset)
else
self:__blitRect(device, nil, {
x = self.x + offset.x,
y = self.y + offset.y
})
self:clean()
end
end
-- regions are comprised of absolute values that coorespond to the output device.
-- canvases have coordinates relative to their parent.
-- canvas layer's stacking order is determined by the position within the array.
-- layers in the beginning of the array are overlayed by layers further down in
-- the array.
function Canvas:__renderLayers(device, offset)
if #self.layers > 0 then
self.regions = self.regions or Region.new(self.x, self.y, self.ex, self.ey)
for i = 1, #self.layers do
local canvas = self.layers[i]
if canvas.visible then
-- punch out this area from the parent's canvas
self:__punch(canvas, offset)
-- get the area to render for this layer
canvas.regions = Region.new(
canvas.x + offset.x,
canvas.y + offset.y,
canvas.ex + offset.x,
canvas.ey + offset.y)
-- punch out any layers that overlap this one
for j = i + 1, #self.layers do
if self.layers[j].visible then
canvas:__punch(self.layers[j], offset)
end
end
if #canvas.regions.region > 0 then
canvas:__renderLayers(device, {
x = canvas.x + offset.x - 1,
y = canvas.y + offset.y - 1,
})
end
canvas.regions = nil
end
end
self:__blitClipped(device, offset)
self.regions = nil
elseif self.regions and #self.regions.region > 0 then
self:__blitClipped(device, offset)
self.regions = nil
else
self:__blitRect(device, nil, {
x = self.x + offset.x,
y = self.y + offset.y
})
self.regions = nil
end
self:clean()
end
function Canvas:__blitClipped(device, offset)
for _,region in ipairs(self.regions.region) do
self:__blitRect(device,
{ x = region[1] - offset.x,
y = region[2] - offset.y,
ex = region[3] - offset.x,
ey = region[4] - offset.y},
{ x = region[1], y = region[2] })
end
end
function Canvas:__punch(rect, offset)
self.regions:subRect(
rect.x + offset.x,
rect.y + offset.y,
rect.ex + offset.x,
rect.ey + offset.y)
end
function Canvas:__blitRect(device, src, tgt)
src = src or { x = 1, y = 1, ex = self.ex - self.x + 1, ey = self.ey - self.y + 1 }
tgt = tgt or self
--[[
-- for visualizing updates on the screen
local drew
for i = 0, src.ey - src.y do
local line = self.lines[src.y + i + (self.offy or 0)]
if line and line.dirty then
drew = true
local t, fg, bg = line.text, line.fg, line.bg
if src.x > 1 or src.ex < self.ex then
t = _sub(t, src.x, src.ex)
fg = _rep(1, src.ex-src.x + 1)
bg = _rep(2, src.ex-src.x + 1)
end
device.setCursorPos(tgt.x, tgt.y + i)
device.blit(t, fg, bg)
end
end
if drew then
os.sleep(.3)
end
]]
for i = 0, src.ey - src.y do
local line = self.lines[src.y + i + (self.offy or 0)]
if line and line.dirty then
local t, fg, bg = line.text, line.fg, line.bg
if src.x > 1 or src.ex < self.ex then
t = _sub(t, src.x, src.ex)
fg = _sub(fg, src.x, src.ex)
bg = _sub(bg, src.x, src.ex)
end
device.setCursorPos(tgt.x, tgt.y + i)
device.blit(t, fg, bg)
end
end
end
return Canvas

View File

@@ -0,0 +1,32 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.ActiveLayer = class(UI.Window)
UI.ActiveLayer.defaults = {
UIElement = 'ActiveLayer',
}
function UI.ActiveLayer:layout()
UI.Window.layout(self)
if not self.canvas then
self.canvas = self:addLayer()
else
self.canvas:resize(self.width, self.height)
end
end
function UI.ActiveLayer:enable(...)
self.canvas:raise()
self.canvas:setVisible(true)
UI.Window.enable(self, ...)
if self.parent.transitionHint then
self:addTransition(self.parent.transitionHint)
end
self:focusFirst()
end
function UI.ActiveLayer:disable()
if self.canvas then
self.canvas:setVisible(false)
end
UI.Window.disable(self)
end

View File

@@ -0,0 +1,66 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
UI.Button = class(UI.Window)
UI.Button.defaults = {
UIElement = 'Button',
text = 'button',
backgroundColor = colors.lightGray,
backgroundFocusColor = colors.gray,
textFocusColor = colors.white,
textInactiveColor = colors.gray,
textColor = colors.black,
centered = true,
height = 1,
focusIndicator = ' ',
event = 'button_press',
accelerators = {
space = 'button_activate',
enter = 'button_activate',
mouse_click = 'button_activate',
}
}
function UI.Button:setParent()
if not self.width and not self.ex then
self.width = #self.text + 2
end
UI.Window.setParent(self)
end
function UI.Button:draw()
local fg = self.textColor
local bg = self.backgroundColor
local ind = ' '
if self.focused then
bg = self.backgroundFocusColor
fg = self.textFocusColor
ind = self.focusIndicator
elseif self.inactive then
fg = self.textInactiveColor
end
local text = ind .. self.text .. ' '
if self.centered then
self:clear(bg)
self:centeredWrite(1 + math.floor(self.height / 2), text, bg, fg)
else
self:write(1, 1, Util.widthify(text, self.width), bg, fg)
end
end
function UI.Button:focus()
if self.focused then
self:scrollIntoView()
end
self:draw()
end
function UI.Button:eventHandler(event)
if event.type == 'button_activate' then
self:emit({ type = self.event, button = self })
return true
end
return false
end

View File

@@ -0,0 +1,63 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
UI.Checkbox = class(UI.Window)
UI.Checkbox.defaults = {
UIElement = 'Checkbox',
nochoice = 'Select',
checkedIndicator = UI.extChars and '\4' or 'X',
leftMarker = UI.extChars and '\124' or '[',
rightMarker = UI.extChars and '\124' or ']',
value = false,
textColor = colors.white,
backgroundColor = colors.black,
backgroundFocusColor = colors.lightGray,
height = 1,
width = 3,
accelerators = {
space = 'checkbox_toggle',
mouse_click = 'checkbox_toggle',
}
}
function UI.Checkbox:draw()
local bg = self.backgroundColor
if self.focused then
bg = self.backgroundFocusColor
end
if type(self.value) == 'string' then
self.value = nil -- TODO: fix form
end
local text = string.format('[%s]', not self.value and ' ' or self.checkedIndicator)
local x = 1
if self.label then
self:write(1, 1, self.label)
x = #self.label + 2
end
self:write(x, 1, text, bg)
self:write(x, 1, self.leftMarker, self.backgroundColor, self.textColor)
self:write(x + 1, 1, not self.value and ' ' or self.checkedIndicator, bg)
self:write(x + 2, 1, self.rightMarker, self.backgroundColor, self.textColor)
end
function UI.Checkbox:focus()
self:draw()
end
function UI.Checkbox:setValue(v)
self.value = v
end
function UI.Checkbox:reset()
self.value = false
end
function UI.Checkbox:eventHandler(event)
if event.type == 'checkbox_toggle' then
self.value = not self.value
self:emit({ type = 'checkbox_change', checked = self.value, element = self })
self:draw()
return true
end
end

View File

@@ -0,0 +1,88 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
UI.Chooser = class(UI.Window)
UI.Chooser.defaults = {
UIElement = 'Chooser',
choices = { },
nochoice = 'Select',
backgroundFocusColor = colors.lightGray,
textInactiveColor = colors.gray,
leftIndicator = UI.extChars and '\17' or '<',
rightIndicator = UI.extChars and '\16' or '>',
height = 1,
}
function UI.Chooser:setParent()
if not self.width and not self.ex then
self.width = 1
for _,v in pairs(self.choices) do
if #v.name > self.width then
self.width = #v.name
end
end
self.width = self.width + 4
end
UI.Window.setParent(self)
end
function UI.Chooser:draw()
local bg = self.backgroundColor
if self.focused then
bg = self.backgroundFocusColor
end
local fg = self.inactive and self.textInactiveColor or self.textColor
local choice = Util.find(self.choices, 'value', self.value)
local value = self.nochoice
if choice then
value = choice.name
end
self:write(1, 1, self.leftIndicator, self.backgroundColor, colors.black)
self:write(2, 1, ' ' .. Util.widthify(tostring(value), self.width-4) .. ' ', bg, fg)
self:write(self.width, 1, self.rightIndicator, self.backgroundColor, colors.black)
end
function UI.Chooser:focus()
self:draw()
end
function UI.Chooser:eventHandler(event)
if event.type == 'key' then
if event.key == 'right' or event.key == 'space' then
local _,k = Util.find(self.choices, 'value', self.value)
local choice
if not k then k = 1 end
if k and k < #self.choices then
choice = self.choices[k+1]
else
choice = self.choices[1]
end
self.value = choice.value
self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice })
self:draw()
return true
elseif event.key == 'left' then
local _,k = Util.find(self.choices, 'value', self.value)
local choice
if k and k > 1 then
choice = self.choices[k-1]
else
choice = self.choices[#self.choices]
end
self.value = choice.value
self:emit({ type = 'choice_change', value = self.value, element = self, choice = choice })
self:draw()
return true
end
elseif event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
if event.x == 1 then
self:emit({ type = 'key', key = 'left' })
return true
elseif event.x == self.width then
self:emit({ type = 'key', key = 'right' })
return true
end
end
end

View File

@@ -0,0 +1,39 @@
local Canvas = require('opus.ui.canvas')
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
UI.Dialog = class(UI.SlideOut)
UI.Dialog.defaults = {
UIElement = 'Dialog',
height = 7,
textColor = colors.black,
backgroundColor = colors.white,
okEvent ='dialog_ok',
cancelEvent = 'dialog_cancel',
}
function UI.Dialog:postInit()
self.y = -self.height
self.titleBar = UI.TitleBar({ event = self.cancelEvent, title = self.title })
end
function UI.Dialog:show(...)
local canvas = self.parent:getCanvas()
self.oldPalette = canvas.palette
canvas:applyPalette(Canvas.darkPalette)
UI.SlideOut.show(self, ...)
end
function UI.Dialog:hide(...)
self.parent:getCanvas().palette = self.oldPalette
UI.SlideOut.hide(self, ...)
self.parent:draw()
end
function UI.Dialog:eventHandler(event)
if event.type == 'dialog_cancel' then
self:hide()
end
return UI.SlideOut.eventHandler(self, event)
end

View File

@@ -0,0 +1,75 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
UI.DropMenu = class(UI.MenuBar)
UI.DropMenu.defaults = {
UIElement = 'DropMenu',
backgroundColor = colors.white,
buttonClass = 'DropMenuItem',
}
function UI.DropMenu:layout()
UI.MenuBar.layout(self)
local maxWidth = 1
for y,child in ipairs(self.children) do
child.x = 1
child.y = y
if #(child.text or '') > maxWidth then
maxWidth = #child.text
end
end
for _,child in ipairs(self.children) do
child.width = maxWidth + 2
if child.spacer then
child.inactive = true
child.text = string.rep('-', child.width - 2)
end
end
self.height = #self.children + 1
self.width = maxWidth + 2
if not self.canvas then
self.canvas = self:addLayer()
else
self.canvas:resize(self.width, self.height)
end
end
function UI.DropMenu:enable()
end
function UI.DropMenu:show(x, y)
self.x, self.y = x, y
self.canvas:move(x, y)
self.canvas:setVisible(true)
UI.Window.enable(self)
self:draw()
self:capture(self)
self:focusFirst()
end
function UI.DropMenu:hide()
self:disable()
self.canvas:setVisible(false)
self:release(self)
end
function UI.DropMenu:eventHandler(event)
if event.type == 'focus_lost' and self.enabled then
if not Util.contains(self.children, event.focused) then
self:hide()
end
elseif event.type == 'mouse_out' and self.enabled then
self:hide()
self:refocus()
else
return UI.MenuBar.eventHandler(self, event)
end
return true
end

View File

@@ -0,0 +1,21 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
--[[-- DropMenuItem --]]--
UI.DropMenuItem = class(UI.Button)
UI.DropMenuItem.defaults = {
UIElement = 'DropMenuItem',
textColor = colors.black,
backgroundColor = colors.white,
textFocusColor = colors.white,
textInactiveColor = colors.lightGray,
backgroundFocusColor = colors.lightGray,
}
function UI.DropMenuItem:eventHandler(event)
if event.type == 'button_activate' then
self.parent:hide()
end
return UI.Button.eventHandler(self, event)
end

View File

@@ -0,0 +1,76 @@
local class = require('opus.class')
local Terminal = require('opus.terminal')
local UI = require('opus.ui')
local colors = _G.colors
UI.Embedded = class(UI.Window)
UI.Embedded.defaults = {
UIElement = 'Embedded',
backgroundColor = colors.black,
textColor = colors.white,
maxScroll = 100,
accelerators = {
up = 'scroll_up',
down = 'scroll_down',
}
}
function UI.Embedded:setParent()
UI.Window.setParent(self)
self.win = Terminal.window(UI.term.device, self.x, self.y, self.width, self.height, false)
self.win.setMaxScroll(self.maxScroll)
local canvas = self:getCanvas()
self.win.getCanvas().parent = canvas
table.insert(canvas.layers, self.win.getCanvas())
self.canvas = self.win.getCanvas()
self.win.setCursorPos(1, 1)
self.win.setBackgroundColor(self.backgroundColor)
self.win.setTextColor(self.textColor)
self.win.clear()
end
function UI.Embedded:layout()
UI.Window.layout(self)
if self.win then
self.win.reposition(self.x, self.y, self.width, self.height)
end
end
function UI.Embedded:draw()
self.canvas:dirty()
end
function UI.Embedded:enable()
self.canvas:setVisible(true)
self.canvas:raise()
if self.visible then
-- the window will automatically update on changes
-- the canvas does not need to be rendereed
self.win.setVisible(true)
end
UI.Window.enable(self)
self.canvas:dirty()
end
function UI.Embedded:disable()
self.canvas:setVisible(false)
self.win.setVisible(false)
UI.Window.disable(self)
end
function UI.Embedded:eventHandler(event)
if event.type == 'scroll_up' then
self.win.scrollUp()
return true
elseif event.type == 'scroll_down' then
self.win.scrollDown()
return true
end
end
function UI.Embedded:focus()
-- allow scrolling
end

View File

@@ -0,0 +1,146 @@
local class = require('opus.class')
local Sound = require('opus.sound')
local UI = require('opus.ui')
local colors = _G.colors
UI.Form = class(UI.Window)
UI.Form.defaults = {
UIElement = 'Form',
values = { },
margin = 2,
event = 'form_complete',
cancelEvent = 'form_cancel',
}
function UI.Form:postInit()
self:createForm()
end
function UI.Form:reset()
for _,child in pairs(self.children) do
if child.reset then
child:reset()
end
end
end
function UI.Form:setValues(values)
self:reset()
self.values = values
for _,child in pairs(self.children) do
if child.formKey then
-- this should be child:setValue(self.values[child.formKey])
-- so chooser can set default choice if null
-- null should be valid as well
child.value = self.values[child.formKey] or ''
end
end
end
function UI.Form:createForm()
self.children = self.children or { }
if not self.labelWidth then
self.labelWidth = 1
for _, child in pairs(self) do
if type(child) == 'table' and child.UIElement then
if child.formLabel then
self.labelWidth = math.max(self.labelWidth, #child.formLabel + 2)
end
end
end
end
local y = self.margin
for _, child in pairs(self) do
if type(child) == 'table' and child.UIElement then
if child.formKey then
child.value = self.values[child.formKey] or ''
end
if child.formLabel then
child.x = self.labelWidth + self.margin - 1
child.y = y
if not child.width and not child.ex then
child.ex = -self.margin
end
table.insert(self.children, UI.Text {
x = self.margin,
y = y,
textColor = colors.black,
width = #child.formLabel,
value = child.formLabel,
})
end
if child.formLabel then
y = y + 1
end
end
end
if not self.manualControls then
table.insert(self.children, UI.Button {
y = -self.margin, x = -12 - self.margin,
text = 'Ok',
event = 'form_ok',
})
table.insert(self.children, UI.Button {
y = -self.margin, x = -7 - self.margin,
text = 'Cancel',
event = self.cancelEvent,
})
end
end
function UI.Form:validateField(field)
if field.required then
if not field.value or #tostring(field.value) == 0 then
return false, 'Field is required'
end
end
if field.validate == 'numeric' then
if #tostring(field.value) > 0 then
if not tonumber(field.value) then
return false, 'Invalid number'
end
end
end
return true
end
function UI.Form:save()
for _,child in pairs(self.children) do
if child.formKey then
local s, m = self:validateField(child)
if not s then
self:setFocus(child)
Sound.play('entity.villager.no', .5)
self:emit({ type = 'form_invalid', message = m, field = child })
return false
end
end
end
for _,child in pairs(self.children) do
if child.formKey then
if child.validate == 'numeric' then
self.values[child.formKey] = tonumber(child.value)
else
self.values[child.formKey] = child.value
end
end
end
return true
end
function UI.Form:eventHandler(event)
if event.type == 'form_ok' then
if not self:save() then
return false
end
self:emit({ type = self.event, UIElement = self, values = self.values })
else
return UI.Window.eventHandler(self, event)
end
return true
end

View File

@@ -0,0 +1,495 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
local os = _G.os
local _rep = string.rep
local _sub = string.sub
local function safeValue(v)
local t = type(v)
if t == 'string' or t == 'number' then
return v
end
return tostring(v)
end
local Writer = class()
function Writer:init(element, y)
self.element = element
self.y = y
self.x = 1
end
function Writer:write(s, width, align, bg, fg)
local len = #tostring(s or '')
if len > width then
s = _sub(s, 1, width)
end
local padding = len < width and _rep(' ', width - len)
if padding then
if align == 'right' then
s = padding .. s
else
s = s .. padding
end
end
self.element:write(self.x, self.y, s, bg, fg)
self.x = self.x + width
end
function Writer:finish(bg)
if self.x <= self.element.width then
self.element:write(self.x, self.y, _rep(' ', self.element.width - self.x + 1), bg)
end
self.x = 1
self.y = self.y + 1
end
--[[-- Grid --]]--
UI.Grid = class(UI.Window)
UI.Grid.defaults = {
UIElement = 'Grid',
index = 1,
inverseSort = false,
disableHeader = false,
headerHeight = 1,
marginRight = 0,
textColor = colors.white,
textSelectedColor = colors.white,
backgroundColor = colors.black,
backgroundSelectedColor = colors.gray,
headerBackgroundColor = colors.cyan,
headerTextColor = colors.white,
headerSortColor = colors.yellow,
unfocusedTextSelectedColor = colors.white,
unfocusedBackgroundSelectedColor = colors.gray,
focusIndicator = UI.extChars and '\183' or '>',
sortIndicator = ' ',
inverseSortIndicator = UI.extChars and '\24' or '^',
values = { },
columns = { },
accelerators = {
enter = 'key_enter',
[ 'control-c' ] = 'copy',
down = 'scroll_down',
up = 'scroll_up',
home = 'scroll_top',
[ 'end' ] = 'scroll_bottom',
pageUp = 'scroll_pageUp',
[ 'control-b' ] = 'scroll_pageUp',
pageDown = 'scroll_pageDown',
[ 'control-f' ] = 'scroll_pageDown',
},
}
function UI.Grid:setParent()
UI.Window.setParent(self)
for _,c in pairs(self.columns) do
c.cw = c.width
if not c.heading then
c.heading = ''
end
end
self:update()
if not self.pageSize then
if self.disableHeader then
self.pageSize = self.height
else
self.pageSize = self.height - self.headerHeight
end
end
end
function UI.Grid:resize()
UI.Window.resize(self)
if self.disableHeader then
self.pageSize = self.height
else
self.pageSize = self.height - self.headerHeight
end
self:adjustWidth()
end
function UI.Grid:adjustWidth()
local t = { } -- cols without width
local w = self.width - #self.columns - 1 - self.marginRight -- width remaining
for _,c in pairs(self.columns) do
if c.width then
c.cw = c.width
w = w - c.cw
else
table.insert(t, c)
end
end
if #t == 0 then
return
end
if #t == 1 then
t[1].cw = #(t[1].heading or '')
t[1].cw = math.max(t[1].cw, w)
return
end
if not self.autospace then
for k,c in ipairs(t) do
c.cw = math.floor(w / (#t - k + 1))
w = w - c.cw
end
else
for _,c in ipairs(t) do
c.cw = #(c.heading or '')
w = w - c.cw
end
-- adjust the size to the length of the value
for key,row in pairs(self.values) do
if w <= 0 then
break
end
row = self:getDisplayValues(row, key)
for _,col in pairs(t) do
local value = row[col.key]
if value then
value = tostring(value)
if #value > col.cw then
w = w + col.cw
col.cw = math.min(#value, w)
w = w - col.cw
if w <= 0 then
break
end
end
end
end
end
-- last column does not get padding (right alignment)
if not self.columns[#self.columns].width then
Util.removeByValue(t, self.columns[#self.columns])
end
-- got some extra room - add some padding
if w > 0 then
for k,c in ipairs(t) do
local padding = math.floor(w / (#t - k + 1))
c.cw = c.cw + padding
w = w - padding
end
end
end
end
function UI.Grid:setPageSize(pageSize)
self.pageSize = pageSize
end
function UI.Grid:getValues()
return self.values
end
function UI.Grid:setValues(t)
self.values = t
self:update()
end
function UI.Grid:setInverseSort(inverseSort)
self.inverseSort = inverseSort
self:update()
self:setIndex(self.index)
end
function UI.Grid:setSortColumn(column)
self.sortColumn = column
end
function UI.Grid:getDisplayValues(row, key)
return row
end
function UI.Grid:getSelected()
if self.sorted then
return self.values[self.sorted[self.index]], self.sorted[self.index]
end
end
function UI.Grid:setSelected(name, value)
if self.sorted then
for k,v in pairs(self.sorted) do
if self.values[v][name] == value then
self:setIndex(k)
return
end
end
end
self:setIndex(1)
end
function UI.Grid:focus()
self:drawRows()
end
function UI.Grid:draw()
if not self.disableHeader then
self:drawHeadings()
end
if self.index <= 0 then
self:setIndex(1)
elseif self.index > #self.sorted then
self:setIndex(#self.sorted)
end
self:drawRows()
end
-- Something about the displayed table has changed
-- resort the table
function UI.Grid:update()
local function sort(a, b)
if not a[self.sortColumn] then
return false
elseif not b[self.sortColumn] then
return true
end
return self:sortCompare(a, b)
end
local function inverseSort(a, b)
return not sort(a, b)
end
local order
if self.sortColumn then
order = sort
if self.inverseSort then
order = inverseSort
end
end
self.sorted = Util.keys(self.values)
if order then
table.sort(self.sorted, function(a,b)
return order(self.values[a], self.values[b])
end)
end
self:adjustWidth()
end
function UI.Grid:drawHeadings()
if self.headerHeight > 1 then
self:clear(self.headerBackgroundColor)
end
local sb = Writer(self, math.ceil(self.headerHeight / 2))
for _,col in ipairs(self.columns) do
local ind = ' '
local color = self.headerTextColor
if col.key == self.sortColumn then
if self.inverseSort then
ind = self.inverseSortIndicator
else
ind = self.sortIndicator
end
color = self.headerSortColor
end
sb:write(ind .. col.heading,
col.cw + 1,
col.align,
self.headerBackgroundColor,
color)
end
sb:finish(self.headerBackgroundColor)
end
function UI.Grid:sortCompare(a, b)
a = safeValue(a[self.sortColumn])
b = safeValue(b[self.sortColumn])
if type(a) == type(b) then
return a < b
end
return tostring(a) < tostring(b)
end
function UI.Grid:drawRows()
local startRow = math.max(1, self:getStartRow())
local sb = Writer(self, self.disableHeader and 1 or self.headerHeight + 1)
local lastRow = math.min(startRow + self.pageSize - 1, #self.sorted)
for index = startRow, lastRow do
local key = self.sorted[index]
local rawRow = self.values[key]
local row = self:getDisplayValues(rawRow, key)
local selected = index == self.index and not self.inactive
local bg = self:getRowBackgroundColor(rawRow, selected)
local fg = self:getRowTextColor(rawRow, selected)
local focused = self.focused and selected
self:drawRow(sb, row, focused, bg, fg)
sb:finish(bg)
end
if sb.y <= self.height then
self:clearArea(1, sb.y, self.width, self.height - sb.y + 1)
end
end
function UI.Grid:drawRow(sb, row, focused, bg, fg)
local ind = focused and self.focusIndicator or ' '
for _,col in pairs(self.columns) do
sb:write(ind .. safeValue(row[col.key] or ''),
col.cw + 1,
col.align,
bg,
fg)
ind = ' '
end
end
function UI.Grid:getRowTextColor(row, selected)
if selected then
if self.focused then
return self.textSelectedColor
end
return self.unfocusedTextSelectedColor
end
return self.textColor
end
function UI.Grid:getRowBackgroundColor(row, selected)
if selected then
if self.focused then
return self.backgroundSelectedColor
end
return self.unfocusedBackgroundSelectedColor
end
return self.backgroundColor
end
function UI.Grid:getIndex()
return self.index
end
function UI.Grid:setIndex(index)
index = math.max(1, index)
self.index = math.min(index, #self.sorted)
local selected = self:getSelected()
if selected ~= self.selected then
self:drawRows()
self.selected = selected
if selected then
self:emit({ type = 'grid_focus_row', selected = selected, element = self })
end
end
end
function UI.Grid:getStartRow()
return math.floor((self.index - 1) / self.pageSize) * self.pageSize + 1
end
function UI.Grid:getPage()
return math.floor(self.index / self.pageSize) + 1
end
function UI.Grid:getPageCount()
local tableSize = Util.size(self.values)
local pc = math.floor(tableSize / self.pageSize)
if tableSize % self.pageSize > 0 then
pc = pc + 1
end
return pc
end
function UI.Grid:nextPage()
self:setPage(self:getPage() + 1)
end
function UI.Grid:previousPage()
self:setPage(self:getPage() - 1)
end
function UI.Grid:setPage(pageNo)
-- 1 based paging
self:setIndex((pageNo-1) * self.pageSize + 1)
end
function UI.Grid:eventHandler(event)
if event.type == 'mouse_click' or
event.type == 'mouse_rightclick' or
event.type == 'mouse_doubleclick' then
if not self.disableHeader then
if event.y <= self.headerHeight then
local col = 2
for _,c in ipairs(self.columns) do
if event.x < col + c.cw then
self:emit({
type = 'grid_sort',
sortColumn = c.key,
inverseSort = self.sortColumn == c.key and not self.inverseSort,
element = self,
})
break
end
col = col + c.cw + 1
end
return true
end
end
local row = self:getStartRow() + event.y - 1
if not self.disableHeader then
row = row - self.headerHeight
end
if row > 0 and row <= Util.size(self.values) then
self:setIndex(row)
if event.type == 'mouse_doubleclick' then
self:emit({ type = 'key_enter' })
elseif event.type == 'mouse_rightclick' then
self:emit({ type = 'grid_select_right', selected = self.selected, element = self })
end
return true
end
return false
elseif event.type == 'grid_sort' then
self.sortColumn = event.sortColumn
self:setInverseSort(event.inverseSort)
self:draw()
elseif event.type == 'scroll_down' then
self:setIndex(self.index + 1)
elseif event.type == 'scroll_up' then
self:setIndex(self.index - 1)
elseif event.type == 'scroll_top' then
self:setIndex(1)
elseif event.type == 'scroll_bottom' then
self:setIndex(Util.size(self.values))
elseif event.type == 'scroll_pageUp' then
self:setIndex(self.index - self.pageSize)
elseif event.type == 'scroll_pageDown' then
self:setIndex(self.index + self.pageSize)
elseif event.type == 'scroll_to' then
self:setIndex(event.offset)
elseif event.type == 'key_enter' then
if self.selected then
self:emit({ type = 'grid_select', selected = self.selected, element = self })
end
elseif event.type == 'copy' then
if self.selected then
os.queueEvent('clipboard_copy', self.selected)
end
else
return false
end
return true
end

View File

@@ -0,0 +1,40 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.Image = class(UI.Window)
UI.Image.defaults = {
UIElement = 'Image',
event = 'button_press',
}
function UI.Image:setParent()
if self.image then
self.height = #self.image
end
if self.image and not self.width then
self.width = #self.image[1]
end
UI.Window.setParent(self)
end
function UI.Image:draw()
self:clear()
if self.image then
for y = 1, #self.image do
local line = self.image[y]
for x = 1, #line do
local ch = line[x]
if type(ch) == 'number' then
if ch > 0 then
self:write(x, y, ' ', ch)
end
else
self:write(x, y, ch)
end
end
end
end
end
function UI.Image:setImage(image)
self.image = image
end

View File

@@ -0,0 +1,60 @@
local class = require('opus.class')
local UI = require('opus.ui')
--[[-- Menu --]]--
UI.Menu = class(UI.Grid)
UI.Menu.defaults = {
UIElement = 'Menu',
disableHeader = true,
columns = { { heading = 'Prompt', key = 'prompt', width = 20 } },
}
function UI.Menu:postInit()
self.values = self.menuItems
self.pageSize = #self.menuItems
end
function UI.Menu:setParent()
UI.Grid.setParent(self)
self.itemWidth = 1
for _,v in pairs(self.values) do
if #v.prompt > self.itemWidth then
self.itemWidth = #v.prompt
end
end
self.columns[1].width = self.itemWidth
if self.centered then
self:center()
else
self.width = self.itemWidth + 2
end
end
function UI.Menu:center()
self.x = (self.width - self.itemWidth + 2) / 2
self.width = self.itemWidth + 2
end
function UI.Menu:eventHandler(event)
if event.type == 'key' then
if event.key == 'enter' then
local selected = self.menuItems[self.index]
self:emit({
type = selected.event or 'menu_select',
selected = selected
})
return true
end
elseif event.type == 'mouse_click' then
if event.y <= #self.menuItems then
UI.Grid.setIndex(self, event.y)
local selected = self.menuItems[self.index]
self:emit({
type = selected.event or 'menu_select',
selected = selected
})
return true
end
end
return UI.Grid.eventHandler(self, event)
end

View File

@@ -0,0 +1,90 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
local function getPosition(element)
local x, y = 1, 1
repeat
x = element.x + x - 1
y = element.y + y - 1
element = element.parent
until not element
return x, y
end
UI.MenuBar = class(UI.Window)
UI.MenuBar.defaults = {
UIElement = 'MenuBar',
buttons = { },
height = 1,
backgroundColor = colors.lightGray,
textColor = colors.black,
spacing = 2,
lastx = 1,
showBackButton = false,
buttonClass = 'MenuItem',
}
function UI.MenuBar:postInit()
self:addButtons(self.buttons)
end
function UI.MenuBar:addButtons(buttons)
if not self.children then
self.children = { }
end
for _,button in pairs(buttons) do
if button.UIElement then
table.insert(self.children, button)
else
local buttonProperties = {
x = self.lastx,
width = #(button.text or 'button') + self.spacing,
centered = false,
}
self.lastx = self.lastx + buttonProperties.width
UI:mergeProperties(buttonProperties, button)
button = UI[self.buttonClass](buttonProperties)
if button.name then
self[button.name] = button
else
table.insert(self.children, button)
end
if button.dropdown then
button.dropmenu = UI.DropMenu { buttons = button.dropdown }
end
end
end
if self.parent then
self:initChildren()
end
end
function UI.MenuBar:getActive(menuItem)
return not menuItem.inactive
end
function UI.MenuBar:eventHandler(event)
if event.type == 'button_press' and event.button.dropmenu then
if event.button.dropmenu.enabled then
event.button.dropmenu:hide()
self:refocus()
return true
else
local x, y = getPosition(event.button)
if x + event.button.dropmenu.width > self.width then
x = self.width - event.button.dropmenu.width + 1
end
for _,c in pairs(event.button.dropmenu.children) do
if not c.spacer then
c.inactive = not self:getActive(c)
end
end
event.button.dropmenu:show(x, y + 1)
end
return true
end
end

View File

@@ -0,0 +1,14 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
--[[-- MenuItem --]]--
UI.MenuItem = class(UI.Button)
UI.MenuItem.defaults = {
UIElement = 'MenuItem',
textColor = colors.black,
backgroundColor = colors.lightGray,
textFocusColor = colors.white,
backgroundFocusColor = colors.lightGray,
}

View File

@@ -0,0 +1,33 @@
local class = require('opus.class')
local UI = require('opus.ui')
UI.NftImage = class(UI.Window)
UI.NftImage.defaults = {
UIElement = 'NftImage',
event = 'button_press',
}
function UI.NftImage:setParent()
if self.image then
self.height = self.image.height
end
if self.image and not self.width then
self.width = self.image.width
end
UI.Window.setParent(self)
end
function UI.NftImage:draw()
if self.image then
for y = 1, self.image.height do
for x = 1, #self.image.text[y] do
self:write(x, y, self.image.text[y][x], self.image.bg[y][x], self.image.fg[y][x])
end
end
else
self:clear()
end
end
function UI.NftImage:setImage(image)
self.image = image
end

View File

@@ -0,0 +1,92 @@
local class = require('opus.class')
local Event = require('opus.event')
local Sound = require('opus.sound')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
UI.Notification = class(UI.Window)
UI.Notification.defaults = {
UIElement = 'Notification',
backgroundColor = colors.gray,
closeInd = UI.extChars and '\215' or '*',
height = 3,
timeout = 3,
anchor = 'bottom',
}
function UI.Notification:draw()
end
function UI.Notification:enable()
end
function UI.Notification:error(value, timeout)
self.backgroundColor = colors.red
Sound.play('entity.villager.no', .5)
self:display(value, timeout)
end
function UI.Notification:info(value, timeout)
self.backgroundColor = colors.lightGray
self:display(value, timeout)
end
function UI.Notification:success(value, timeout)
self.backgroundColor = colors.green
self:display(value, timeout)
end
function UI.Notification:cancel()
if self.timer then
Event.off(self.timer)
self.timer = nil
end
if self.canvas then
self.enabled = false
self.canvas:removeLayer()
self.canvas = nil
end
end
function UI.Notification:display(value, timeout)
self:cancel()
self.enabled = true
local lines = Util.wordWrap(value, self.width - 3)
self.height = #lines
if self.anchor == 'bottom' then
self.y = self.parent.height - self.height + 1
self.canvas = self:addLayer(self.backgroundColor, self.textColor)
self:addTransition('expandUp', { ticks = self.height })
else
self.canvas = self:addLayer(self.backgroundColor, self.textColor)
self.y = 1
end
self.canvas:setVisible(true)
self:clear()
for k,v in pairs(lines) do
self:write(2, k, v)
end
timeout = timeout or self.timeout
if timeout > 0 then
self.timer = Event.onTimeout(timeout, function()
self:cancel()
self:sync()
end)
else
self:write(self.width, 1, self.closeInd)
self:sync()
end
end
function UI.Notification:eventHandler(event)
if event.type == 'mouse_click' then
if event.x == self.width then
self:cancel()
return true
end
end
end

View File

@@ -0,0 +1,18 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
UI.ProgressBar = class(UI.Window)
UI.ProgressBar.defaults = {
UIElement = 'ProgressBar',
progressColor = colors.lime,
backgroundColor = colors.gray,
height = 1,
value = 0,
}
function UI.ProgressBar:draw()
self:clear()
local width = math.ceil(self.value / 100 * self.width)
self:clearArea(1, 1, width, self.height, self.progressColor)
end

View File

@@ -0,0 +1,74 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
UI.ScrollBar = class(UI.Window)
UI.ScrollBar.defaults = {
UIElement = 'ScrollBar',
lineChar = '|',
sliderChar = UI.extChars and '\127' or '#',
upArrowChar = UI.extChars and '\30' or '^',
downArrowChar = UI.extChars and '\31' or 'v',
scrollbarColor = colors.lightGray,
width = 1,
x = -1,
ey = -1,
}
function UI.ScrollBar:draw()
local view = self.parent:getViewArea()
if view.totalHeight > view.height then
local maxScroll = view.totalHeight - view.height
local percent = view.offsetY / maxScroll
local sliderSize = math.max(1, Util.round(view.height / view.totalHeight * (view.height - 2)))
local x = self.width
local row = view.y
if not view.static then -- does the container scroll ?
self.height = view.totalHeight
end
for i = 1, view.height - 2 do
self:write(x, row + i, self.lineChar, nil, self.scrollbarColor)
end
local y = Util.round((view.height - 2 - sliderSize) * percent)
for i = 1, sliderSize do
self:write(x, row + y + i, self.sliderChar, nil, self.scrollbarColor)
end
local color = self.scrollbarColor
if view.offsetY > 0 then
color = colors.white
end
self:write(x, row, self.upArrowChar, nil, color)
color = self.scrollbarColor
if view.offsetY + view.height < view.totalHeight then
color = colors.white
end
self:write(x, row + view.height - 1, self.downArrowChar, nil, color)
end
end
function UI.ScrollBar:eventHandler(event)
if event.type == 'mouse_click' or event.type == 'mouse_doubleclick' then
if event.x == 1 then
local view = self.parent:getViewArea()
if view.totalHeight > view.height then
if event.y == view.y then
self:emit({ type = 'scroll_up'})
elseif event.y == view.y + view.height - 1 then
self:emit({ type = 'scroll_down'})
else
local percent = (event.y - view.y) / (view.height - 2)
local y = math.floor((view.totalHeight - view.height) * percent)
self:emit({ type = 'scroll_to', offset = y })
end
end
return true
end
end
end

View File

@@ -0,0 +1,60 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
--[[-- ScrollingGrid --]]--
UI.ScrollingGrid = class(UI.Grid)
UI.ScrollingGrid.defaults = {
UIElement = 'ScrollingGrid',
scrollOffset = 0,
marginRight = 1,
}
function UI.ScrollingGrid:postInit()
self.scrollBar = UI.ScrollBar()
end
function UI.ScrollingGrid:drawRows()
UI.Grid.drawRows(self)
self.scrollBar:draw()
end
function UI.ScrollingGrid:getViewArea()
local y = 1
if not self.disableHeader then
y = y + self.headerHeight
end
return {
static = true, -- the container doesn't scroll
y = y, -- scrollbar Y
height = self.pageSize, -- viewable height
totalHeight = Util.size(self.values), -- total height
offsetY = self.scrollOffset, -- scroll offset
}
end
function UI.ScrollingGrid:getStartRow()
local ts = Util.size(self.values)
if ts < self.pageSize then
self.scrollOffset = 0
end
return self.scrollOffset + 1
end
function UI.ScrollingGrid:setIndex(index)
if index < self.scrollOffset + 1 then
self.scrollOffset = index - 1
elseif index - self.scrollOffset > self.pageSize then
self.scrollOffset = index - self.pageSize
end
if self.scrollOffset < 0 then
self.scrollOffset = 0
else
local ts = Util.size(self.values)
if self.pageSize + self.scrollOffset + 1 > ts then
self.scrollOffset = math.max(0, ts - self.pageSize)
end
end
UI.Grid.setIndex(self, index)
end

View File

@@ -0,0 +1,52 @@
local class = require('opus.class')
local UI = require('opus.ui')
--[[-- SlideOut --]]--
UI.SlideOut = class(UI.Window)
UI.SlideOut.defaults = {
UIElement = 'SlideOut',
pageType = 'modal',
}
function UI.SlideOut:layout()
UI.Window.layout(self)
if not self.canvas then
self.canvas = self:addLayer()
else
self.canvas:resize(self.width, self.height)
end
end
function UI.SlideOut:enable()
end
function UI.SlideOut:show(...)
self:addTransition('expandUp')
self.canvas:raise()
self.canvas:setVisible(true)
UI.Window.enable(self, ...)
self:draw()
self:capture(self)
self:focusFirst()
end
function UI.SlideOut:disable()
self.canvas:setVisible(false)
UI.Window.disable(self)
end
function UI.SlideOut:hide()
self:disable()
self:release(self)
self:refocus()
end
function UI.SlideOut:eventHandler(event)
if event.type == 'slide_show' then
self:show()
return true
elseif event.type == 'slide_hide' then
self:hide()
return true
end
end

View File

@@ -0,0 +1,98 @@
local class = require('opus.class')
local Event = require('opus.event')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
UI.StatusBar = class(UI.Window)
UI.StatusBar.defaults = {
UIElement = 'StatusBar',
backgroundColor = colors.lightGray,
textColor = colors.gray,
height = 1,
ey = -1,
}
function UI.StatusBar:adjustWidth()
-- Can only have 1 adjustable width
if self.columns then
local w = self.width - #self.columns - 1
for _,c in pairs(self.columns) do
if c.width then
c.cw = c.width -- computed width
w = w - c.width
end
end
for _,c in pairs(self.columns) do
if not c.width then
c.cw = w
end
end
end
end
function UI.StatusBar:resize()
UI.Window.resize(self)
self:adjustWidth()
end
function UI.StatusBar:setParent()
UI.Window.setParent(self)
self:adjustWidth()
end
function UI.StatusBar:setStatus(status)
if self.values ~= status then
self.values = status
self:draw()
end
end
function UI.StatusBar:setValue(name, value)
if not self.values then
self.values = { }
end
self.values[name] = value
end
function UI.StatusBar:getValue(name)
if self.values then
return self.values[name]
end
end
function UI.StatusBar:timedStatus(status, timeout)
self:write(2, 1, Util.widthify(status, self.width-2), self.backgroundColor)
Event.on(timeout or 3, function()
if self.enabled then
self:draw()
self:sync()
end
end)
end
function UI.StatusBar:getColumnWidth(name)
local c = Util.find(self.columns, 'key', name)
return c and c.cw
end
function UI.StatusBar:setColumnWidth(name, width)
local c = Util.find(self.columns, 'key', name)
if c then
c.cw = width
end
end
function UI.StatusBar:draw()
if not self.values then
self:clear()
elseif type(self.values) == 'string' then
self:write(1, 1, Util.widthify(' ' .. self.values, self.width))
else
local s = ''
for _,c in ipairs(self.columns) do
s = s .. ' ' .. Util.widthify(tostring(self.values[c.key] or ''), c.cw)
end
self:write(1, 1, Util.widthify(s, self.width))
end
end

View File

@@ -0,0 +1,12 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
UI.Tab = class(UI.ActiveLayer)
UI.Tab.defaults = {
UIElement = 'Tab',
tabTitle = 'tab',
backgroundColor = colors.cyan,
y = 2,
}

View File

@@ -0,0 +1,45 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
UI.TabBar = class(UI.MenuBar)
UI.TabBar.defaults = {
UIElement = 'TabBar',
buttonClass = 'TabBarMenuItem',
selectedBackgroundColor = colors.cyan,
}
function UI.TabBar:enable()
UI.MenuBar.enable(self)
if not Util.find(self.children, 'selected', true) then
local menuItem = self:getFocusables()[1]
if menuItem then
menuItem.selected = true
end
end
end
function UI.TabBar:eventHandler(event)
if event.type == 'tab_select' then
local selected, si = Util.find(self.children, 'uid', event.button.uid)
local previous, pi = Util.find(self.children, 'selected', true)
if si ~= pi then
selected.selected = true
if previous then
previous.selected = false
self:emit({ type = 'tab_change', current = si, last = pi, tab = selected })
end
end
UI.MenuBar.draw(self)
end
return UI.MenuBar.eventHandler(self, event)
end
function UI.TabBar:selectTab(text)
local menuItem = Util.find(self.children, 'text', text)
if menuItem then
menuItem.selected = true
end
end

View File

@@ -0,0 +1,25 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
--[[-- TabBarMenuItem --]]--
UI.TabBarMenuItem = class(UI.Button)
UI.TabBarMenuItem.defaults = {
UIElement = 'TabBarMenuItem',
event = 'tab_select',
textColor = colors.black,
selectedBackgroundColor = colors.cyan,
unselectedBackgroundColor = colors.lightGray,
backgroundColor = colors.lightGray,
}
function UI.TabBarMenuItem:draw()
if self.selected then
self.backgroundColor = self.selectedBackgroundColor
self.backgroundFocusColor = self.selectedBackgroundColor
else
self.backgroundColor = self.unselectedBackgroundColor
self.backgroundFocusColor = self.unselectedBackgroundColor
end
UI.Button.draw(self)
end

View File

@@ -0,0 +1,89 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
UI.Tabs = class(UI.Window)
UI.Tabs.defaults = {
UIElement = 'Tabs',
}
function UI.Tabs:postInit()
self:add(self)
end
function UI.Tabs:add(children)
local buttons = { }
for _,child in pairs(children) do
if type(child) == 'table' and child.UIElement and child.tabTitle then
child.y = 2
table.insert(buttons, {
text = child.tabTitle,
event = 'tab_select',
tabUid = child.uid,
})
end
end
if not self.tabBar then
self.tabBar = UI.TabBar({
buttons = buttons,
})
else
self.tabBar:addButtons(buttons)
end
if self.parent then
return UI.Window.add(self, children)
end
end
function UI.Tabs:selectTab(tab)
local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid)
if menuItem then
self.tabBar:emit({ type = 'tab_select', button = { uid = menuItem.uid } })
end
end
function UI.Tabs:setActive(tab, active)
local menuItem = Util.find(self.tabBar.children, 'tabUid', tab.uid)
if menuItem then
menuItem.inactive = not active
end
end
function UI.Tabs:enable()
self.enabled = true
self.transitionHint = nil
self.tabBar:enable()
local menuItem = Util.find(self.tabBar.children, 'selected', true)
for _,child in pairs(self.children) do
if child.uid == menuItem.tabUid then
child:enable()
self:emit({ type = 'tab_activate', activated = child })
elseif child.tabTitle then
child:disable()
end
end
end
function UI.Tabs:eventHandler(event)
if event.type == 'tab_change' then
local tab = self:find(event.tab.tabUid)
if event.current > event.last then
self.transitionHint = 'slideLeft'
else
self.transitionHint = 'slideRight'
end
for _,child in pairs(self.children) do
if child.uid == event.tab.tabUid then
child:enable()
elseif child.tabTitle then
child:disable()
end
end
self:emit({ type = 'tab_activate', activated = tab })
tab:draw()
end
end

View File

@@ -0,0 +1,20 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
UI.Text = class(UI.Window)
UI.Text.defaults = {
UIElement = 'Text',
value = '',
height = 1,
}
function UI.Text:setParent()
if not self.width and not self.ex then
self.width = #tostring(self.value)
end
UI.Window.setParent(self)
end
function UI.Text:draw()
self:write(1, 1, Util.widthify(self.value, self.width, self.align))
end

View File

@@ -0,0 +1,36 @@
local class = require('opus.class')
local UI = require('opus.ui')
--[[-- TextArea --]]--
UI.TextArea = class(UI.Viewport)
UI.TextArea.defaults = {
UIElement = 'TextArea',
marginRight = 2,
value = '',
}
function UI.TextArea:postInit()
self.scrollBar = UI.ScrollBar()
end
function UI.TextArea:setText(text)
self:reset()
self.value = text
self:draw()
end
function UI.TextArea:focus()
-- allow keyboard scrolling
end
function UI.TextArea:draw()
self:clear()
-- self:setCursorPos(1, 1)
self.cursorX, self.cursorY = 1, 1
self:print(self.value)
for _,child in pairs(self.children) do
if child.enabled then
child:draw()
end
end
end

View File

@@ -0,0 +1,124 @@
local class = require('opus.class')
local entry = require('opus.entry')
local UI = require('opus.ui')
local Util = require('opus.util')
local colors = _G.colors
local _rep = string.rep
UI.TextEntry = class(UI.Window)
UI.TextEntry.defaults = {
UIElement = 'TextEntry',
value = '',
shadowText = '',
focused = false,
textColor = colors.white,
shadowTextColor = colors.gray,
backgroundColor = colors.black, -- colors.lightGray,
backgroundFocusColor = colors.black, --lightGray,
height = 1,
limit = 6,
accelerators = {
[ 'control-c' ] = 'copy',
}
}
function UI.TextEntry:postInit()
self.value = tostring(self.value)
self.entry = entry({ limit = self.limit, offset = 2 })
end
function UI.TextEntry:layout()
UI.Window.layout(self)
self.entry.width = self.width - 2
end
function UI.TextEntry:setValue(value)
self.value = value
self.entry:unmark()
self.entry.value = tostring(value)
self.entry:updateScroll()
end
function UI.TextEntry:setPosition(pos)
self.entry.pos = pos
self.entry.value = tostring(self.value)
self.entry:updateScroll()
end
function UI.TextEntry:draw()
local bg = self.backgroundColor
local tc = self.textColor
if self.focused then
bg = self.backgroundFocusColor
end
local text = tostring(self.value)
if #text > 0 then
if self.entry.scroll > 0 then
text = text:sub(1 + self.entry.scroll)
end
if self.mask then
text = _rep('*', #text)
end
else
tc = self.shadowTextColor
text = self.shadowText
end
self:write(1, 1, ' ' .. Util.widthify(text, self.width - 2) .. ' ', bg, tc)
if self.entry.mark.active then
local tx = math.max(self.entry.mark.x - self.entry.scroll, 0)
local tex = self.entry.mark.ex - self.entry.scroll
if tex > self.width - 2 then -- unsure about this
tex = self.width - 2 - tx
end
if tx ~= tex then
self:write(tx + 2, 1, text:sub(tx + 1, tex), colors.gray, tc)
end
end
if self.focused then
self:setCursorPos(self.entry.pos - self.entry.scroll + 2, 1)
end
end
function UI.TextEntry:reset()
self.entry:reset()
self.value = ''
self:draw()
self:updateCursor()
end
function UI.TextEntry:updateCursor()
self:setCursorPos(self.entry.pos - self.entry.scroll + 2, 1)
end
function UI.TextEntry:focus()
self:draw()
if self.focused then
self:setCursorBlink(true)
else
self:setCursorBlink(false)
end
end
function UI.TextEntry:eventHandler(event)
local text = self.value
self.entry.value = tostring(text)
if event.ie and self.entry:process(event.ie) then
if self.entry.textChanged then
self.value = self.entry.value
self:draw()
if text ~= self.value then
self:emit({ type = 'text_change', text = self.value, element = self })
end
elseif self.entry.posChanged then
self:updateCursor()
end
return true
end
return false
end

View File

@@ -0,0 +1,65 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
local os = _G.os
UI.Throttle = class(UI.Window)
UI.Throttle.defaults = {
UIElement = 'Throttle',
backgroundColor = colors.gray,
bordercolor = colors.cyan,
height = 4,
width = 10,
timeout = .075,
ctr = 0,
image = {
' //) (O )~@ &~&-( ?Q ',
' //) (O )- @ \\-( ?) && ',
' //) (O ), @ \\-(?) && ',
' //) (O ). @ \\-d ) (@ '
}
}
function UI.Throttle:setParent()
self.x = math.ceil((self.parent.width - self.width) / 2)
self.y = math.ceil((self.parent.height - self.height) / 2)
UI.Window.setParent(self)
end
function UI.Throttle:enable()
self.c = os.clock()
self.enabled = false
end
function UI.Throttle:disable()
if self.canvas then
self.enabled = false
self.canvas:removeLayer()
self.canvas = nil
self.ctr = 0
end
end
function UI.Throttle:update()
local cc = os.clock()
if cc > self.c + self.timeout then
os.sleep(0)
self.c = os.clock()
self.enabled = true
if not self.canvas then
self.canvas = self:addLayer(self.backgroundColor, self.borderColor)
self.canvas:setVisible(true)
self:clear(self.borderColor)
end
local image = self.image[self.ctr + 1]
local width = self.width - 2
for i = 0, #self.image do
self:write(2, i + 1, image:sub(width * i + 1, width * i + width),
self.backgroundColor, self.textColor)
end
self.ctr = (self.ctr + 1) % #self.image
self:sync()
end
end

View File

@@ -0,0 +1,73 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
local _rep = string.rep
local _sub = string.sub
-- For manipulating text in a fixed width string
local SB = class()
function SB:init(width)
self.width = width
self.buf = _rep(' ', width)
end
function SB:insert(x, str, width)
if x < 1 then
x = self.width + x + 1
end
width = width or #str
if x + width - 1 > self.width then
width = self.width - x
end
if width > 0 then
self.buf = _sub(self.buf, 1, x - 1) .. _sub(str, 1, width) .. _sub(self.buf, x + width)
end
end
function SB:fill(x, ch, width)
width = width or self.width - x + 1
self:insert(x, _rep(ch, width))
end
function SB:center(str)
self:insert(math.max(1, math.ceil((self.width - #str + 1) / 2)), str)
end
function SB:get()
return self.buf
end
UI.TitleBar = class(UI.Window)
UI.TitleBar.defaults = {
UIElement = 'TitleBar',
height = 1,
textColor = colors.white,
backgroundColor = colors.cyan,
title = '',
frameChar = UI.extChars and '\140' or '-',
closeInd = UI.extChars and '\215' or '*',
}
function UI.TitleBar:draw()
local sb = SB(self.width)
sb:fill(2, self.frameChar, sb.width - 3)
sb:center(string.format(' %s ', self.title))
if self.previousPage or self.event then
sb:insert(-1, self.closeInd)
else
sb:insert(-2, self.frameChar)
end
self:write(1, 1, sb:get())
end
function UI.TitleBar:eventHandler(event)
if event.type == 'mouse_click' then
if (self.previousPage or self.event) and event.x == self.width then
if self.event then
self:emit({ type = self.event, element = self })
elseif type(self.previousPage) == 'string' or
type(self.previousPage) == 'table' then
UI:setPage(self.previousPage)
else
UI:setPreviousPage()
end
return true
end
end
end

View File

@@ -0,0 +1,18 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
UI.VerticalMeter = class(UI.Window)
UI.VerticalMeter.defaults = {
UIElement = 'VerticalMeter',
backgroundColor = colors.gray,
meterColor = colors.lime,
width = 1,
value = 0,
}
function UI.VerticalMeter:draw()
local height = self.height - math.ceil(self.value / 100 * self.height)
self:clear()
self:clearArea(1, height + 1, self.width, self.height, self.meterColor)
end

View File

@@ -0,0 +1,100 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
--[[-- Viewport --]]--
UI.Viewport = class(UI.Window)
UI.Viewport.defaults = {
UIElement = 'Viewport',
backgroundColor = colors.cyan,
accelerators = {
down = 'scroll_down',
up = 'scroll_up',
home = 'scroll_top',
[ 'end' ] = 'scroll_bottom',
pageUp = 'scroll_pageUp',
[ 'control-b' ] = 'scroll_pageUp',
pageDown = 'scroll_pageDown',
[ 'control-f' ] = 'scroll_pageDown',
},
}
function UI.Viewport:layout()
UI.Window.layout(self)
if not self.canvas then
self.canvas = self:addLayer()
else
self.canvas:resize(self.width, self.height)
end
end
function UI.Viewport:enable()
UI.Window.enable(self)
self.canvas:setVisible(true)
end
function UI.Viewport:disable()
UI.Window.disable(self)
self.canvas:setVisible(false)
end
function UI.Viewport:setScrollPosition(offset)
local oldOffset = self.offy
self.offy = math.max(offset, 0)
self.offy = math.min(self.offy, math.max(#self.canvas.lines, self.height) - self.height)
if self.offy ~= oldOffset then
if self.scrollBar then
self.scrollBar:draw()
end
self.canvas.offy = offset
self.canvas:dirty()
end
end
function UI.Viewport:write(x, y, text, bg, tc)
if y > #self.canvas.lines then
for i = #self.canvas.lines, y do
self.canvas.lines[i + 1] = { }
self.canvas:clearLine(i + 1, self.backgroundColor, self.textColor)
end
end
return UI.Window.write(self, x, y, text, bg, tc)
end
function UI.Viewport:reset()
self.offy = 0
self.canvas.offy = 0
for i = self.height + 1, #self.canvas.lines do
self.canvas.lines[i] = nil
end
end
function UI.Viewport:getViewArea()
return {
y = (self.offy or 0) + 1,
height = self.height,
totalHeight = #self.canvas.lines,
offsetY = self.offy or 0,
}
end
function UI.Viewport:eventHandler(event)
if event.type == 'scroll_down' then
self:setScrollPosition(self.offy + 1)
elseif event.type == 'scroll_up' then
self:setScrollPosition(self.offy - 1)
elseif event.type == 'scroll_top' then
self:setScrollPosition(0)
elseif event.type == 'scroll_bottom' then
self:setScrollPosition(10000000)
elseif event.type == 'scroll_pageUp' then
self:setScrollPosition(self.offy - self.height)
elseif event.type == 'scroll_pageDown' then
self:setScrollPosition(self.offy + self.height)
elseif event.type == 'scroll_to' then
self:setScrollPosition(event.offset)
else
return false
end
return true
end

View File

@@ -0,0 +1,124 @@
local class = require('opus.class')
local UI = require('opus.ui')
local Util = require('opus.util')
UI.Wizard = class(UI.Window)
UI.Wizard.defaults = {
UIElement = 'Wizard',
pages = { },
}
function UI.Wizard:postInit()
self.cancelButton = UI.Button {
x = 2, y = -1,
text = 'Cancel',
event = 'cancel',
}
self.previousButton = UI.Button {
x = -18, y = -1,
text = '< Back',
event = 'previousView',
}
self.nextButton = UI.Button {
x = -9, y = -1,
text = 'Next >',
event = 'nextView',
}
Util.merge(self, self.pages)
--for _, child in pairs(self.pages) do
-- child.ey = -2
--end
end
function UI.Wizard:add(pages)
Util.merge(self.pages, pages)
Util.merge(self, pages)
for _, child in pairs(self.pages) do
child.ey = child.ey or -2
end
if self.parent then
self:initChildren()
end
end
function UI.Wizard:getPage(index)
return Util.find(self.pages, 'index', index)
end
function UI.Wizard:enable(...)
self.enabled = true
self.index = 1
self.transitionHint = nil
local initial = self:getPage(1)
for _,child in pairs(self.children) do
if child == initial or not child.index then
child:enable(...)
else
child:disable()
end
end
self:emit({ type = 'enable_view', next = initial })
end
function UI.Wizard:isViewValid()
local currentView = self:getPage(self.index)
return not currentView.validate and true or currentView:validate()
end
function UI.Wizard:eventHandler(event)
if event.type == 'nextView' then
local currentView = self:getPage(self.index)
if self:isViewValid() then
self.index = self.index + 1
local nextView = self:getPage(self.index)
currentView:emit({ type = 'enable_view', next = nextView, current = currentView })
end
elseif event.type == 'previousView' then
local currentView = self:getPage(self.index)
local nextView = self:getPage(self.index - 1)
if nextView then
self.index = self.index - 1
currentView:emit({ type = 'enable_view', prev = nextView, current = currentView })
end
return true
elseif event.type == 'wizard_complete' then
if self:isViewValid() then
self:emit({ type = 'accept' })
end
elseif event.type == 'enable_view' then
local current = event.next or event.prev
if not current then error('property "index" is required on wizard pages') end
if event.current then
if event.next then
self.transitionHint = 'slideLeft'
elseif event.prev then
self.transitionHint = 'slideRight'
end
event.current:disable()
end
if self:getPage(self.index - 1) then
self.previousButton:enable()
else
self.previousButton:disable()
end
if self:getPage(self.index + 1) then
self.nextButton.text = 'Next >'
self.nextButton.event = 'nextView'
else
self.nextButton.text = 'Accept'
self.nextButton.event = 'wizard_complete'
end
-- a new current view
current:enable()
current:emit({ type = 'view_enabled', view = current })
self:draw()
end
end

View File

@@ -0,0 +1,11 @@
local class = require('opus.class')
local UI = require('opus.ui')
local colors = _G.colors
UI.WizardPage = class(UI.ActiveLayer)
UI.WizardPage.defaults = {
UIElement = 'WizardPage',
backgroundColor = colors.cyan,
ey = -2,
}

View File

@@ -0,0 +1,262 @@
--
-- tek.lib.region
-- Written by Timm S. Mueller <tmueller at schulze-mueller.de>
--
-- Copyright 2008 - 2016 by the authors and contributors:
--
-- * Timm S. Muller <tmueller at schulze-mueller.de>
-- * Franciska Schulze <fschulze at schulze-mueller.de>
-- * Tobias Schwinger <tschwinger at isonews2.com>
--
-- https://opensource.org/licenses/MIT
--
-- Some comments have been removed to reduce file size, see:
-- https://github.com/technosaurus/tekui/blob/master/etc/region.lua
-- for the full source
local insert = table.insert
local ipairs = ipairs
local max = math.max
local min = math.min
local setmetatable = setmetatable
local unpack = unpack or table.unpack
local Region = { }
Region._VERSION = "Region 11.3"
Region.__index = Region
-- x0, y0, x1, y1 = Region.intersect(d1, d2, d3, d4, s1, s2, s3, s4):
-- Returns the coordinates of a rectangle where a rectangle specified by
-- the coordinates s1, s2, s3, s4 overlaps with the rectangle specified
-- by the coordinates d1, d2, d3, d4. The return value is '''nil''' if
-- the rectangles do not overlap.
function Region.intersect(d1, d2, d3, d4, s1, s2, s3, s4)
if s3 >= d1 and s1 <= d3 and s4 >= d2 and s2 <= d4 then
return max(s1, d1), max(s2, d2), min(s3, d3), min(s4, d4)
end
end
-- insertrect: insert rect to table, merging with an existing one if possible
local function insertrect(d, s1, s2, s3, s4)
for i = 1, min(4, #d) do
local a = d[i]
local a1, a2, a3, a4 = a[1], a[2], a[3], a[4]
if a2 == s2 and a4 == s4 then
if a3 + 1 == s1 then
a[3] = s3
return
elseif a1 == s3 + 1 then
a[1] = s1
return
end
elseif a1 == s1 and a3 == s3 then
if a4 + 1 == s2 then
a[4] = s4
return
elseif a2 == s4 + 1 then
a[2] = s2
return
end
end
end
insert(d, 1, { s1, s2, s3, s4 })
end
-- cutrect: cut rect d into table of new rects, using rect s as a punch
local function cutrect(d1, d2, d3, d4, s1, s2, s3, s4)
if not Region.intersect(d1, d2, d3, d4, s1, s2, s3, s4) then
return { { d1, d2, d3, d4 } }
end
local r = { }
if d1 < s1 then
insertrect(r, d1, d2, s1 - 1, d4)
d1 = s1
end
if d2 < s2 then
insertrect(r, d1, d2, d3, s2 - 1)
d2 = s2
end
if d3 > s3 then
insertrect(r, s3 + 1, d2, d3, d4)
d3 = s3
end
if d4 > s4 then
insertrect(r, d1, s4 + 1, d3, d4)
end
return r
end
-- cutregion: cut region d, using s as a punch
local function cutregion(d, s1, s2, s3, s4)
local r = { }
for _, dr in ipairs(d) do
local d1, d2, d3, d4 = dr[1], dr[2], dr[3], dr[4]
for _, t in ipairs(cutrect(d1, d2, d3, d4, s1, s2, s3, s4)) do
insertrect(r, t[1], t[2], t[3], t[4])
end
end
return r
end
-- region = Region.new(r1, r2, r3, r4): Creates a new region from the given
-- coordinates.
function Region.new(r1, r2, r3, r4)
if r1 then
return setmetatable({ region = { { r1, r2, r3, r4 } } }, Region)
end
return setmetatable({ region = { } }, Region)
end
-- self = region:setRect(r1, r2, r3, r4): Resets an existing region
-- to the specified rectangle.
function Region:setRect(r1, r2, r3, r4)
self.region = { { r1, r2, r3, r4 } }
return self
end
-- region:orRect(r1, r2, r3, r4): Logical ''or''s a rectangle to a region
function Region:orRect(s1, s2, s3, s4)
self.region = cutregion(self.region, s1, s2, s3, s4)
insertrect(self.region, s1, s2, s3, s4)
end
-- region:orRegion(region): Logical ''or''s another region to a region
function Region:orRegion(s)
for _, r in ipairs(s) do
self:orRect(r[1], r[2], r[3], r[4])
end
end
-- region:andRect(r1, r2, r3, r4): Logical ''and''s a rectange to a region
function Region:andRect(s1, s2, s3, s4)
local r = { }
for _, d in ipairs(self.region) do
local t1, t2, t3, t4 =
Region.intersect(d[1], d[2], d[3], d[4], s1, s2, s3, s4)
if t1 then
insertrect(r, t1, t2, t3, t4)
end
end
self.region = r
end
-- region:xorRect(r1, r2, r3, r4): Logical ''xor''s a rectange to a region
function Region:xorRect(s1, s2, s3, s4)
local r1 = { }
local r2 = { { s1, s2, s3, s4 } }
for _, d in ipairs(self.region) do
local d1, d2, d3, d4 = d[1], d[2], d[3], d[4]
for _, t in ipairs(cutrect(d1, d2, d3, d4, s1, s2, s3, s4)) do
insertrect(r1, t[1], t[2], t[3], t[4])
end
r2 = cutregion(r2, d1, d2, d3, d4)
end
self.region = r1
self:orRegion(r2)
end
-- self = region:subRect(r1, r2, r3, r4): Subtracts a rectangle from a region
function Region:subRect(s1, s2, s3, s4)
local r1 = { }
for _, d in ipairs(self.region) do
local d1, d2, d3, d4 = d[1], d[2], d[3], d[4]
for _, t in ipairs(cutrect(d1, d2, d3, d4, s1, s2, s3, s4)) do
insertrect(r1, t[1], t[2], t[3], t[4])
end
end
self.region = r1
return self
end
-- region:getRect - gets an iterator on the rectangles in a region [internal]
function Region:getRects()
local index = 0
return function(object)
index = index + 1
if object[index] then
return unpack(object[index])
end
end, self.region
end
-- success = region:checkIntersect(x0, y0, x1, y1): Returns a boolean
-- indicating whether a rectangle specified by its coordinates overlaps
-- with a region.
function Region:checkIntersect(s1, s2, s3, s4)
for _, d in ipairs(self.region) do
if Region.intersect(d[1], d[2], d[3], d[4], s1, s2, s3, s4) then
return true
end
end
return false
end
-- region:subRegion(region2): Subtracts {{region2}} from {{region}}.
function Region:subRegion(region)
if region then
for r1, r2, r3, r4 in region:getRects() do
self:subRect(r1, r2, r3, r4)
end
end
end
-- region:andRegion(r): Logically ''and''s a region to a region
function Region:andRegion(s)
local r = { }
for _, s in ipairs(s.region) do
for _, d in ipairs(self.region) do
local t1, t2, t3, t4 =
Region.intersect(d[1], d[2], d[3], d[4],
s[1], s[2], s[3], s[4])
if t1 then
insertrect(r, t1, t2, t3, t4)
end
end
end
self.region = r
end
-- region:forEach(func, obj, ...): For each rectangle in a region, calls the
-- specified function according the following scheme:
-- func(obj, x0, y0, x1, y1, ...)
-- Extra arguments are passed through to the function.
function Region:forEach(func, obj, ...)
for x0, y0, x1, y1 in self:getRects() do
func(obj, x0, y0, x1, y1, ...)
end
end
-- region:shift(dx, dy): Shifts a region by delta x and y.
function Region:shift(dx, dy)
for _, r in ipairs(self.region) do
r[1] = r[1] + dx
r[2] = r[2] + dy
r[3] = r[3] + dx
r[4] = r[4] + dy
end
end
-- region:isEmpty(): Returns '''true''' if a region is empty.
function Region:isEmpty()
return #self.region == 0
end
-- minx, miny, maxx, maxy = region:get(): Get region's min/max extents
function Region:get()
if #self.region > 0 then
local minx = 1000000 -- ui.HUGE
local miny = 1000000
local maxx = 0
local maxy = 0
for _, r in ipairs(self.region) do
minx = min(minx, r[1])
miny = min(miny, r[2])
maxx = max(maxx, r[3])
maxy = max(maxy, r[4])
end
return minx, miny, maxx, maxy
end
end
return Region

View File

@@ -0,0 +1,53 @@
local Tween = require('opus.ui.tween')
local Transition = { }
function Transition.slideLeft(args)
local ticks = args.ticks or 10
local easing = args.easing or 'outQuint'
local pos = { x = args.ex }
local tween = Tween.new(ticks, pos, { x = args.x }, easing)
args.canvas:move(pos.x, args.canvas.y)
return function()
local finished = tween:update(1)
args.canvas:move(math.floor(pos.x), args.canvas.y)
args.canvas:dirty()
return not finished
end
end
function Transition.slideRight(args)
local ticks = args.ticks or 10
local easing = args.easing or'outQuint'
local pos = { x = -args.canvas.width }
local tween = Tween.new(ticks, pos, { x = 1 }, easing)
args.canvas:move(pos.x, args.canvas.y)
return function()
local finished = tween:update(1)
args.canvas:move(math.floor(pos.x), args.canvas.y)
args.canvas:dirty()
return not finished
end
end
function Transition.expandUp(args)
local ticks = args.ticks or 3
local easing = args.easing or 'linear'
local pos = { y = args.ey + 1 }
local tween = Tween.new(ticks, pos, { y = args.y }, easing)
args.canvas:move(args.x, pos.y)
return function()
local finished = tween:update(1)
args.canvas:move(args.x, math.floor(pos.y))
args.canvas:dirty()
return not finished
end
end
return Transition

View File

@@ -0,0 +1,350 @@
local tween = {
_VERSION = 'tween 2.1.1',
_DESCRIPTION = 'tweening for lua',
_URL = 'https://github.com/kikito/tween.lua',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2014 Enrique García Cota, Yuichi Tateno, Emmanuel Oga
Licence details: https://opensource.org/licenses/MIT
]]
}
-- easing
-- Adapted from https://github.com/EmmanuelOga/easing. See LICENSE.txt for credits.
-- For all easing functions:
-- t = time == how much time has to pass for the tweening to complete
-- b = begin == starting property value
-- c = change == ending - beginning
-- d = duration == running time. How much time has passed *right now*
local pow, sin, cos, pi, sqrt, abs, asin = math.pow, math.sin, math.cos, math.pi, math.sqrt, math.abs, math.asin
-- linear
local function linear(t, b, c, d) return c * t / d + b end
-- quad
local function inQuad(t, b, c, d) return c * pow(t / d, 2) + b end
local function outQuad(t, b, c, d)
t = t / d
return -c * t * (t - 2) + b
end
local function inOutQuad(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 2) + b end
return -c / 2 * ((t - 1) * (t - 3) - 1) + b
end
local function outInQuad(t, b, c, d)
if t < d / 2 then return outQuad(t * 2, b, c / 2, d) end
return inQuad((t * 2) - d, b + c / 2, c / 2, d)
end
-- cubic
local function inCubic (t, b, c, d) return c * pow(t / d, 3) + b end
local function outCubic(t, b, c, d) return c * (pow(t / d - 1, 3) + 1) + b end
local function inOutCubic(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * t * t * t + b end
t = t - 2
return c / 2 * (t * t * t + 2) + b
end
local function outInCubic(t, b, c, d)
if t < d / 2 then return outCubic(t * 2, b, c / 2, d) end
return inCubic((t * 2) - d, b + c / 2, c / 2, d)
end
-- quart
local function inQuart(t, b, c, d) return c * pow(t / d, 4) + b end
local function outQuart(t, b, c, d) return -c * (pow(t / d - 1, 4) - 1) + b end
local function inOutQuart(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 4) + b end
return -c / 2 * (pow(t - 2, 4) - 2) + b
end
local function outInQuart(t, b, c, d)
if t < d / 2 then return outQuart(t * 2, b, c / 2, d) end
return inQuart((t * 2) - d, b + c / 2, c / 2, d)
end
-- quint
local function inQuint(t, b, c, d) return c * pow(t / d, 5) + b end
local function outQuint(t, b, c, d) return c * (pow(t / d - 1, 5) + 1) + b end
local function inOutQuint(t, b, c, d)
t = t / d * 2
if t < 1 then return c / 2 * pow(t, 5) + b end
return c / 2 * (pow(t - 2, 5) + 2) + b
end
local function outInQuint(t, b, c, d)
if t < d / 2 then return outQuint(t * 2, b, c / 2, d) end
return inQuint((t * 2) - d, b + c / 2, c / 2, d)
end
-- sine
local function inSine(t, b, c, d) return -c * cos(t / d * (pi / 2)) + c + b end
local function outSine(t, b, c, d) return c * sin(t / d * (pi / 2)) + b end
local function inOutSine(t, b, c, d) return -c / 2 * (cos(pi * t / d) - 1) + b end
local function outInSine(t, b, c, d)
if t < d / 2 then return outSine(t * 2, b, c / 2, d) end
return inSine((t * 2) -d, b + c / 2, c / 2, d)
end
-- expo
local function inExpo(t, b, c, d)
if t == 0 then return b end
return c * pow(2, 10 * (t / d - 1)) + b - c * 0.001
end
local function outExpo(t, b, c, d)
if t == d then return b + c end
return c * 1.001 * (-pow(2, -10 * t / d) + 1) + b
end
local function inOutExpo(t, b, c, d)
if t == 0 then return b end
if t == d then return b + c end
t = t / d * 2
if t < 1 then return c / 2 * pow(2, 10 * (t - 1)) + b - c * 0.0005 end
return c / 2 * 1.0005 * (-pow(2, -10 * (t - 1)) + 2) + b
end
local function outInExpo(t, b, c, d)
if t < d / 2 then return outExpo(t * 2, b, c / 2, d) end
return inExpo((t * 2) - d, b + c / 2, c / 2, d)
end
-- circ
local function inCirc(t, b, c, d) return(-c * (sqrt(1 - pow(t / d, 2)) - 1) + b) end
local function outCirc(t, b, c, d) return(c * sqrt(1 - pow(t / d - 1, 2)) + b) end
local function inOutCirc(t, b, c, d)
t = t / d * 2
if t < 1 then return -c / 2 * (sqrt(1 - t * t) - 1) + b end
t = t - 2
return c / 2 * (sqrt(1 - t * t) + 1) + b
end
local function outInCirc(t, b, c, d)
if t < d / 2 then return outCirc(t * 2, b, c / 2, d) end
return inCirc((t * 2) - d, b + c / 2, c / 2, d)
end
-- elastic
local function calculatePAS(p,a,c,d)
p, a = p or d * 0.3, a or 0
if a < abs(c) then return p, c, p / 4 end -- p, a, s
return p, a, p / (2 * pi) * asin(c/a) -- p,a,s
end
local function inElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
t = t - 1
return -(a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b
end
local function outElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d
if t == 1 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p) + c + b
end
local function inOutElastic(t, b, c, d, a, p)
local s
if t == 0 then return b end
t = t / d * 2
if t == 2 then return b + c end
p,a,s = calculatePAS(p,a,c,d)
t = t - 1
if t < 0 then return -0.5 * (a * pow(2, 10 * t) * sin((t * d - s) * (2 * pi) / p)) + b end
return a * pow(2, -10 * t) * sin((t * d - s) * (2 * pi) / p ) * 0.5 + c + b
end
local function outInElastic(t, b, c, d, a, p)
if t < d / 2 then return outElastic(t * 2, b, c / 2, d, a, p) end
return inElastic((t * 2) - d, b + c / 2, c / 2, d, a, p)
end
-- back
local function inBack(t, b, c, d, s)
s = s or 1.70158
t = t / d
return c * t * t * ((s + 1) * t - s) + b
end
local function outBack(t, b, c, d, s)
s = s or 1.70158
t = t / d - 1
return c * (t * t * ((s + 1) * t + s) + 1) + b
end
local function inOutBack(t, b, c, d, s)
s = (s or 1.70158) * 1.525
t = t / d * 2
if t < 1 then return c / 2 * (t * t * ((s + 1) * t - s)) + b end
t = t - 2
return c / 2 * (t * t * ((s + 1) * t + s) + 2) + b
end
local function outInBack(t, b, c, d, s)
if t < d / 2 then return outBack(t * 2, b, c / 2, d, s) end
return inBack((t * 2) - d, b + c / 2, c / 2, d, s)
end
-- bounce
local function outBounce(t, b, c, d)
t = t / d
if t < 1 / 2.75 then return c * (7.5625 * t * t) + b end
if t < 2 / 2.75 then
t = t - (1.5 / 2.75)
return c * (7.5625 * t * t + 0.75) + b
elseif t < 2.5 / 2.75 then
t = t - (2.25 / 2.75)
return c * (7.5625 * t * t + 0.9375) + b
end
t = t - (2.625 / 2.75)
return c * (7.5625 * t * t + 0.984375) + b
end
local function inBounce(t, b, c, d) return c - outBounce(d - t, 0, c, d) + b end
local function inOutBounce(t, b, c, d)
if t < d / 2 then return inBounce(t * 2, 0, c, d) * 0.5 + b end
return outBounce(t * 2 - d, 0, c, d) * 0.5 + c * .5 + b
end
local function outInBounce(t, b, c, d)
if t < d / 2 then return outBounce(t * 2, b, c / 2, d) end
return inBounce((t * 2) - d, b + c / 2, c / 2, d)
end
tween.easing = {
linear = linear,
inQuad = inQuad, outQuad = outQuad, inOutQuad = inOutQuad, outInQuad = outInQuad,
inCubic = inCubic, outCubic = outCubic, inOutCubic = inOutCubic, outInCubic = outInCubic,
inQuart = inQuart, outQuart = outQuart, inOutQuart = inOutQuart, outInQuart = outInQuart,
inQuint = inQuint, outQuint = outQuint, inOutQuint = inOutQuint, outInQuint = outInQuint,
inSine = inSine, outSine = outSine, inOutSine = inOutSine, outInSine = outInSine,
inExpo = inExpo, outExpo = outExpo, inOutExpo = inOutExpo, outInExpo = outInExpo,
inCirc = inCirc, outCirc = outCirc, inOutCirc = inOutCirc, outInCirc = outInCirc,
inElastic = inElastic, outElastic = outElastic, inOutElastic = inOutElastic, outInElastic = outInElastic,
inBack = inBack, outBack = outBack, inOutBack = inOutBack, outInBack = outInBack,
inBounce = inBounce, outBounce = outBounce, inOutBounce = inOutBounce, outInBounce = outInBounce
}
-- private stuff
local function copyTables(destination, keysTable, valuesTable)
valuesTable = valuesTable or keysTable
local mt = getmetatable(keysTable)
if mt and getmetatable(destination) == nil then
setmetatable(destination, mt)
end
for k,v in pairs(keysTable) do
if type(v) == 'table' then
destination[k] = copyTables({}, v, valuesTable[k])
else
destination[k] = valuesTable[k]
end
end
return destination
end
local function checkSubjectAndTargetRecursively(subject, target, path)
path = path or {}
local targetType, newPath
for k,targetValue in pairs(target) do
targetType, newPath = type(targetValue), copyTables({}, path)
table.insert(newPath, tostring(k))
if targetType == 'number' then
assert(type(subject[k]) == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' is missing from subject or isn't a number")
elseif targetType == 'table' then
checkSubjectAndTargetRecursively(subject[k], targetValue, newPath)
else
assert(targetType == 'number', "Parameter '" .. table.concat(newPath,'/') .. "' must be a number or table of numbers")
end
end
end
local function checkNewParams(duration, subject, target, easing)
assert(type(duration) == 'number' and duration > 0, "duration must be a positive number. Was " .. tostring(duration))
local tsubject = type(subject)
assert(tsubject == 'table' or tsubject == 'userdata', "subject must be a table or userdata. Was " .. tostring(subject))
assert(type(target)== 'table', "target must be a table. Was " .. tostring(target))
assert(type(easing)=='function', "easing must be a function. Was " .. tostring(easing))
checkSubjectAndTargetRecursively(subject, target)
end
local function getEasingFunction(easing)
easing = easing or "linear"
if type(easing) == 'string' then
local name = easing
easing = tween.easing[name]
if type(easing) ~= 'function' then
error("The easing function name '" .. name .. "' is invalid")
end
end
return easing
end
local function performEasingOnSubject(subject, target, initial, clock, duration, easing)
local t,b,c,d
for k,v in pairs(target) do
if type(v) == 'table' then
performEasingOnSubject(subject[k], v, initial[k], clock, duration, easing)
else
t,b,c,d = clock, initial[k], v - initial[k], duration
subject[k] = easing(t,b,c,d)
end
end
end
-- Tween methods
local Tween = {}
local Tween_mt = {__index = Tween}
function Tween:set(clock)
assert(type(clock) == 'number', "clock must be a positive number or 0")
self.initial = self.initial or copyTables({}, self.target, self.subject)
self.clock = clock
if self.clock <= 0 then
self.clock = 0
copyTables(self.subject, self.initial)
elseif self.clock >= self.duration then -- the tween has expired
self.clock = self.duration
copyTables(self.subject, self.target)
else
performEasingOnSubject(self.subject, self.target, self.initial, self.clock, self.duration, self.easing)
end
return self.clock >= self.duration
end
function Tween:reset()
return self:set(0)
end
function Tween:update(dt)
assert(type(dt) == 'number', "dt must be a number")
return self:set(self.clock + dt)
end
-- Public interface
function tween.new(duration, subject, target, easing)
easing = getEasingFunction(easing)
checkNewParams(duration, subject, target, easing)
return setmetatable({
duration = duration,
subject = subject,
target = target,
easing = easing,
clock = 0
}, Tween_mt)
end
return tween

801
sys/modules/opus/util.lua Normal file
View File

@@ -0,0 +1,801 @@
local Util = { }
local fs = _G.fs
local http = _G.http
local os = _G.os
local term = _G.term
local textutils = _G.textutils
local _sformat = string.format
local _srep = string.rep
local _ssub = string.sub
function Util.hexToByteArray(str)
local r = {}
str = tostring(str)
for b in str:gmatch("%x%x?") do
r[#r+1] = tonumber(b, 16)
end
return r
end
function Util.byteArrayToHex(tbl)
return ("%02x"):rep(#tbl):format(unpack(tbl))
end
function Util.tryTimed(timeout, f, ...)
local c = os.clock()
repeat
local ret = f(...)
if ret then
return ret
end
until os.clock()-c >= timeout
end
function Util.tryTimes(attempts, f, ...)
local result
for _ = 1, attempts do
result = { f(...) }
if result[1] then
return unpack(result)
end
end
return unpack(result)
end
function Util.timer()
local ct = os.clock()
return function()
return os.clock() - ct
end
end
Util.Timer = Util.timer -- deprecate
function Util.throttle(fn)
local ts = os.clock()
local timeout = .095
return function(...)
local nts = os.clock()
if nts > ts + timeout then
os.sleep(0)
ts = os.clock()
if fn then
fn(...)
end
end
end
end
function Util.tostring(pattern, ...)
local function serialize(tbl, width)
local str = '{\n'
for k, v in pairs(tbl) do
local value
if type(v) == 'table' then
value = _sformat('table: %d', Util.size(v))
else
value = tostring(v)
end
str = str .. _sformat(' %s: %s\n', k, value)
end
--if #str < width then
--str = str:gsub('\n', '') .. ' }'
--else
str = str .. '}'
--end
return str
end
if type(pattern) == 'string' then
if select('#', ...) == 0 then
return pattern
end
return _sformat(pattern, ...)
elseif type(pattern) == 'table' then
return serialize(pattern, term.current().getSize())
end
return tostring(pattern)
end
function Util.print(pattern, ...)
print(Util.tostring(pattern, ...))
end
function Util.getVersion()
local version
if _G._CC_VERSION then
version = tonumber(_G._CC_VERSION:match('[%d]+%.?[%d][%d]'))
end
if not version and _G._HOST then
version = tonumber(_G._HOST:match('[%d]+%.?[%d][%d]'))
end
return version or 1.7
end
function Util.getMinecraftVersion()
local mcVersion = _G._MC_VERSION or 'unknown'
if _G._HOST then
local version = _G._HOST:match('%S+ %S+ %((%S.+)%)')
if version then
mcVersion = version:match('Minecraft (%S+)') or version
end
end
return mcVersion
end
function Util.checkMinecraftVersion(minVersion)
local version = Util.getMinecraftVersion()
local function convert(v)
local m1, m2, m3 = v:match('(%d)%.(%d)%.?(%d?)')
return tonumber(m1) * 10000 + tonumber(m2) * 100 + (tonumber(m3) or 0)
end
return convert(version) >= convert(tostring(minVersion))
end
function Util.signum(num)
if num > 0 then
return 1
elseif num < 0 then
return -1
else
return 0
end
end
function Util.clamp(num, low, high)
return num < low and low or num > high and high or num
end
-- http://lua-users.org/wiki/SimpleRound
function Util.round(num, idp)
local mult = 10^(idp or 0)
return Util.signum(num) * math.floor(math.abs(num) * mult + 0.5) / mult
end
function Util.randomFloat(max, min)
min = min or 0
max = max or 1
return (max-min) * math.random() + min
end
--[[ Table functions ]] --
function Util.clear(t)
local keys = Util.keys(t)
for _,k in pairs(keys) do
t[k] = nil
end
end
function Util.empty(t)
return not next(t)
end
function Util.key(t, value)
for k,v in pairs(t) do
if v == value then
return k
end
end
end
function Util.keys(t)
local keys = { }
for k in pairs(t) do
keys[#keys+1] = k
end
return keys
end
function Util.merge(obj, args)
if args then
for k,v in pairs(args) do
obj[k] = v
end
end
return obj
end
function Util.deepMerge(obj, args)
if args then
for k,v in pairs(args) do
if type(v) == 'table' then
if not obj[k] then
obj[k] = { }
end
Util.deepMerge(obj[k], v)
else
obj[k] = v
end
end
end
end
function Util.transpose(t)
local tt = { }
for k,v in pairs(t) do
tt[v] = k
end
return tt
end
function Util.contains(t, value)
for k,v in pairs(t) do
if v == value then
return k
end
end
end
function Util.find(t, name, value)
for k,v in pairs(t) do
if v[name] == value then
return v, k
end
end
end
function Util.findAll(t, name, value)
local rt = { }
for _,v in pairs(t) do
if v[name] == value then
table.insert(rt, v)
end
end
return rt
end
function Util.shallowCopy(t)
if not t then error('Util.shallowCopy: invalid table', 2) end
local t2 = { }
for k,v in pairs(t) do
t2[k] = v
end
return t2
end
function Util.deepCopy(t)
if type(t) ~= 'table' then
return t
end
--local mt = getmetatable(t)
local res = {}
for k,v in pairs(t) do
if type(v) == 'table' then
v = Util.deepCopy(v)
end
res[k] = v
end
--setmetatable(res,mt)
return res
end
-- http://snippets.luacode.org/?p=snippets/Filter_a_table_in-place_119
function Util.filterInplace(t, predicate)
local j = 1
for i = 1,#t do
local v = t[i]
if predicate(v) then
t[j] = v
j = j + 1
end
end
while t[j] ~= nil do
t[j] = nil
j = j + 1
end
return t
end
function Util.filter(it, f)
local ot = { }
for k,v in pairs(it) do
if f(v) then
ot[k] = v
end
end
return ot
end
function Util.reduce(t, fn, acc)
acc = acc or 0
for _, v in pairs(t) do
acc = fn(acc, v)
end
return acc
end
function Util.size(list)
if type(list) == 'table' then
local length = 0
for _ in pairs(list) do
length = length + 1
end
return length
end
return 0
end
local function isArray(value)
-- dubious
return type(value) == "table" and (value[1] or next(value) == nil)
end
function Util.removeByValue(t, e)
for k,v in pairs(t) do
if v == e then
if isArray(t) then
table.remove(t, k)
else
t[k] = nil
end
break
end
end
end
function Util.any(t, fn)
for _,v in pairs(t) do
if fn(v) then
return true
end
end
end
function Util.every(t, fn)
for _,v in pairs(t) do
if not fn(v) then
return false
end
end
return true
end
function Util.each(list, func)
for index, value in pairs(list) do
func(value, index, list)
end
end
function Util.rpairs(t)
local tkeys = Util.keys(t)
local i = #tkeys
return function()
local key = tkeys[i]
local k,v = key, t[key]
i = i - 1
if v then
return k, v
end
end
end
-- http://stackoverflow.com/questions/15706270/sort-a-table-in-lua
function Util.spairs(t, order)
local keys = Util.keys(t)
-- if order function given, sort by it by passing the table and keys a, b,
-- otherwise just sort the keys
if order then
table.sort(keys, function(a,b) return order(t[a], t[b]) end)
else
table.sort(keys)
end
-- return the iterator function
local i = 0
return function()
i = i + 1
if keys[i] then
return keys[i], t[keys[i]]
end
end
end
function Util.first(t, order)
local keys = Util.keys(t)
if order then
table.sort(keys, function(a,b) return order(t[a], t[b]) end)
else
table.sort(keys)
end
return keys[1], t[keys[1]]
end
--[[ File functions ]]--
function Util.readFile(fname)
local f = fs.open(fname, "r")
if f then
local t = f.readAll()
f.close()
return t
end
end
function Util.backup(fname)
local backup = fname .. '.bak'
if backup then
fs.delete(backup)
end
fs.copy(fname, backup)
end
function Util.writeFile(fname, data)
if not fname or not data then error('Util.writeFile: invalid parameters', 2) end
if fs.exists(fname) then
local diff = #data - fs.getSize(fname)
if diff > 0 then
if fs.getFreeSpace(fs.getDir(fname)) < diff then
error('Insufficient disk space for ' .. fname)
end
end
end
local file = io.open(fname, "w")
if not file then
error('Unable to open ' .. fname, 2)
end
file:write(data)
file:close()
end
function Util.readLines(fname)
local file = fs.open(fname, "r")
if file then
local t = {}
local line = file.readLine()
while line do
table.insert(t, line)
line = file.readLine()
end
file.close()
return t
end
end
function Util.writeLines(fname, lines)
local file = fs.open(fname, 'w')
if file then
for _,line in ipairs(lines) do
file.writeLine(line)
end
file.close()
return true
end
end
function Util.readTable(fname)
local t = Util.readFile(fname)
if t then
return textutils.unserialize(t)
end
end
function Util.writeTable(fname, data)
Util.writeFile(fname, textutils.serialize(data))
end
function Util.loadTable(fname)
local fc = Util.readFile(fname)
if not fc then
return false, 'Unable to read file'
end
local s, m = loadstring('return ' .. fc, fname)
if s then
s, m = pcall(s)
if s then
return m
end
end
return s, m
end
--[[ loading and running functions ]] --
function Util.httpGet(url, headers, isBinary)
local h, msg = http.get(url, headers, isBinary)
if h then
local contents = h.readAll()
h.close()
return contents
end
return h, msg
end
function Util.download(url, filename)
local contents, msg = Util.httpGet(url)
if not contents then
error(_sformat('Failed to download %s\n%s', url, msg), 2)
end
if filename then
Util.writeFile(filename, contents)
end
return contents
end
function Util.loadUrl(url, env) -- loadfile equivalent
local c, msg = Util.httpGet(url)
if not c then
return c, msg
end
return load(c, url, nil, env)
end
function Util.runUrl(env, url, ...) -- os.run equivalent
setmetatable(env, { __index = _G })
local fn, m = Util.loadUrl(url, env)
if fn then
return pcall(fn, ...)
end
return fn, m
end
function Util.run(env, path, ...)
if type(env) ~= 'table' then error('Util.run: env must be a table', 2) end
setmetatable(env, { __index = _G })
local fn, m = loadfile(path, env)
if fn then
return pcall(fn, ...)
end
return fn, m
end
function Util.runFunction(env, fn, ...)
setfenv(fn, env)
setmetatable(env, { __index = _G })
return pcall(fn, ...)
end
--[[ String functions ]] --
function Util.toBytes(n)
if not tonumber(n) then error('Util.toBytes: n must be a number', 2) end
if n >= 1000000 or n <= -1000000 then
return _sformat('%sM', math.floor(n/1000000 * 10) / 10)
elseif n >= 10000 or n <= -10000 then
return _sformat('%sK', math.floor(n/1000))
elseif n >= 1000 or n <= -1000 then
return _sformat('%sK', math.floor(n/1000 * 10) / 10)
end
return tostring(n)
end
function Util.insertString(str, istr, pos)
return str:sub(1, pos - 1) .. istr .. str:sub(pos)
end
function Util.split(str, pattern)
if not str or type(str) ~= 'string' then error('Util.split: Invalid parameters', 2) end
pattern = pattern or "(.-)\n"
local t = {}
local function helper(line) table.insert(t, line) return "" end
helper((str:gsub(pattern, helper)))
return t
end
function Util.matches(str, pattern)
pattern = pattern or '%S+'
local t = { }
for s in str:gmatch(pattern) do
table.insert(t, s)
end
return t
end
function Util.startsWith(s, match)
return _ssub(s, 1, #match) == match
end
-- return a fixed length string using specified alignment
function Util.widthify(s, len, align)
s = s or ''
local slen = #s
if slen > len then
return _ssub(s, 1, len)
elseif slen == len then
return s
elseif align == 'center' then
local space = math.floor((len - slen) / 2)
s = _srep(' ', space) .. s
return s .. _srep(' ', len - #s)
elseif align == 'right' then
return _srep(' ', len - slen) .. s
end
return s .. _srep(' ', len - slen)
end
-- http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76
function Util.trim(s)
return s:find'^%s*$' and '' or s:match'^%s*(.*%S)'
end
-- trim whitespace from left end of string
function Util.triml(s)
return s:match'^%s*(.*)'
end
-- trim whitespace from right end of string
function Util.trimr(s)
return s:find'^%s*$' and '' or s:match'^(.*%S)'
end
-- end http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76
-- word wrapping based on:
-- https://www.rosettacode.org/wiki/Word_wrap#Lua and
-- http://lua-users.org/wiki/StringRecipes
local function paragraphwrap(text, linewidth, res)
linewidth = linewidth or 75
local spaceleft = linewidth
local line = { }
for word in text:gmatch("%S+") do
local len = #word + 1
--if colorMode then
-- word:gsub('()@([@%d])', function(pos, c) len = len - 2 end)
--end
if len > spaceleft then
table.insert(res, table.concat(line, ' '))
line = { word }
spaceleft = linewidth - len - 1
else
table.insert(line, word)
spaceleft = spaceleft - len
end
end
table.insert(res, table.concat(line, ' '))
return table.concat(res, '\n')
end
-- end word wrapping
function Util.wordWrap(str, limit)
local longLines = Util.split(str)
local lines = { }
for _,line in ipairs(longLines) do
paragraphwrap(line, limit, lines)
end
return lines
end
-- https://github.com/MightyPirates/OpenComputers
function Util.parse(...)
local params = table.pack(...)
local args = {}
local options = {}
local doneWithOptions = false
for i = 1, params.n do
local param = params[i]
if not doneWithOptions and type(param) == "string" then
if param == "--" then
doneWithOptions = true -- stop processing options at `--`
elseif param:sub(1, 2) == "--" then
local key, value = param:match("%-%-(.-)=(.*)")
if not key then
key, value = param:sub(3), true
end
options[key] = value
elseif param:sub(1, 1) == "-" and param ~= "-" then
for j = 2, string.len(param) do
options[string.sub(param, j, j)] = true
end
else
table.insert(args, param)
end
else
table.insert(args, param)
end
end
return args, options
end
function Util.args(arg)
local options, args = { }, { }
local k = 1
while k <= #arg do
local v = arg[k]
if _ssub(v, 1, 1) == '-' then
local opt = _ssub(v, 2)
options[opt] = arg[k + 1]
k = k + 1
else
table.insert(args, v)
end
k = k + 1
end
return options, args
end
-- http://lua-users.org/wiki/AlternativeGetOpt
local function getopt( arg, options )
local tab = {}
for k, v in ipairs(arg) do
if type(v) == 'string' then
if _ssub( v, 1, 2) == "--" then
local x = string.find( v, "=", 1, true )
if x then tab[ _ssub( v, 3, x-1 ) ] = _ssub( v, x+1 )
else tab[ _ssub( v, 3 ) ] = true
end
elseif _ssub( v, 1, 1 ) == "-" then
local y = 2
local l = string.len(v)
local jopt
while ( y <= l ) do
jopt = _ssub( v, y, y )
if string.find( options, jopt, 1, true ) then
if y < l then
tab[ jopt ] = _ssub( v, y+1 )
y = l
else
tab[ jopt ] = arg[ k + 1 ]
end
else
tab[ jopt ] = true
end
y = y + 1
end
end
end
end
return tab
end
function Util.showOptions(options)
print('Arguments: ')
for _, v in pairs(options) do
print(_sformat('-%s %s', v.arg, v.desc))
end
end
function Util.getOptions(options, args, ignoreInvalid)
local argLetters = ''
for _,o in pairs(options) do
if o.type ~= 'flag' then
argLetters = argLetters .. o.arg
end
end
local rawOptions = getopt(args, argLetters)
for k,ro in pairs(rawOptions) do
local found = false
for _,o in pairs(options) do
if o.arg == k then
found = true
if o.type == 'number' then
o.value = tonumber(ro)
elseif o.type == 'help' then
Util.showOptions(options)
return false
else
o.value = ro
end
end
end
if not found and not ignoreInvalid then
print('Invalid argument')
Util.showOptions(options)
return false
end
end
return true, Util.size(rawOptions)
end
return Util