3 Commits

Author SHA1 Message Date
kepler155c
7b086865c8 Update README.md 2018-11-18 17:48:43 -05:00
kepler155c
db878c81b2 Update README.md 2018-11-18 11:18:54 -05:00
kepler155c
a7f1354f97 Update README.md 2018-11-18 11:18:29 -05:00
73 changed files with 3933 additions and 3626 deletions

View File

@@ -12,7 +12,14 @@
* Run scripts on single or groups of computers (GUI) * Run scripts on single or groups of computers (GUI)
* Turtle follow (with GPS) and turtle come to you (without GPS) * Turtle follow (with GPS) and turtle come to you (without GPS)
## Install ## Install (MC 1.8+)
```
pastebin run uzghlbnc
```
Select either master-1.8 for stable or develop-1.8 for the upcoming release
## Install (MC 1.7)
``` ```
pastebin run sj4VMVJj pastebin run sj4VMVJj
reboot reboot

View File

@@ -1,5 +1,5 @@
local Ansi = setmetatable({ }, { local Ansi = setmetatable({ }, {
__call = function(_, ...) __call = function(self, ...)
local str = '\027[' local str = '\027['
for k,v in ipairs({ ...}) do for k,v in ipairs({ ...}) do
if k == 1 then if k == 1 then

View File

@@ -19,7 +19,7 @@ return function(base)
-- expose a constructor which can be called by <classname>(<args>) -- expose a constructor which can be called by <classname>(<args>)
setmetatable(c, { setmetatable(c, {
__call = function(class_tbl, ...) __call = function(class_tbl, ...)
local obj = { } local obj = {}
setmetatable(obj,c) setmetatable(obj,c)
if class_tbl.init then if class_tbl.init then
class_tbl.init(obj, ...) class_tbl.init(obj, ...)
@@ -36,7 +36,7 @@ return function(base)
c.is_a = c.is_a =
function(self, klass) function(self, klass)
local m = getmetatable(self) local m = getmetatable(self)
while m do while m do
if m == klass then return true end if m == klass then return true end
m = m._base m = m._base
end end

View File

@@ -1,11 +1,8 @@
local Util = require('util') local Util = require('util')
local fs = _G.fs
local shell = _ENV.shell
local Config = { } local Config = { }
function Config.load(fname, data) Config.load = function(fname, data)
local filename = 'usr/config/' .. fname local filename = 'usr/config/' .. fname
if not fs.exists('usr/config') then if not fs.exists('usr/config') then
@@ -19,26 +16,9 @@ function Config.load(fname, data)
end end
end end
function Config.loadWithCheck(fname, data) Config.update = function(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
Config.load(fname, data)
end
function Config.update(fname, data)
local filename = 'usr/config/' .. fname local filename = 'usr/config/' .. fname
Util.writeTable(filename, data) Util.writeTable(filename, data)
end end
return Config return Config

View File

@@ -1,5 +1,3 @@
local os = _G.os
local Event = { local Event = {
uid = 1, -- unique id for handlers uid = 1, -- unique id for handlers
routines = { }, -- coroutines routines = { }, -- coroutines
@@ -24,9 +22,6 @@ function Routine:terminate()
end end
function Routine:resume(event, ...) function Routine:resume(event, ...)
--if coroutine.status(self.co) == 'running' then
--return
--end
if not self.co then if not self.co then
error('Cannot resume a dead routine') error('Cannot resume a dead routine')
@@ -128,13 +123,13 @@ function Event.waitForEvent(event, timeout)
local e = { os.pullEvent() } local e = { os.pullEvent() }
if e[1] == event then if e[1] == event then
return table.unpack(e) return table.unpack(e)
end end
until e[1] == 'timer' and e[2] == timerId until e[1] == 'timer' and e[2] == timerId
end end
function Event.addRoutine(fn) function Event.addRoutine(fn)
local r = { local r = {
co = coroutine.create(fn), co = coroutine.create(fn),
uid = nextUID() uid = nextUID()
} }
setmetatable(r, { __index = Routine }) setmetatable(r, { __index = Routine })

View File

@@ -1,7 +1,5 @@
local git = require('git') local git = require('git')
local fs = _G.fs
local gitfs = { } local gitfs = { }
function gitfs.mount(dir, repo) function gitfs.mount(dir, repo)

View File

@@ -1,8 +1,6 @@
local fs = _G.fs
local linkfs = { } local linkfs = { }
local methods = { 'exists', 'getFreeSpace', 'getSize', local methods = { 'exists', 'getFreeSpace', 'getSize',
'isDir', 'isReadOnly', 'list', 'listEx', 'makeDir', 'open', 'getDrive' } 'isDir', 'isReadOnly', 'list', 'listEx', 'makeDir', 'open', 'getDrive' }
for _,m in pairs(methods) do for _,m in pairs(methods) do
@@ -12,7 +10,7 @@ for _,m in pairs(methods) do
end end
end end
function linkfs.mount(_, source) function linkfs.mount(dir, source)
if not source then if not source then
error('Source is required') error('Source is required')
end end
@@ -25,7 +23,7 @@ function linkfs.mount(_, source)
end end
return { return {
source = source source = source
} }
end end
function linkfs.copy(node, s, t) function linkfs.copy(node, s, t)

View File

@@ -1,13 +1,11 @@
local Socket = require('socket') local Socket = require('socket')
local synchronized = require('sync') local synchronized = require('sync')
local fs = _G.fs
local netfs = { } local netfs = { }
local function remoteCommand(node, msg) local function remoteCommand(node, msg)
for _ = 1, 2 do for i = 1, 2 do
if not node.socket then if not node.socket then
node.socket = Socket.connect(node.id, 139) node.socket = Socket.connect(node.id, 139)
end end
@@ -51,7 +49,7 @@ for _,m in pairs(methods) do
end end
end end
function netfs.mount(_, id, directory) function netfs.mount(dir, id, directory)
if not id or not tonumber(id) then if not id or not tonumber(id) then
error('ramfs syntax: computerId [directory]') error('ramfs syntax: computerId [directory]')
end end

View File

@@ -1,10 +1,8 @@
local Util = require('util') local Util = require('util')
local fs = _G.fs
local ramfs = { } local ramfs = { }
function ramfs.mount(_, nodeType) function ramfs.mount(dir, nodeType)
if nodeType == 'directory' then if nodeType == 'directory' then
return { return {
nodes = { }, nodes = { },
@@ -36,7 +34,7 @@ function ramfs.isReadOnly()
return false return false
end end
function ramfs.makeDir(_, dir) function ramfs.makeDir(node, dir)
fs.mount(dir, 'ramfs', 'directory') fs.mount(dir, 'ramfs', 'directory')
end end
@@ -48,10 +46,10 @@ function ramfs.getDrive()
return 'ram' return 'ram'
end end
function ramfs.list(node, dir) function ramfs.list(node, dir, full)
if node.nodes and node.mountPoint == dir then if node.nodes and node.mountPoint == dir then
local files = { } local files = { }
for k in pairs(node.nodes) do for k,v in pairs(node.nodes) do
table.insert(files, k) table.insert(files, k)
end end
return files return files

View File

@@ -1,10 +1,9 @@
local Util = require('util') local synchronized = require('sync')
local Util = require('util')
local fs = _G.fs
local urlfs = { } local urlfs = { }
function urlfs.mount(_, url) function urlfs.mount(dir, url)
if not url then if not url then
error('URL is required') error('URL is required')
end end
@@ -13,7 +12,7 @@ function urlfs.mount(_, url)
} }
end end
function urlfs.delete(_, dir) function urlfs.delete(node, dir)
fs.unmount(dir) fs.unmount(dir)
end end
@@ -50,7 +49,9 @@ function urlfs.open(node, fn, fl)
local c = node.cache local c = node.cache
if not c then if not c then
c = Util.httpGet(node.url) synchronized(node.url, function()
c = Util.download(node.url)
end)
if c then if c then
node.cache = c node.cache = c
node.size = #c node.size = #c

View File

@@ -6,9 +6,9 @@ local FILE_URL = 'https://raw.githubusercontent.com/%s/%s/%s/%s'
local git = { } local git = { }
function git.list(repository) function git.list(repo)
local t = Util.split(repository, '(.-)/') local t = Util.split(repo, '(.-)/')
local user = t[1] local user = t[1]
local repo = t[2] local repo = t[2]
@@ -33,7 +33,7 @@ function git.list(repository)
local list = { } local list = { }
for _,v in pairs(data.tree) do for k,v in pairs(data.tree) do
if v.type == "blob" then if v.type == "blob" then
v.path = v.path:gsub("%s","%%20") v.path = v.path:gsub("%s","%%20")
list[v.path] = { list[v.path] = {

View File

@@ -1,9 +1,5 @@
local GPS = { } local GPS = { }
local device = _G.device
local gps = _G.gps
local turtle = _G.turtle
function GPS.locate(timeout, debug) function GPS.locate(timeout, debug)
local pt = { } local pt = { }
timeout = timeout or 10 timeout = timeout or 10
@@ -18,6 +14,7 @@ function GPS.isAvailable()
end end
function GPS.getPoint(timeout, debug) function GPS.getPoint(timeout, debug)
local pt = GPS.locate(timeout, debug) local pt = GPS.locate(timeout, debug)
if not pt then if not pt then
return return
@@ -27,7 +24,7 @@ function GPS.getPoint(timeout, debug)
pt.y = math.floor(pt.y) pt.y = math.floor(pt.y)
pt.z = math.floor(pt.z) pt.z = math.floor(pt.z)
if _G.pocket then if pocket then
pt.y = pt.y - 1 pt.y = pt.y - 1
end end
@@ -50,7 +47,7 @@ function GPS.getHeading(timeout)
while not turtle.forward() do while not turtle.forward() do
turtle.turnRight() turtle.turnRight()
if turtle.getHeading() == heading then if turtle.getHeading() == heading then
_G.printError('GPS.getPoint: Unable to move forward') printError('GPS.getPoint: Unable to move forward')
return return
end end
end end
@@ -82,13 +79,13 @@ function GPS.getPointAndHeading(timeout)
end end
-- from stock gps API -- from stock gps API
local function trilaterate(A, B, C) local function trilaterate( A, B, C )
local a2b = B.position - A.position local a2b = B.position - A.position
local a2c = C.position - A.position local a2c = C.position - A.position
if math.abs( a2b:normalize():dot( a2c:normalize() ) ) > 0.999 then if math.abs( a2b:normalize():dot( a2c:normalize() ) ) > 0.999 then
return return nil
end end
local d = a2b:length() local d = a2b:length()
local ex = a2b:normalize( ) local ex = a2b:normalize( )
@@ -103,22 +100,22 @@ local function trilaterate(A, B, C)
local x = (r1*r1 - r2*r2 + d*d) / (2*d) 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 y = (r1*r1 - r3*r3 - x*x + (x-i)*(x-i) + j*j) / (2*j)
local result = A.position + (ex * x) + (ey * y) local result = A.position + (ex * x) + (ey * y)
local zSquared = r1*r1 - x*x - y*y local zSquared = r1*r1 - x*x - y*y
if zSquared > 0 then if zSquared > 0 then
local z = math.sqrt( zSquared ) local z = math.sqrt( zSquared )
local result1 = result + (ez * z) local result1 = result + (ez * z)
local result2 = result - (ez * z) local result2 = result - (ez * z)
local rounded1, rounded2 = result1:round(), result2:round() local rounded1, rounded2 = result1:round(), result2:round()
if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then if rounded1.x ~= rounded2.x or rounded1.y ~= rounded2.y or rounded1.z ~= rounded2.z then
return rounded1, rounded2 return rounded1, rounded2
else else
return rounded1 return rounded1
end end
end end
return result:round() return result:round()
end end
@@ -132,7 +129,7 @@ local function narrow( p1, p2, fix )
return p1:round() return p1:round()
else else
return p2:round() return p2:round()
end end
end end
-- end stock gps api -- end stock gps api

View File

@@ -1,55 +1,42 @@
local DEFAULT_UPATH = 'https://raw.githubusercontent.com/kepler155c/opus/develop/sys/apis' local DEFAULT_UPATH = 'https://raw.githubusercontent.com/kepler155c/opus/master/sys/apis'
local PASTEBIN_URL = 'http://pastebin.com/raw' local PASTEBIN_URL = 'http://pastebin.com/raw'
local GIT_URL = 'https://raw.githubusercontent.com' local GIT_URL = 'https://raw.githubusercontent.com'
local fs = _G.fs -- fix broken http get
local http = _G.http local syncLocks = { }
local os = _G.os
if not http._patched then local function sync(obj, fn)
-- fix broken http get local key = tostring(obj)
local syncLocks = { } if syncLocks[key] then
local cos = tostring(coroutine.running())
local function sync(obj, fn) table.insert(syncLocks[key], cos)
local key = tostring(obj) repeat
if syncLocks[key] then local _, co = os.pullEvent('sync_lock')
local cos = tostring(coroutine.running()) until co == cos
table.insert(syncLocks[key], cos) else
repeat syncLocks[key] = { }
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 end
local s, m = pcall(fn)
-- todo -- completely replace http.get with function that local co = table.remove(syncLocks[key], 1)
-- checks for success on permanent redirects (minecraft 1.75 bug) if co then
os.queueEvent('sync_lock', co)
http._patched = http.get else
function http.get(url, headers) syncLocks[key] = nil
local s, m end
sync(url, function() if not s then
s, m = http._patched(url, headers) error(m)
end)
return s, m
end end
end end
local function loadUrl(url) local function loadUrl(url)
local c local c
local h = http.get(url) sync(url, function()
if h then local h = http.get(url)
c = h.readAll() if h then
h.close() c = h.readAll()
end h.close()
end
end)
if c and #c > 0 then if c and #c > 0 then
return c return c
end end
@@ -57,7 +44,7 @@ end
local function requireWrapper(env) local function requireWrapper(env)
local function standardSearcher(modname) local function standardSearcher(modname, env, shell)
if package.loaded[modname] then if package.loaded[modname] then
return function() return function()
return package.loaded[modname] return package.loaded[modname]
@@ -65,18 +52,18 @@ local function requireWrapper(env)
end end
end end
local function shellSearcher(modname) local function shellSearcher(modname, env, shell)
local fname = modname:gsub('%.', '/') .. '.lua' local fname = modname:gsub('%.', '/') .. '.lua'
if env.shell and type(env.shell.dir) == 'function' then if shell and type(shell.dir) == 'function' then
local path = env.shell.resolve(fname) local path = shell.resolve(fname)
if fs.exists(path) and not fs.isDir(path) then if fs.exists(path) and not fs.isDir(path) then
return loadfile(path, env) return loadfile(path, env)
end end
end end
end end
local function pathSearcher(modname) local function pathSearcher(modname, env, shell)
local fname = modname:gsub('%.', '/') .. '.lua' local fname = modname:gsub('%.', '/') .. '.lua'
for dir in string.gmatch(package.path, "[^:]+") do for dir in string.gmatch(package.path, "[^:]+") do
@@ -88,7 +75,7 @@ local function requireWrapper(env)
end end
-- require('BniCQPVf') -- require('BniCQPVf')
local function pastebinSearcher(modname) local function pastebinSearcher(modname, env, shell)
if #modname == 8 and not modname:match('%W') then if #modname == 8 and not modname:match('%W') then
local url = PASTEBIN_URL .. '/' .. modname local url = PASTEBIN_URL .. '/' .. modname
local c = loadUrl(url) local c = loadUrl(url)
@@ -99,7 +86,7 @@ local function requireWrapper(env)
end end
-- require('kepler155c.opus.master.sys.apis.util') -- require('kepler155c.opus.master.sys.apis.util')
local function gitSearcher(modname) local function gitSearcher(modname, env, shell)
local fname = modname:gsub('%.', '/') .. '.lua' local fname = modname:gsub('%.', '/') .. '.lua'
local _, count = fname:gsub("/", "") local _, count = fname:gsub("/", "")
if count >= 3 then if count >= 3 then
@@ -111,7 +98,7 @@ local function requireWrapper(env)
end end
end end
local function urlSearcher(modname) local function urlSearcher(modname, env, shell)
local fname = modname:gsub('%.', '/') .. '.lua' local fname = modname:gsub('%.', '/') .. '.lua'
if fname:sub(1, 1) ~= '/' then if fname:sub(1, 1) ~= '/' then
@@ -126,7 +113,7 @@ local function requireWrapper(env)
end end
-- place package and require function into env -- place package and require function into env
env.package = { package = {
path = LUA_PATH or 'sys/apis', path = LUA_PATH or 'sys/apis',
upath = LUA_UPATH or DEFAULT_UPATH, upath = LUA_UPATH or DEFAULT_UPATH,
config = '/\n:\n?\n!\n-', config = '/\n:\n?\n!\n-',
@@ -147,14 +134,14 @@ local function requireWrapper(env)
} }
} }
function env.require(modname) function require(modname)
for _,searcher in ipairs(package.loaders) do for _,searcher in ipairs(package.loaders) do
local fn, msg = searcher(modname) local fn, msg = searcher(modname, env, shell)
if fn then if fn then
local module, msg2 = fn(modname, env) local module, msg = fn(modname, env)
if not module then if not module then
error(msg2 or (modname .. ' module returned nil'), 2) error(msg or (modname .. ' module returned nil'), 2)
end end
package.loaded[modname] = module package.loaded[modname] = module
return module return module
@@ -166,11 +153,10 @@ local function requireWrapper(env)
error('Unable to find module ' .. modname) error('Unable to find module ' .. modname)
end end
return env.require -- backwards compatible return require -- backwards compatible
end end
return function(env) return function(env)
env = env or getfenv(2)
setfenv(requireWrapper, env) setfenv(requireWrapper, env)
return requireWrapper(env) return requireWrapper(env)
end end

View File

@@ -1,150 +0,0 @@
local Util = require('util')
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 = {
pressed = { },
}
function input:modifierPressed()
return self.pressed[keys.leftCtrl] or
self.pressed[keys.rightCtrl] or
self.pressed[keys.leftAlt] or
self.pressed[keys.rightAlt]
end
function input:toCode(ch, code)
local result = { }
if self.pressed[keys.leftCtrl] or self.pressed[keys.rightCtrl] then
table.insert(result, 'control')
end
if self.pressed[keys.leftAlt] or self.pressed[keys.rightAlt] then
table.insert(result, 'alt')
end
if self.pressed[keys.leftShift] or self.pressed[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.pressed = { }
self.fired = nil
self.timer = nil
self.mch = nil
self.mfired = nil
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
return input:toCode(keys.getName(code), code)
end
else
self.pressed[code] = true
if self:modifierPressed() and not modifiers[code] or code == 57 then
self.fired = true
return input:toCode(keys.getName(code), code)
else
self.fired = false
end
end
elseif event == 'char' then
if not self:modifierPressed() then
self.fired = true
return input:toCode(code)
end
elseif event == 'key_up' then
if not self.fired then
if self.pressed[code] then
self.fired = true
local ch = input:toCode(keys.getName(code), code)
self.pressed[code] = nil
return ch
end
end
self.pressed[code] = nil
elseif event == 'paste' then
self.pressed[keys.leftCtrl] = nil
self.pressed[keys.rightCtrl] = nil
self.fired = true
return input:toCode('paste', 255)
elseif event == 'mouse_click' then
local buttons = { 'mouse_click', 'mouse_rightclick' }
self.mch = buttons[code]
self.mfired = nil
elseif event == 'mouse_drag' then
self.mfired = true
self.fired = true
return input:toCode('mouse_drag', 255)
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 self.mfired
elseif event == "mouse_scroll" then
local directions = {
[ -1 ] = 'scrollUp',
[ 1 ] = 'scrollDown'
}
self.fired = true
return input:toCode(directions[code], 255)
end
end
function input:test()
while true do
local ch = self:translate(os.pullEvent())
if ch then
print('GOT: ' .. tostring(ch))
end
end
end
return input

View File

@@ -189,7 +189,7 @@ function json.parseObject(str)
local val = {} local val = {}
while str:sub(1, 1) ~= "}" do while str:sub(1, 1) ~= "}" do
local k, v local k, v = nil, nil
k, v, str = json.parseMember(str) k, v, str = json.parseMember(str)
val[k] = v val[k] = v
str = removeWhite(str) str = removeWhite(str)

View File

@@ -0,0 +1,105 @@
-- Various assertion function for API methods argument-checking
if (...) then
-- Dependancies
local _PATH = (...):gsub('%.core.assert$','')
local Utils = require (_PATH .. '.core.utils')
-- Local references
local lua_type = type
local floor = math.floor
local concat = table.concat
local next = next
local pairs = pairs
local getmetatable = getmetatable
-- Is I an integer ?
local function isInteger(i)
return lua_type(i) ==('number') and (floor(i)==i)
end
-- Override lua_type to return integers
local function type(v)
return isInteger(v) and 'int' or lua_type(v)
end
-- Does the given array contents match a predicate type ?
local function arrayContentsMatch(t,...)
local n_count = Utils.arraySize(t)
if n_count < 1 then return false end
local init_count = t[0] and 0 or 1
local n_count = (t[0] and n_count-1 or n_count)
local types = {...}
if types then types = concat(types) end
for i=init_count,n_count,1 do
if not t[i] then return false end
if types then
if not types:match(type(t[i])) then return false end
end
end
return true
end
-- Checks if arg is a valid array map
local function isMap(m)
if not arrayContentsMatch(m, 'table') then return false end
local lsize = Utils.arraySize(m[next(m)])
for k,v in pairs(m) do
if not arrayContentsMatch(m[k], 'string', 'int') then return false end
if Utils.arraySize(v)~=lsize then return false end
end
return true
end
-- Checks if s is a valid string map
local function isStringMap(s)
if lua_type(s) ~= 'string' then return false end
local w
for row in s:gmatch('[^\n\r]+') do
if not row then return false end
w = w or #row
if w ~= #row then return false end
end
return true
end
-- Does instance derive straight from class
local function derives(instance, class)
return getmetatable(instance) == class
end
-- Does instance inherits from class
local function inherits(instance, class)
return (getmetatable(getmetatable(instance)) == class)
end
-- Is arg a boolean
local function isBoolean(b)
return (b==true or b==false)
end
-- Is arg nil ?
local function isNil(n)
return (n==nil)
end
local function matchType(value, types)
return types:match(type(value))
end
return {
arrayContentsMatch = arrayContentsMatch,
derives = derives,
inherits = inherits,
isInteger = isInteger,
isBool = isBoolean,
isMap = isMap,
isStrMap = isStringMap,
isOutOfRange = isOutOfRange,
isNil = isNil,
type = type,
matchType = matchType
}
end

View File

@@ -1,14 +1,14 @@
--- A light implementation of Binary heaps data structure. --- A light implementation of Binary heaps data structure.
-- While running a search, some search algorithms (Astar, Dijkstra, Jump Point Search) have to maintains -- 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, -- 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 -- 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). -- 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> -- 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 -- 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 -- examined during a search. As such, retrieving the lower-cost node is faster and globally makes the search end
-- up quickly. -- up quickly.
-- --
-- This module is internally used by the library on purpose. -- This module is internally used by the library on purpose.
-- It should normally not be used explicitely, yet it remains fully accessible. -- It should normally not be used explicitely, yet it remains fully accessible.
-- --
@@ -23,7 +23,7 @@ if (...) then
-- Dependency -- Dependency
local Utils = require((...):gsub('%.bheap$','.utils')) local Utils = require((...):gsub('%.bheap$','.utils'))
-- Local reference -- Local reference
local floor = math.floor local floor = math.floor
@@ -40,7 +40,7 @@ if (...) then
else pIndex = (index-1)/2 else pIndex = (index-1)/2
end end
if not heap._sort(heap._heap[pIndex], heap._heap[index]) then if not heap._sort(heap._heap[pIndex], heap._heap[index]) then
heap._heap[pIndex], heap._heap[index] = heap._heap[pIndex], heap._heap[index] =
heap._heap[index], heap._heap[pIndex] heap._heap[index], heap._heap[pIndex]
percolate_up(heap, pIndex) percolate_up(heap, pIndex)
end end
@@ -89,7 +89,7 @@ if (...) then
-- @class function -- @class function
-- @treturn bool __true__ of no item is queued in the heap, __false__ otherwise -- @treturn bool __true__ of no item is queued in the heap, __false__ otherwise
-- @usage -- @usage
-- if myHeap:empty() then -- if myHeap:empty() then
-- print('Heap is empty!') -- print('Heap is empty!')
-- end -- end
function heap:empty() function heap:empty()
@@ -129,7 +129,7 @@ if (...) then
-- @class function -- @class function
-- @treturn value a value previously pushed into the heap -- @treturn value a value previously pushed into the heap
-- @usage -- @usage
-- while not myHeap:empty() do -- while not myHeap:empty() do
-- local lowestValue = myHeap:pop() -- local lowestValue = myHeap:pop()
-- ... -- ...
-- end -- end
@@ -148,18 +148,18 @@ if (...) then
end end
--- Restores the `heap` property. --- Restores the `heap` property.
-- Reorders the `heap` with respect to the comparison function being used. -- 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`. -- 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. -- Otherwise, the whole `heap` will be cheacked.
-- @class function -- @class function
-- @tparam[opt] value item the modified value -- @tparam[opt] value item the modified value
-- @treturn heap self (the calling `heap` itself, can be chained) -- @treturn heap self (the calling `heap` itself, can be chained)
-- @usage myHeap:heapify() -- @usage myHeap:heapify()
function heap:heapify(item) function heap:heapify(item)
if self._size == 0 then return end if self._size == 0 then return end
if item then if item then
local i = Utils.indexOf(self._heap,item) local i = Utils.indexOf(self._heap,item)
if i then if i then
percolate_down(self, i) percolate_down(self, i)
percolate_up(self, i) percolate_up(self, i)
end end

View File

@@ -0,0 +1,98 @@
--- Heuristic functions for search algorithms.
-- A <a href="http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html">distance heuristic</a>
-- provides an *estimate of the optimal distance cost* from a given location to a target.
-- As such, it guides the pathfinder to the goal, helping it to decide which route is the best.
--
-- This script holds the definition of some built-in heuristics available through jumper.
--
-- Distance functions are internally used by the `pathfinder` to evaluate the optimal path
-- from the start location to the goal. These functions share the same prototype:
-- local function myHeuristic(nodeA, nodeB)
-- -- function body
-- end
-- Jumper features some built-in distance heuristics, namely `MANHATTAN`, `EUCLIDIAN`, `DIAGONAL`, `CARDINTCARD`.
-- You can also supply your own heuristic function, following the same template as above.
local abs = math.abs
local sqrt = math.sqrt
local sqrt2 = sqrt(2)
local max, min = math.max, math.min
local Heuristics = {}
--- Manhattan distance.
-- <br/>This heuristic is the default one being used by the `pathfinder` object.
-- <br/>Evaluates as <code>distance = |dx|+|dy|</code>
-- @class function
-- @tparam node nodeA a node
-- @tparam node nodeB another node
-- @treturn number the distance from __nodeA__ to __nodeB__
-- @usage
-- -- First method
-- pathfinder:setHeuristic('MANHATTAN')
-- -- Second method
-- local Distance = require ('jumper.core.heuristics')
-- pathfinder:setHeuristic(Distance.MANHATTAN)
function Heuristics.MANHATTAN(nodeA, nodeB)
local dx = abs(nodeA._x - nodeB._x)
local dy = abs(nodeA._y - nodeB._y)
local dz = abs(nodeA._z - nodeB._z)
return (dx + dy + dz)
end
--- Euclidian distance.
-- <br/>Evaluates as <code>distance = squareRoot(dx*dx+dy*dy)</code>
-- @class function
-- @tparam node nodeA a node
-- @tparam node nodeB another node
-- @treturn number the distance from __nodeA__ to __nodeB__
-- @usage
-- -- First method
-- pathfinder:setHeuristic('EUCLIDIAN')
-- -- Second method
-- local Distance = require ('jumper.core.heuristics')
-- pathfinder:setHeuristic(Distance.EUCLIDIAN)
function Heuristics.EUCLIDIAN(nodeA, nodeB)
local dx = nodeA._x - nodeB._x
local dy = nodeA._y - nodeB._y
local dz = nodeA._z - nodeB._z
return sqrt(dx*dx+dy*dy+dz*dz)
end
--- Diagonal distance.
-- <br/>Evaluates as <code>distance = max(|dx|, abs|dy|)</code>
-- @class function
-- @tparam node nodeA a node
-- @tparam node nodeB another node
-- @treturn number the distance from __nodeA__ to __nodeB__
-- @usage
-- -- First method
-- pathfinder:setHeuristic('DIAGONAL')
-- -- Second method
-- local Distance = require ('jumper.core.heuristics')
-- pathfinder:setHeuristic(Distance.DIAGONAL)
function Heuristics.DIAGONAL(nodeA, nodeB)
local dx = abs(nodeA._x - nodeB._x)
local dy = abs(nodeA._y - nodeB._y)
return max(dx,dy)
end
--- Cardinal/Intercardinal distance.
-- <br/>Evaluates as <code>distance = min(dx, dy)*squareRoot(2) + max(dx, dy) - min(dx, dy)</code>
-- @class function
-- @tparam node nodeA a node
-- @tparam node nodeB another node
-- @treturn number the distance from __nodeA__ to __nodeB__
-- @usage
-- -- First method
-- pathfinder:setHeuristic('CARDINTCARD')
-- -- Second method
-- local Distance = require ('jumper.core.heuristics')
-- pathfinder:setHeuristic(Distance.CARDINTCARD)
function Heuristics.CARDINTCARD(nodeA, nodeB)
local dx = abs(nodeA._x - nodeB._x)
local dy = abs(nodeA._y - nodeB._y)
return min(dx,dy) * sqrt2 + max(dx,dy) - min(dx,dy)
end
return Heuristics

View File

@@ -0,0 +1,32 @@
local addNode(self, node, nextNode, ed)
if not self._pathDB[node] then self._pathDB[node] = {} end
self._pathDB[node][ed] = (nextNode == ed and node or nextNode)
end
-- Path lookupTable
local lookupTable = {}
lookupTable.__index = lookupTable
function lookupTable:new()
local lut = {_pathDB = {}}
return setmetatable(lut, lookupTable)
end
function lookupTable:addPath(path)
local st, ed = path._nodes[1], path._nodes[#path._nodes]
for node, count in path:nodes() do
local nextNode = path._nodes[count+1]
if nextNode then addNode(self, node, nextNode, ed) end
end
end
function lookupTable:hasPath(nodeA, nodeB)
local found
found = self._pathDB[nodeA] and self._path[nodeA][nodeB]
if found then return true, true end
found = self._pathDB[nodeB] and self._path[nodeB][nodeA]
if found then return true, false end
return false
end
return lookupTable

View File

@@ -4,38 +4,97 @@
-- and then cached within the `grid`. -- and then cached within the `grid`.
-- --
-- In the following implementation, nodes can be compared using the `<` operator. The comparison is -- 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 -- 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. -- to the next neighbouring node having the lowest `f` cost. See `core.bheap` for more details.
-- --
if (...) then if (...) then
local assert = assert
--- The `Node` class.<br/>
-- This class is callable.
-- Therefore,_ <code>Node(...)</code> _acts as a shortcut to_ <code>Node:new(...)</code>.
-- @type Node
local Node = {} local Node = {}
Node.__index = Node Node.__index = Node
--- Inits a new `node`
-- @class function
-- @tparam int x the x-coordinate of the node on the collision map
-- @tparam int y the y-coordinate of the node on the collision map
-- @treturn node a new `node`
-- @usage local node = Node(3,4)
function Node:new(x,y,z) function Node:new(x,y,z)
return setmetatable({x = x, y = y, z = z }, Node) return setmetatable({_x = x, _y = y, _z = z, _clearance = {}}, Node)
end end
-- Enables the use of operator '<' to compare nodes. -- 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 -- 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.__lt(A,B) return (A._f < B._f) end
function Node:getX() return self.x end --- Returns x-coordinate of a `node`
function Node:getY() return self.y end -- @class function
function Node:getZ() return self.z end -- @treturn number the x-coordinate of the `node`
-- @usage local x = node:getX()
function Node:getX() return self._x end
--- Returns y-coordinate of a `node`
-- @class function
-- @treturn number the y-coordinate of the `node`
-- @usage local y = node:getY()
function Node:getY() return self._y end
function Node:getZ() return self._z end
--- Returns x and y coordinates of a `node`
-- @class function
-- @treturn number the x-coordinate of the `node`
-- @treturn number the y-coordinate of the `node`
-- @usage local x, y = node:getPos()
function Node:getPos() return self._x, self._y, self._z end
--- Returns the amount of true [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric)
-- for a given `node`
-- @class function
-- @tparam string|int|func walkable the value for walkable locations in the collision map array.
-- @treturn int the clearance of the `node`
-- @usage
-- -- Assuming walkable was 0
-- local clearance = node:getClearance(0)
function Node:getClearance(walkable)
return self._clearance[walkable]
end
--- Removes the clearance value for a given walkable.
-- @class function
-- @tparam string|int|func walkable the value for walkable locations in the collision map array.
-- @treturn node self (the calling `node` itself, can be chained)
-- @usage
-- -- Assuming walkable is defined
-- node:removeClearance(walkable)
function Node:removeClearance(walkable)
self._clearance[walkable] = nil
return self
end
--- Clears temporary cached attributes of a `node`. --- Clears temporary cached attributes of a `node`.
-- Deletes the attributes cached within a given node after a pathfinding call. -- 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. -- This function is internally used by the search algorithms, so you should not use it explicitely.
-- @class function
-- @treturn node self (the calling `node` itself, can be chained)
-- @usage
-- local thisNode = Node(1,2)
-- thisNode:reset()
function Node:reset() function Node:reset()
self._g, self._h, self._f = nil, nil, nil self._g, self._h, self._f = nil, nil, nil
self._opened, self._closed, self._parent = nil, nil, nil self._opened, self._closed, self._parent = nil, nil, nil
return self return self
end end
return setmetatable(Node, return setmetatable(Node,
{__call = function(_,...) {__call = function(self,...)
return Node:new(...) return Node:new(...)
end} end}
) )
end end

View File

@@ -6,25 +6,44 @@
-- It should normally not be used explicitely, yet it remains fully accessible. -- It should normally not be used explicitely, yet it remains fully accessible.
-- --
if (...) then if (...) then
local t_remove = table.remove -- Dependencies
local _PATH = (...):match('(.+)%.path$')
local Heuristic = require (_PATH .. '.heuristics')
-- Local references
local abs, max = math.abs, math.max
local t_insert, t_remove = table.insert, table.remove
--- The `Path` class.<br/>
-- This class is callable.
-- Therefore, <em><code>Path(...)</code></em> acts as a shortcut to <em><code>Path:new(...)</code></em>.
-- @type Path
local Path = {} local Path = {}
Path.__index = Path Path.__index = Path
--- Inits a new `path`.
-- @class function
-- @treturn path a `path`
-- @usage local p = Path()
function Path:new() function Path:new()
return setmetatable({_nodes = {}}, Path) return setmetatable({_nodes = {}}, Path)
end end
--- Iterates on each single `node` along a `path`. At each step of iteration, --- Iterates on each single `node` along a `path`. At each step of iteration,
-- returns the `node` plus a count value. Aliased as @{Path:nodes} -- returns the `node` plus a count value. Aliased as @{Path:nodes}
-- @class function
-- @treturn node a `node`
-- @treturn int the count for the number of nodes
-- @see Path:nodes
-- @usage -- @usage
-- for node, count in p:iter() do -- for node, count in p:iter() do
-- ... -- ...
-- end -- end
function Path:nodes() function Path:iter()
local i = 1 local i,pathLen = 1,#self._nodes
return function() return function()
if self._nodes[i] then if self._nodes[i] then
i = i+1 i = i+1
@@ -32,35 +51,150 @@ if (...) then
end end
end end
end end
--- `Path` compression modifier. Given a `path`, eliminates useless nodes to return a lighter `path` --- Iterates on each single `node` along a `path`. At each step of iteration,
-- consisting of straight moves. Does the opposite of @{Path:fill} -- returns a `node` plus a count value. Alias for @{Path:iter}
-- @class function -- @class function
-- @treturn path self (the calling `path` itself, can be chained) -- @name Path:nodes
-- @treturn node a `node`
-- @treturn int the count for the number of nodes
-- @see Path:iter
-- @usage
-- for node, count in p:nodes() do
-- ...
-- end
Path.nodes = Path.iter
--- Evaluates the `path` length
-- @class function
-- @treturn number the `path` length
-- @usage local len = p:getLength()
function Path:getLength()
local len = 0
for i = 2,#self._nodes do
len = len + Heuristic.EUCLIDIAN(self._nodes[i], self._nodes[i-1])
end
return len
end
--- Counts the number of steps.
-- Returns the number of waypoints (nodes) in the current path.
-- @class function
-- @tparam node node a node to be added to the path
-- @tparam[opt] int index the index at which the node will be inserted. If omitted, the node will be appended after the last node in the path.
-- @treturn path self (the calling `path` itself, can be chained)
-- @usage local nSteps = p:countSteps()
function Path:addNode(node, index)
index = index or #self._nodes+1
t_insert(self._nodes, index, node)
return self
end
--- `Path` filling modifier. Interpolates between non contiguous nodes along a `path`
-- to build a fully continuous `path`. This maybe useful when using search algorithms such as Jump Point Search.
-- Does the opposite of @{Path:filter}
-- @class function
-- @treturn path self (the calling `path` itself, can be chained)
-- @see Path:filter
-- @usage p:fill()
function Path:fill()
local i = 2
local xi,yi,dx,dy
local N = #self._nodes
local incrX, incrY
while true do
xi,yi = self._nodes[i]._x,self._nodes[i]._y
dx,dy = xi-self._nodes[i-1]._x,yi-self._nodes[i-1]._y
if (abs(dx) > 1 or abs(dy) > 1) then
incrX = dx/max(abs(dx),1)
incrY = dy/max(abs(dy),1)
t_insert(self._nodes, i, self._grid:getNodeAt(self._nodes[i-1]._x + incrX, self._nodes[i-1]._y +incrY))
N = N+1
else i=i+1
end
if i>N then break end
end
return self
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 -- @see Path:fill
-- @usage p:filter() -- @usage p:filter()
function Path:filter() function Path:filter()
local i = 2 local i = 2
local xi,yi,zi,dx,dy,dz, olddx, olddy, olddz local xi,yi,dx,dy, olddx, olddy
xi,yi,zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z xi,yi = self._nodes[i]._x, self._nodes[i]._y
dx, dy,dz = xi - self._nodes[i-1].x, yi-self._nodes[i-1].y, zi-self._nodes[i-1].z dx, dy = xi - self._nodes[i-1]._x, yi-self._nodes[i-1]._y
while true do while true do
olddx, olddy, olddz = dx, dy, dz olddx, olddy = dx, dy
if self._nodes[i+1] then if self._nodes[i+1] then
i = i+1 i = i+1
xi, yi, zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z xi, yi = self._nodes[i]._x, self._nodes[i]._y
dx, dy, dz = xi - self._nodes[i-1].x, yi - self._nodes[i-1].y, zi - self._nodes[i-1].z dx, dy = xi - self._nodes[i-1]._x, yi - self._nodes[i-1]._y
if olddx == dx and olddy == dy and olddz == dz then if olddx == dx and olddy == dy then
t_remove(self._nodes, i-1) t_remove(self._nodes, i-1)
i = i - 1 i = i - 1
end end
else break end else break end
end end
return self return self
end end
--- Clones a `path`.
-- @class function
-- @treturn path a `path`
-- @usage local p = path:clone()
function Path:clone()
local p = Path:new()
for node in self:nodes() do p:addNode(node) end
return p
end
--- Checks if a `path` is equal to another. It also supports *filtered paths* (see @{Path:filter}).
-- @class function
-- @tparam path p2 a path
-- @treturn boolean a boolean
-- @usage print(myPath:isEqualTo(anotherPath))
function Path:isEqualTo(p2)
local p1 = self:clone():filter()
local p2 = p2:clone():filter()
for node, count in p1:nodes() do
if not p2._nodes[count] then return false end
local n = p2._nodes[count]
if n._x~=node._x or n._y~=node._y then return false end
end
return true
end
--- Reverses a `path`.
-- @class function
-- @treturn path self (the calling `path` itself, can be chained)
-- @usage myPath:reverse()
function Path:reverse()
local _nodes = {}
for i = #self._nodes,1,-1 do
_nodes[#_nodes+1] = self._nodes[i]
end
self._nodes = _nodes
return self
end
--- Appends a given `path` to self.
-- @class function
-- @tparam path p a path
-- @treturn path self (the calling `path` itself, can be chained)
-- @usage myPath:append(anotherPath)
function Path:append(p)
for node in p:nodes() do self:addNode(node) end
return self
end
return setmetatable(Path, return setmetatable(Path,
{__call = function(_,...) {__call = function(self,...)
return Path:new(...) return Path:new(...)
end end
}) })

View File

@@ -5,20 +5,125 @@ if (...) then
-- Dependencies -- Dependencies
local _PATH = (...):gsub('%.utils$','') local _PATH = (...):gsub('%.utils$','')
local Path = require (_PATH .. '.path') local Path = require (_PATH .. '.path')
local Node = require (_PATH .. '.node')
-- Local references -- Local references
local pairs = pairs local pairs = pairs
local type = type
local t_insert = table.insert local t_insert = table.insert
local assert = assert
local coroutine = coroutine
-- Raw array items count -- Raw array items count
local function arraySize(t) local function arraySize(t)
local count = 0 local count = 0
for _ in pairs(t) do for k,v in pairs(t) do
count = count+1 count = count+1
end end
return count return count
end end
-- Parses a string map and builds an array map
local function stringMapToArray(str)
local map = {}
local w, h
for line in str:gmatch('[^\n\r]+') do
if line then
w = not w and #line or w
assert(#line == w, 'Error parsing map, rows must have the same size!')
h = (h or 0) + 1
map[h] = {}
for char in line:gmatch('.') do
map[h][#map[h]+1] = char
end
end
end
return map
end
-- Collects and returns the keys of a given array
local function getKeys(t)
local keys = {}
for k,v in pairs(t) do keys[#keys+1] = k end
return keys
end
-- Calculates the bounds of a 2d array
local function getArrayBounds(map)
local min_x, max_x
local min_y, max_y
for y in pairs(map) do
min_y = not min_y and y or (y<min_y and y or min_y)
max_y = not max_y and y or (y>max_y and y or max_y)
for x in pairs(map[y]) do
min_x = not min_x and x or (x<min_x and x or min_x)
max_x = not max_x and x or (x>max_x and x or max_x)
end
end
return min_x,max_x,min_y,max_y
end
-- Converts an array to a set of nodes
local function arrayToNodes(map)
local min_x, max_x
local min_y, max_y
local min_z, max_z
local nodes = {}
for y in pairs(map) do
min_y = not min_y and y or (y<min_y and y or min_y)
max_y = not max_y and y or (y>max_y and y or max_y)
nodes[y] = {}
for x in pairs(map[y]) do
min_x = not min_x and x or (x<min_x and x or min_x)
max_x = not max_x and x or (x>max_x and x or max_x)
nodes[y][x] = {}
for z in pairs(map[y][x]) do
min_z = not min_z and z or (z<min_z and z or min_z)
max_z = not max_z and z or (z>max_z and z or max_z)
nodes[y][x][z] = Node:new(x,y,z)
end
end
end
return nodes,
(min_x or 0), (max_x or 0),
(min_y or 0), (max_y or 0),
(min_z or 0), (max_z or 0)
end
-- Iterator, wrapped within a coroutine
-- Iterates around a given position following the outline of a square
local function around()
local iterf = function(x0, y0, z0, s)
local x, y, z = x0-s, y0-s, z0-s
coroutine.yield(x, y, z)
repeat
x = x + 1
coroutine.yield(x,y,z)
until x == x0+s
repeat
y = y + 1
coroutine.yield(x,y,z)
until y == y0 + s
repeat
z = z + 1
coroutine.yield(x,y,z)
until z == z0 + s
repeat
x = x - 1
coroutine.yield(x, y,z)
until x == x0-s
repeat
y = y - 1
coroutine.yield(x,y,z)
until y == y0-s+1
repeat
z = z - 1
coroutine.yield(x,y,z)
until z == z0-s+1
end
return coroutine.create(iterf)
end
-- Extract a path from a given start/end position -- Extract a path from a given start/end position
local function traceBackPath(finder, node, startNode) local function traceBackPath(finder, node, startNode)
local path = Path:new() local path = Path:new()
@@ -46,11 +151,17 @@ if (...) then
local function outOfRange(i,low,up) local function outOfRange(i,low,up)
return (i< low or i > up) return (i< low or i > up)
end end
return { return {
arraySize = arraySize, arraySize = arraySize,
getKeys = getKeys,
indexOf = indexOf, indexOf = indexOf,
outOfRange = outOfRange, outOfRange = outOfRange,
getArrayBounds = getArrayBounds,
arrayToNodes = arrayToNodes,
strToMap = stringMapToArray,
around = around,
drAround = drAround,
traceBackPath = traceBackPath traceBackPath = traceBackPath
} }

View File

@@ -2,8 +2,7 @@
-- Implementation of the `grid` class. -- Implementation of the `grid` class.
-- The `grid` is a implicit graph which represents the 2D -- The `grid` is a implicit graph which represents the 2D
-- world map layout on which the `pathfinder` object will run. -- world map layout on which the `pathfinder` object will run.
-- During a search, the `pathfinder` object needs to save some critical values. -- During a search, the `pathfinder` object needs to save some critical values. These values are cached within each `node`
-- These values are cached within each `node`
-- object, and the whole set of nodes are tight inside the `grid` object itself. -- object, and the whole set of nodes are tight inside the `grid` object itself.
if (...) then if (...) then
@@ -13,10 +12,16 @@ if (...) then
-- Local references -- Local references
local Utils = require (_PATH .. '.core.utils') local Utils = require (_PATH .. '.core.utils')
local Assert = require (_PATH .. '.core.assert')
local Node = require (_PATH .. '.core.node') local Node = require (_PATH .. '.core.node')
-- Local references -- Local references
local pairs = pairs
local assert = assert
local next = next
local setmetatable = setmetatable local setmetatable = setmetatable
local floor = math.floor
local coroutine = coroutine
-- Offsets for straights moves -- Offsets for straights moves
local straightOffsets = { local straightOffsets = {
@@ -25,67 +30,390 @@ if (...) then
{x = 0, y = 0, z = 1} --[[U]], {x = 0, y = -0, z = -1}, --[[D]] {x = 0, y = 0, z = 1} --[[U]], {x = 0, y = -0, z = -1}, --[[D]]
} }
-- Offsets for diagonal moves
local diagonalOffsets = {
{x = -1, y = -1} --[[NW]], {x = 1, y = -1}, --[[NE]]
{x = -1, y = 1} --[[SW]], {x = 1, y = 1}, --[[SE]]
}
--- The `Grid` class.<br/>
-- This class is callable.
-- Therefore,_ <code>Grid(...)</code> _acts as a shortcut to_ <code>Grid:new(...)</code>.
-- @type Grid
local Grid = {} local Grid = {}
Grid.__index = Grid Grid.__index = Grid
function Grid:new(dim) -- Specialized grids
local newGrid = { } local PreProcessGrid = setmetatable({},Grid)
newGrid._min_x, newGrid._max_x = dim.x, dim.ex local PostProcessGrid = setmetatable({},Grid)
newGrid._min_y, newGrid._max_y = dim.y, dim.ey PreProcessGrid.__index = PreProcessGrid
newGrid._min_z, newGrid._max_z = dim.z, dim.ez PostProcessGrid.__index = PostProcessGrid
newGrid._nodes = { } PreProcessGrid.__call = function (self,x,y,z)
newGrid._width = (newGrid._max_x-newGrid._min_x)+1 return self:getNodeAt(x,y,z)
newGrid._height = (newGrid._max_y-newGrid._min_y)+1 end
newGrid._length = (newGrid._max_z-newGrid._min_z)+1 PostProcessGrid.__call = function (self,x,y,z,create)
return setmetatable(newGrid,Grid) if create then return self:getNodeAt(x,y,z) end
return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z]
end end
function Grid:isWalkableAt(x, y, z) --- Inits a new `grid`
local node = self:getNodeAt(x,y,z) -- @class function
return node and node.walkable ~= 1 -- @tparam table|string map A collision map - (2D array) with consecutive indices (starting at 0 or 1)
-- or a `string` with line-break chars (<code>\n</code> or <code>\r</code>) as row delimiters.
-- @tparam[opt] bool cacheNodeAtRuntime When __true__, returns an empty `grid` instance, so that
-- later on, indexing a non-cached `node` will cause it to be created and cache within the `grid` on purpose (i.e, when needed).
-- This is a __memory-safe__ option, in case your dealing with some tight memory constraints.
-- Defaults to __false__ when omitted.
-- @treturn grid a new `grid` instance
-- @usage
-- -- A simple 3x3 grid
-- local myGrid = Grid:new({{0,0,0},{0,0,0},{0,0,0}})
--
-- -- A memory-safe 3x3 grid
-- myGrid = Grid('000\n000\n000', true)
function Grid:new(map, cacheNodeAtRuntime)
if type(map) == 'string' then
assert(Assert.isStrMap(map), 'Wrong argument #1. Not a valid string map')
map = Utils.strToMap(map)
end
--assert(Assert.isMap(map),('Bad argument #1. Not a valid map'))
assert(Assert.isBool(cacheNodeAtRuntime) or Assert.isNil(cacheNodeAtRuntime),
('Bad argument #2. Expected \'boolean\', got %s.'):format(type(cacheNodeAtRuntime)))
if cacheNodeAtRuntime then
return PostProcessGrid:new(map,walkable)
end
return PreProcessGrid:new(map,walkable)
end end
--- Checks if `node` at [x,y] is __walkable__.
-- Will check if `node` at location [x,y] both *exists* on the collision map and *is walkable*
-- @class function
-- @tparam int x the x-location of the node
-- @tparam int y the y-location of the node
-- @tparam[opt] string|int|func walkable the value for walkable locations in the collision map array (see @{Grid:new}).
-- Defaults to __false__ when omitted.
-- If this parameter is a function, it should be prototyped as __f(value)__ and return a `boolean`:
-- __true__ when value matches a __walkable__ `node`, __false__ otherwise. If this parameter is not given
-- while location [x,y] __is valid__, this actual function returns __true__.
-- @tparam[optchain] int clearance the amount of clearance needed. Defaults to 1 (normal clearance) when not given.
-- @treturn bool __true__ if `node` exists and is __walkable__, __false__ otherwise
-- @usage
-- -- Always true
-- print(myGrid:isWalkableAt(2,3))
--
-- -- True if node at [2,3] collision map value is 0
-- print(myGrid:isWalkableAt(2,3,0))
--
-- -- True if node at [2,3] collision map value is 0 and has a clearance higher or equal to 2
-- print(myGrid:isWalkableAt(2,3,0,2))
--
function Grid:isWalkableAt(x, y, z, walkable, clearance)
local nodeValue = self._map[y] and self._map[y][x] and self._map[y][x][z]
if nodeValue then
if not walkable then return true end
else
return false
end
local hasEnoughClearance = not clearance and true or false
if not hasEnoughClearance then
if not self._isAnnotated[walkable] then return false end
local node = self:getNodeAt(x,y,z)
local nodeClearance = node:getClearance(walkable)
hasEnoughClearance = (nodeClearance >= clearance)
end
if self._eval then
return walkable(nodeValue) and hasEnoughClearance
end
return ((nodeValue == walkable) and hasEnoughClearance)
end
--- Returns the `grid` width.
-- @class function
-- @treturn int the `grid` width
-- @usage print(myGrid:getWidth())
function Grid:getWidth() function Grid:getWidth()
return self._width return self._width
end end
--- Returns the `grid` height.
-- @class function
-- @treturn int the `grid` height
-- @usage print(myGrid:getHeight())
function Grid:getHeight() function Grid:getHeight()
return self._height return self._height
end end
--- Returns the collision map.
-- @class function
-- @treturn map the collision map (see @{Grid:new})
-- @usage local map = myGrid:getMap()
function Grid:getMap()
return self._map
end
--- Returns the set of nodes.
-- @class function
-- @treturn {{node,...},...} an array of nodes
-- @usage local nodes = myGrid:getNodes()
function Grid:getNodes() function Grid:getNodes()
return self._nodes return self._nodes
end end
--- Returns the `grid` bounds. Returned values corresponds to the upper-left
-- and lower-right coordinates (in tile units) of the actual `grid` instance.
-- @class function
-- @treturn int the upper-left corner x-coordinate
-- @treturn int the upper-left corner y-coordinate
-- @treturn int the lower-right corner x-coordinate
-- @treturn int the lower-right corner y-coordinate
-- @usage local left_x, left_y, right_x, right_y = myGrid:getBounds()
function Grid:getBounds() function Grid:getBounds()
return self._min_x, self._min_y, self._min_z, self._max_x, self._max_y, self._max_z return self._min_x, self._min_y, self._min_z, self._max_x, self._max_y, self._max_z
end end
--- Returns neighbours. The returned value is an array of __walkable__ nodes neighbouring a given `node`. --- Returns neighbours. The returned value is an array of __walkable__ nodes neighbouring a given `node`.
-- @class function
-- @tparam node node a given `node`
-- @tparam[opt] string|int|func walkable the value for walkable locations in the collision map array (see @{Grid:new}).
-- Defaults to __false__ when omitted.
-- @tparam[optchain] bool allowDiagonal when __true__, allows adjacent nodes are included (8-neighbours).
-- Defaults to __false__ when omitted.
-- @tparam[optchain] bool tunnel When __true__, allows the `pathfinder` to tunnel through walls when heading diagonally.
-- @tparam[optchain] int clearance When given, will prune for the neighbours set all nodes having a clearance value lower than the passed-in value
-- Defaults to __false__ when omitted.
-- @treturn {node,...} an array of nodes neighbouring a given node -- @treturn {node,...} an array of nodes neighbouring a given node
function Grid:getNeighbours(node) -- @usage
-- local aNode = myGrid:getNodeAt(5,6)
-- local neighbours = myGrid:getNeighbours(aNode, 0, true)
function Grid:getNeighbours(node, walkable, allowDiagonal, tunnel, clearance)
local neighbours = {} local neighbours = {}
for i = 1,#straightOffsets do for i = 1,#straightOffsets do
local n = self:getNodeAt( local n = self:getNodeAt(
node.x + straightOffsets[i].x, node._x + straightOffsets[i].x,
node.y + straightOffsets[i].y, node._y + straightOffsets[i].y,
node.z + straightOffsets[i].z node._z + straightOffsets[i].z
) )
if n and self:isWalkableAt(n.x, n.y, n.z) then if n and self:isWalkableAt(n._x, n._y, n._z, walkable, clearance) then
neighbours[#neighbours+1] = n neighbours[#neighbours+1] = n
end end
end end
if not allowDiagonal then return neighbours end
tunnel = not not tunnel
for i = 1,#diagonalOffsets do
local n = self:getNodeAt(
node._x + diagonalOffsets[i].x,
node._y + diagonalOffsets[i].y
)
if n and self:isWalkableAt(n._x, n._y, walkable, clearance) then
if tunnel then
neighbours[#neighbours+1] = n
else
local skipThisNode = false
local n1 = self:getNodeAt(node._x+diagonalOffsets[i].x, node._y)
local n2 = self:getNodeAt(node._x, node._y+diagonalOffsets[i].y)
if ((n1 and n2) and not self:isWalkableAt(n1._x, n1._y, walkable, clearance) and not self:isWalkableAt(n2._x, n2._y, walkable, clearance)) then
skipThisNode = true
end
if not skipThisNode then neighbours[#neighbours+1] = n end
end
end
end
return neighbours return neighbours
end end
function Grid:getNodeAt(x,y,z) --- Grid iterator. Iterates on every single node
-- in the `grid`. Passing __lx, ly, ex, ey__ arguments will iterate
-- only on nodes inside the bounding-rectangle delimited by those given coordinates.
-- @class function
-- @tparam[opt] int lx the leftmost x-coordinate of the rectangle. Default to the `grid` leftmost x-coordinate (see @{Grid:getBounds}).
-- @tparam[optchain] int ly the topmost y-coordinate of the rectangle. Default to the `grid` topmost y-coordinate (see @{Grid:getBounds}).
-- @tparam[optchain] int ex the rightmost x-coordinate of the rectangle. Default to the `grid` rightmost x-coordinate (see @{Grid:getBounds}).
-- @tparam[optchain] int ey the bottom-most y-coordinate of the rectangle. Default to the `grid` bottom-most y-coordinate (see @{Grid:getBounds}).
-- @treturn node a `node` on the collision map, upon each iteration step
-- @treturn int the iteration count
-- @usage
-- for node, count in myGrid:iter() do
-- print(node:getX(), node:getY(), count)
-- end
function Grid:iter(lx,ly,lz,ex,ey,ez)
local min_x = lx or self._min_x
local min_y = ly or self._min_y
local min_z = lz or self._min_z
local max_x = ex or self._max_x
local max_y = ey or self._max_y
local max_z = ez or self._max_z
local x, y, z
z = min_z
return function()
x = not x and min_x or x+1
if x > max_x then
x = min_x
y = y+1
end
y = not y and min_y or y+1
if y > max_y then
y = min_y
z = z+1
end
if z > max_z then
z = nil
end
return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z] or self:getNodeAt(x,y,z)
end
end
--- Grid iterator. Iterates on each node along the outline (border) of a squared area
-- centered on the given node.
-- @tparam node node a given `node`
-- @tparam[opt] int radius the area radius (half-length). Defaults to __1__ when not given.
-- @treturn node a `node` at each iteration step
-- @usage
-- for node in myGrid:around(node, 2) do
-- ...
-- end
function Grid:around(node, radius)
local x, y, z = node._x, node._y, node._z
radius = radius or 1
local _around = Utils.around()
local _nodes = {}
repeat
local state, x, y, z = coroutine.resume(_around,x,y,z,radius)
local nodeAt = state and self:getNodeAt(x, y, z)
if nodeAt then _nodes[#_nodes+1] = nodeAt end
until (not state)
local _i = 0
return function()
_i = _i+1
return _nodes[_i]
end
end
--- Each transformation. Calls the given function on each `node` in the `grid`,
-- passing the `node` as the first argument to function __f__.
-- @class function
-- @tparam func f a function prototyped as __f(node,...)__
-- @tparam[opt] vararg ... args to be passed to function __f__
-- @treturn grid self (the calling `grid` itself, can be chained)
-- @usage
-- local function printNode(node)
-- print(node:getX(), node:getY())
-- end
-- myGrid:each(printNode)
function Grid:each(f,...)
for node in self:iter() do f(node,...) end
return self
end
--- Each (in range) transformation. Calls a function on each `node` in the range of a rectangle of cells,
-- passing the `node` as the first argument to function __f__.
-- @class function
-- @tparam int lx the leftmost x-coordinate coordinate of the rectangle
-- @tparam int ly the topmost y-coordinate of the rectangle
-- @tparam int ex the rightmost x-coordinate of the rectangle
-- @tparam int ey the bottom-most y-coordinate of the rectangle
-- @tparam func f a function prototyped as __f(node,...)__
-- @tparam[opt] vararg ... args to be passed to function __f__
-- @treturn grid self (the calling `grid` itself, can be chained)
-- @usage
-- local function printNode(node)
-- print(node:getX(), node:getY())
-- end
-- myGrid:eachRange(1,1,8,8,printNode)
function Grid:eachRange(lx,ly,ex,ey,f,...)
for node in self:iter(lx,ly,ex,ey) do f(node,...) end
return self
end
--- Map transformation.
-- Calls function __f(node,...)__ on each `node` in a given range, passing the `node` as the first arg to function __f__ and replaces
-- it with the returned value. Therefore, the function should return a `node`.
-- @class function
-- @tparam func f a function prototyped as __f(node,...)__
-- @tparam[opt] vararg ... args to be passed to function __f__
-- @treturn grid self (the calling `grid` itself, can be chained)
-- @usage
-- local function nothing(node)
-- return node
-- end
-- myGrid:imap(nothing)
function Grid:imap(f,...)
for node in self:iter() do
node = f(node,...)
end
return self
end
--- Map in range transformation.
-- Calls function __f(node,...)__ on each `node` in a rectangle range, passing the `node` as the first argument to the function and replaces
-- it with the returned value. Therefore, the function should return a `node`.
-- @class function
-- @tparam int lx the leftmost x-coordinate coordinate of the rectangle
-- @tparam int ly the topmost y-coordinate of the rectangle
-- @tparam int ex the rightmost x-coordinate of the rectangle
-- @tparam int ey the bottom-most y-coordinate of the rectangle
-- @tparam func f a function prototyped as __f(node,...)__
-- @tparam[opt] vararg ... args to be passed to function __f__
-- @treturn grid self (the calling `grid` itself, can be chained)
-- @usage
-- local function nothing(node)
-- return node
-- end
-- myGrid:imap(1,1,6,6,nothing)
function Grid:imapRange(lx,ly,ex,ey,f,...)
for node in self:iter(lx,ly,ex,ey) do
node = f(node,...)
end
return self
end
-- Specialized grids
-- Inits a preprocessed grid
function PreProcessGrid:new(map)
local newGrid = {}
newGrid._map = map
newGrid._nodes, newGrid._min_x, newGrid._max_x, newGrid._min_y, newGrid._max_y, newGrid._min_z, newGrid._max_z = Utils.arrayToNodes(newGrid._map)
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
newGrid._isAnnotated = {}
return setmetatable(newGrid,PreProcessGrid)
end
-- Inits a postprocessed grid
function PostProcessGrid:new(map)
local newGrid = {}
newGrid._map = map
newGrid._nodes = {}
newGrid._min_x, newGrid._max_x, newGrid._min_y, newGrid._max_y = Utils.getArrayBounds(newGrid._map)
newGrid._width = (newGrid._max_x-newGrid._min_x)+1
newGrid._height = (newGrid._max_y-newGrid._min_y)+1
newGrid._isAnnotated = {}
return setmetatable(newGrid,PostProcessGrid)
end
--- Returns the `node` at location [x,y].
-- @class function
-- @name Grid:getNodeAt
-- @tparam int x the x-coordinate coordinate
-- @tparam int y the y-coordinate coordinate
-- @treturn node a `node`
-- @usage local aNode = myGrid:getNodeAt(2,2)
-- Gets the node at location <x,y> on a preprocessed grid
function PreProcessGrid:getNodeAt(x,y,z)
return self._nodes[y] and self._nodes[y][x] and self._nodes[y][x][z] or nil
end
-- Gets the node at location <x,y> on a postprocessed grid
function PostProcessGrid:getNodeAt(x,y,z)
if not x or not y or not z then return end 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(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(y,self._min_y,self._max_y) then return end
if Utils.outOfRange(z,self._min_z,self._max_z) 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] then self._nodes[y] = {} end
if not self._nodes[y][x] then self._nodes[y][x] = {} 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 if not self._nodes[y][x][z] then self._nodes[y][x][z] = Node:new(x,y,z) end

View File

@@ -28,6 +28,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
--]] --]]
--- The Pathfinder class
--
-- Implementation of the `pathfinder` class.
local _VERSION = "" local _VERSION = ""
local _RELEASEDATE = "" local _RELEASEDATE = ""
@@ -36,46 +41,333 @@ if (...) then
-- Dependencies -- Dependencies
local _PATH = (...):gsub('%.pathfinder$','') local _PATH = (...):gsub('%.pathfinder$','')
local Utils = require (_PATH .. '.core.utils') local Utils = require (_PATH .. '.core.utils')
local Assert = require (_PATH .. '.core.assert')
local Heap = require (_PATH .. '.core.bheap')
local Heuristic = require (_PATH .. '.core.heuristics')
local Grid = require (_PATH .. '.grid')
local Path = require (_PATH .. '.core.path')
-- Internalization -- Internalization
local t_insert, t_remove = table.insert, table.remove
local floor = math.floor
local pairs = pairs local pairs = pairs
local assert = assert local assert = assert
local setmetatable = setmetatable local type = type
local setmetatable, getmetatable = setmetatable, getmetatable
--- Finders (search algorithms implemented). Refers to the search algorithms actually implemented in Jumper. --- Finders (search algorithms implemented). Refers to the search algorithms actually implemented in Jumper.
--
-- <li>[A*](http://en.wikipedia.org/wiki/A*_search_algorithm)</li> -- <li>[A*](http://en.wikipedia.org/wiki/A*_search_algorithm)</li>
-- <li>[Dijkstra](http://en.wikipedia.org/wiki/Dijkstra%27s_algorithm)</li>
-- <li>[Theta Astar](http://aigamedev.com/open/tutorials/theta-star-any-angle-paths/)</li>
-- <li>[BFS](http://en.wikipedia.org/wiki/Breadth-first_search)</li>
-- <li>[DFS](http://en.wikipedia.org/wiki/Depth-first_search)</li>
-- <li>[JPS](http://harablog.wordpress.com/2011/09/07/jump-point-search/)</li>
-- @finder Finders
-- @see Pathfinder:getFinders
local Finders = { local Finders = {
['ASTAR'] = require (_PATH .. '.search.astar'), ['ASTAR'] = require (_PATH .. '.search.astar'),
-- ['DIJKSTRA'] = require (_PATH .. '.search.dijkstra'),
-- ['THETASTAR'] = require (_PATH .. '.search.thetastar'),
['BFS'] = require (_PATH .. '.search.bfs'),
-- ['DFS'] = require (_PATH .. '.search.dfs'),
-- ['JPS'] = require (_PATH .. '.search.jps')
} }
-- Will keep track of all nodes expanded during the search -- Will keep track of all nodes expanded during the search
-- to easily reset their properties for the next pathfinding call -- to easily reset their properties for the next pathfinding call
local toClear = {} local toClear = {}
--- Search modes. Refers to the search modes. In ORTHOGONAL mode, 4-directions are only possible when moving,
-- including North, East, West, South. In DIAGONAL mode, 8-directions are possible when moving,
-- including North, East, West, South and adjacent directions.
--
-- <li>ORTHOGONAL</li>
-- <li>DIAGONAL</li>
-- @mode Modes
-- @see Pathfinder:getModes
local searchModes = {['DIAGONAL'] = true, ['ORTHOGONAL'] = true}
-- Performs a traceback from the goal node to the start node -- Performs a traceback from the goal node to the start node
-- Only happens when the path was found -- Only happens when the path was found
--- The `Pathfinder` class.<br/>
-- This class is callable.
-- Therefore,_ <code>Pathfinder(...)</code> _acts as a shortcut to_ <code>Pathfinder:new(...)</code>.
-- @type Pathfinder
local Pathfinder = {} local Pathfinder = {}
Pathfinder.__index = Pathfinder Pathfinder.__index = Pathfinder
function Pathfinder:new(heuristic) --- Inits a new `pathfinder`
-- @class function
-- @tparam grid grid a `grid`
-- @tparam[opt] string finderName the name of the `Finder` (search algorithm) to be used for search.
-- Defaults to `ASTAR` when not given (see @{Pathfinder:getFinders}).
-- @tparam[optchain] string|int|func walkable the value for __walkable__ nodes.
-- If this parameter is a function, it should be prototyped as __f(value)__, returning a boolean:
-- __true__ when value matches a __walkable__ `node`, __false__ otherwise.
-- @treturn pathfinder a new `pathfinder` instance
-- @usage
-- -- Example one
-- local finder = Pathfinder:new(myGrid, 'ASTAR', 0)
--
-- -- Example two
-- local function walkable(value)
-- return value > 0
-- end
-- local finder = Pathfinder(myGrid, 'JPS', walkable)
function Pathfinder:new(grid, finderName, walkable)
local newPathfinder = {} local newPathfinder = {}
setmetatable(newPathfinder, Pathfinder) setmetatable(newPathfinder, Pathfinder)
self._finder = Finders.ASTAR --newPathfinder:setGrid(grid)
self._heuristic = heuristic newPathfinder:setFinder(finderName)
--newPathfinder:setWalkable(walkable)
newPathfinder:setMode('DIAGONAL')
newPathfinder:setHeuristic('MANHATTAN')
newPathfinder:setTunnelling(false)
return newPathfinder return newPathfinder
end end
--- Evaluates [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric)
-- for the whole `grid`. It should be called only once, unless the collision map or the
-- __walkable__ attribute changes. The clearance values are calculated and cached within the grid nodes.
-- @class function
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage myFinder:annotateGrid()
function Pathfinder:annotateGrid()
assert(self._walkable, 'Finder must implement a walkable value')
for x=self._grid._max_x,self._grid._min_x,-1 do
for y=self._grid._max_y,self._grid._min_y,-1 do
local node = self._grid:getNodeAt(x,y)
if self._grid:isWalkableAt(x,y,self._walkable) then
local nr = self._grid:getNodeAt(node._x+1, node._y)
local nrd = self._grid:getNodeAt(node._x+1, node._y+1)
local nd = self._grid:getNodeAt(node._x, node._y+1)
if nr and nrd and nd then
local m = nrd._clearance[self._walkable] or 0
m = (nd._clearance[self._walkable] or 0)<m and (nd._clearance[self._walkable] or 0) or m
m = (nr._clearance[self._walkable] or 0)<m and (nr._clearance[self._walkable] or 0) or m
node._clearance[self._walkable] = m+1
else
node._clearance[self._walkable] = 1
end
else node._clearance[self._walkable] = 0
end
end
end
self._grid._isAnnotated[self._walkable] = true
return self
end
--- Removes [clearance](http://aigamedev.com/open/tutorial/clearance-based-pathfinding/#TheTrueClearanceMetric)values.
-- Clears cached clearance values for the current __walkable__.
-- @class function
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage myFinder:clearAnnotations()
function Pathfinder:clearAnnotations()
assert(self._walkable, 'Finder must implement a walkable value')
for node in self._grid:iter() do
node:removeClearance(self._walkable)
end
self._grid._isAnnotated[self._walkable] = false
return self
end
--- Sets the `grid`. Defines the given `grid` as the one on which the `pathfinder` will perform the search.
-- @class function
-- @tparam grid grid a `grid`
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage myFinder:setGrid(myGrid)
function Pathfinder:setGrid(grid) function Pathfinder:setGrid(grid)
assert(Assert.inherits(grid, Grid), 'Wrong argument #1. Expected a \'grid\' object')
self._grid = grid self._grid = grid
self._grid._eval = self._walkable and type(self._walkable) == 'function'
return self return self
end end
--- Calculates a `path`. Returns the `path` from start to end location --- Returns the `grid`. This is a reference to the actual `grid` used by the `pathfinder`.
-- @class function
-- @treturn grid the `grid`
-- @usage local myGrid = myFinder:getGrid()
function Pathfinder:getGrid()
return self._grid
end
--- Sets the __walkable__ value or function.
-- @class function
-- @tparam string|int|func walkable the value for walkable nodes.
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage
-- -- Value '0' is walkable
-- myFinder:setWalkable(0)
--
-- -- Any value greater than 0 is walkable
-- myFinder:setWalkable(function(n)
-- return n>0
-- end
function Pathfinder:setWalkable(walkable)
assert(Assert.matchType(walkable,'stringintfunctionnil'),
('Wrong argument #1. Expected \'string\', \'number\' or \'function\', got %s.'):format(type(walkable)))
self._walkable = walkable
self._grid._eval = type(self._walkable) == 'function'
return self
end
--- Gets the __walkable__ value or function.
-- @class function
-- @treturn string|int|func the `walkable` value or function
-- @usage local walkable = myFinder:getWalkable()
function Pathfinder:getWalkable()
return self._walkable
end
--- Defines the `finder`. It refers to the search algorithm used by the `pathfinder`.
-- Default finder is `ASTAR`. Use @{Pathfinder:getFinders} to get the list of available finders.
-- @class function
-- @tparam string finderName the name of the `finder` to be used for further searches.
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage
-- --To use Breadth-First-Search
-- myFinder:setFinder('BFS')
-- @see Pathfinder:getFinders
function Pathfinder:setFinder(finderName)
if not finderName then
if not self._finder then
finderName = 'ASTAR'
else return
end
end
assert(Finders[finderName],'Not a valid finder name!')
self._finder = finderName
return self
end
--- Returns the name of the `finder` being used.
-- @class function
-- @treturn string the name of the `finder` to be used for further searches.
-- @usage local finderName = myFinder:getFinder()
function Pathfinder:getFinder()
return self._finder
end
--- Returns the list of all available finders names.
-- @class function
-- @treturn {string,...} array of built-in finders names.
-- @usage
-- local finders = myFinder:getFinders()
-- for i, finderName in ipairs(finders) do
-- print(i, finderName)
-- end
function Pathfinder:getFinders()
return Utils.getKeys(Finders)
end
--- Sets a heuristic. This is a function internally used by the `pathfinder` to find the optimal path during a search.
-- Use @{Pathfinder:getHeuristics} to get the list of all available `heuristics`. One can also define
-- his own `heuristic` function.
-- @class function
-- @tparam func|string heuristic `heuristic` function, prototyped as __f(dx,dy)__ or as a `string`.
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @see Pathfinder:getHeuristics
-- @see core.heuristics
-- @usage myFinder:setHeuristic('MANHATTAN')
function Pathfinder:setHeuristic(heuristic)
assert(Heuristic[heuristic] or (type(heuristic) == 'function'),'Not a valid heuristic!')
self._heuristic = Heuristic[heuristic] or heuristic
return self
end
--- Returns the `heuristic` used. Returns the function itself.
-- @class function
-- @treturn func the `heuristic` function being used by the `pathfinder`
-- @see core.heuristics
-- @usage local h = myFinder:getHeuristic()
function Pathfinder:getHeuristic()
return self._heuristic
end
--- Gets the list of all available `heuristics`.
-- @class function
-- @treturn {string,...} array of heuristic names.
-- @see core.heuristics
-- @usage
-- local heur = myFinder:getHeuristic()
-- for i, heuristicName in ipairs(heur) do
-- ...
-- end
function Pathfinder:getHeuristics()
return Utils.getKeys(Heuristic)
end
--- Defines the search `mode`.
-- The default search mode is the `DIAGONAL` mode, which implies 8-possible directions when moving (north, south, east, west and diagonals).
-- In `ORTHOGONAL` mode, only 4-directions are allowed (north, south, east and west).
-- Use @{Pathfinder:getModes} to get the list of all available search modes.
-- @class function
-- @tparam string mode the new search `mode`.
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @see Pathfinder:getModes
-- @see Modes
-- @usage myFinder:setMode('ORTHOGONAL')
function Pathfinder:setMode(mode)
assert(searchModes[mode],'Invalid mode')
self._allowDiagonal = (mode == 'DIAGONAL')
return self
end
--- Returns the search mode.
-- @class function
-- @treturn string the current search mode
-- @see Modes
-- @usage local mode = myFinder:getMode()
function Pathfinder:getMode()
return (self._allowDiagonal and 'DIAGONAL' or 'ORTHOGONAL')
end
--- Gets the list of all available search modes.
-- @class function
-- @treturn {string,...} array of search modes.
-- @see Modes
-- @usage local modes = myFinder:getModes()
-- for modeName in ipairs(modes) do
-- ...
-- end
function Pathfinder:getModes()
return Utils.getKeys(searchModes)
end
--- Enables tunnelling. Defines the ability for the `pathfinder` to tunnel through walls when heading diagonally.
-- This feature __is not compatible__ with Jump Point Search algorithm (i.e. enabling it will not affect Jump Point Search)
-- @class function
-- @tparam bool bool a boolean
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage myFinder:setTunnelling(true)
function Pathfinder:setTunnelling(bool)
assert(Assert.isBool(bool), ('Wrong argument #1. Expected boolean, got %s'):format(type(bool)))
self._tunnel = bool
return self
end
--- Returns tunnelling feature state.
-- @class function
-- @treturn bool tunnelling feature actual state
-- @usage local isTunnellingEnabled = myFinder:getTunnelling()
function Pathfinder:getTunnelling()
return self._tunnel
end
--- Calculates a `path`. Returns the `path` from location __[startX, startY]__ to location __[endX, endY]__.
-- Both locations must exist on the collision map. The starting location can be unwalkable. -- Both locations must exist on the collision map. The starting location can be unwalkable.
-- @class function
-- @tparam int startX the x-coordinate for the starting location
-- @tparam int startY the y-coordinate for the starting location
-- @tparam int endX the x-coordinate for the goal location
-- @tparam int endY the y-coordinate for the goal location
-- @tparam int clearance the amount of clearance (i.e the pathing agent size) to consider
-- @treturn path a path (array of nodes) when found, otherwise nil -- @treturn path a path (array of nodes) when found, otherwise nil
-- @usage local path = myFinder:getPath(1,1,5,5) -- @usage local path = myFinder:getPath(1,1,5,5)
function Pathfinder:getPath(startX, startY, startZ, ih, endX, endY, endZ, oh) function Pathfinder:getPath(startX, startY, startZ, ih, endX, endY, endZ, oh, clearance)
self:reset() self:reset()
local startNode = self._grid:getNodeAt(startX, startY, startZ) local startNode = self._grid:getNodeAt(startX, startY, startZ)
local endNode = self._grid:getNodeAt(endX, endY, endZ) local endNode = self._grid:getNodeAt(endX, endY, endZ)
@@ -83,21 +375,20 @@ if (...) then
return nil return nil
end end
startNode.heading = ih startNode._heading = ih
endNode.heading = oh endNode._heading = oh
assert(startNode, ('Invalid location [%d, %d, %d]'):format(startX, startY, startZ)) assert(startNode, ('Invalid location [%d, %d, %d]'):format(startX, startY, startZ))
assert(endNode and self._grid:isWalkableAt(endX, endY, endZ), assert(endNode and self._grid:isWalkableAt(endX, endY, endZ),
('Invalid or unreachable location [%d, %d, %d]'):format(endX, endY, endZ)) ('Invalid or unreachable location [%d, %d, %d]'):format(endX, endY, endZ))
local _endNode = self._finder(self, startNode, endNode, toClear) local _endNode = Finders[self._finder](self, startNode, endNode, clearance, toClear)
if _endNode then if _endNode then
return Utils.traceBackPath(self, _endNode, startNode) return Utils.traceBackPath(self, _endNode, startNode)
end end
return nil return nil
end end
--- Resets the `pathfinder`. This function is called internally between --- Resets the `pathfinder`. This function is called internally between successive pathfinding calls, so you should not
-- successive pathfinding calls, so you should not
-- use it explicitely, unless under specific circumstances. -- use it explicitely, unless under specific circumstances.
-- @class function -- @class function
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained) -- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
@@ -108,6 +399,7 @@ if (...) then
return self return self
end end
-- Returns Pathfinder class -- Returns Pathfinder class
Pathfinder._VERSION = _VERSION Pathfinder._VERSION = _VERSION
Pathfinder._RELEASEDATE = _RELEASEDATE Pathfinder._RELEASEDATE = _RELEASEDATE
@@ -116,4 +408,5 @@ if (...) then
return self:new(...) return self:new(...)
end end
}) })
end end

View File

@@ -5,42 +5,51 @@
if (...) then if (...) then
-- Internalization -- Internalization
local ipairs = ipairs
local huge = math.huge local huge = math.huge
-- Dependancies -- Dependancies
local _PATH = (...):match('(.+)%.search.astar$') local _PATH = (...):match('(.+)%.search.astar$')
local Heuristics = require (_PATH .. '.core.heuristics')
local Heap = require (_PATH.. '.core.bheap') local Heap = require (_PATH.. '.core.bheap')
-- Updates G-cost -- Updates G-cost
local function computeCost(node, neighbour, heuristic) local function computeCost(node, neighbour, finder, clearance, heuristic)
local mCost, heading = heuristic(neighbour, node) -- Heuristics.EUCLIDIAN(neighbour, node) local mCost, heading = heuristic(neighbour, node) -- Heuristics.EUCLIDIAN(neighbour, node)
if node._g + mCost < neighbour._g then if node._g + mCost < neighbour._g then
neighbour._parent = node neighbour._parent = node
neighbour._g = node._g + mCost neighbour._g = node._g + mCost
neighbour.heading = heading neighbour._heading = heading
end end
end end
-- Updates vertex node-neighbour -- Updates vertex node-neighbour
local function updateVertex(openList, node, neighbour, endNode, heuristic) local function updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval)
local oldG = neighbour._g local oldG = neighbour._g
computeCost(node, neighbour, heuristic) local cmpCost = overrideCostEval or computeCost
cmpCost(node, neighbour, finder, clearance, heuristic)
if neighbour._g < oldG then if neighbour._g < oldG then
if neighbour._opened then neighbour._opened = false end local nClearance = neighbour._clearance[finder._walkable]
neighbour._h = heuristic(endNode, neighbour) local pushThisNode = clearance and nClearance and (nClearance >= clearance)
neighbour._f = neighbour._g + neighbour._h if (clearance and pushThisNode) or (not clearance) then
openList:push(neighbour) if neighbour._opened then neighbour._opened = false end
neighbour._opened = true neighbour._h = heuristic(endNode, neighbour)
neighbour._f = neighbour._g + neighbour._h
openList:push(neighbour)
neighbour._opened = true
end
end end
end end
-- Calculates a path. -- Calculates a path.
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`. -- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
return function (finder, startNode, endNode, toClear) return function (finder, startNode, endNode, clearance, toClear, overrideHeuristic, overrideCostEval)
local heuristic = overrideHeuristic or finder._heuristic
local openList = Heap() local openList = Heap()
startNode._g = 0 startNode._g = 0
startNode._h = finder._heuristic(endNode, startNode) startNode._h = heuristic(endNode, startNode)
startNode._f = startNode._g + startNode._h startNode._f = startNode._g + startNode._h
openList:push(startNode) openList:push(startNode)
toClear[startNode] = true toClear[startNode] = true
@@ -50,28 +59,30 @@ if (...) then
local node = openList:pop() local node = openList:pop()
node._closed = true node._closed = true
if node == endNode then return node end if node == endNode then return node end
local neighbours = finder._grid:getNeighbours(node) local neighbours = finder._grid:getNeighbours(node, finder._walkable, finder._allowDiagonal, finder._tunnel)
for i = 1,#neighbours do for i = 1,#neighbours do
local neighbour = neighbours[i] local neighbour = neighbours[i]
if not neighbour._closed then if not neighbour._closed then
toClear[neighbour] = true toClear[neighbour] = true
if not neighbour._opened then if not neighbour._opened then
neighbour._g = huge neighbour._g = huge
neighbour._parent = nil neighbour._parent = nil
end end
updateVertex(openList, node, neighbour, endNode, finder._heuristic) updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval)
end end
end end
--[[ --[[
printf('x:%d y:%d z:%d g:%d', node.x, node.y, node.z, node._g) printf('x:%d y:%d z:%d g:%d', node._x, node._y, node._z, node._g)
for i = 1,#neighbours do for i = 1,#neighbours do
local n = neighbours[i] 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) 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
--]] --]]
end end
return nil
return nil
end end
end end

View File

@@ -0,0 +1,46 @@
-- Breadth-First search algorithm
if (...) then
-- Internalization
local t_remove = table.remove
local function breadth_first_search(finder, openList, node, endNode, clearance, toClear)
local neighbours = finder._grid:getNeighbours(node, finder._walkable, finder._allowDiagonal, finder._tunnel)
for i = 1,#neighbours do
local neighbour = neighbours[i]
if not neighbour._closed and not neighbour._opened then
local nClearance = neighbour._clearance[finder._walkable]
local pushThisNode = clearance and nClearance and (nClearance >= clearance)
if (clearance and pushThisNode) or (not clearance) then
openList[#openList+1] = neighbour
neighbour._opened = true
neighbour._parent = node
toClear[neighbour] = true
end
end
end
end
-- Calculates a path.
-- Returns the path from location `<startX, startY>` to location `<endX, endY>`.
return function (finder, startNode, endNode, clearance, toClear)
local openList = {} -- We'll use a FIFO queue (simple array)
openList[1] = startNode
startNode._opened = true
toClear[startNode] = true
local node
while (#openList > 0) do
node = openList[1]
t_remove(openList,1)
node._closed = true
if node == endNode then return node end
breadth_first_search(finder, openList, node, endNode, clearance, toClear)
end
return nil
end
end

View File

@@ -21,12 +21,8 @@ function NFT.parse(imageText)
} }
local num = 1 local num = 1
local lines = Util.split(imageText) local index = 1
while #lines[#lines] == 0 do for _,sLine in ipairs(Util.split(imageText)) do
table.remove(lines, #lines)
end
for _,sLine in ipairs(lines) do
table.insert(image.fg, { }) table.insert(image.fg, { })
table.insert(image.bg, { }) table.insert(image.bg, { })
table.insert(image.text, { }) table.insert(image.text, { })
@@ -51,7 +47,7 @@ function NFT.parse(imageText)
fgNext = false fgNext = false
else else
if nextChar ~= " " and currFG == nil then if nextChar ~= " " and currFG == nil then
currFG = _G.colors.white currFG = colours.white
end end
image.bg[num][writeIndex] = currBG image.bg[num][writeIndex] = currBG
image.fg[num][writeIndex] = currFG image.fg[num][writeIndex] = currFG

49
sys/apis/opus.lua Normal file
View File

@@ -0,0 +1,49 @@
local Opus = { }
local function runDir(directory, open)
if not fs.exists(directory) then
return true
end
local success = true
local files = fs.list(directory)
table.sort(files)
for _,file in ipairs(files) do
os.sleep(0)
local result, err = open(directory .. '/' .. file)
if result then
if term.isColor() then
term.setTextColor(colors.green)
end
term.write('[PASS] ')
term.setTextColor(colors.white)
term.write(fs.combine(directory, file))
else
if term.isColor() then
term.setTextColor(colors.red)
end
term.write('[FAIL] ')
term.setTextColor(colors.white)
term.write(fs.combine(directory, file))
if err then
printError(err)
end
success = false
end
print()
end
return success
end
function Opus.loadServices()
return runDir('sys/services', shell.openHiddenTab)
end
function Opus.autorun()
local s = runDir('sys/autorun', shell.run)
return runDir('usr/autorun', shell.run) and s
end
return Opus

View File

@@ -1,16 +1,16 @@
local Event = require('event') local Util = require('util')
local Socket = require('socket')
local Util = require('util')
local Peripheral = Util.shallowCopy(_G.peripheral) local Peripheral = { }
local function getDeviceList()
function Peripheral.getList()
if _G.device then if _G.device then
return _G.device return _G.device
end end
local deviceList = { } local deviceList = { }
for _,side in pairs(Peripheral.getNames()) do
for _,side in pairs(peripheral.getNames()) do
Peripheral.addDevice(deviceList, side) Peripheral.addDevice(deviceList, side)
end end
@@ -19,14 +19,14 @@ end
function Peripheral.addDevice(deviceList, side) function Peripheral.addDevice(deviceList, side)
local name = side local name = side
local ptype = Peripheral.getType(side) local ptype = peripheral.getType(side)
if not ptype then if not ptype then
return return
end end
if ptype == 'modem' then if ptype == 'modem' then
if Peripheral.call(name, 'isWireless') then if peripheral.call(name, 'isWireless') then
ptype = 'wireless_modem' ptype = 'wireless_modem'
else else
ptype = 'wired_modem' ptype = 'wired_modem'
@@ -52,10 +52,10 @@ function Peripheral.addDevice(deviceList, side)
name = uniqueName name = uniqueName
end end
local s, m = pcall(function() deviceList[name] = Peripheral.wrap(side) end) local s, m pcall(function() deviceList[name] = peripheral.wrap(side) end)
if not s and m then if not s and m then
_G.printError('wrap failed') printError('wrap failed')
_G.printError(m) printError(m)
end end
if deviceList[name] then if deviceList[name] then
@@ -70,15 +70,15 @@ function Peripheral.addDevice(deviceList, side)
end end
function Peripheral.getBySide(side) function Peripheral.getBySide(side)
return Util.find(Peripheral.getList(), 'side', side) return Util.find(getDeviceList(), 'side', side)
end end
function Peripheral.getByType(typeName) function Peripheral.getByType(typeName)
return Util.find(Peripheral.getList(), 'type', typeName) return Util.find(getDeviceList(), 'type', typeName)
end end
function Peripheral.getByMethod(method) function Peripheral.getByMethod(method)
for _,p in pairs(Peripheral.getList()) do for _,p in pairs(getDeviceList()) do
if p[method] then if p[method] then
return p return p
end end
@@ -92,9 +92,7 @@ function Peripheral.get(args)
args = { type = args } args = { type = args }
end end
if args.name then args = args or { type = pType }
return _G.device[args.name]
end
if args.type then if args.type then
local p = Peripheral.getByType(args.type) local p = Peripheral.getByType(args.type)
@@ -118,113 +116,4 @@ function Peripheral.get(args)
end end
end end
local function getProxy(pi)
local socket = Socket.connect(pi.host, 189)
if not socket then
error("Timed out attaching peripheral: " .. pi.uri)
end
socket:write(pi.path)
local proxy = socket:read(3)
if not proxy then
error("Timed out attaching peripheral: " .. pi.uri)
end
local methods = proxy.methods
proxy.methods = nil
for _,method in pairs(methods) do
proxy[method] = function(...)
socket:write({ fn = method, args = { ... } })
local resp = socket:read()
if not resp then
error("Timed out communicating with peripheral: " .. pi.uri)
end
return table.unpack(resp)
end
end
if proxy.blit then
local methods = { 'clear', 'clearLine', 'setCursorPos', 'write', 'blit',
'setTextColor', 'setTextColour', 'setBackgroundColor',
'setBackgroundColour', 'scroll', 'setCursorBlink', }
local queue = nil
for _,method in pairs(methods) do
proxy[method] = function(...)
if not queue then
queue = { }
Event.onTimeout(0, function()
if not socket:write({ fn = 'fastBlit', args = { queue } }) then
error("Timed out communicating with peripheral: " .. pi.uri)
end
queue = nil
socket:read()
end)
end
table.insert(queue, {
fn = method,
args = { ... },
})
end
end
end
if proxy.type == 'monitor' then
Event.addRoutine(function()
while true do
local event = socket:read()
if not event then
break
end
if not Util.empty(event) then
os.queueEvent(table.unpack(event))
end
end
end)
end
return proxy
end
--[[
Parse a uri into it's components
Examples:
monitor = { name = 'monitor' }
side/top = { side = 'top' }
method/list = { method = 'list' }
12://name/monitor = { host = 12, name = 'monitor' }
]]--
local function parse(uri)
local pi = Util.split(uri:gsub('^%d*://', ''), '(.-)/')
if #pi == 1 then
pi = {
'name',
pi[1],
}
end
return {
host = uri:match('^(%d*)%:'), -- 12
uri = uri, -- 12://name/monitor
path = uri:gsub('^%d*://', ''), -- name/monitor
[ pi[1] ] = pi[2], -- name = 'monitor'
}
end
function Peripheral.lookup(uri)
local pi = parse(uri)
if pi.host then
return getProxy(pi)
end
return Peripheral.get(pi)
end
return Peripheral return Peripheral

View File

@@ -2,48 +2,6 @@ local Util = require('util')
local Point = { } 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) function Point.copy(pt)
return { x = pt.x, y = pt.y, z = pt.z } return { x = pt.x, y = pt.y, z = pt.z }
end end
@@ -69,7 +27,7 @@ function Point.subtract(a, b)
end end
-- Euclidian distance -- Euclidian distance
function Point.distance(a, b) function Point.pythagoreanDistance(a, b)
return math.sqrt( return math.sqrt(
math.pow(a.x - b.x, 2) + math.pow(a.x - b.x, 2) +
math.pow(a.y - b.y, 2) + math.pow(a.y - b.y, 2) +
@@ -79,7 +37,7 @@ end
-- turtle distance (manhattan) -- turtle distance (manhattan)
function Point.turtleDistance(a, b) function Point.turtleDistance(a, b)
if a.y and b.y then if a.y and b.y then
return math.abs(a.x - b.x) + return math.abs(a.x - b.x) +
math.abs(a.y - b.y) + math.abs(a.y - b.y) +
math.abs(a.z - b.z) math.abs(a.z - b.z)
else else
@@ -99,20 +57,28 @@ function Point.calculateTurns(ih, oh)
end end
function Point.calculateHeading(pta, ptb) 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 local heading
heading = zd < 0 and 1 or 3
elseif (pta.heading % 2) == 1 and xd ~= 0 then if (pta.heading % 2) == 0 and pta.z ~= ptb.z then
heading = xd < 0 and 0 or 2 if ptb.z > pta.z then
elseif pta.heading == 0 and xd > 0 then heading = 1
else
heading = 3
end
elseif (pta.heading % 2) == 1 and pta.x ~= ptb.x then
if ptb.x > pta.x then
heading = 0
else
heading = 2
end
elseif pta.heading == 0 and pta.x > ptb.x then
heading = 2 heading = 2
elseif pta.heading == 2 and xd < 0 then elseif pta.heading == 2 and pta.x < ptb.x then
heading = 0 heading = 0
elseif pta.heading == 1 and zd > 0 then elseif pta.heading == 1 and pta.z > ptb.z then
heading = 3 heading = 3
elseif pta.heading == 3 and zd < 0 then elseif pta.heading == 3 and pta.z < ptb.z then
heading = 1 heading = 1
end end
@@ -150,31 +116,25 @@ function Point.calculateMoves(pta, ptb, distance)
heading = ptb.heading heading = ptb.heading
end end
end end
return moves, heading return moves, heading
end end
-- given a set of points, find the one taking the least moves -- given a set of points, find the one taking the least moves
function Point.closest(reference, pts) function Point.closest(reference, pts)
if #pts == 1 then local lpt, lm -- lowest
return pts[1]
end
local lm, lpt = math.huge
for _,pt in pairs(pts) do for _,pt in pairs(pts) do
local distance = Point.turtleDistance(reference, pt) local m = Point.calculateMoves(reference, pt)
if distance < lm then if not lm or m < lm then
local m = Point.calculateMoves(reference, pt, distance) lpt = pt
if m < lm then lm = m
lpt = pt
lm = m
end
end end
end end
return lpt return lpt
end end
function Point.eachClosest(spt, ipts, fn) function Point.eachClosest(spt, ipts, fn)
local pts = Util.shallowCopy(ipts) local pts = Util.shallowCopy(ipts)
while #pts > 0 do while #pts > 0 do
local pt = Point.closest(spt, pts) local pt = Point.closest(spt, pts)
@@ -189,87 +149,13 @@ end
function Point.adjacentPoints(pt) function Point.adjacentPoints(pt)
local pts = { } local pts = { }
for i = 0, 5 do for _, hi in pairs(turtle.getHeadings()) do
local hi = Point.headings[i]
table.insert(pts, { x = pt.x + hi.xd, y = pt.y + hi.yd, z = pt.z + hi.zd }) table.insert(pts, { x = pt.x + hi.xd, y = pt.y + hi.yd, z = pt.z + hi.zd })
end end
return pts return pts
end 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) function Point.normalizeBox(box)
return { return {
x = math.min(box.x, box.ex), x = math.min(box.x, box.ex),
@@ -290,17 +176,33 @@ function Point.inBox(pt, box)
pt.z <= box.ez pt.z <= box.ez
end 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 return Point
--[[
Box = { }
function Box.contain(boundingBox, containedBox)
local shiftX = boundingBox.ax - containedBox.ax
if shiftX > 0 then
containedBox.ax = containedBox.ax + shiftX
containedBox.bx = containedBox.bx + shiftX
end
local shiftZ = boundingBox.az - containedBox.az
if shiftZ > 0 then
containedBox.az = containedBox.az + shiftZ
containedBox.bz = containedBox.bz + shiftZ
end
shiftX = boundingBox.bx - containedBox.bx
if shiftX < 0 then
containedBox.ax = containedBox.ax + shiftX
containedBox.bx = containedBox.bx + shiftX
end
shiftZ = boundingBox.bz - containedBox.bz
if shiftZ < 0 then
containedBox.az = containedBox.az + shiftZ
containedBox.bz = containedBox.bz + shiftZ
end
end
--]]

View File

@@ -9,10 +9,6 @@ function Security.verifyPassword(password)
return config.password and password == config.password return config.password and password == config.password
end end
function Security.hasPassword()
return not not config.password
end
function Security.getSecretKey() function Security.getSecretKey()
Config.load('os', config) Config.load('os', config)
if not config.secretKey then if not config.secretKey then
@@ -32,7 +28,7 @@ function Security.getPublicKey()
local function modexp(base, exponent, modulo) local function modexp(base, exponent, modulo)
local remainder = base local remainder = base
for _ = 1, exponent-1 do for i = 1, exponent-1 do
remainder = remainder * remainder remainder = remainder * remainder
if remainder >= modulo then if remainder >= modulo then
remainder = remainder % modulo remainder = remainder % modulo

View File

@@ -3,14 +3,11 @@ local Logger = require('logger')
local Security = require('security') local Security = require('security')
local Util = require('util') local Util = require('util')
local device = _G.device
local os = _G.os
local socketClass = { } local socketClass = { }
function socketClass:read(timeout) function socketClass:read(timeout)
local data, distance = _G.transport.read(self) local data, distance = transport.read(self)
if data then if data then
return data, distance return data, distance
end end
@@ -26,7 +23,7 @@ function socketClass:read(timeout)
local e, id = os.pullEvent() local e, id = os.pullEvent()
if e == 'transport_' .. self.sport then if e == 'transport_' .. self.sport then
data, distance = _G.transport.read(self) data, distance = transport.read(self)
if data then if data then
os.cancelTimer(timerId) os.cancelTimer(timerId)
return data, distance return data, distance
@@ -37,14 +34,13 @@ function socketClass:read(timeout)
break break
end end
timerId = os.startTimer(5) timerId = os.startTimer(5)
self:ping()
end end
end end
end end
function socketClass:write(data) function socketClass:write(data)
if self.connected then if self.connected then
_G.transport.write(self, { transport.write(self, {
type = 'DATA', type = 'DATA',
seq = self.wseq, seq = self.wseq,
data = data, data = data,
@@ -55,7 +51,10 @@ end
function socketClass:ping() function socketClass:ping()
if self.connected then if self.connected then
_G.transport.ping(self) transport.write(self, {
type = 'PING',
seq = self.wseq,
})
return true return true
end end
end end
@@ -69,7 +68,7 @@ function socketClass:close()
self.connected = false self.connected = false
end end
device.wireless_modem.close(self.sport) device.wireless_modem.close(self.sport)
_G.transport.close(self) transport.close(self)
end end
local Socket = { } local Socket = { }
@@ -106,7 +105,7 @@ end
function Socket.connect(host, port) function Socket.connect(host, port)
local socket = newSocket(host == os.getComputerID()) local socket = newSocket(host == os.getComputerID())
socket.dhost = tonumber(host) socket.dhost = host
Logger.log('socket', 'connecting to ' .. port) Logger.log('socket', 'connecting to ' .. port)
socket.transmit(port, socket.sport, { socket.transmit(port, socket.sport, {
@@ -123,29 +122,23 @@ function Socket.connect(host, port)
local e, id, sport, dport, msg = os.pullEvent() local e, id, sport, dport, msg = os.pullEvent()
if e == 'modem_message' and if e == 'modem_message' and
sport == socket.sport and sport == socket.sport and
msg.dhost == socket.shost then msg.dhost == socket.shost and
msg.type == 'CONN' then
socket.dport = dport
socket.connected = true
Logger.log('socket', 'connection established to %d %d->%d',
host, socket.sport, socket.dport)
os.cancelTimer(timerId) os.cancelTimer(timerId)
if msg.type == 'CONN' then transport.open(socket)
socket.dport = dport return socket
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 == 'REJE' then
return false, 'Password not set on target or not trusted'
end
end end
until e == 'timer' and id == timerId until e == 'timer' and id == timerId
socket:close() socket:close()
return false, 'Connection timed out'
end end
local function trusted(msg, port) local function trusted(msg, port)
@@ -155,11 +148,6 @@ local function trusted(msg, port)
return true return true
end 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 trustList = Util.readTable('usr/.known_hosts') or { }
local pubKey = trustList[msg.shost] local pubKey = trustList[msg.shost]
@@ -177,21 +165,20 @@ function Socket.server(port)
Logger.log('socket', 'Waiting for connections on port ' .. port) Logger.log('socket', 'Waiting for connections on port ' .. port)
while true do while true do
local _, _, sport, dport, msg = os.pullEvent('modem_message') local e, _, sport, dport, msg = os.pullEvent('modem_message')
if sport == port and if sport == port and
msg and msg and
msg.dhost == os.getComputerID() and msg.dhost == os.getComputerID() and
msg.type == 'OPEN' then 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 trusted(msg, port) then if trusted(msg, port) then
local socket = newSocket(msg.shost == os.getComputerID())
socket.dport = dport
socket.dhost = msg.shost
socket.connected = true socket.connected = true
socket.wseq = msg.wseq
socket.rseq = msg.rseq
socket.transmit(socket.dport, socket.sport, { socket.transmit(socket.dport, socket.sport, {
type = 'CONN', type = 'CONN',
dhost = socket.dhost, dhost = socket.dhost,
@@ -199,16 +186,9 @@ function Socket.server(port)
}) })
Logger.log('socket', 'Connection established %d->%d', socket.sport, socket.dport) Logger.log('socket', 'Connection established %d->%d', socket.sport, socket.dport)
_G.transport.open(socket) transport.open(socket)
return socket return socket
end end
socket.transmit(socket.dport, socket.sport, {
type = 'REJE',
dhost = socket.dhost,
shost = socket.shost,
})
socket:close()
end end
end end
end end

View File

@@ -1,7 +1,5 @@
local syncLocks = { } local syncLocks = { }
local os = _G.os
return function(obj, fn) return function(obj, fn)
local key = tostring(obj) local key = tostring(obj)
if syncLocks[key] then if syncLocks[key] then

View File

@@ -1,15 +1,14 @@
local Util = require('util') local Util = require('util')
local colors = _G.colors
local term = _G.term
local _gsub = string.gsub
local Terminal = { } local Terminal = { }
local _sgsub = string.gsub
function Terminal.scrollable(ct, size) function Terminal.scrollable(ct, size)
local size = size or 25
local w, h = ct.getSize() local w, h = ct.getSize()
local win = _G.window.create(ct, 1, 1, w, h + size, true) local win = window.create(ct, 1, 1, w, h + size, true)
local oldWin = Util.shallowCopy(win) local oldWin = Util.shallowCopy(win)
local scrollPos = 0 local scrollPos = 0
@@ -88,7 +87,7 @@ function Terminal.toGrayscale(ct)
local methods = { 'setBackgroundColor', 'setBackgroundColour', local methods = { 'setBackgroundColor', 'setBackgroundColour',
'setTextColor', 'setTextColour' } 'setTextColor', 'setTextColour' }
for _,v in pairs(methods) do for _,v in pairs(methods) do
local fn = ct[v] local fn = ct[v]
ct[v] = function(c) ct[v] = function(c)
fn(scolors[c]) fn(scolors[c])
end end
@@ -111,7 +110,10 @@ function Terminal.toGrayscale(ct)
local function translate(s) local function translate(s)
if s then if s then
s = _gsub(s, "%w", bcolors) for k,v in pairs(bcolors) do
s = _sgsub(s, k, v)
end
-- s = _sgsub(s, "%d+", bcolors) -- not working in cc 1.75 ???
end end
return s return s
end end
@@ -137,9 +139,9 @@ end
function Terminal.copy(it, ot) function Terminal.copy(it, ot)
ot = ot or { } ot = ot or { }
for k,v in pairs(it) do for k,v in pairs(it) do
if type(v) == 'function' then if type(v) == 'function' then
ot[k] = v ot[k] = v
end end
end end
return ot return ot
end end
@@ -149,8 +151,8 @@ function Terminal.mirror(ct, dt)
ct[k] = function(...) ct[k] = function(...)
local ret = { f(...) } local ret = { f(...) }
if dt[k] then if dt[k] then
dt[k](...) dt[k](...)
end end
return unpack(ret) return unpack(ret)
end end
end end
@@ -163,7 +165,7 @@ function Terminal.readPassword(prompt)
local fn = term.current().write local fn = term.current().write
term.current().write = function() end term.current().write = function() end
local s local s
pcall(function() s = _G.read(prompt) end) pcall(function() s = read(prompt) end)
term.current().write = fn term.current().write = fn
if s == '' then if s == '' then

View File

@@ -1,73 +1,118 @@
_G.requireInjector() requireInjector(getfenv(1))
local Grid = require('jumper.grid') local Grid = require ("jumper.grid")
local Pathfinder = require('jumper.pathfinder') local Pathfinder = require ("jumper.pathfinder")
local Point = require('point') local Point = require('point')
local Util = require('util') local Util = require('util')
local turtle = _G.turtle local WALKABLE = 0
local function addBlock(grid, b, dim) local function createMap(dim)
if Point.inBox(b, dim) then local map = { }
local node = grid:getNodeAt(b.x, b.y, b.z) for z = 1, dim.ez do
if node then local row = {}
node.walkable = 1 for x = 1, dim.ex do
local col = { }
for y = 1, dim.ey do
table.insert(col, WALKABLE)
end
table.insert(row, col)
end end
table.insert(map, row)
end end
return map
end
local function addBlock(map, dim, b)
map[b.z + dim.oz][b.x + dim.ox][b.y + dim.oy] = 1
end end
-- map shrinks/grows depending upon blocks encountered -- map shrinks/grows depending upon blocks encountered
-- the map will encompass any blocks encountered, the turtle position, and the destination -- the map will encompass any blocks encountered, the turtle position, and the destination
local function mapDimensions(dest, blocks, boundingBox, dests) local function mapDimensions(dest, blocks, boundingBox)
local box = Point.makeBox(turtle.point, turtle.point) local sx, sz, sy = turtle.point.x, turtle.point.z, turtle.point.y
local ex, ez, ey = turtle.point.x, turtle.point.z, turtle.point.y
Point.expandBox(box, dest) local function adjust(pt)
if pt.x < sx then
for _,d in pairs(dests) do sx = pt.x
Point.expandBox(box, d) end
if pt.z < sz then
sz = pt.z
end
if pt.y < sy then
sy = pt.y
end
if pt.x > ex then
ex = pt.x
end
if pt.z > ez then
ez = pt.z
end
if pt.y > ey then
ey = pt.y
end
end end
for _,b in pairs(blocks) do adjust(dest)
Point.expandBox(box, b)
for _,b in ipairs(blocks) do
adjust(b)
end end
-- expand one block out in all directions -- expand one block out in all directions
if boundingBox then if boundingBox then
box.x = math.max(box.x - 1, boundingBox.x) sx = math.max(sx - 1, boundingBox.x)
box.z = math.max(box.z - 1, boundingBox.z) sz = math.max(sz - 1, boundingBox.z)
box.y = math.max(box.y - 1, boundingBox.y) sy = math.max(sy - 1, boundingBox.y)
box.ex = math.min(box.ex + 1, boundingBox.ex) ex = math.min(ex + 1, boundingBox.ex)
box.ez = math.min(box.ez + 1, boundingBox.ez) ez = math.min(ez + 1, boundingBox.ez)
box.ey = math.min(box.ey + 1, boundingBox.ey) ey = math.min(ey + 1, boundingBox.ey)
else else
box.x = box.x - 1 sx = sx - 1
box.z = box.z - 1 sz = sz - 1
box.y = box.y - 1 sy = sy - 1
box.ex = box.ex + 1 ex = ex + 1
box.ez = box.ez + 1 ez = ez + 1
box.ey = box.ey + 1 ey = ey + 1
end end
return box return {
ex = ex - sx + 1,
ez = ez - sz + 1,
ey = ey - sy + 1,
ox = -sx + 1,
oz = -sz + 1,
oy = -sy + 1
}
end end
local function nodeToPoint(node) -- shifting and coordinate flipping
return { x = node.x, y = node.y, z = node.z, heading = node.heading } local function pointToMap(dim, pt)
return { x = pt.x + dim.ox, z = pt.y + dim.oy, y = pt.z + dim.oz }
end end
local function heuristic(n, node) local function nodeToPoint(dim, node)
return Point.calculateMoves(node, n) return { x = node:getX() - dim.ox, z = node:getY() - dim.oz, y = node:getZ() - dim.oy }
-- { x = node.x, y = node.y, z = node.z, heading = node.heading }, end
-- { x = n.x, y = n.y, z = n.z, heading = n.heading })
local heuristic = function(n, node)
local m, h = Point.calculateMoves(
{ x = node._x, z = node._y, y = node._z, heading = node._heading },
{ x = n._x, z = n._y, y = n._z, heading = n._heading })
return m, h
end end
local function dimsAreEqual(d1, d2) local function dimsAreEqual(d1, d2)
return d1.ex == d2.ex and return d1.ex == d2.ex and
d1.ey == d2.ey and d1.ey == d2.ey and
d1.ez == d2.ez and d1.ez == d2.ez and
d1.x == d2.x and d1.ox == d2.ox and
d1.y == d2.y and d1.oy == d2.oy and
d1.z == d2.z d1.oz == d2.oz
end end
-- turtle sensor returns blocks in relation to the world - not turtle orientation -- turtle sensor returns blocks in relation to the world - not turtle orientation
@@ -75,6 +120,7 @@ end
-- really kinda dumb since it returns the coordinates as offsets of our location -- really kinda dumb since it returns the coordinates as offsets of our location
-- instead of true coordinates -- instead of true coordinates
local function addSensorBlocks(blocks, sblocks) local function addSensorBlocks(blocks, sblocks)
for _,b in pairs(sblocks) do for _,b in pairs(sblocks) do
if b.type ~= 'AIR' then if b.type ~= 'AIR' then
local pt = { x = turtle.point.x, y = turtle.point.y + b.y, z = turtle.point.z } local pt = { x = turtle.point.x, y = turtle.point.y + b.y, z = turtle.point.z }
@@ -94,53 +140,63 @@ local function addSensorBlocks(blocks, sblocks)
end end
end end
local function selectDestination(pts, box, grid) local function selectDestination(pts, box, map, dim)
while #pts > 0 do while #pts > 0 do
local pt = Point.closest(turtle.point, pts) local pt = Point.closest(turtle.point, pts)
if box and not Point.inBox(pt, box) then
Util.removeByValue(pts, pt) if (box and not Point.inBox(pt, box)) or
else map[pt.z + dim.oz][pt.x + dim.ox][pt.y + dim.oy] == 1 then
if grid:isWalkableAt(pt.x, pt.y, pt.z) then
return pt
end
Util.removeByValue(pts, pt) Util.removeByValue(pts, pt)
else
return pt
end end
end end
end end
local function pathTo(dest, options) local function pathTo(dest, options)
local blocks = options.blocks or turtle.getState().blocks or { } local blocks = options.blocks or turtle.getState().blocks or { }
local dests = options.dest or { dest } -- support alternative destinations local dests = options.dest or { dest } -- support alternative destinations
local box = options.box or turtle.getState().box local box = options.box or turtle.getState().box
local lastDim
local grid local lastDim = nil
local map = nil
local grid = nil
if box then if box then
box = Point.normalizeBox(box) box = Point.normalizeBox(box)
end end
-- Creates a pathfinder object -- Creates a pathfinder object
local finder = Pathfinder(heuristic) local myFinder = Pathfinder(grid, 'ASTAR', walkable)
myFinder:setMode('ORTHOGONAL')
myFinder:setHeuristic(heuristic)
while turtle.point.x ~= dest.x or turtle.point.z ~= dest.z or turtle.point.y ~= dest.y do 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 -- map expands as we encounter obstacles
local dim = mapDimensions(dest, blocks, box, dests) local dim = mapDimensions(dest, blocks, box)
-- reuse map if possible -- reuse map if possible
if not lastDim or not dimsAreEqual(dim, lastDim) then if not lastDim or not dimsAreEqual(dim, lastDim) then
map = createMap(dim)
-- Creates a grid object -- Creates a grid object
grid = Grid(dim) grid = Grid(map)
finder:setGrid(grid) myFinder:setGrid(grid)
myFinder:setWalkable(WALKABLE)
lastDim = dim lastDim = dim
end end
for _,b in pairs(blocks) do
addBlock(grid, b, dim) for _,b in ipairs(blocks) do
addBlock(map, dim, b)
end end
dest = selectDestination(dests, box, grid) dest = selectDestination(dests, box, map, dim)
if not dest then if not dest then
-- error('failed to reach destination')
return false, 'failed to reach destination' return false, 'failed to reach destination'
end end
if turtle.point.x == dest.x and turtle.point.z == dest.z and turtle.point.y == dest.y then if turtle.point.x == dest.x and turtle.point.z == dest.z and turtle.point.y == dest.y then
@@ -148,50 +204,29 @@ local function pathTo(dest, options)
end end
-- Define start and goal locations coordinates -- Define start and goal locations coordinates
local startPt = turtle.point local startPt = pointToMap(dim, turtle.point)
local endPt = pointToMap(dim, dest)
-- Calculates the path, and its length -- Calculates the path, and its length
local path = finder:getPath( local path = myFinder:getPath(startPt.x, startPt.y, startPt.z, turtle.point.heading, endPt.x, endPt.y, endPt.z, dest.heading)
startPt.x, startPt.y, startPt.z, turtle.point.heading,
dest.x, dest.y, dest.z, dest.heading)
if not path then if not path then
Util.removeByValue(dests, dest) Util.removeByValue(dests, dest)
else else
path:filter() for node, count in path:nodes() do
local pt = nodeToPoint(dim, node)
for node in path:nodes() do if turtle.abort then
local pt = nodeToPoint(node)
if turtle.isAborted() then
return false, 'aborted' return false, 'aborted'
end 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 -- use single turn method so the turtle doesn't turn around
-- when encountering obstacles -- when encountering obstacles -- IS THIS RIGHT ??
if not turtle.gotoSingleTurn(pt.x, pt.y, pt.z, pt.heading) then if not turtle.gotoSingleTurn(pt.x, pt.z, pt.y, node.heading) then
local bpt = Point.nearestTo(turtle.point, pt) table.insert(blocks, pt)
--if device.turtlesensorenvironment then
table.insert(blocks, bpt) -- addSensorBlocks(blocks, device.turtlesensorenvironment.sonicScan())
-- really need to check if the block we ran into was a turtle. --end
-- 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 break
end end
end end
@@ -223,12 +258,6 @@ return {
turtle.getState().blocks = blocks turtle.getState().blocks = blocks
end, end,
addBlock = function(block)
if turtle.getState().blocks then
table.insert(turtle.getState().blocks, block)
end
end,
reset = function() reset = function()
turtle.getState().box = nil turtle.getState().box = nil
turtle.getState().blocks = nil turtle.getState().blocks = nil

File diff suppressed because it is too large Load Diff

View File

@@ -2,40 +2,50 @@ local class = require('class')
local Region = require('ui.region') local Region = require('ui.region')
local Util = require('util') local Util = require('util')
local _rep = string.rep local _srep = string.rep
local _sub = string.sub local _ssub = string.sub
local _gsub = string.gsub
local colors = _G.colors
local Canvas = class() local mapColorToGray = {
[ colors.white ] = colors.white,
Canvas.colorPalette = { } [ colors.orange ] = colors.lightGray,
Canvas.darkPalette = { } [ colors.magenta ] = colors.lightGray,
Canvas.grayscalePalette = { } [ 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,
}
local mapColorToPaint = { }
for n = 1, 16 do for n = 1, 16 do
Canvas.colorPalette[2 ^ (n - 1)] = _sub("0123456789abcdef", n, n) mapColorToPaint[2 ^ (n - 1)] = _ssub("0123456789abcdef", n, n)
Canvas.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n)
Canvas.darkPalette[2 ^ (n - 1)] = _sub("8777777f77fff77f", n, n)
end end
local mapGrayToPaint = { }
for n = 0, 15 do
local gs = mapColorToGray[2 ^ n]
mapGrayToPaint[2 ^ n] = mapColorToPaint[gs]
end
local Canvas = class()
function Canvas:init(args) function Canvas:init(args)
self.x = 1 self.x = 1
self.y = 1 self.y = 1
self.layers = { } self.layers = { }
Util.merge(self, args) Util.merge(self, args)
self.ex = self.x + self.width - 1 self.height = self.ey - self.y + 1
self.ey = self.y + self.height - 1 self.width = self.ex - self.x + 1
if not self.palette then
if self.isColor then
self.palette = Canvas.colorPalette
else
self.palette = Canvas.grayscalePalette
end
end
self.lines = { } self.lines = { }
for i = 1, self.height do for i = 1, self.height do
@@ -43,12 +53,6 @@ function Canvas:init(args)
end end
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) function Canvas:resize(w, h)
for i = self.height, h do for i = self.height, h do
self.lines[i] = { } self.lines[i] = { }
@@ -60,25 +64,29 @@ function Canvas:resize(w, h)
if w ~= self.width then if w ~= self.width then
for i = 1, self.height do for i = 1, self.height do
self.lines[i] = { dirty = true } self.lines[i] = { }
end end
end end
self.ex = self.x + w - 1 self.ex = self.x + w - 1
self.ey = self.y + h - 1 self.ey = self.y + h - 1
self.width = w self.width = w
self.height = h self.height = h
self:dirty()
end
function Canvas:colorToPaintColor(c)
if self.isColor then
return mapColorToPaint[c]
end
return mapGrayToPaint[c]
end end
function Canvas:copy() function Canvas:copy()
local b = Canvas({ local b = Canvas({ x = self.x, y = self.y, ex = self.ex, ey = self.ey })
x = self.x, for i = 1, self.ey - self.y + 1 do
y = self.y,
width = self.width,
height = self.height,
isColor = self.isColor,
})
for i = 1, self.height do
b.lines[i].text = self.lines[i].text b.lines[i].text = self.lines[i].text
b.lines[i].fg = self.lines[i].fg b.lines[i].fg = self.lines[i].fg
b.lines[i].bg = self.lines[i].bg b.lines[i].bg = self.lines[i].bg
@@ -86,14 +94,16 @@ function Canvas:copy()
return b return b
end end
function Canvas:addLayer(layer) function Canvas:addLayer(layer, bg, fg)
local canvas = Canvas({ local canvas = Canvas({
x = layer.x, x = layer.x,
y = layer.y, y = layer.y,
width = layer.width, ex = layer.x + layer.width - 1,
height = layer.height, ey = layer.y + layer.height - 1,
isColor = self.isColor, isColor = self.isColor,
}) })
canvas:clear(bg, fg)
canvas.parent = self canvas.parent = self
table.insert(self.layers, canvas) table.insert(self.layers, canvas)
return canvas return canvas
@@ -119,10 +129,10 @@ end
function Canvas:write(x, y, text, bg, fg) function Canvas:write(x, y, text, bg, fg)
if bg then if bg then
bg = _rep(self.palette[bg], #text) bg = _srep(self:colorToPaintColor(bg), #text)
end end
if fg then if fg then
fg = _rep(self.palette[fg], #text) fg = _srep(self:colorToPaintColor(fg), #text)
end end
self:writeBlit(x, y, text, bg, fg) self:writeBlit(x, y, text, bg, fg)
end end
@@ -134,24 +144,24 @@ function Canvas:writeBlit(x, y, text, bg, fg)
-- fix ffs -- fix ffs
if x < 1 then if x < 1 then
text = _sub(text, 2 - x) text = _ssub(text, 2 - x)
if bg then if bg then
bg = _sub(bg, 2 - x) bg = _ssub(bg, 2 - x)
end end
if bg then if bg then
fg = _sub(fg, 2 - x) fg = _ssub(fg, 2 - x)
end end
width = width + x - 1 width = width + x - 1
x = 1 x = 1
end end
if x + width - 1 > self.width then if x + width - 1 > self.width then
text = _sub(text, 1, self.width - x + 1) text = _ssub(text, 1, self.width - x + 1)
if bg then if bg then
bg = _sub(bg, 1, self.width - x + 1) bg = _ssub(bg, 1, self.width - x + 1)
end end
if bg then if bg then
fg = _sub(fg, 1, self.width - x + 1) fg = _ssub(fg, 1, self.width - x + 1)
end end
width = #text width = #text
end end
@@ -162,13 +172,13 @@ function Canvas:writeBlit(x, y, text, bg, fg)
if pos == 1 and width == self.width then if pos == 1 and width == self.width then
return rstr return rstr
elseif pos == 1 then elseif pos == 1 then
return rstr .. _sub(sstr, pos+width) return rstr .. _ssub(sstr, pos+width)
elseif pos + width > self.width then elseif pos + width > self.width then
return _sub(sstr, 1, pos-1) .. rstr return _ssub(sstr, 1, pos-1) .. rstr
end end
return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width) return _ssub(sstr, 1, pos-1) .. rstr .. _ssub(sstr, pos+width)
end end
local line = self.lines[y] local line = self.lines[y]
line.dirty = true line.dirty = true
line.text = replace(line.text, x, text, width) line.text = replace(line.text, x, text, width)
@@ -194,10 +204,11 @@ function Canvas:reset()
end end
function Canvas:clear(bg, fg) function Canvas:clear(bg, fg)
local text = _rep(' ', self.width) local width = self.ex - self.x + 1
fg = _rep(self.palette[fg or colors.white], self.width) local text = _srep(' ', width)
bg = _rep(self.palette[bg or colors.black], self.width) fg = _srep(self:colorToPaintColor(fg), width)
for i = 1, self.height do bg = _srep(self:colorToPaintColor(bg), width)
for i = 1, self.ey - self.y + 1 do
self:writeLine(i, text, fg, bg) self:writeLine(i, text, fg, bg)
end end
end end
@@ -214,7 +225,7 @@ function Canvas:blitClipped(device)
self:blit(device, self:blit(device,
{ x = region[1] - self.x + 1, { x = region[1] - self.x + 1,
y = region[2] - self.y + 1, y = region[2] - self.y + 1,
ex = region[3]- self.x + 1, ex = region[3]- self.x + 1,
ey = region[4] - self.y + 1 }, ey = region[4] - self.y + 1 },
{ x = region[1], y = region[2] }) { x = region[1], y = region[2] })
end end
@@ -248,7 +259,7 @@ function Canvas:dirty()
end end
function Canvas:clean() function Canvas:clean()
for _, line in pairs(self.lines) do for y, line in pairs(self.lines) do
line.dirty = false line.dirty = false
end end
end end
@@ -282,9 +293,9 @@ function Canvas:blit(device, src, tgt)
if line and line.dirty then if line and line.dirty then
local t, fg, bg = line.text, line.fg, line.bg local t, fg, bg = line.text, line.fg, line.bg
if src.x > 1 or src.ex < self.ex then if src.x > 1 or src.ex < self.ex then
t = _sub(t, src.x, src.ex) t = _ssub(t, src.x, src.ex)
fg = _sub(fg, src.x, src.ex) fg = _ssub(fg, src.x, src.ex)
bg = _sub(bg, src.x, src.ex) bg = _ssub(bg, src.x, src.ex)
end end
--if tgt.y + i > self.ey then -- wrong place to do clipping ?? --if tgt.y + i > self.ey then -- wrong place to do clipping ??
-- break -- break
@@ -295,30 +306,15 @@ function Canvas:blit(device, src, tgt)
end end
end end
function Canvas:applyPalette(palette) function Canvas.convertWindow(win, parent, x, y)
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.convertWindow(win, parent, wx, wy)
local w, h = win.getSize() local w, h = win.getSize()
win.canvas = Canvas({ win.canvas = Canvas({
x = wx, x = x,
y = wy, y = y,
width = w, ex = x + w - 1,
height = h, ey = y + h - 1,
isColor = win.isColor(), isColor = win.isColor(),
}) })
@@ -327,10 +323,10 @@ function Canvas.convertWindow(win, parent, wx, wy)
end end
function win.clearLine() function win.clearLine()
local _, y = win.getCursorPos() local x, y = win.getCursorPos()
win.canvas:write(1, win.canvas:write(1,
y, y,
_rep(' ', win.canvas.width), _srep(' ', win.canvas.width),
win.getBackgroundColor(), win.getBackgroundColor(),
win.getTextColor()) win.getTextColor())
end end
@@ -354,7 +350,7 @@ function Canvas.convertWindow(win, parent, wx, wy)
end end
function win.scroll() function win.scroll()
error('scroll: not implemented') error('CWin:scroll: not implemented')
end end
function win.reposition(x, y, width, height) function win.reposition(x, y, width, height)

View File

@@ -1,9 +1,6 @@
local UI = require('ui') local UI = require('ui')
local Util = require('util') local Util = require('util')
local colors = _G.colors
local fs = _G.fs
return function(args) return function(args)
local columns = { local columns = {
@@ -26,7 +23,7 @@ return function(args)
-- rey = args.rey or -3, -- rey = args.rey or -3,
height = args.height, height = args.height,
width = args.width, width = args.width,
title = 'Select File', title = 'Select file',
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
x = 2, x = 2,
y = 2, y = 2,
@@ -89,7 +86,7 @@ return function(args)
return row return row
end end
function selectFile.grid:getRowTextColor(file) function selectFile.grid:getRowTextColor(file, selected)
if file.isDir then if file.isDir then
return colors.cyan return colors.cyan
end end

View File

@@ -1,90 +0,0 @@
local Tween = require('ui.tween')
local Transition = { }
function Transition.slideLeft(args)
local ticks = args.ticks or 6
local easing = args.easing or 'outQuint'
local pos = { x = args.ex }
local tween = Tween.new(ticks, pos, { x = args.x }, easing)
local lastScreen = args.canvas:copy()
return function(device)
local finished = tween:update(1)
local x = math.floor(pos.x)
lastScreen:dirty()
lastScreen:blit(device, {
x = args.ex - x + args.x,
y = args.y,
ex = args.ex,
ey = args.ey },
{ x = args.x, y = args.y })
args.canvas:blit(device, {
x = args.x,
y = args.y,
ex = args.ex - x + args.x,
ey = args.ey },
{ x = x, y = args.y })
return not finished
end
end
function Transition.slideRight(args)
local ticks = args.ticks or 6
local easing = args.easing or'outQuint'
local pos = { x = args.x }
local tween = Tween.new(ticks, pos, { x = args.ex }, easing)
local lastScreen = args.canvas:copy()
return function(device)
local finished = tween:update(1)
local x = math.floor(pos.x)
lastScreen:dirty()
lastScreen:blit(device, {
x = args.x,
y = args.y,
ex = args.ex - x + args.x,
ey = args.ey },
{ x = x, y = args.y })
args.canvas:blit(device, {
x = args.ex - x + args.x,
y = args.y,
ex = args.ex,
ey = args.ey },
{ x = args.x, y = args.y })
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)
return function(device)
local finished = tween:update(1)
args.canvas:blit(device, nil, { x = args.x, y = math.floor(pos.y) })
return not finished
end
end
function Transition.grow(args)
local ticks = args.ticks or 3
local easing = args.easing or 'linear'
local tween = Tween.new(ticks,
{ x = args.width / 2 - 1, y = args.height / 2 - 1, w = 1, h = 1 },
{ x = 1, y = 1, w = args.width, h = args.height }, easing)
return function(device)
local finished = tween:update(1)
local subj = tween.subject
local rect = { x = math.floor(subj.x), y = math.floor(subj.y) }
rect.ex = math.floor(rect.x + subj.w - 1)
rect.ey = math.floor(rect.y + subj.h - 1)
args.canvas:blit(device, rect, { x = args.x + rect.x - 1, y = args.y + rect.y - 1})
return not finished
end
end
return Transition

View File

@@ -1,11 +1,5 @@
local Util = { } local Util = { }
local fs = _G.fs
local http = _G.http
local os = _G.os
local term = _G.term
local textutils = _G.textutils
function Util.tryTimed(timeout, f, ...) function Util.tryTimed(timeout, f, ...)
local c = os.clock() local c = os.clock()
repeat repeat
@@ -18,7 +12,7 @@ end
function Util.tryTimes(attempts, f, ...) function Util.tryTimes(attempts, f, ...)
local result local result
for _ = 1, attempts do for i = 1, attempts do
result = { f(...) } result = { f(...) }
if result[1] then if result[1] then
return unpack(result) return unpack(result)
@@ -78,37 +72,16 @@ end
function Util.getVersion() function Util.getVersion()
local version local version
if _G._CC_VERSION then if _CC_VERSION then
version = tonumber(_G._CC_VERSION:match('[%d]+%.?[%d][%d]')) version = tonumber(_CC_VERSION:gmatch('[%d]+%.?[%d][%d]', '%1')())
end end
if not version and _G._HOST then if not version and _HOST then
version = tonumber(_G._HOST:match('[%d]+%.?[%d][%d]')) version = tonumber(_HOST:gmatch('[%d]+%.?[%d][%d]', '%1')())
end end
return version or 1.7 return version or 1.7
end 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
-- http://lua-users.org/wiki/SimpleRound -- http://lua-users.org/wiki/SimpleRound
function Util.round(num, idp) function Util.round(num, idp)
local mult = 10^(idp or 0) local mult = 10^(idp or 0)
@@ -141,7 +114,7 @@ function Util.key(t, value)
end end
function Util.keys(t) function Util.keys(t)
local keys = { } local keys = {}
for k in pairs(t) do for k in pairs(t) do
keys[#keys+1] = k keys[#keys+1] = k
end end
@@ -179,14 +152,6 @@ function Util.transpose(t)
return tt return tt
end 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) function Util.find(t, name, value)
for k,v in pairs(t) do for k,v in pairs(t) do
if v[name] == value then if v[name] == value then
@@ -197,7 +162,7 @@ end
function Util.findAll(t, name, value) function Util.findAll(t, name, value)
local rt = { } local rt = { }
for _,v in pairs(t) do for k,v in pairs(t) do
if v[name] == value then if v[name] == value then
table.insert(rt, v) table.insert(rt, v)
end end
@@ -252,7 +217,7 @@ end
function Util.filter(it, f) function Util.filter(it, f)
local ot = { } local ot = { }
for k,v in pairs(it) do for k,v in pairs(it) do
if f(v) then if f(k, v) then
ot[k] = v ot[k] = v
end end
end end
@@ -262,9 +227,7 @@ end
function Util.size(list) function Util.size(list)
if type(list) == 'table' then if type(list) == 'table' then
local length = 0 local length = 0
for _ in pairs(list) do table.foreach(list, function() length = length + 1 end)
length = length + 1
end
return length return length
end end
return 0 return 0
@@ -285,25 +248,12 @@ function Util.each(list, func)
end end
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 -- http://stackoverflow.com/questions/15706270/sort-a-table-in-lua
function Util.spairs(t, order) function Util.spairs(t, order)
local keys = Util.keys(t) local keys = Util.keys(t)
-- if order function given, sort by it by passing the table and keys a, b, -- if order function given, sort by it by passing the table and keys a, b,
-- otherwise just sort the keys -- otherwise just sort the keys
if order then if order then
table.sort(keys, function(a,b) return order(t[a], t[b]) end) table.sort(keys, function(a,b) return order(t[a], t[b]) end)
else else
@@ -367,7 +317,7 @@ function Util.writeLines(fname, lines)
local file = fs.open(fname, 'w') local file = fs.open(fname, 'w')
if file then if file then
for _,line in ipairs(lines) do for _,line in ipairs(lines) do
file.writeLine(line) line = file.writeLine(line)
end end
file.close() file.close()
return true return true
@@ -401,18 +351,13 @@ function Util.loadTable(fname)
end end
--[[ loading and running functions ]] -- --[[ loading and running functions ]] --
function Util.httpGet(url, headers)
local h, msg = http.get(url, headers)
if h then
local contents = h.readAll()
h.close()
return contents
end
return h, msg
end
function Util.download(url, filename) function Util.download(url, filename)
local contents = Util.httpGet(url) local h = http.get(url)
if not h then
error('Failed to download ' .. url)
end
local contents = h.readAll()
h.close()
if not contents then if not contents then
error('Failed to download ' .. url) error('Failed to download ' .. url)
end end
@@ -424,18 +369,23 @@ function Util.download(url, filename)
end end
function Util.loadUrl(url, env) -- loadfile equivalent function Util.loadUrl(url, env) -- loadfile equivalent
local c, msg = Util.httpGet(url) local s, m = pcall(function()
if not c then local c = Util.download(url)
return c, msg return load(c, url, nil, env)
end)
if s then
return m
end end
return load(c, url, nil, env) return s, m
end end
function Util.runUrl(env, url, ...) -- os.run equivalent function Util.runUrl(env, url, ...) -- os.run equivalent
setmetatable(env, { __index = _G }) setmetatable(env, { __index = _G })
local fn, m = Util.loadUrl(url, env) local fn, m = Util.loadUrl(url, env)
if fn then if fn then
return pcall(fn, ...) local args = { ... }
return pcall(function() return fn(table.unpack(args)) end)
end end
return fn, m return fn, m
end end
@@ -444,7 +394,8 @@ function Util.run(env, path, ...)
setmetatable(env, { __index = _G }) setmetatable(env, { __index = _G })
local fn, m = loadfile(path, env) local fn, m = loadfile(path, env)
if fn then if fn then
return pcall(fn, ...) local args = { ... }
return pcall(function() return fn(table.unpack(args)) end)
end end
return fn, m return fn, m
end end
@@ -452,29 +403,30 @@ end
function Util.runFunction(env, fn, ...) function Util.runFunction(env, fn, ...)
setfenv(fn, env) setfenv(fn, env)
setmetatable(env, { __index = _G }) setmetatable(env, { __index = _G })
return pcall(fn, ...)
local args = { ... }
return pcall(function() return fn(table.unpack(args)) end)
end end
--[[ String functions ]] -- --[[ String functions ]] --
function Util.toBytes(n) 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 if n >= 1000000 or n <= -1000000 then
return string.format('%sM', math.floor(n/1000000 * 10) / 10) return string.format('%sM', Util.round(n/1000000, 1))
elseif n >= 1000 or n <= -1000 then elseif n >= 1000 or n <= -1000 then
return string.format('%sK', math.floor(n/1000 * 10) / 10) return string.format('%sK', Util.round(n/1000, 1))
end end
return tostring(n) return tostring(n)
end end
function Util.insertString(str, istr, pos) function Util.insertString(os, is, pos)
return str:sub(1, pos - 1) .. istr .. str:sub(pos) return os:sub(1, pos - 1) .. is .. os:sub(pos)
end end
function Util.split(str, pattern) function Util.split(str, pattern)
pattern = pattern or "(.-)\n" pattern = pattern or "(.-)\n"
local t = {} local t = {}
local function helper(line) table.insert(t, line) return "" end local function helper(line) table.insert(t, line) return "" end
helper((str:gsub(pattern, helper))) helper((str:gsub(pattern, helper)))
return t return t
end end
@@ -506,12 +458,12 @@ end
function Util.trim(s) function Util.trim(s)
return s:find'^%s*$' and '' or s:match'^%s*(.*%S)' return s:find'^%s*$' and '' or s:match'^%s*(.*%S)'
end end
-- trim whitespace from left end of string -- trim whitespace from left end of string
function Util.triml(s) function Util.triml(s)
return s:match'^%s*(.*)' return s:match'^%s*(.*)'
end end
-- trim whitespace from right end of string -- trim whitespace from right end of string
function Util.trimr(s) function Util.trimr(s)
return s:find'^%s*$' and '' or s:match'^(.*%S)' return s:find'^%s*$' and '' or s:match'^(.*%S)'
@@ -596,7 +548,7 @@ end
function Util.showOptions(options) function Util.showOptions(options)
print('Arguments: ') print('Arguments: ')
for _, v in pairs(options) do for k, v in pairs(options) do
print(string.format('-%s %s', v.arg, v.desc)) print(string.format('-%s %s', v.arg, v.desc))
end end
end end
@@ -609,6 +561,7 @@ function Util.getOptions(options, args, ignoreInvalid)
end end
end end
local rawOptions = getopt(args, argLetters) local rawOptions = getopt(args, argLetters)
local argCount = 0
for k,ro in pairs(rawOptions) do for k,ro in pairs(rawOptions) do
local found = false local found = false

View File

@@ -1,16 +1,10 @@
_G.requireInjector() requireInjector(getfenv(1))
local Config = require('config') local Config = require('config')
local Event = require('event') local Event = require('event')
local UI = require('ui') local UI = require('ui')
local Util = require('util') local Util = require('util')
local colors = _G.colors
local fs = _G.fs
local multishell = _ENV.multishell
local os = _G.os
local shell = _ENV.shell
multishell.setTitle(multishell.getCurrent(), 'Files') multishell.setTitle(multishell.getCurrent(), 'Files')
UI:configure('Files', ...) UI:configure('Files', ...)
@@ -26,7 +20,7 @@ local marked = { }
local directories = { } local directories = { }
local cutMode = false local cutMode = false
local function formatSize(size) function formatSize(size)
if size >= 1000000 then if size >= 1000000 then
return string.format('%dM', math.floor(size/1000000, 2)) return string.format('%dM', math.floor(size/1000000, 2))
elseif size >= 1000 then elseif size >= 1000 then
@@ -38,42 +32,54 @@ end
local Browser = UI.Page { local Browser = UI.Page {
menuBar = UI.MenuBar { menuBar = UI.MenuBar {
buttons = { buttons = {
{ text = '^-', event = 'updir' }, { text = '^-', event = 'updir' },
{ text = 'File', dropdown = { { text = 'File', event = 'dropdown', dropdown = 'fileMenu' },
{ text = 'Run', event = 'run' }, { text = 'Edit', event = 'dropdown', dropdown = 'editMenu' },
{ text = 'Edit e', event = 'edit' }, { text = 'View', event = 'dropdown', dropdown = 'viewMenu' },
{ text = 'Shell s', event = 'shell' },
UI.MenuBar.spacer,
{ text = 'Quit q', event = 'quit' },
} },
{ text = 'Edit', dropdown = {
{ text = 'Cut ^x', event = 'cut' },
{ text = 'Copy ^c', event = 'copy' },
{ text = 'Paste ^v', event = 'paste' },
UI.MenuBar.spacer,
{ text = 'Mark m', event = 'mark' },
{ text = 'Unmark all u', event = 'unmark' },
UI.MenuBar.spacer,
{ text = 'Delete del', event = 'delete' },
} },
{ text = 'View', dropdown = {
{ text = 'Refresh r', event = 'refresh' },
{ text = 'Hidden ^h', event = 'toggle_hidden' },
{ text = 'Dir Size ^s', event = 'toggle_dirSize' },
} },
}, },
}, },
fileMenu = UI.DropMenu {
buttons = {
{ text = 'Run', event = 'run' },
{ text = 'Edit e', event = 'edit' },
{ text = 'Shell s', event = 'shell' },
UI.Text { value = ' ------------ ' },
{ text = 'Quit q', event = 'quit' },
UI.Text { },
}
},
editMenu = UI.DropMenu {
buttons = {
{ text = 'Cut ^x', event = 'cut' },
{ text = 'Copy ^c', event = 'copy' },
{ text = 'Paste ^v', event = 'paste' },
UI.Text { value = ' --------------- ' },
{ text = 'Mark m', event = 'mark' },
{ text = 'Unmark all u', event = 'unmark' },
UI.Text { value = ' --------------- ' },
{ text = 'Delete del', event = 'delete' },
UI.Text { },
}
},
viewMenu = UI.DropMenu {
buttons = {
{ text = 'Refresh r', event = 'refresh' },
{ text = 'Hidden ^h', event = 'toggle_hidden' },
{ text = 'Dir Size ^s', event = 'toggle_dirSize' },
UI.Text { },
}
},
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
columns = { columns = {
{ heading = 'Name', key = 'name' }, { heading = 'Name', key = 'name' },
{ key = 'flags', width = 2 }, { key = 'flags', width = 2 },
{ heading = 'Size', key = 'fsize', width = 5 }, { heading = 'Size', key = 'fsize', width = 6 },
}, },
sortColumn = 'name', sortColumn = 'name',
y = 2, ey = -2, y = 2, ey = -2,
}, },
statusBar = UI.StatusBar { statusBar = UI.StatusBar {
columns = { columns = {
{ key = 'status' }, { key = 'status' },
{ key = 'totalSize', width = 6 }, { key = 'totalSize', width = 6 },
}, },
@@ -90,7 +96,6 @@ local Browser = UI.Page {
d = 'delete', d = 'delete',
delete = 'delete', delete = 'delete',
[ 'control-h' ] = 'toggle_hidden', [ 'control-h' ] = 'toggle_hidden',
[ 'control-s' ] = 'toggle_dirSize',
[ 'control-x' ] = 'cut', [ 'control-x' ] = 'cut',
[ 'control-c' ] = 'copy', [ 'control-c' ] = 'copy',
paste = 'paste', paste = 'paste',
@@ -102,16 +107,6 @@ function Browser:enable()
self:setFocus(self.grid) self:setFocus(self.grid)
end end
function Browser.menuBar:getActive(menuItem)
local file = Browser.grid:getSelected()
if file then
if menuItem.event == 'edit' or menuItem.event == 'run' then
return not file.isDir
end
end
return true
end
function Browser.grid:sortCompare(a, b) function Browser.grid:sortCompare(a, b)
if self.sortColumn == 'fsize' then if self.sortColumn == 'fsize' then
return a.size < b.size return a.size < b.size
@@ -124,7 +119,7 @@ function Browser.grid:sortCompare(a, b)
return a.isDir return a.isDir
end end
function Browser.grid:getRowTextColor(file) function Browser.grid:getRowTextColor(file, selected)
if file.marked then if file.marked then
return colors.green return colors.green
end end
@@ -137,6 +132,13 @@ function Browser.grid:getRowTextColor(file)
return colors.white return colors.white
end end
function Browser.grid:getRowBackgroundColorX(file, selected)
if selected then
return colors.gray
end
return self.backgroundColor
end
function Browser.grid:eventHandler(event) function Browser.grid:eventHandler(event)
if event.type == 'copy' then -- let copy be handled by parent if event.type == 'copy' then -- let copy be handled by parent
return false return false
@@ -162,13 +164,14 @@ function Browser:setStatus(status, ...)
end end
function Browser:unmarkAll() function Browser:unmarkAll()
for _,m in pairs(marked) do for k,m in pairs(marked) do
m.marked = false m.marked = false
end end
Util.clear(marked) Util.clear(marked)
end end
function Browser:getDirectory(directory) function Browser:getDirectory(directory)
local s, dir = pcall(function() local s, dir = pcall(function()
local dir = directories[directory] local dir = directories[directory]
@@ -202,6 +205,7 @@ function Browser:updateDirectory(dir)
dir.size = #files dir.size = #files
for _, file in pairs(files) do for _, file in pairs(files) do
file.fullName = fs.combine(dir.name, file.name) file.fullName = fs.combine(dir.name, file.name)
file.directory = directory
file.flags = '' file.flags = ''
if not file.isDir then if not file.isDir then
dir.totalSize = dir.totalSize + file.size dir.totalSize = dir.totalSize + file.size
@@ -235,7 +239,7 @@ function Browser:setDir(dirName, noStatus)
if self.dir then if self.dir then
self.dir.index = self.grid:getIndex() self.dir.index = self.grid:getIndex()
end end
local DIR = fs.combine('', dirName) DIR = fs.combine('', dirName)
shell.setDir(DIR) shell.setDir(DIR)
local s, dir = self:getDirectory(DIR) local s, dir = self:getDirectory(DIR)
if s then if s then
@@ -348,7 +352,7 @@ function Browser:eventHandler(event)
self.statusBar:sync() self.statusBar:sync()
local _, ch = os.pullEvent('char') local _, ch = os.pullEvent('char')
if ch == 'y' or ch == 'Y' then if ch == 'y' or ch == 'Y' then
for _,m in pairs(marked) do for k,m in pairs(marked) do
pcall(function() pcall(function()
fs.delete(m.fullName) fs.delete(m.fullName)
end) end)
@@ -375,7 +379,7 @@ function Browser:eventHandler(event)
end end
elseif event.type == 'paste' then elseif event.type == 'paste' then
for _,m in pairs(copied) do for k,m in pairs(copied) do
local s, m = pcall(function() local s, m = pcall(function()
if cutMode then if cutMode then
fs.move(m.fullName, fs.combine(self.dir.name, m.name)) fs.move(m.fullName, fs.combine(self.dir.name, m.name))

View File

@@ -1,20 +1,14 @@
_G.requireInjector() requireInjector(getfenv(1))
local Event = require('event')
local UI = require('ui') local UI = require('ui')
local Util = require('util')
local colors = _G.colors
local help = _G.help
local multishell = _ENV.multishell
multishell.setTitle(multishell.getCurrent(), 'Help') multishell.setTitle(multishell.getCurrent(), 'Help')
UI:configure('Help', ...) UI:configure('Help', ...)
local topics = { } local files = { }
for _,topic in pairs(help.topics()) do for _,f in pairs(help.topics()) do
if help.lookup(topic) then table.insert(files, { name = f })
table.insert(topics, { name = topic })
end
end end
local page = UI.Page { local page = UI.Page {
@@ -28,9 +22,9 @@ local page = UI.Page {
}, },
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
y = 4, y = 4,
values = topics, values = files,
columns = { columns = {
{ heading = 'Topic', key = 'name' }, { heading = 'Name', key = 'name' },
}, },
sortColumn = 'name', sortColumn = 'name',
}, },
@@ -40,51 +34,36 @@ local page = UI.Page {
}, },
} }
local topicPage = UI.Page { local function showHelp(name)
backgroundColor = colors.black, UI.term:reset()
titleBar = UI.TitleBar { shell.run('help ' .. name)
title = 'text', print('Press enter to return')
previousPage = true, repeat
}, os.pullEvent('key')
helpText = UI.TextArea { local _, k = os.pullEvent('key_up')
backgroundColor = colors.black, until k == keys.enter
x = 2, ex = -1, y = 3, ey = -2,
},
accelerators = {
q = 'back',
backspace = 'back',
},
}
function topicPage:eventHandler(event)
if event.type == 'back' then
UI:setPreviousPage()
end
return UI.Page.eventHandler(self, event)
end end
function page:eventHandler(event) function page:eventHandler(event)
if event.type == 'quit' then if event.type == 'quit' then
UI:exitPullEvents() Event.exitPullEvents()
elseif event.type == 'grid_select' then elseif event.type == 'grid_select' then
if self.grid:getSelected() then if self.grid:getSelected() then
local name = self.grid:getSelected().name showHelp(self.grid:getSelected().name)
local f = help.lookup(name) self:setFocus(self.filter)
self:draw()
topicPage.titleBar.title = name
topicPage.helpText:setText(Util.readFile(f))
UI:setPage(topicPage)
end end
elseif event.type == 'text_change' then elseif event.type == 'text_change' then
if #event.text == 0 then local text = event.text
self.grid.values = topics if #text == 0 then
self.grid.values = files
else else
self.grid.values = { } self.grid.values = { }
for _,f in pairs(topics) do for _,f in pairs(files) do
if string.find(f.name, event.text) then if string.find(f.name, text) then
table.insert(self.grid.values, f) table.insert(self.grid.values, f)
end end
end end
@@ -93,7 +72,7 @@ function page:eventHandler(event)
self.grid:setIndex(1) self.grid:setIndex(1)
self.grid:draw() self.grid:draw()
else else
return UI.Page.eventHandler(self, event) UI.Page.eventHandler(self, event)
end end
end end

View File

@@ -1,28 +1,21 @@
local injector = _G.requireInjector or load(http.get('https://raw.githubusercontent.com/kepler155c/opus/master/sys/apis/injector.lua').readAll())() requireInjector = requireInjector or load(http.get('https://raw.githubusercontent.com/kepler155c/opus/master/sys/apis/injector.lua').readAll())()
injector() requireInjector(getfenv(1))
local Event = require('event') local Event = require('event')
local History = require('history') local History = require('history')
local Peripheral = require('peripheral') local UI = require('ui')
local UI = require('ui') local Util = require('util')
local Util = require('util')
local colors = _G.colors local sandboxEnv = setmetatable(Util.shallowCopy(getfenv(1)), { __index = _G })
local multishell = _ENV.multishell
local os = _G.os
local textutils = _G.textutils
local sandboxEnv = setmetatable(Util.shallowCopy(_ENV), { __index = _G })
sandboxEnv.exit = function() Event.exitPullEvents() end sandboxEnv.exit = function() Event.exitPullEvents() end
sandboxEnv._echo = function( ... ) return { ... } end sandboxEnv._echo = function( ... ) return ... end
injector(sandboxEnv) requireInjector(sandboxEnv)
multishell.setTitle(multishell.getCurrent(), 'Lua') multishell.setTitle(multishell.getCurrent(), 'Lua')
UI:configure('Lua', ...) UI:configure('Lua', ...)
local command = '' local command = ''
local history = History.load('usr/.lua_history', 25) local history = History.load('usr/.lua_history', 25)
local extChars = Util.getVersion() > 1.76
local page = UI.Page { local page = UI.Page {
menuBar = UI.MenuBar { menuBar = UI.MenuBar {
@@ -41,13 +34,9 @@ local page = UI.Page {
up = 'history_back', up = 'history_back',
down = 'history_forward', down = 'history_forward',
mouse_rightclick = 'clear_prompt', mouse_rightclick = 'clear_prompt',
[ 'control-space' ] = 'autocomplete', -- [ 'control-space' ] = 'autocomplete',
}, },
}, },
indicator = UI.Text {
backgroundColor = colors.black,
y = 2, x = -1, width = 1,
},
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
y = 3, y = 3,
columns = { columns = {
@@ -60,17 +49,6 @@ local page = UI.Page {
notification = UI.Notification(), notification = UI.Notification(),
} }
function page.indicator:showResult(s)
local values = {
[ true ] = { c = colors.green, i = (extChars and '\003') or '+' },
[ false ] = { c = colors.red, i = 'x' }
}
self.textColor = values[s].c
self.value = values[s].i
self:draw()
end
function page:setPrompt(value, focus) function page:setPrompt(value, focus)
self.prompt:setValue(value) self.prompt:setValue(value)
self.prompt.scroll = 0 self.prompt.scroll = 0
@@ -93,6 +71,7 @@ function page:enable()
end end
local function autocomplete(env, oLine, x) local function autocomplete(env, oLine, x)
local sLine = oLine:sub(1, x) local sLine = oLine:sub(1, x)
local nStartPos = sLine:find("[a-zA-Z0-9_%.]+$") local nStartPos = sLine:find("[a-zA-Z0-9_%.]+$")
if nStartPos then if nStartPos then
@@ -102,7 +81,10 @@ local function autocomplete(env, oLine, x)
if #sLine > 0 then if #sLine > 0 then
local results = textutils.complete(sLine, env) local results = textutils.complete(sLine, env)
if #results == 1 then if #results == 0 then
-- setError('No completions available')
elseif #results == 1 then
return Util.insertString(oLine, results[1], x + 1) return Util.insertString(oLine, results[1], x + 1)
elseif #results > 1 then elseif #results > 1 then
@@ -118,6 +100,8 @@ local function autocomplete(env, oLine, x)
end end
if #prefix > 0 then if #prefix > 0 then
return Util.insertString(oLine, prefix, x + 1) return Util.insertString(oLine, prefix, x + 1)
else
-- setStatus('Too many results')
end end
end end
end end
@@ -125,14 +109,15 @@ local function autocomplete(env, oLine, x)
end end
function page:eventHandler(event) function page:eventHandler(event)
if event.type == 'global' then if event.type == 'global' then
self:setPrompt('', true) self:setPrompt('', true)
self:executeStatement('_G') self:executeStatement('getfenv(0)')
command = nil command = nil
elseif event.type == 'local' then elseif event.type == 'local' then
self:setPrompt('', true) self:setPrompt('', true)
self:executeStatement('_ENV') self:executeStatement('getfenv(1)')
command = nil command = nil
elseif event.type == 'autocomplete' then elseif event.type == 'autocomplete' then
@@ -144,7 +129,11 @@ function page:eventHandler(event)
elseif event.type == 'device' then elseif event.type == 'device' then
if not _G.device then if not _G.device then
sandboxEnv.device = Peripheral.getList() sandboxEnv.device = { }
for _,side in pairs(peripheral.getNames()) do
local key = string.format('%s:%s', peripheral.getType(side), side)
sandboxEnv.device[ key ] = peripheral.wrap(side)
end
end end
self:setPrompt('device', true) self:setPrompt('device', true)
self:executeStatement('device') self:executeStatement('device')
@@ -198,7 +187,8 @@ function page:setResult(result)
local t = { } local t = { }
local function safeValue(v) local function safeValue(v)
if type(v) == 'string' or type(v) == 'number' then local t = type(v)
if t == 'string' or t == 'number' then
return v return v
end end
return tostring(v) return tostring(v)
@@ -215,8 +205,8 @@ function page:setResult(result)
if type(v) == 'table' then if type(v) == 'table' then
if Util.size(v) == 0 then if Util.size(v) == 0 then
entry.value = 'table: (empty)' entry.value = 'table: (empty)'
else else
entry.value = tostring(v) entry.value = 'table'
end end
end end
table.insert(t, entry) table.insert(t, entry)
@@ -235,6 +225,7 @@ function page:setResult(result)
end end
function page.grid:eventHandler(event) function page.grid:eventHandler(event)
local entry = self:getSelected() local entry = self:getSelected()
local function commandAppend() local function commandAppend()
@@ -252,7 +243,7 @@ function page.grid:eventHandler(event)
if type(entry.rawName) == 'number' then if type(entry.rawName) == 'number' then
return command .. '[' .. entry.name .. ']' return command .. '[' .. entry.name .. ']'
end end
if entry.name:match("%W") or if entry.name:match("%W") or
entry.name:sub(1, 1):match("%d") then entry.name:sub(1, 1):match("%d") then
return command .. "['" .. tostring(entry.name) .. "']" return command .. "['" .. tostring(entry.name) .. "']"
end end
@@ -268,10 +259,9 @@ function page.grid:eventHandler(event)
elseif event.type == 'grid_select' then elseif event.type == 'grid_select' then
page:setPrompt(commandAppend(), true) page:setPrompt(commandAppend(), true)
page:executeStatement(commandAppend()) page:executeStatement(commandAppend())
elseif event.type == 'copy' then elseif event.type == 'copy' then
if entry then if entry then
os.queueEvent('clipboard_copy', entry.rawValue) clipboard.setData(entry.rawValue)
end end
else else
return UI.ScrollingGrid.eventHandler(self, event) return UI.ScrollingGrid.eventHandler(self, event)
@@ -280,16 +270,10 @@ function page.grid:eventHandler(event)
end end
function page:rawExecute(s) function page:rawExecute(s)
local fn, m local fn, m = load('return _echo(' ..s.. ');', 'lua', nil, sandboxEnv)
fn = load('return (' ..s.. ')', 'lua', nil, sandboxEnv)
if fn then if fn then
fn = load('return {' ..s.. '}', 'lua', nil, sandboxEnv) m = { pcall(fn) }
end fn = table.remove(m, 1)
if fn then
fn, m = pcall(fn)
if #m == 1 then if #m == 1 then
m = m[1] m = m[1]
end end
@@ -305,6 +289,7 @@ function page:rawExecute(s)
end end
function page:executeStatement(statement) function page:executeStatement(statement)
command = statement command = statement
local s, m = self:rawExecute(command) local s, m = self:rawExecute(command)
@@ -318,7 +303,6 @@ function page:executeStatement(statement)
self.notification:error(m, 5) self.notification:error(m, 5)
end end
end end
self.indicator:showResult(not not s)
end end
local args = { ... } local args = { ... }

View File

@@ -1,17 +1,10 @@
_G.requireInjector() requireInjector(getfenv(1))
local Event = require('event') local Event = require('event')
local Socket = require('socket') local Socket = require('socket')
local UI = require('ui') local UI = require('ui')
local Util = require('util') local Util = require('util')
local colors = _G.colors
local device = _G.device
local multishell = _ENV.multishell
local network = _G.network
local os = _G.os
local shell = _ENV.shell
multishell.setTitle(multishell.getCurrent(), 'Network') multishell.setTitle(multishell.getCurrent(), 'Network')
UI:configure('Network', ...) UI:configure('Network', ...)
@@ -29,18 +22,10 @@ end
local page = UI.Page { local page = UI.Page {
menuBar = UI.MenuBar { menuBar = UI.MenuBar {
buttons = { buttons = {
{ text = 'Connect', dropdown = { { text = 'Telnet', event = 'telnet' },
{ text = 'Telnet t', event = 'telnet' }, { text = 'VNC', event = 'vnc' },
{ text = 'VNC v', event = 'vnc' }, { text = 'Trust', event = 'trust' },
UI.MenuBar.spacer, { text = 'Reboot', event = 'reboot' },
{ text = 'Reboot r', event = 'reboot' },
} },
--{ text = 'Chat', event = 'chat' },
{ text = 'Trust', dropdown = {
{ text = 'Establish', event = 'trust' },
{ text = 'Remove', event = 'untrust' },
} },
{ text = 'Help', event = 'help' },
}, },
}, },
grid = UI.ScrollingGrid { grid = UI.ScrollingGrid {
@@ -52,15 +37,13 @@ local page = UI.Page {
}, },
notification = UI.Notification { }, notification = UI.Notification { },
accelerators = { accelerators = {
t = 'telnet',
v = 'vnc',
r = 'reboot',
q = 'quit', q = 'quit',
c = 'clear', c = 'clear',
}, },
} }
local function sendCommand(host, command) local function sendCommand(host, command)
if not device.wireless_modem then if not device.wireless_modem then
page.notification:error('Wireless modem not present') page.notification:error('Wireless modem not present')
return return
@@ -82,7 +65,7 @@ end
function page:eventHandler(event) function page:eventHandler(event)
local t = self.grid:getSelected() local t = self.grid:getSelected()
if t then if t then
if event.type == 'telnet' then if event.type == 'telnet' or event.type == 'grid_select' then
multishell.openTab({ multishell.openTab({
path = 'sys/apps/telnet.lua', path = 'sys/apps/telnet.lua',
focused = true, focused = true,
@@ -96,69 +79,20 @@ function page:eventHandler(event)
args = { t.id }, args = { t.id },
title = t.label, title = t.label,
}) })
elseif event.type == 'clear' then
Util.clear(network)
page.grid:update()
page.grid:draw()
elseif event.type == 'trust' then elseif event.type == 'trust' then
shell.openForegroundTab('trust ' .. t.id) shell.openForegroundTab('trust ' .. t.id)
elseif event.type == 'untrust' then
local trustList = Util.readTable('usr/.known_hosts') or { }
trustList[t.id] = nil
Util.writeTable('usr/.known_hosts', trustList)
elseif event.type == 'chat' then
multishell.openTab({
path = 'sys/apps/shell',
args = { 'chat join opusChat-' .. t.id .. ' guest-' .. os.getComputerID() },
title = 'Chatroom',
focused = true,
})
elseif event.type == 'reboot' then elseif event.type == 'reboot' then
sendCommand(t.id, 'reboot') sendCommand(t.id, 'reboot')
elseif event.type == 'shutdown' then elseif event.type == 'shutdown' then
sendCommand(t.id, 'shutdown') sendCommand(t.id, 'shutdown')
end end
end end
if event.type == 'help' then if event.type == 'quit' then
UI:setPage(UI.Dialog {
title = 'Network Help',
height = 10,
backgroundColor = colors.white,
text = UI.TextArea {
x = 2, y = 2,
backgroundColor = colors.white,
value = [[
In order to connect to another computer:
1. The target computer must have a password set (run 'password' from the shell prompt).
2. From this computer, click trust and enter the password for that computer.
This only needs to be done once.
]],
},
accelerators = {
q = 'cancel',
}
})
elseif event.type == 'quit' then
Event.exitPullEvents() Event.exitPullEvents()
end end
UI.Page.eventHandler(self, event) UI.Page.eventHandler(self, event)
end end
function page.menuBar:getActive(menuItem)
local t = page.grid:getSelected()
if menuItem.event == 'untrust' then
local trustList = Util.readTable('usr/.known_hosts') or { }
return t and trustList[t.id]
end
return not not t
end
function page.grid:getRowTextColor(row, selected) function page.grid:getRowTextColor(row, selected)
if not row.active then if not row.active then
return colors.orange return colors.orange
@@ -190,14 +124,14 @@ Event.onInterval(1, function()
page:sync() page:sync()
end) end)
Event.on('device_attach', function(_, deviceName) Event.on('device_attach', function(h, deviceName)
if deviceName == 'wireless_modem' then if deviceName == 'wireless_modem' then
page.notification:success('Modem connected') page.notification:success('Modem connected')
page:sync() page:sync()
end end
end) end)
Event.on('device_detach', function(_, deviceName) Event.on('device_detach', function(h, deviceName)
if deviceName == 'wireless_modem' then if deviceName == 'wireless_modem' then
page.notification:error('Wireless modem not attached') page.notification:error('Wireless modem not attached')
page:sync() page:sync()

View File

@@ -1,4 +1,4 @@
_G.requireInjector() requireInjector(getfenv(1))
local class = require('class') local class = require('class')
local Config = require('config') local Config = require('config')
@@ -10,13 +10,19 @@ local Tween = require('ui.tween')
local UI = require('ui') local UI = require('ui')
local Util = require('util') local Util = require('util')
local fs = _G.fs
local multishell = _ENV.multishell
local pocket = _G.pocket
local term = _G.term
local turtle = _G.turtle
local REGISTRY_DIR = 'usr/.registry' local REGISTRY_DIR = 'usr/.registry'
local TEMPLATE = [[
local env = { }
for k,v in pairs(getfenv(1)) do
env[k] = v
end
setmetatable(env, { __index = _G })
local s, m = os.run(env, 'sys/apps/appRun.lua', %s, ...)
if not s then
error(m)
end
]]
multishell.setTitle(multishell.getCurrent(), 'Overview') multishell.setTitle(multishell.getCurrent(), 'Overview')
UI:configure('Overview', ...) UI:configure('Overview', ...)
@@ -53,7 +59,7 @@ local function loadApplications()
end end
Util.each(applications, function(v, k) v.key = k end) Util.each(applications, function(v, k) v.key = k end)
applications = Util.filter(applications, function(a) applications = Util.filter(applications, function(_, a)
if a.disabled then if a.disabled then
return false return false
end end
@@ -87,14 +93,13 @@ end
local buttons = { } local buttons = { }
local categories = { } local categories = { }
table.insert(buttons, { text = 'Recent', event = 'category' })
for _,f in pairs(applications) do for _,f in pairs(applications) do
if not categories[f.category] then if not categories[f.category] then
categories[f.category] = true categories[f.category] = true
table.insert(buttons, { text = f.category }) table.insert(buttons, { text = f.category, event = 'category' })
end end
end end
table.sort(buttons, function(a, b) return a.text < b.text end)
table.insert(buttons, 1, { text = 'Recent' })
table.insert(buttons, { text = '+', event = 'new' }) table.insert(buttons, { text = '+', event = 'new' })
local function parseIcon(iconText) local function parseIcon(iconText)
@@ -118,16 +123,15 @@ local function parseIcon(iconText)
end end
UI.VerticalTabBar = class(UI.TabBar) UI.VerticalTabBar = class(UI.TabBar)
function UI.VerticalTabBar:setParent() function UI.VerticalTabBar:init(args)
UI.TabBar.init(self, args)
self.x = 1 self.x = 1
self.width = 8 self.width = 8
self.height = nil self.height = nil
self.ey = -1 self.ey = -1
UI.TabBar.setParent(self)
for k,c in pairs(self.children) do for k,c in pairs(self.children) do
c.x = 1 c.x = 1
c.y = k + 1 c.y = k + 1
c.ox, c.oy = c.x, c.y
c.width = 8 c.width = 8
end end
end end
@@ -144,7 +148,7 @@ local page = UI.Page {
tabBar = UI.VerticalTabBar { tabBar = UI.VerticalTabBar {
buttons = buttons, buttons = buttons,
}, },
container = UI.Viewport { container = UI.ViewportWindow {
x = cx, x = cx,
y = cy, y = cy,
}, },
@@ -155,17 +159,28 @@ local page = UI.Page {
f = 'files', f = 'files',
s = 'shell', s = 'shell',
l = 'lua', l = 'lua',
[ 'control-l' ] = 'refresh',
[ 'control-n' ] = 'new', [ 'control-n' ] = 'new',
delete = 'delete', delete = 'delete',
}, },
} }
function page:draw()
self.tabBar:draw()
self.container:draw()
end
UI.Icon = class(UI.Window) UI.Icon = class(UI.Window)
UI.Icon.defaults = { function UI.Icon:init(args)
UIElement = 'Icon', local defaults = {
width = 14, UIElement = 'Icon',
height = 4, width = 14,
} height = 4,
}
UI:setProperties(defaults, args)
UI.Window.init(self, defaults)
end
function UI.Icon:eventHandler(event) function UI.Icon:eventHandler(event)
if event.type == 'mouse_click' then if event.type == 'mouse_click' then
self:setFocus(self.button) self:setFocus(self.button)
@@ -179,7 +194,7 @@ function UI.Icon:eventHandler(event)
return UI.Window.eventHandler(self, event) return UI.Window.eventHandler(self, event)
end end
function page.container:setCategory(categoryName, animate) function page.container:setCategory(categoryName)
-- reset the viewport window -- reset the viewport window
self.children = { } self.children = { }
@@ -208,7 +223,7 @@ function page.container:setCategory(categoryName, animate)
end end
else else
filtered = filter(applications, function(a) filtered = filter(applications, function(a)
return a.category == categoryName -- and fs.exists(a.run) return a.category == categoryName -- and fs.exists(a.run)
end) end)
table.sort(filtered, function(a, b) return a.title < b.title end) table.sort(filtered, function(a, b) return a.title < b.title end)
@@ -240,9 +255,7 @@ function page.container:setCategory(categoryName, animate)
y = 4, y = 4,
text = title, text = title,
backgroundColor = self.backgroundColor, backgroundColor = self.backgroundColor,
backgroundFocusColor = colors.gray, --backgroundFocusColor = colors.gray,
textColor = colors.white,
textFocusColor = colors.white,
width = #title + 2, width = #title + 2,
event = 'button', event = 'button',
app = program, app = program,
@@ -257,7 +270,7 @@ function page.container:setCategory(categoryName, animate)
local col, row = gutter, 2 local col, row = gutter, 2
local count = #self.children local count = #self.children
local r = math.random(1, 5) local r = math.random(1, 4)
-- reposition all children -- reposition all children
for k,child in ipairs(self.children) do for k,child in ipairs(self.children) do
if r == 1 then if r == 1 then
@@ -272,21 +285,9 @@ function page.container:setCategory(categoryName, animate)
elseif r == 4 then elseif r == 4 then
child.x = self.width - col child.x = self.width - col
child.y = row child.y = row
elseif r == 5 then
child.x = col
child.y = row
if k == #self.children then
child.x = self.width
child.y = self.height
end
end end
child.tween = Tween.new(6, child, { x = col, y = row }, 'linear') child.tween = Tween.new(6, child, { x = col, y = row }, 'linear')
if not animate then
child.x = col
child.y = row
end
if k < count then if k < count then
col = col + child.width col = col + child.width
if col + self.children[k + 1].width + gutter - 2 > self.width then if col + self.children[k + 1].width + gutter - 2 > self.width then
@@ -297,30 +298,31 @@ function page.container:setCategory(categoryName, animate)
end end
self:initChildren() self:initChildren()
if animate then -- need to fix transitions under layers
local function transition(args) local transition = { i = 1, parent = self, children = self.children }
local i = 1 function transition:update(device)
return function(device) self.parent:clear()
self:clear() for _,child in ipairs(self.children) do
for _,child in pairs(self.children) do child.tween:update(1)
child.tween:update(1) child.x = math.floor(child.x)
child.x = math.floor(child.x) child.y = math.floor(child.y)
child.y = math.floor(child.y) child:draw()
child:draw()
end
args.canvas:blit(device, args, args)
i = i + 1
return i < 7
end
end end
self:addTransition(transition) self.canvas:blit(device, self, self)
self.i = self.i + 1
return self.i < 7
end end
self:addTransition(transition)
end
function page.container:draw()
UI.ViewportWindow.draw(self)
end end
function page:refresh() function page:refresh()
local pos = self.container.offy local pos = self.container.offy
self:focusFirst(self) self:focusFirst(self)
self.container:setCategory(config.currentCategory) self.container:setCategory(config.currentCategory)
self.container:setScrollPosition(pos) self.container:setScrollPosition(pos)
end end
@@ -331,9 +333,11 @@ end
function page:eventHandler(event) function page:eventHandler(event)
if event.type == 'tab_select' then if event.type == 'category' then
self.container:setCategory(event.button.text, true) self.tabBar:selectTab(event.button.text)
self.container:setCategory(event.button.text)
self.container:draw() self.container:draw()
self:sync()
config.currentCategory = event.button.text config.currentCategory = event.button.text
Config.update('Overview', config) Config.update('Overview', config)
@@ -380,7 +384,14 @@ function page:eventHandler(event)
event.focused.parent:scrollIntoView() event.focused.parent:scrollIntoView()
end end
elseif event.type == 'refresh' then -- remove this after fixing notification elseif event.type == 'tab_change' then
if event.current > event.last then
--self.container:setTransition(UI.effect.slideLeft)
else
--self.container:setTransition(UI.effect.slideRight)
end
elseif event.type == 'refresh' then
loadApplications() loadApplications()
self:refresh() self:refresh()
self:draw() self:draw()
@@ -422,7 +433,7 @@ local formWidth = math.max(UI.term.width - 8, 26)
local editor = UI.Dialog { local editor = UI.Dialog {
height = 11, height = 11,
width = formWidth, width = formWidth,
title = 'Edit Application', title = 'Edit application',
form = UI.Form { form = UI.Form {
y = 2, y = 2,
height = 9, height = 9,
@@ -439,7 +450,7 @@ local editor = UI.Dialog {
required = true, required = true,
}, },
loadIcon = UI.Button { loadIcon = UI.Button {
x = 11, y = 6, x = 11, y = 6,
text = 'Icon', event = 'loadIcon', help = 'Select icon' text = 'Icon', event = 'loadIcon', help = 'Select icon'
}, },
image = UI.NftImage { image = UI.NftImage {
@@ -495,6 +506,7 @@ function editor:eventHandler(event)
width = self.width, width = self.width,
height = self.height, height = self.height,
}) })
--fileui:setTransition(UI.effect.explode)
UI:setPage(fileui, fs.getDir(self.iconFile), function(fileName) UI:setPage(fileui, fs.getDir(self.iconFile), function(fileName)
if fileName then if fileName then
self.iconFile = fileName self.iconFile = fileName
@@ -549,4 +561,5 @@ page.tabBar:selectTab(config.currentCategory or 'Apps')
page.container:setCategory(config.currentCategory or 'Apps') page.container:setCategory(config.currentCategory or 'Apps')
UI:setPage(page) UI:setPage(page)
UI:pullEvents() Event.pullEvents()
UI.term:reset()

View File

@@ -1,17 +1,9 @@
_G.requireInjector() requireInjector(getfenv(1))
local Config = require('config') local Config = require('config')
local Security = require('security') local Event = require('event')
local SHA1 = require('sha1') local UI = require('ui')
local UI = require('ui') local Util = require('util')
local Util = require('util')
local fs = _G.fs
local multishell = _ENV.multishell
local os = _G.os
local settings = _G.settings
local shell = _ENV.shell
local turtle = _G.turtle
multishell.setTitle(multishell.getCurrent(), 'System') multishell.setTitle(multishell.getCurrent(), 'System')
UI:configure('System', ...) UI:configure('System', ...)
@@ -19,7 +11,7 @@ UI:configure('System', ...)
local env = { local env = {
path = shell.path(), path = shell.path(),
aliases = shell.aliases(), aliases = shell.aliases(),
lua_path = _ENV.LUA_PATH, lua_path = LUA_PATH,
} }
Config.load('shell', env) Config.load('shell', env)
@@ -38,6 +30,7 @@ local systemPage = UI.Page {
}, },
grid = UI.Grid { grid = UI.Grid {
y = 4, y = 4,
values = paths,
disableHeader = true, disableHeader = true,
columns = { { key = 'value' } }, columns = { { key = 'value' } },
autospace = true, autospace = true,
@@ -45,9 +38,9 @@ local systemPage = UI.Page {
}, },
aliasTab = UI.Window { aliasTab = UI.Window {
tabTitle = 'Alias', tabTitle = 'Aliases',
alias = UI.TextEntry { alias = UI.TextEntry {
x = 2, y = 2, ex = -2, x = 2, y = 2, ex = -2,
limit = 32, limit = 32,
shadowText = 'Alias', shadowText = 'Alias',
}, },
@@ -61,6 +54,8 @@ local systemPage = UI.Page {
}, },
grid = UI.Grid { grid = UI.Grid {
y = 5, y = 5,
values = aliases,
autospace = true,
sortColumn = 'alias', sortColumn = 'alias',
columns = { columns = {
{ heading = 'Alias', key = 'alias' }, { heading = 'Alias', key = 'alias' },
@@ -72,37 +67,6 @@ local systemPage = UI.Page {
}, },
}, },
passwordTab = UI.Window {
tabTitle = 'Password',
oldPass = UI.TextEntry {
x = 2, y = 2, ex = -2,
limit = 32,
mask = true,
shadowText = 'old password',
inactive = not Security.getPassword(),
},
newPass = UI.TextEntry {
y = 3, x = 2, ex = -2,
limit = 32,
mask = true,
shadowText = 'new password',
accelerators = {
enter = 'new_password',
},
},
button = UI.Button {
x = 2, y = 5,
text = 'Update',
event = 'update_password',
},
info = UI.TextArea {
x = 2, ex = -2,
y = 7,
inactive = true,
value = 'Add a password to enable other computers to connect to this one.',
}
},
infoTab = UI.Window { infoTab = UI.Window {
tabTitle = 'Info', tabTitle = 'Info',
labelText = UI.Text { labelText = UI.Text {
@@ -123,12 +87,12 @@ local systemPage = UI.Page {
{ name = '', value = '' }, { name = '', value = '' },
{ name = 'CC version', value = Util.getVersion() }, { name = 'CC version', value = Util.getVersion() },
{ name = 'Lua version', value = _VERSION }, { name = 'Lua version', value = _VERSION },
{ name = 'MC version', value = Util.getMinecraftVersion() }, { name = 'MC version', value = _MC_VERSION or 'unknown' },
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) }, { name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
{ name = 'Computer ID', value = tostring(os.getComputerID()) }, { name = 'Computer ID', value = tostring(os.getComputerID()) },
{ name = 'Day', value = tostring(os.day()) }, { name = 'Day', value = tostring(os.day()) },
}, },
inactive = true, selectable = false,
columns = { columns = {
{ key = 'name', width = 12 }, { key = 'name', width = 12 },
{ key = 'value' }, { key = 'value' },
@@ -142,99 +106,6 @@ local systemPage = UI.Page {
}, },
} }
if turtle then
local Home = require('turtle.home')
local values = { }
Config.load('gps', values.home or { })
systemPage.tabs:add({
gpsTab = UI.Window {
tabTitle = 'GPS',
labelText = UI.Text {
x = 3, y = 2,
value = 'On restart, return to this location'
},
grid = UI.Grid {
x = 3, ex = -3, y = 4,
height = 2,
values = values,
inactive = true,
columns = {
{ heading = 'x', key = 'x' },
{ heading = 'y', key = 'y' },
{ heading = 'z', key = 'z' },
},
},
button1 = UI.Button {
x = 3, y = 7,
text = 'Set home',
event = 'gps_set',
},
button2 = UI.Button {
ex = -3, y = 7, width = 7,
text = 'Clear',
event = 'gps_clear',
},
},
})
function systemPage.tabs.gpsTab:eventHandler(event)
if event.type == 'gps_set' then
systemPage.notification:info('Determining location', 10)
systemPage:sync()
if Home.set() then
Config.load('gps', values)
self.grid:setValues(values.home or { })
self.grid:draw()
systemPage.notification:success('Location set')
else
systemPage.notification:error('Unable to determine location')
end
return true
elseif event.type == 'gps_clear' then
fs.delete('usr/config/gps')
self.grid:setValues({ })
self.grid:draw()
return true
end
end
end
if settings then
local values = { }
for _,v in pairs(settings.getNames()) do
table.insert(values, {
name = v,
value = not not settings.get(v),
})
end
systemPage.tabs:add({
settingsTab = UI.Window {
tabTitle = 'Settings',
grid = UI.Grid {
y = 1,
values = values,
autospace = true,
sortColumn = 'name',
columns = {
{ heading = 'Setting', key = 'name' },
{ heading = 'Value', key = 'value' },
},
},
}
})
function systemPage.tabs.settingsTab:eventHandler(event)
if event.type == 'grid_select' then
event.selected.value = not event.selected.value
settings.set(event.selected.name, event.selected.value)
settings.save('.settings')
self.grid:draw()
return true
end
end
end
function systemPage.tabs.pathTab.grid:draw() function systemPage.tabs.pathTab.grid:draw()
self.values = { } self.values = { }
for _,v in ipairs(Util.split(env.path, '(.-):')) do for _,v in ipairs(Util.split(env.path, '(.-):')) do
@@ -245,6 +116,7 @@ function systemPage.tabs.pathTab.grid:draw()
end end
function systemPage.tabs.pathTab:eventHandler(event) function systemPage.tabs.pathTab:eventHandler(event)
if event.type == 'update_path' then if event.type == 'update_path' then
env.path = self.entry.value env.path = self.entry.value
self.grid:setIndex(self.grid:getIndex()) self.grid:setIndex(self.grid:getIndex())
@@ -257,6 +129,7 @@ end
function systemPage.tabs.aliasTab.grid:draw() function systemPage.tabs.aliasTab.grid:draw()
self.values = { } self.values = { }
local aliases = { }
for k,v in pairs(env.aliases) do for k,v in pairs(env.aliases) do
table.insert(self.values, { alias = k, path = v }) table.insert(self.values, { alias = k, path = v })
end end
@@ -265,6 +138,7 @@ function systemPage.tabs.aliasTab.grid:draw()
end end
function systemPage.tabs.aliasTab:eventHandler(event) function systemPage.tabs.aliasTab:eventHandler(event)
if event.type == 'delete_alias' then if event.type == 'delete_alias' then
env.aliases[self.grid:getSelected().alias] = nil env.aliases[self.grid:getSelected().alias] = nil
self.grid:setIndex(self.grid:getIndex()) self.grid:setIndex(self.grid:getIndex())
@@ -285,22 +159,6 @@ function systemPage.tabs.aliasTab:eventHandler(event)
end end
end end
function systemPage.tabs.passwordTab:eventHandler(event)
if event.type == 'update_password' then
if #self.newPass.value == 0 then
systemPage.notification:error('Invalid password')
elseif Security.getPassword() and not Security.verifyPassword(SHA1.sha1(self.oldPass.value)) then
systemPage.notification:error('Passwords do not match')
else
Security.updatePassword(SHA1.sha1(self.newPass.value))
self.oldPass.inactive = false
systemPage.notification:success('Password updated')
end
return true
end
end
function systemPage.tabs.infoTab:eventHandler(event) function systemPage.tabs.infoTab:eventHandler(event)
if event.type == 'update_label' then if event.type == 'update_label' then
os.setComputerLabel(self.label.value) os.setComputerLabel(self.label.value)
@@ -310,8 +168,9 @@ function systemPage.tabs.infoTab:eventHandler(event)
end end
function systemPage:eventHandler(event) function systemPage:eventHandler(event)
if event.type == 'quit' then if event.type == 'quit' then
UI:exitPullEvents() Event.exitPullEvents()
elseif event.type == 'tab_activate' then elseif event.type == 'tab_activate' then
event.activated:focusFirst() event.activated:focusFirst()
else else
@@ -321,4 +180,5 @@ function systemPage:eventHandler(event)
end end
UI:setPage(systemPage) UI:setPage(systemPage)
UI:pullEvents() Event.pullEvents()
UI.term:reset()

View File

@@ -1,11 +1,9 @@
_G.requireInjector() requireInjector(getfenv(1))
local Event = require('event') local Event = require('event')
local UI = require('ui') local UI = require('ui')
local Util = require('util') local Util = require('util')
local multishell = _ENV.multishell
multishell.setTitle(multishell.getCurrent(), 'Tabs') multishell.setTitle(multishell.getCurrent(), 'Tabs')
UI:configure('Tabs', ...) UI:configure('Tabs', ...)

View File

@@ -1,54 +1,43 @@
local sandboxEnv = { }
for k,v in pairs(_ENV) do
sandboxEnv[k] = v
end
_G.requireInjector()
local Config = require('config')
local Input = require('input')
local Util = require('util')
local colors = _G.colors
local fs = _G.fs
local keys = _G.keys
local multishell = _ENV.multishell
local os = _G.os
local printError = _G.printError
local shell = _ENV.shell
local term = _G.term
local window = _G.window
local parentTerm = term.current()
local w,h = parentTerm.getSize()
local tabs = { }
local currentTab
local _tabId = 0
local overviewId
local runningTab
local tabsDirty = false
local closeInd = '*'
local hooks = { }
local hotkeys = { }
local downState = { }
multishell.term = term.current()
-- Default label -- Default label
if not os.getComputerLabel() then if not os.getComputerLabel() then
local id = os.getComputerID() local id = os.getComputerID()
if _G.turtle then if turtle then
os.setComputerLabel('turtle_' .. id) os.setComputerLabel('turtle_' .. id)
elseif _G.pocket then elseif pocket then
os.setComputerLabel('pocket_' .. id) os.setComputerLabel('pocket_' .. id)
elseif _G.commands then elseif commands then
os.setComputerLabel('command_' .. id) os.setComputerLabel('command_' .. id)
else else
os.setComputerLabel('computer_' .. id) os.setComputerLabel('computer_' .. id)
end end
end end
if Util.getVersion() >= 1.76 then multishell.term = term.current()
local defaultEnv = { }
for k,v in pairs(getfenv(1)) do
defaultEnv[k] = v
end
requireInjector(getfenv(1))
local Config = require('config')
local Opus = require('opus')
local Util = require('util')
local SESSION_FILE = 'usr/config/multishell.session'
local parentTerm = term.current()
local w,h = parentTerm.getSize()
local tabs = {}
local currentTab
local _tabId = 0
local overviewTab
local runningTab
local tabsDirty = false
local closeInd = '*'
if Util.getVersion() >= 1.79 then
closeInd = '\215' closeInd = '\215'
end end
@@ -70,6 +59,7 @@ local config = {
focusBackgroundColor = colors.gray, focusBackgroundColor = colors.gray,
}, },
} }
Config.load('multishell', config) Config.load('multishell', config)
local _colors = config.standard local _colors = config.standard
@@ -79,11 +69,92 @@ end
local function redrawMenu() local function redrawMenu()
if not tabsDirty then if not tabsDirty then
os.queueEvent('multishell_redraw') os.queueEvent('multishell', 'draw')
tabsDirty = true tabsDirty = true
end end
end end
-- Draw menu
local function draw()
tabsDirty = false
parentTerm.setBackgroundColor( _colors.tabBarBackgroundColor )
if currentTab and currentTab.isOverview then
parentTerm.setTextColor( _colors.focusTextColor )
else
parentTerm.setTextColor( _colors.tabBarTextColor )
end
parentTerm.setCursorPos( 1, 1 )
parentTerm.clearLine()
parentTerm.write('+')
local tabX = 2
local function compareTab(a, b)
return a.tabId < b.tabId
end
for _,tab in Util.spairs(tabs, compareTab) do
if tab.hidden and tab ~= currentTab or tab.isOverview then
tab.sx = nil
tab.ex = nil
else
tab.sx = tabX + 1
tab.ex = tabX + #tab.title
tabX = tabX + #tab.title + 1
end
end
for _,tab in Util.spairs(tabs) do
if tab.sx then
if tab == currentTab then
parentTerm.setTextColor(_colors.focusTextColor)
parentTerm.setBackgroundColor(_colors.focusBackgroundColor)
else
parentTerm.setTextColor(_colors.textColor)
parentTerm.setBackgroundColor(_colors.backgroundColor)
end
parentTerm.setCursorPos(tab.sx, 1)
parentTerm.write(tab.title)
end
end
if currentTab and not currentTab.isOverview then
parentTerm.setTextColor(_colors.focusTextColor)
parentTerm.setBackgroundColor(_colors.backgroundColor)
parentTerm.setCursorPos( w, 1 )
parentTerm.write(closeInd)
end
if currentTab then
currentTab.window.restoreCursor()
end
end
local function selectTab( tab )
if not tab then
for _,ftab in pairs(tabs) do
if not ftab.hidden then
tab = ftab
break
end
end
end
if not tab then
tab = overviewTab
end
if currentTab and currentTab ~= tab then
currentTab.window.setVisible(false)
if tab and not currentTab.hidden then
tab.previousTabId = currentTab.tabId
end
end
if tab then
currentTab = tab
tab.window.setVisible(true)
end
end
local function resumeTab(tab, event, eventData) local function resumeTab(tab, event, eventData)
if not tab or coroutine.status(tab.co) == 'dead' then if not tab or coroutine.status(tab.co) == 'dead' then
return return
@@ -108,50 +179,18 @@ local function resumeTab(tab, event, eventData)
end end
end end
local function selectTab(tab)
if not tab then
for _,ftab in pairs(tabs) do
if not ftab.hidden then
tab = ftab
break
end
end
end
if not tab then
tab = tabs[overviewId]
end
if currentTab and currentTab ~= tab then
currentTab.window.setVisible(false)
if coroutine.status(currentTab.co) == 'suspended' then
-- the process that opens a new tab won't get the lose focus event
-- os.queueEvent('multishell_notifyfocus', currentTab.tabId)
--resumeTab(currentTab, 'multishell_losefocus')
end
if tab and not currentTab.hidden then
tab.previousTabId = currentTab.tabId
end
end
if tab ~= currentTab then
currentTab = tab
tab.window.setVisible(true)
resumeTab(tab, 'multishell_focus')
end
end
local function nextTabId() local function nextTabId()
_tabId = _tabId + 1 _tabId = _tabId + 1
return _tabId return _tabId
end end
local function launchProcess(tab) local function launchProcess(tab)
tab.tabId = nextTabId() tab.tabId = nextTabId()
tab.timestamp = os.clock() tab.timestamp = os.clock()
tab.window = window.create(parentTerm, 1, 2, w, h - 1, false) tab.window = window.create(parentTerm, 1, 2, w, h - 1, false)
tab.terminal = tab.window tab.terminal = tab.window
tab.env = Util.shallowCopy(tab.env or sandboxEnv) tab.env = Util.shallowCopy(tab.env or defaultEnv)
tab.co = coroutine.create(function() tab.co = coroutine.create(function()
@@ -174,6 +213,9 @@ local function launchProcess(tab)
while true do while true do
local e, code = os.pullEventRaw('key') local e, code = os.pullEventRaw('key')
if e == 'terminate' or e == 'key' and code == keys.enter then if e == 'terminate' or e == 'key' and code == keys.enter then
if tab.isOverview then
os.queueEvent('multishell', 'terminate')
end
break break
end end
end end
@@ -183,20 +225,78 @@ local function launchProcess(tab)
local previousTab local previousTab
if tab.previousTabId then if tab.previousTabId then
previousTab = tabs[tab.previousTabId] previousTab = tabs[tab.previousTabId]
if previousTab and previousTab.hidden then
previousTab = nil
end
end end
selectTab(previousTab) selectTab(previousTab)
end end
redrawMenu() redrawMenu()
saveSession()
end) end)
tabs[tab.tabId] = tab tabs[tab.tabId] = tab
resumeTab(tab) resumeTab(tab)
return tab return tab
end end
local function resizeWindows()
local windowY = 2
local windowHeight = h-1
local keys = Util.keys(tabs)
for _,key in pairs(keys) do
local tab = tabs[key]
local x,y = tab.window.getCursorPos()
if y > windowHeight then
tab.window.scroll( y - windowHeight )
tab.window.setCursorPos( x, windowHeight )
end
tab.window.reposition( 1, windowY, w, windowHeight )
end
-- Pass term_resize to all processes
local keys = Util.keys(tabs)
for _,key in pairs(keys) do
resumeTab(tabs[key], "term_resize")
end
end
local function saveSession()
local t = { }
for _,process in pairs(tabs) do
if process.path and not process.isOverview and not process.hidden then
table.insert(t, {
path = process.path,
args = process.args,
})
end
end
--Util.writeTable(SESSION_FILE, t)
end
local control
local hotkeys = { }
local function processKeyEvent(event, code)
if event == 'key_up' then
if code == keys.leftCtrl or code == keys.rightCtrl then
control = false
end
elseif event == 'char' then
control = false
elseif event == 'key' then
if code == keys.leftCtrl or code == keys.rightCtrl then
control = true
elseif control then
local hotkey = hotkeys[code]
control = false
if hotkey then
hotkey()
end
end
end
end
function multishell.addHotkey(code, fn) function multishell.addHotkey(code, fn)
hotkeys[code] = fn hotkeys[code] = fn
end end
@@ -226,12 +326,10 @@ function multishell.getTitle(tabId)
end end
end end
function multishell.setTitle(tabId, title) function multishell.setTitle(tabId, sTitle)
local tab = tabs[tabId] local tab = tabs[tabId]
if tab then if tab then
if not tab.isOverview then tab.title = sTitle or ''
tab.title = title or ''
end
redrawMenu() redrawMenu()
end end
end end
@@ -247,7 +345,23 @@ function multishell.getTab(tabId)
end end
function multishell.terminate(tabId) function multishell.terminate(tabId)
os.queueEvent('multishell_terminate', tabId) local tab = tabs[tabId]
if tab and not tab.isOverview then
if coroutine.status(tab.co) ~= 'dead' then
--os.queueEvent('multishell', 'terminate', tab)
resumeTab(tab, "terminate")
else
tabs[tabId] = nil
if tab == currentTab then
local previousTab
if tab.previousTabId then
previousTab = tabs[tab.previousTabId]
end
selectTab(previousTab)
end
redrawMenu()
end
end
end end
function multishell.getTabs() function multishell.getTabs()
@@ -264,9 +378,11 @@ function multishell.launch( tProgramEnv, sProgramPath, ... )
end end
function multishell.openTab(tab) function multishell.openTab(tab)
if not tab.title and tab.path then if not tab.title and tab.path then
tab.title = fs.getName(tab.path) tab.title = fs.getName(tab.path)
end end
tab.title = tab.title or 'untitled' tab.title = tab.title or 'untitled'
local previousTerm = term.current() local previousTerm = term.current()
@@ -283,6 +399,10 @@ function multishell.openTab(tab)
redrawMenu() redrawMenu()
end end
if not tab.hidden then
saveSession()
end
return tab.tabId return tab.tabId
end end
@@ -290,9 +410,6 @@ function multishell.hideTab(tabId)
local tab = tabs[tabId] local tab = tabs[tabId]
if tab then if tab then
tab.hidden = true tab.hidden = true
if currentTab.tabId == tabId then
selectTab(tabs[currentTab.previousTabId])
end
redrawMenu() redrawMenu()
end end
end end
@@ -306,311 +423,202 @@ function multishell.unhideTab(tabId)
end end
function multishell.getCount() function multishell.getCount()
return Util.size(tabs) local count
for _,tab in pairs(tabs) do
count = count + 1
end
return count
end end
function multishell.hook(event, fn) -- control-o - overview
if type(event) == 'table' then multishell.addHotkey(24, function()
for _,v in pairs(event) do multishell.setFocus(overviewTab.tabId)
multishell.hook(v, fn)
end
else
if not hooks[event] then
hooks[event] = { }
end
table.insert(hooks[event], fn)
end
end
-- you can only unhook from within the function that hooked
function multishell.unhook(event, fn)
local eventHooks = hooks[event]
if eventHooks then
Util.removeByValue(eventHooks, fn)
if #eventHooks == 0 then
hooks[event] = nil
end
end
end
multishell.hook('multishell_terminate', function(_, eventData)
local tabId = eventData[1] or -1
local tab = tabs[tabId]
if tab and not tab.isOverview then
if coroutine.status(tab.co) ~= 'dead' then
resumeTab(tab, "terminate")
end
end
return true
end) end)
multishell.hook('multishell_redraw', function() -- control-backspace
tabsDirty = false multishell.addHotkey(14, function()
local tabId = multishell.getFocus()
local function write(x, text, bg, fg) local tab = tabs[tabId]
parentTerm.setBackgroundColor(bg) if not tab.isOverview then
parentTerm.setTextColor(fg) os.queueEvent('multishell', 'terminateTab', tabId)
parentTerm.setCursorPos(x, 1) tab = Util.shallowCopy(tab)
parentTerm.write(text) tab.isDead = false
tab.focused = true
multishell.openTab(tab)
end end
end)
local bg = _colors.tabBarBackgroundColor -- control-tab - next tab
parentTerm.setBackgroundColor(bg) multishell.addHotkey(15, function()
parentTerm.setCursorPos(1, 1)
parentTerm.clearLine()
local function compareTab(a, b) local function compareTab(a, b)
return a.tabId < b.tabId return a.tabId < b.tabId
end end
local visibleTabs = { }
for _,tab in pairs(tabs) do
if tab.hidden and tab ~= currentTab then
tab.width = 0
else
tab.width = #tab.title + 1
end
end
local function width()
local tw = 0
Util.each(tabs, function(t) tw = tw + t.width end)
return tw
end
while width() > w - 3 do
local tab = select(2,
Util.spairs(tabs, function(a, b) return a.width > b.width end)())
tab.width = tab.width - 1
end
local tabX = 0
for _,tab in Util.spairs(tabs, compareTab) do for _,tab in Util.spairs(tabs, compareTab) do
if tab.width > 0 then if not tab.hidden then
tab.sx = tabX + 1 table.insert(visibleTabs, tab)
tab.ex = tabX + tab.width end
tabX = tabX + tab.width end
if tab ~= currentTab then for k,tab in ipairs(visibleTabs) do
write(tab.sx, tab.title:sub(1, tab.width - 1), if tab.tabId == currentTab.tabId then
_colors.backgroundColor, _colors.textColor) if k < #visibleTabs then
multishell.setFocus(visibleTabs[k + 1].tabId)
return
end end
end end
end end
if #visibleTabs > 0 then
if currentTab then multishell.setFocus(visibleTabs[1].tabId)
write(currentTab.sx - 1,
' ' .. currentTab.title:sub(1, currentTab.width - 1) .. ' ',
_colors.focusBackgroundColor, _colors.focusTextColor)
if not currentTab.isOverview then
write(w, closeInd, _colors.backgroundColor, _colors.focusTextColor)
end
end end
if currentTab then
currentTab.window.restoreCursor()
end
return true
end)
multishell.hook('term_resize', function(_, eventData)
if not eventData[1] then --- TEST
w,h = parentTerm.getSize()
local windowHeight = h-1
for _,key in pairs(Util.keys(tabs)) do
local tab = tabs[key]
local x,y = tab.window.getCursorPos()
if y > windowHeight then
tab.window.scroll(y - windowHeight)
tab.window.setCursorPos(x, windowHeight)
end
tab.window.reposition(1, 2, w, windowHeight)
end
redrawMenu()
end
end)
-- downstate should be stored in the tab (maybe)
multishell.hook('key_up', function(_, eventData)
local code = eventData[1]
if downState[code] ~= currentTab then
downState[code] = nil
return true
end
downState[code] = nil
end)
multishell.hook('key', function(_, eventData)
local code = eventData[1]
local firstPress = not eventData[2]
if firstPress then
downState[code] = currentTab
else
--key was pressed initially in a previous window
if downState[code] ~= currentTab then
return true
end
end
end)
multishell.hook({ 'key', 'key_up', 'char', 'paste' }, function(event, eventData)
local code = Input:translate(event, eventData[1], eventData[2])
if code and hotkeys[code] then
hotkeys[code](event, eventData)
end
end)
multishell.hook('mouse_click', function(_, eventData)
local x, y = eventData[2], eventData[3]
if y == 1 then
if x == 1 then
multishell.setFocus(overviewId)
elseif x == w then
if currentTab then
multishell.terminate(currentTab.tabId)
end
else
for _,tab in pairs(tabs) do
if not tab.hidden and tab.sx then
if x >= tab.sx and x <= tab.ex then
multishell.setFocus(tab.tabId)
break
end
end
end
end
downState.mouse = nil
return true
end
downState.mouse = currentTab
eventData[3] = eventData[3] - 1
end)
multishell.hook({ 'mouse_up', 'mouse_drag' }, function(event, eventData)
if downState.mouse ~= currentTab then
-- don't send mouse up as the mouse click event was on another window
if event == 'mouse_up' then
downState.mouse = nil
end
return true -- stop propagation
end
eventData[3] = eventData[3] - 1
end)
multishell.hook('mouse_scroll', function(_, eventData)
local dir, y = eventData[1], eventData[3]
if y == 1 then
return true
end
if currentTab.terminal.scrollUp then
if dir == -1 then
currentTab.terminal.scrollUp()
else
currentTab.terminal.scrollDown()
end
end
eventData[3] = y - 1
end) end)
local function startup() local function startup()
local success = true local hasError
local session = Util.readTable(SESSION_FILE)
local function runDir(directory, open) local overviewId = multishell.openTab({
if not fs.exists(directory) then path = 'sys/apps/Overview.lua',
return true focused = true,
end hidden = true,
isOverview = true,
})
overviewTab = tabs[overviewId]
local files = fs.list(directory) if not Opus.loadServices() then
table.sort(files) hasError = true
end
for _,file in ipairs(files) do if not Opus.autorun() then
os.sleep(0) hasError = true
local result, err = open(directory .. '/' .. file) end
if result then
if term.isColor() then if session then
term.setTextColor(colors.green) for _,v in pairs(session) do
end multishell.openTab(v)
term.write('[PASS] ')
term.setTextColor(colors.white)
term.write(fs.combine(directory, file))
else
if term.isColor() then
term.setTextColor(colors.red)
end
term.write('[FAIL] ')
term.setTextColor(colors.white)
term.write(fs.combine(directory, file))
if err then
_G.printError(err)
end
success = false
end
print()
end end
end end
runDir('sys/services', shell.openHiddenTab) if hasError then
runDir('sys/autorun', shell.run)
runDir('usr/autorun', shell.run)
if not success then
print() print()
error('An autorun program has errored') error('An autorun program has errored')
end end
end end
overviewId = multishell.openTab({ -- Begin
path = 'sys/apps/Overview.lua', parentTerm.clear()
isOverview = true,
})
tabs[overviewId].title = '+'
multishell.openTab({ multishell.openTab({
focused = true, focused = true,
fn = startup, fn = startup,
env = defaultEnv,
title = 'Autorun', title = 'Autorun',
}) })
local currentTabEvents = Util.transpose { if not overviewTab or coroutine.status(overviewTab.co) == 'dead' then
'char', 'key', 'key_up', --error('Overview aborted')
'mouse_click', 'mouse_drag', 'mouse_scroll', 'mouse_up', end
'paste', 'terminate',
} if not currentTab then
multishell.setFocus(overviewTab.tabId)
end
draw()
local lastClicked
while true do while true do
-- Get the event
local tEventData = { os.pullEventRaw() } local tEventData = { os.pullEventRaw() }
local sEvent = table.remove(tEventData, 1) local sEvent = table.remove(tEventData, 1)
local stopPropagation
local eventHooks = hooks[sEvent] if sEvent == 'key_up' then
if eventHooks then processKeyEvent(sEvent, tEventData[1])
for i = #eventHooks, 1, -1 do
stopPropagation = eventHooks[i](sEvent, tEventData)
if stopPropagation then
break
end
end
end end
if not stopPropagation then if sEvent == "term_resize" then
if currentTabEvents[sEvent] then -- Resize event
resumeTab(currentTab, sEvent, tEventData) w,h = parentTerm.getSize()
resizeWindows()
redrawMenu()
else elseif sEvent == 'multishell' then
-- Passthrough to all processes local action = tEventData[1]
for _,key in pairs(Util.keys(tabs)) do
resumeTab(tabs[key], sEvent, tEventData) if action == 'terminate' then
break
elseif action == 'terminateTab' then
multishell.terminate(tEventData[2])
elseif action == 'draw' then
draw()
end
elseif sEvent == "char" or
sEvent == "key" or
sEvent == "paste" or
sEvent == "terminate" then
processKeyEvent(sEvent, tEventData[1])
-- Keyboard event - Passthrough to current process
resumeTab(currentTab, sEvent, tEventData)
elseif sEvent == "mouse_click" then
local button, x, y = tEventData[1], tEventData[2], tEventData[3]
lastClicked = nil
if y == 1 then
-- Switch process
local w, h = parentTerm.getSize()
if x == 1 then
multishell.setFocus(overviewTab.tabId)
elseif x == w then
if currentTab then
multishell.terminate(currentTab.tabId)
end
else
for _,tab in pairs(tabs) do
if not tab.hidden and tab.sx then
if x >= tab.sx and x <= tab.ex then
multishell.setFocus(tab.tabId)
break
end
end
end
end end
elseif currentTab then
-- Passthrough to current process
lastClicked = currentTab
resumeTab(currentTab, sEvent, { button, x, y-1 })
end
elseif sEvent == "mouse_up" then
if currentTab and lastClicked == currentTab then
local button, x, y = tEventData[1], tEventData[2], tEventData[3]
resumeTab(currentTab, sEvent, { button, x, y-1 })
end
elseif sEvent == "mouse_drag" or sEvent == "mouse_scroll" then
-- Other mouse event
local p1, x, y = tEventData[1], tEventData[2], tEventData[3]
if currentTab and (y ~= 1) then
if currentTab.terminal.scrollUp then
if p1 == -1 then
currentTab.terminal.scrollUp()
else
currentTab.terminal.scrollDown()
end
else
-- Passthrough to current process
resumeTab(currentTab, sEvent, { p1, x, y-1 })
end
end
else
-- Other event
-- Passthrough to all processes
local keys = Util.keys(tabs)
for _,key in pairs(keys) do
resumeTab(tabs[key], sEvent, tEventData)
end end
end end
end end

View File

@@ -1,4 +1,4 @@
_G.requireInjector() requireInjector(getfenv(1))
local Security = require('security') local Security = require('security')
local SHA1 = require('sha1') local SHA1 = require('sha1')

View File

@@ -1,32 +1,28 @@
local parentShell = _ENV.shell local parentShell = shell
_ENV.shell = { } shell = { }
_ENV.multishell = _ENV.multishell or { } multishell = multishell or { }
local fs = _G.fs
local shell = _ENV.shell
local multishell = _ENV.multishell
local sandboxEnv = setmetatable({ }, { __index = _G }) local sandboxEnv = setmetatable({ }, { __index = _G })
for k,v in pairs(_ENV) do for k,v in pairs(getfenv(1)) do
sandboxEnv[k] = v sandboxEnv[k] = v
end end
sandboxEnv.shell = shell sandboxEnv.shell = shell
sandboxEnv.multishell = multishell sandboxEnv.multishell = multishell
_G.requireInjector() requireInjector(getfenv(1))
local Util = require('util') local Util = require('util')
local DIR = (parentShell and parentShell.dir()) or "" local DIR = (parentShell and parentShell.dir()) or ""
local PATH = (parentShell and parentShell.path()) or ".:/rom/programs" local PATH = (parentShell and parentShell.path()) or ".:/rom/programs"
local tAliases = (parentShell and parentShell.aliases()) or {} local ALIASES = (parentShell and parentShell.aliases()) or {}
local tCompletionInfo = (parentShell and parentShell.getCompletionInfo()) or {} local tCompletionInfo = (parentShell and parentShell.getCompletionInfo()) or {}
local bExit = false local bExit = false
local tProgramStack = {} local tProgramStack = {}
local function tokenise( ... ) local function parseCommandLine( ... )
local sLine = table.concat( { ... }, " " ) local sLine = table.concat( { ... }, " " )
local tWords = {} local tWords = {}
local bQuoted = false local bQuoted = false
@@ -41,61 +37,45 @@ local function tokenise( ... )
bQuoted = not bQuoted bQuoted = not bQuoted
end end
return tWords return table.remove(tWords, 1), tWords
end
local function run(env, ...)
local args = tokenise(...)
local command = table.remove(args, 1) or error('No such program')
local isUrl = not not command:match("^(https?:)")
local path, loadFn
if isUrl then
path = command
loadFn = Util.loadUrl
else
path = shell.resolveProgram(command) or error('No such program')
loadFn = loadfile
end
local fn, err = loadFn(path, env)
if not fn then
error(err)
end
if multishell and multishell.setTitle then
multishell.setTitle(multishell.getCurrent(), fs.getName(path))
end
if isUrl then
tProgramStack[#tProgramStack + 1] = path:match("^https?://([^/:]+:?[0-9]*/?.*)$")
else
tProgramStack[#tProgramStack + 1] = path
end
local r = { fn(table.unpack(args)) }
tProgramStack[#tProgramStack] = nil
return table.unpack(r)
end end
-- Install shell API -- Install shell API
function shell.run(...) function shell.run(...)
local oldTitle
if multishell and multishell.getTitle then local path, args = parseCommandLine(...)
oldTitle = multishell.getTitle(multishell.getCurrent()) local isUrl = not not path:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$")
if not isUrl then
path = shell.resolveProgram(path)
end end
local env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G }) if path then
local r = { pcall(run, env, ...) } tProgramStack[#tProgramStack + 1] = path
local oldTitle
if multishell and multishell.setTitle then if multishell and multishell.getTitle then
multishell.setTitle(multishell.getCurrent(), oldTitle or 'shell') oldTitle = multishell.getTitle(multishell.getCurrent())
multishell.setTitle(multishell.getCurrent(), fs.getName(path))
end
local result, err
local env = Util.shallowCopy(sandboxEnv)
if isUrl then
result, err = Util.runUrl(env, path, unpack(args))
else
result, err = Util.run(env, path, unpack(args))
end
tProgramStack[#tProgramStack] = nil
if multishell and multishell.getTitle then
multishell.setTitle(multishell.getCurrent(), oldTitle or 'shell')
end
return result, err
end end
return false, 'No such program'
return table.unpack(r)
end end
function shell.exit() function shell.exit()
@@ -117,8 +97,9 @@ function shell.resolve( _sPath )
end end
function shell.resolveProgram( _sCommand ) function shell.resolveProgram( _sCommand )
if tAliases[_sCommand] ~= nil then
_sCommand = tAliases[_sCommand] if ALIASES[ _sCommand ] ~= nil then
_sCommand = ALIASES[ _sCommand ]
end end
local path = shell.resolve(_sCommand) local path = shell.resolve(_sCommand)
@@ -138,7 +119,7 @@ function shell.resolveProgram( _sCommand )
end end
return nil return nil
end end
-- Otherwise, look on the path variable -- Otherwise, look on the path variable
for sPath in string.gmatch(PATH or '', "[^:]+") do for sPath in string.gmatch(PATH or '', "[^:]+") do
sPath = fs.combine(sPath, _sCommand ) sPath = fs.combine(sPath, _sCommand )
@@ -149,19 +130,20 @@ function shell.resolveProgram( _sCommand )
return sPath .. '.lua' return sPath .. '.lua'
end end
end end
-- Not found -- Not found
return nil return nil
end end
function shell.programs( _bIncludeHidden ) function shell.programs( _bIncludeHidden )
local tItems = {} local tItems = {}
-- Add programs from the path -- Add programs from the path
for sPath in string.gmatch(PATH, "[^:]+") do for sPath in string.gmatch(PATH, "[^:]+") do
sPath = shell.resolve(sPath) sPath = shell.resolve(sPath)
if fs.isDir( sPath ) then if fs.isDir( sPath ) then
local tList = fs.list( sPath ) local tList = fs.list( sPath )
for _,sFile in pairs( tList ) do for n,sFile in pairs( tList ) do
if not fs.isDir( fs.combine( sPath, sFile ) ) and if not fs.isDir( fs.combine( sPath, sFile ) ) and
(_bIncludeHidden or string.sub( sFile, 1, 1 ) ~= ".") then (_bIncludeHidden or string.sub( sFile, 1, 1 ) ~= ".") then
tItems[ sFile ] = true tItems[ sFile ] = true
@@ -172,96 +154,15 @@ function shell.programs( _bIncludeHidden )
-- Sort and return -- Sort and return
local tItemList = {} local tItemList = {}
for sItem in pairs( tItems ) do for sItem, b in pairs( tItems ) do
table.insert( tItemList, sItem ) table.insert( tItemList, sItem )
end end
table.sort( tItemList ) table.sort( tItemList )
return tItemList return tItemList
end end
local function completeProgram( sLine ) function shell.complete(sLine) end
if #sLine > 0 and string.sub( sLine, 1, 1 ) == "/" then function shell.completeProgram(sProgram) end
-- Add programs from the root
return fs.complete( sLine, "", true, false )
else
local tResults = {}
local tSeen = {}
-- Add aliases
for sAlias in pairs( tAliases ) do
if #sAlias > #sLine and string.sub( sAlias, 1, #sLine ) == sLine then
local sResult = string.sub( sAlias, #sLine + 1 )
if not tSeen[ sResult ] then
table.insert( tResults, sResult )
tSeen[ sResult ] = true
end
end
end
-- Add programs from the path
local tPrograms = shell.programs()
for n=1,#tPrograms do
local sProgram = tPrograms[n]
if #sProgram > #sLine and string.sub( sProgram, 1, #sLine ) == sLine then
local sResult = string.sub( sProgram, #sLine + 1 )
if not tSeen[ sResult ] then
table.insert( tResults, sResult )
tSeen[ sResult ] = true
end
end
end
-- Sort and return
table.sort( tResults )
return tResults
end
end
local function completeProgramArgument( sProgram, nArgument, sPart, tPreviousParts )
local tInfo = tCompletionInfo[ sProgram ]
if tInfo then
return tInfo.fnComplete( shell, nArgument, sPart, tPreviousParts )
end
return nil
end
function shell.complete(sLine)
if #sLine > 0 then
local tWords = tokenise( sLine )
local nIndex = #tWords
if string.sub( sLine, #sLine, #sLine ) == " " then
nIndex = nIndex + 1
end
if nIndex == 1 then
local sBit = tWords[1] or ""
local sPath = shell.resolveProgram( sBit )
if tCompletionInfo[ sPath ] then
return { " " }
else
local tResults = completeProgram( sBit )
for n=1,#tResults do
local sResult = tResults[n]
local cPath = shell.resolveProgram( sBit .. sResult )
if tCompletionInfo[ cPath ] then
tResults[n] = sResult .. " "
end
end
return tResults
end
elseif nIndex > 1 then
local sPath = shell.resolveProgram( tWords[1] )
local sPart = tWords[nIndex] or ""
local tPreviousParts = tWords
tPreviousParts[nIndex] = nil
return completeProgramArgument( sPath , nIndex - 1, sPart, tPreviousParts )
end
end
end
function shell.completeProgram( sProgram )
return completeProgram( sProgram )
end
function shell.setCompletionFunction(sProgram, fnComplete) function shell.setCompletionFunction(sProgram, fnComplete)
tCompletionInfo[sProgram] = { fnComplete = fnComplete } tCompletionInfo[sProgram] = { fnComplete = fnComplete }
@@ -276,30 +177,29 @@ function shell.getRunningProgram()
end end
function shell.setAlias( _sCommand, _sProgram ) function shell.setAlias( _sCommand, _sProgram )
tAliases[_sCommand] = _sProgram ALIASES[ _sCommand ] = _sProgram
end end
function shell.clearAlias( _sCommand ) function shell.clearAlias( _sCommand )
tAliases[_sCommand] = nil ALIASES[ _sCommand ] = nil
end end
function shell.aliases() function shell.aliases()
local tCopy = {} local tCopy = {}
for sAlias, sCommand in pairs(tAliases) do for sAlias, sCommand in pairs(ALIASES) do
tCopy[sAlias] = sCommand tCopy[sAlias] = sCommand
end end
return tCopy return tCopy
end end
function shell.newTab(tabInfo, ...) function shell.newTab(tabInfo, ...)
local args = tokenise(...) local path, args = parseCommandLine(...)
local path = table.remove(args, 1)
path = shell.resolveProgram(path) path = shell.resolveProgram(path)
if path then if path then
tabInfo.path = path tabInfo.path = path
tabInfo.env = sandboxEnv tabInfo.env = sandboxEnv
tabInfo.args = args tabInfo.args = Util.shallowCopy(args)
tabInfo.title = fs.getName(path) tabInfo.title = fs.getName(path)
if path ~= 'sys/apps/shell' then if path ~= 'sys/apps/shell' then
@@ -329,19 +229,40 @@ end
local tArgs = { ... } local tArgs = { ... }
if #tArgs > 0 then if #tArgs > 0 then
local env = setmetatable(Util.shallowCopy(sandboxEnv), { __index = _G })
return run(env, ...) local path, args = parseCommandLine(...)
if not path then
error('No such program')
end
local isUrl = not not path:match("^(https?:)//(([^/:]+):?([0-9]*))(/?.*)$")
if not isUrl then
path = shell.resolveProgram(path)
if not path then
error('No such program')
end
end
local fn, err
if isUrl then
fn, err = Util.loadUrl(path, getfenv(1))
else
fn, err = loadfile(path, getfenv(1))
end
if not fn then
error(err)
end
tProgramStack[#tProgramStack + 1] = path
return fn(table.unpack(args))
end end
local Config = require('config') local Config = require('config')
local History = require('history') local History = require('history')
local colors = _G.colors
local keys = _G.keys
local os = _G.os
local term = _G.term
local textutils = _G.textutils
local config = { local config = {
standard = { standard = {
textColor = colors.white, textColor = colors.white,
@@ -364,37 +285,96 @@ local config = {
displayDirectory = true, displayDirectory = true,
} }
Config.load('shellprompt', config) --Config.load('shell', config)
local _colors = config.standard local _colors = config.standard
if term.isColor() then if term.isColor() then
_colors = config.color _colors = config.color
end end
local function autocompleteArgument(program, words) local function autocompleteFile(results, words)
local function getBaseDir(path)
if #path > 1 then
if path:sub(-1) ~= '/' then
path = fs.getDir(path)
end
end
if path:sub(1, 1) == '/' then
path = fs.combine(path, '')
else
path = fs.combine(shell.dir(), path)
end
while not fs.isDir(path) do
path = fs.getDir(path)
end
return path
end
local function getRawPath(path)
local baseDir = ''
if path:sub(1, 1) ~= '/' then
baseDir = shell.dir()
end
if #path > 1 then
if path:sub(-1) ~= '/' then
path = fs.getDir(path)
end
end
if fs.isDir(fs.combine(baseDir, path)) then
return path
end
return fs.getDir(path)
end
local match = words[#words] or ''
local startDir = getBaseDir(match)
local rawPath = getRawPath(match)
if fs.isDir(startDir) then
local files = fs.list(startDir)
for _,f in pairs(files) do
local path = fs.combine(rawPath, f)
if fs.isDir(fs.combine(startDir, f)) then
results[path .. '/'] = 'directory'
else
results[path .. ' '] = 'program'
end
end
end
end
local function autocompleteProgram(results, words)
if #words == 1 then
local files = shell.programs(true)
for _,f in ipairs(files) do
results[f .. ' '] = 'program'
end
for f in pairs(ALIASES) do
results[f .. ' '] = 'program'
end
end
end
local function autocompleteArgument(results, program, words)
local word = '' local word = ''
if #words > 1 then if #words > 1 then
word = words[#words] word = words[#words]
end end
local tInfo = tCompletionInfo[program] local tInfo = tCompletionInfo[program]
return tInfo.fnComplete(shell, #words - 1, word, words) local args = tInfo.fnComplete(shell, #words - 1, word, words)
if args then
Util.filterInplace(args, function(f)
return not Util.key(args, f .. '/')
end)
for _,arg in ipairs(args) do
results[word .. arg] = 'argument'
end
end
end end
local function autocompleteAnything(line, words) local function autocomplete(line, suggestions)
local results = shell.complete(line)
if results and #results == 0 and #words == 1 then
results = nil
end
if not results then
results = fs.complete(words[#words] or '', shell.dir(), true, false)
end
return results
end
local function autocomplete(line)
local words = { } local words = { }
for word in line:gmatch("%S+") do for word in line:gmatch("%S+") do
table.insert(words, word) table.insert(words, word)
@@ -402,68 +382,39 @@ local function autocomplete(line)
if line:match(' $') then if line:match(' $') then
table.insert(words, '') table.insert(words, '')
end end
local results = { }
if #words == 0 then if #words == 0 then
words = { '' } files = autocompleteFile(results, words)
end
local results
local program = shell.resolveProgram(words[1])
if tCompletionInfo[program] then
results = autocompleteArgument(program, words) or { }
else else
results = autocompleteAnything(line, words) or { } local program = shell.resolveProgram(words[1])
if tCompletionInfo[program] then
autocompleteArgument(results, program, words)
else
autocompleteProgram(results, words)
autocompleteFile(results, words)
end
end end
Util.filterInplace(results, function(f) local match = words[#words] or ''
return not Util.key(results, f .. '/') local files = { }
end) for f in pairs(results) do
local w = words[#words] or '' if f:sub(1, #match) == match then
for k,arg in pairs(results) do table.insert(files, f)
results[k] = w .. arg end
end end
if #results == 1 then if #files == 1 then
words[#words] = results[1] words[#words] = files[1]
return table.concat(words, ' ') return table.concat(words, ' ')
elseif #results > 1 then elseif #files > 1 and suggestions then
local function someComplete()
-- ugly (complete as much as possible)
local word = words[#words] or ''
local i = #word + 1
while true do
local ch
for _,f in ipairs(results) do
if #f < i then
words[#words] = string.sub(f, 1, i - 1)
return table.concat(words, ' ')
end
if not ch then
ch = string.sub(f, i, i)
elseif string.sub(f, i, i) ~= ch then
if i == #word + 1 then
return
end
words[#words] = string.sub(f, 1, i - 1)
return table.concat(words, ' ')
end
end
i = i + 1
end
end
local t = someComplete()
if t then
return t
end
print() print()
local word = words[#words] or '' local word = words[#words] or ''
local prefix = word:match("(.*/)") or '' local prefix = word:match("(.*/)") or ''
if #prefix > 0 then if #prefix > 0 then
for _,f in ipairs(results) do for _,f in ipairs(files) do
if f:match("^" .. prefix) ~= prefix then if f:match("^" .. prefix) ~= prefix then
prefix = '' prefix = ''
break break
@@ -472,8 +423,8 @@ local function autocomplete(line)
end end
local tDirs, tFiles = { }, { } local tDirs, tFiles = { }, { }
for _,f in ipairs(results) do for _,f in ipairs(files) do
if fs.isDir(shell.resolve(f)) then if results[f] == 'directory' then
f = f:gsub(prefix, '', 1) f = f:gsub(prefix, '', 1)
table.insert(tDirs, f) table.insert(tDirs, f)
else else
@@ -485,14 +436,14 @@ local function autocomplete(line)
table.sort(tFiles) table.sort(tFiles)
if #tDirs > 0 and #tDirs < #tFiles then if #tDirs > 0 and #tDirs < #tFiles then
local tw = term.getSize() local w = term.getSize()
local nMaxLen = tw / 8 local nMaxLen = w / 8
for _,sItem in pairs(results) do for n, sItem in pairs(files) do
nMaxLen = math.max(string.len(sItem) + 1, nMaxLen) nMaxLen = math.max(string.len(sItem) + 1, nMaxLen)
end end
local nCols = math.floor(w / nMaxLen) local nCols = math.floor(w / nMaxLen)
if #tDirs < nCols then if #tDirs < nCols then
for _ = #tDirs + 1, nCols do for i = #tDirs + 1, nCols do
table.insert(tDirs, '') table.insert(tDirs, '')
end end
end end
@@ -506,11 +457,35 @@ local function autocomplete(line)
term.setTextColour(_colors.promptTextColor) term.setTextColour(_colors.promptTextColor)
term.setBackgroundColor(_colors.promptBackgroundColor) term.setBackgroundColor(_colors.promptBackgroundColor)
term.write("$ " ) write("$ " )
term.setTextColour(_colors.commandTextColor) term.setTextColour(_colors.commandTextColor)
term.setBackgroundColor(colors.black) term.setBackgroundColor(colors.black)
return line return line
elseif #files > 1 then
-- ugly (complete as much as possible)
local word = words[#words] or ''
local i = #word + 1
while true do
local ch
for _,f in ipairs(files) do
if #f < i then
words[#words] = string.sub(f, 1, i - 1)
return table.concat(words, ' ')
end
if not ch then
ch = string.sub(f, i, i)
elseif string.sub(f, i, i) ~= ch then
if i == #word + 1 then
return
end
words[#words] = string.sub(f, 1, i - 1)
return table.concat(words, ' ')
end
end
i = i + 1
end
end end
end end
@@ -519,6 +494,7 @@ local function shellRead(history)
local sLine = "" local sLine = ""
local nPos = 0 local nPos = 0
local lastPattern
local w = term.getSize() local w = term.getSize()
local sx = term.getCursorPos() local sx = term.getCursorPos()
@@ -531,7 +507,7 @@ local function shellRead(history)
nScroll = (sx + nPos) - w nScroll = (sx + nPos) - w
end end
local _,cy = term.getCursorPos() local cx,cy = term.getCursorPos()
term.setCursorPos( sx, cy ) term.setCursorPos( sx, cy )
if sReplace then if sReplace then
term.write( string.rep( sReplace, math.max( string.len(sLine) - nScroll, 0 ) ) ) term.write( string.rep( sReplace, math.max( string.len(sLine) - nScroll, 0 ) ) )
@@ -542,7 +518,7 @@ local function shellRead(history)
end end
while true do while true do
local sEvent, param = os.pullEventRaw() local sEvent, param, param2 = os.pullEventRaw()
if sEvent == "char" then if sEvent == "char" then
sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 ) sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 )
@@ -566,7 +542,10 @@ local function shellRead(history)
break break
elseif param == keys.tab then elseif param == keys.tab then
if nPos == #sLine then if nPos == #sLine then
local cline = autocomplete(sLine) local showSuggestions = lastPattern == sLine
lastPattern = sLine
local cline = autocomplete(sLine, showSuggestions)
if cline then if cline then
sLine = cline sLine = cline
nPos = #sLine nPos = #sLine
@@ -602,7 +581,7 @@ local function shellRead(history)
if nPos > 0 then if nPos > 0 then
redraw(" ") redraw(" ")
sLine = string.sub( sLine, 1, nPos - 1 ) .. string.sub( sLine, nPos + 1 ) sLine = string.sub( sLine, 1, nPos - 1 ) .. string.sub( sLine, nPos + 1 )
nPos = nPos - 1 nPos = nPos - 1
redraw() redraw()
end end
elseif param == keys.home then elseif param == keys.home then
@@ -626,7 +605,7 @@ local function shellRead(history)
end end
end end
local _, cy = term.getCursorPos() local cx, cy = term.getCursorPos()
term.setCursorPos( w + 1, cy ) term.setCursorPos( w + 1, cy )
print() print()
term.setCursorBlink( false ) term.setCursorBlink( false )
@@ -643,7 +622,7 @@ while not bExit do
end end
term.setTextColour(_colors.promptTextColor) term.setTextColour(_colors.promptTextColor)
term.setBackgroundColor(_colors.promptBackgroundColor) term.setBackgroundColor(_colors.promptBackgroundColor)
term.write("$ " ) write("$ " )
term.setTextColour(_colors.commandTextColor) term.setTextColour(_colors.commandTextColor)
term.setBackgroundColor(colors.black) term.setBackgroundColor(colors.black)
local sLine = shellRead(history) local sLine = shellRead(history)
@@ -656,9 +635,9 @@ while not bExit do
end end
term.setTextColour(_colors.textColor) term.setTextColour(_colors.textColor)
if #sLine > 0 then if #sLine > 0 then
local result, err = shell.run(sLine) local result, err = shell.run( sLine )
if not result and err then if not result and err then
_G.printError(err) printError(err)
end end
end end
end end

View File

@@ -1,14 +1,10 @@
_G.requireInjector() requireInjector(getfenv(1))
local Event = require('event') local Event = require('event')
local Socket = require('socket') local Socket = require('socket')
local Terminal = require('terminal') local Terminal = require('terminal')
local Util = require('util') local Util = require('util')
local os = _G.os
local read = _G.read
local term = _G.term
local remoteId local remoteId
local args = { ... } local args = { ... }
if #args == 1 then if #args == 1 then
@@ -23,10 +19,10 @@ if not remoteId then
end end
print('connecting...') print('connecting...')
local socket, msg = Socket.connect(remoteId, 23) local socket = Socket.connect(remoteId, 23)
if not socket then if not socket then
error(msg) error('Unable to connect to ' .. remoteId .. ' on port 23')
end end
local ct = Util.shallowCopy(term.current()) local ct = Util.shallowCopy(term.current())

View File

@@ -1,4 +1,4 @@
_G.requireInjector() requireInjector(getfenv(1))
local Crypto = require('crypto') local Crypto = require('crypto')
local Security = require('security') local Security = require('security')
@@ -6,8 +6,6 @@ local SHA1 = require('sha1')
local Socket = require('socket') local Socket = require('socket')
local Terminal = require('terminal') local Terminal = require('terminal')
local os = _G.os
local remoteId local remoteId
local args = { ... } local args = { ... }
@@ -15,7 +13,7 @@ if #args == 1 then
remoteId = tonumber(args[1]) remoteId = tonumber(args[1])
else else
print('Enter host ID') print('Enter host ID')
remoteId = tonumber(_G.read()) remoteId = tonumber(read())
end end
if not remoteId then if not remoteId then
@@ -29,15 +27,16 @@ if not password then
end end
print('connecting...') print('connecting...')
local socket, msg = Socket.connect(remoteId, 19) local socket = Socket.connect(remoteId, 19)
if not socket then if not socket then
error(msg) error('Unable to connect to ' .. remoteId .. ' on port 19')
end end
local publicKey = Security.getPublicKey() local publicKey = Security.getPublicKey()
local password = SHA1.sha1(password)
socket:write(Crypto.encrypt({ pk = publicKey, dh = os.getComputerID() }, SHA1.sha1(password))) socket:write(Crypto.encrypt({ pk = publicKey, dh = os.getComputerID() }, password))
local data = socket:read(2) local data = socket:read(2)
socket:close() socket:close()

View File

@@ -1,21 +1,17 @@
_G.requireInjector() requireInjector(getfenv(1))
local Event = require('event') local Event = require('event')
local Socket = require('socket') local Socket = require('socket')
local Terminal = require('terminal') local Terminal = require('terminal')
local Util = require('util') local Util = require('util')
local colors = _G.colors
local multishell = _ENV.multishell
local term = _G.term
local remoteId local remoteId
local args = { ... } local args = { ... }
if #args == 1 then if #args == 1 then
remoteId = tonumber(args[1]) remoteId = tonumber(args[1])
else else
print('Enter host ID') print('Enter host ID')
remoteId = tonumber(_G.read()) remoteId = tonumber(read())
end end
if not remoteId then if not remoteId then
@@ -25,10 +21,10 @@ end
multishell.setTitle(multishell.getCurrent(), 'VNC-' .. remoteId) multishell.setTitle(multishell.getCurrent(), 'VNC-' .. remoteId)
print('connecting...') print('connecting...')
local socket, msg = Socket.connect(remoteId, 5900) local socket = Socket.connect(remoteId, 5900)
if not socket then if not socket then
error(msg) error('Unable to connect to ' .. remoteId .. ' on port 5900')
end end
local function writeTermInfo() local function writeTermInfo()
@@ -77,7 +73,7 @@ while true do
print() print()
print('Connection lost') print('Connection lost')
print('Press enter to exit') print('Press enter to exit')
_G.read() read()
break break
end end

View File

@@ -1,21 +0,0 @@
_G.requireInjector()
local Util = require('util')
local multishell = _ENV.multishell
local textutils = _G.textutils
local data
multishell.hook('clipboard_copy', function(_, args)
data = args[1]
end)
multishell.addHotkey('shift-paste', function(_, args)
if type(data) == 'table' then
local s, m = pcall(textutils.serialize, data)
data = (s and m) or Util.tostring(data)
end
-- replace the event paste data with our internal data
args[1] = Util.tostring(data or '')
end)

View File

@@ -1,56 +0,0 @@
_G.requireInjector()
local Util = require('util')
local multishell = _ENV.multishell
-- overview
multishell.addHotkey('control-o', function()
for _,tab in pairs(multishell.getTabs()) do
if tab.isOverview then
multishell.setFocus(tab.tabId)
end
end
end)
-- restart tab
multishell.addHotkey('control-backspace', function()
local tabs = multishell.getTabs()
local tabId = multishell.getFocus()
local tab = tabs[tabId]
if not tab.isOverview then
multishell.terminate(tabId)
tab = Util.shallowCopy(tab)
tab.isDead = false
tab.focused = true
multishell.openTab(tab)
end
end)
-- next tab
multishell.addHotkey('control-tab', function()
local tabs = multishell.getTabs()
local visibleTabs = { }
local currentTabId = multishell.getFocus()
local function compareTab(a, b)
return a.tabId < b.tabId
end
for _,tab in Util.spairs(tabs, compareTab) do
if not tab.hidden then
table.insert(visibleTabs, tab)
end
end
for k,tab in ipairs(visibleTabs) do
if tab.tabId == currentTabId then
if k < #visibleTabs then
multishell.setFocus(visibleTabs[k + 1].tabId)
return
end
end
end
if #visibleTabs > 0 then
multishell.setFocus(visibleTabs[1].tabId)
end
end)

View File

@@ -1,22 +1,18 @@
-- Loads the Opus environment regardless if the file system is local or not -- Loads the Opus environment regardless if the file system is local or not
local colors = _G.colors
local fs = _G.fs
local http = _G.http
local shell = _ENV.shell
local term = _G.term
local w, h = term.getSize() local w, h = term.getSize()
local str = 'Loading Opus...'
term.setTextColor(colors.white) term.setTextColor(colors.white)
if term.isColor() then if term.isColor() then
term.setBackgroundColor(colors.black) term.setBackgroundColor(colors.cyan)
term.clear() term.clear()
local opus = { local opus = {
'fffff00', '9999900',
'ffff07000', '999907000',
'ff00770b00 4444', '9900770b00 4444',
'ff077777444444444', '99077777444444444',
'f07777744444444444', '907777744444444444',
'f0000777444444444', '90000777444444444',
'070000111744444', '070000111744444',
'777770000', '777770000',
'7777000000', '7777000000',
@@ -29,22 +25,17 @@ if term.isColor() then
end end
end end
term.setCursorPos((w - 18) / 2, h) term.setCursorPos((w - #str) / 2, h)
term.write('Loading Opus...') term.write(str)
term.setCursorPos(w, h) term.setCursorPos(w, h)
local GIT_REPO = 'kepler155c/opus/develop' local GIT_REPO = 'kepler155c/opus/master'
local BASE = 'https://raw.githubusercontent.com/' .. GIT_REPO local BASE = 'https://raw.githubusercontent.com/' .. GIT_REPO
local sandboxEnv = setmetatable({ }, { __index = _G })
for k,v in pairs(_ENV) do
sandboxEnv[k] = v
end
local function makeEnv() local function makeEnv()
local env = setmetatable({ }, { __index = _G }) local env = setmetatable({ }, { __index = _G })
for k,v in pairs(sandboxEnv) do for k,v in pairs(getfenv(1)) do
env[k] = v env[k] = v
end end
return env return env
end end
@@ -61,10 +52,10 @@ end
local function runUrl(file, ...) local function runUrl(file, ...)
local url = BASE .. '/' .. file local url = BASE .. '/' .. file
local u = http.get(url) local h = http.get(url)
if u then if h then
local fn = load(u.readAll(), url, nil, makeEnv()) local fn, m = load(h.readAll(), url, nil, makeEnv())
u.close() h.close()
if fn then if fn then
return fn(...) return fn(...)
end end
@@ -95,8 +86,9 @@ end
if not fs.exists('usr/autorun') then if not fs.exists('usr/autorun') then
fs.makeDir('usr/autorun') fs.makeDir('usr/autorun')
end end
if not fs.exists('usr/etc/fstab') then if not fs.exists('usr/etc/fstab') or not fs.exists('usr/etc/fstab.ignore') then
Util.writeFile('usr/etc/fstab', 'usr gitfs kepler155c/opus-apps/develop') Util.writeFile('usr/etc/fstab', 'usr gitfs kepler155c/opus-apps/master')
Util.writeFile('usr/etc/fstab.ignore', 'forced fstab overwrite')
end end
if not fs.exists('usr/config/shell') then if not fs.exists('usr/config/shell') then
Util.writeTable('usr/config/shell', { Util.writeTable('usr/config/shell', {
@@ -117,7 +109,7 @@ if config.aliases then
end end
end end
shell.setPath(config.path) shell.setPath(config.path)
sandboxEnv.LUA_PATH = config.lua_path LUA_PATH = config.lua_path
-- extensions -- extensions
local dir = 'sys/extensions' local dir = 'sys/extensions'

View File

@@ -1,15 +1,37 @@
local pullEvent = os.pullEventRaw local pullEvent = os.pullEventRaw
local redirect = term.redirect
local current = term.current
local shutdown = os.shutdown local shutdown = os.shutdown
os.pullEventRaw = function() local cos = { }
error('die')
os.pullEventRaw = function(...)
local co = coroutine.running()
if not cos[co] then
cos[co] = true
error('die')
end
return pullEvent(...)
end end
os.shutdown = function() os.shutdown = function()
os.pullEventRaw = pullEvent end
os.shutdown = shutdown
os.run(getfenv(1), 'sys/boot/multishell.boot') term.current = function()
term.redirect = function()
os.pullEventRaw = pullEvent
os.shutdown = shutdown
term.current = current
term.redirect = redirect
term.redirect(term.native())
--for co in pairs(cos) do
-- print(tostring(co) .. ' ' .. coroutine.status(co))
--end
os.run(getfenv(1), 'sys/boot/multishell.boot')
os.run(getfenv(1), 'rom/programs/shell')
end
error('die')
end end
os.queueEvent('modem_message') os.queueEvent('modem_message')

View File

@@ -88,16 +88,6 @@
run = "simpleMiner.lua", run = "simpleMiner.lua",
requires = 'turtle', requires = 'turtle',
}, },
[ "131260cbfbb0c821f8eae5e7c3c296c7aa4d50b9" ] = {
title = "Music",
category = "Apps",
icon = "\030 \031f === \
\030 \031f | |\
\030 \031fo| o|\
",
run = "usr/apps/music.lua",
requires = 'turtle',
},
c47ae15370cfe1ed2781eedc1dc2547d12d9e972 = { c47ae15370cfe1ed2781eedc1dc2547d12d9e972 = {
title = "Help", title = "Help",
category = "Apps", category = "Apps",
@@ -232,7 +222,7 @@
[ "d8c298dd41e4a4ec20e8307901797b64688b3b77" ] = { [ "d8c298dd41e4a4ec20e8307901797b64688b3b77" ] = {
title = "GPS Deploy", title = "GPS Deploy",
category = "Apps", category = "Apps",
run = "http://pastebin.com/raw/VXAyXqBv", run = "http://pastebin.com/raw/qLthLak5",
requires = "turtle", requires = "turtle",
}, },
[ "53a5d150062b1e03206b9e15854b81060e3c7552" ] = { [ "53a5d150062b1e03206b9e15854b81060e3c7552" ] = {
@@ -243,21 +233,13 @@
\030f\03131\0308\031f \030f\03131\031e3", \030f\03131\0308\031f \030f\03131\031e3",
run = "https://pastebin.com/raw/nsKrHTbN", run = "https://pastebin.com/raw/nsKrHTbN",
}, },
[ "a2accffe95b2c8be30e8a05e0c6ab7e8f5966f43" ] = {
title = "Strafe",
category = "Games",
icon = "\0308\031f \0300 \0308 \
\0308\031f \0300 \030f \
\0300\031f \030f ",
run = "https://pastebin.com/raw/jyDH7mLH",
},
[ "48d6857f6b2869d031f463b13aa34df47e18c548" ] = { [ "48d6857f6b2869d031f463b13aa34df47e18c548" ] = {
title = "Breakout", title = "Breakout",
category = "Games", category = "Games",
icon = "\0301\031f \0309 \030c \030b \030e \030c \0306 \ icon = "\0301\031f \0309 \030c \030b \030e \030c \0306 \
\030 \031f \ \030 \031f \
\030 \031f \0300 \0310 ", \030 \031f \0300 \0310 ",
run = "https://gist.github.com/LDDestroier/c7528d95bc0103545c2a/raw", run = "https://pastebin.com/raw/LTRYaSKt",
}, },
[ "8d59207c8a84153b3e9f035cc3b6ec7a23671323" ] = { [ "8d59207c8a84153b3e9f035cc3b6ec7a23671323" ] = {
title = "Micropaint", title = "Micropaint",

View File

@@ -1,12 +1,12 @@
{ {
ScrollBar = { ScrollingGrid = {
lineChar = '|', lineChar = '|',
sliderChar = '\127', sliderChar = '\127',
upArrowChar = '\30', upArrowChar = '\30',
downArrowChar = '\31', downArrowChar = '\31',
}, },
Button = { Button = {
--focusIndicator = '\183', focusIndicator = '\183',
}, },
Grid = { Grid = {
focusIndicator = '\183', focusIndicator = '\183',

View File

@@ -0,0 +1,43 @@
if _G.clipboard then
return
end
requireInjector(getfenv(1))
local Util = require('util')
_G.clipboard = { internal, data }
function clipboard.getData()
return clipboard.data
end
function clipboard.setData(data)
clipboard.data = data
if data then
clipboard.useInternal(true)
end
end
function clipboard.getText()
if clipboard.data then
return Util.tostring(clipboard.data)
end
end
function clipboard.isInternal()
return clipboard.internal
end
function clipboard.useInternal(mode)
if mode ~= clipboard.internal then
clipboard.internal = mode
os.queueEvent('clipboard_mode', mode)
end
end
if multishell and multishell.addHotkey then
multishell.addHotkey(20, function()
clipboard.useInternal(not clipboard.isInternal())
end)
end

View File

@@ -1,11 +1,13 @@
_G.requireInjector() if _G.device then
return
end
requireInjector(getfenv(1))
local Peripheral = require('peripheral') local Peripheral = require('peripheral')
_G.device = Peripheral.getList() _G.device = { }
-- register the main term in the devices list for _,side in pairs(peripheral.getNames()) do
_G.device.terminal = _G.term.current() Peripheral.addDevice(device, side)
_G.device.terminal.side = 'terminal' end
_G.device.terminal.type = 'terminal'
_G.device.terminal.name = 'terminal'

58
sys/extensions/tgps.lua Normal file
View File

@@ -0,0 +1,58 @@
if not turtle or turtle.enableGPS then
return
end
requireInjector(getfenv(1))
local GPS = require('gps')
local Config = require('config')
function turtle.enableGPS(timeout)
if turtle.point.gps then
return turtle.point
end
local pt = GPS.getPointAndHeading(timeout)
if pt then
turtle.setPoint(pt, true)
return turtle.point
end
end
function turtle.gotoGPSHome()
local config = { }
Config.load('gps', config)
if config.home then
if turtle.enableGPS() then
turtle.pathfind(config.home)
end
end
end
function turtle.setGPSHome()
local config = { }
Config.load('gps', config)
if turtle.point.gps then
config.home = turtle.point
Config.update('gps', config)
else
local pt = GPS.getPoint()
if pt then
local originalHeading = turtle.point.heading
local heading = GPS.getHeading()
if heading then
local turns = (turtle.point.heading - originalHeading) % 4
pt.heading = (heading - turns) % 4
config.home = pt
Config.update('gps', config)
pt = GPS.getPoint()
pt.heading = heading
turtle.setPoint(pt, true)
turtle.gotoPoint(config.home)
end
end
end
end

View File

@@ -1,31 +1,24 @@
if not _G.turtle then if not turtle or turtle.getPoint then
return return
end end
_G.requireInjector() requireInjector(getfenv(1))
local Pathing = require('turtle.pathfind')
local GPS = require('gps')
local Point = require('point') local Point = require('point')
local synchronized = require('sync') local synchronized = require('sync')
local Util = require('util') local Util = require('util')
local Pathing = require('turtle.pathfind')
local os = _G.os
local peripheral = _G.peripheral
local turtle = _G.turtle
local function noop() end local function noop() end
local headings = Point.headings
local state = { }
turtle.pathfind = Pathing.pathfind turtle.pathfind = Pathing.pathfind
turtle.point = { x = 0, y = 0, z = 0, heading = 0 } turtle.point = { x = 0, y = 0, z = 0, heading = 0 }
turtle.status = 'idle'
turtle.abort = false
local state = { }
function turtle.getPoint() return turtle.point end function turtle.getPoint() return turtle.point end
function turtle.getState() return state end function turtle.getState() return state end
function turtle.isAborted() return state.abort end
function turtle.getStatus() return state.status end
function turtle.setStatus(s) state.status = s end
local function _defaultMove(action) local function _defaultMove(action)
while not action.move() do while not action.move() do
@@ -48,13 +41,14 @@ function turtle.setPoint(pt, isGPS)
end end
function turtle.resetState() function turtle.resetState()
state.abort = false --turtle.abort = false -- should be part of state
state.status = 'idle' --turtle.status = 'idle' -- should be part of state
state.attackPolicy = noop state.attackPolicy = noop
state.digPolicy = noop state.digPolicy = noop
state.movePolicy = _defaultMove state.movePolicy = _defaultMove
state.moveCallback = noop state.moveCallback = noop
Pathing.reset() Pathing.reset()
return true return true
end end
@@ -62,10 +56,13 @@ function turtle.reset()
turtle.point.x = 0 turtle.point.x = 0
turtle.point.y = 0 turtle.point.y = 0
turtle.point.z = 0 turtle.point.z = 0
turtle.point.heading = 0 -- should be facing turtle.point.heading = 0
turtle.point.gps = false turtle.point.gps = false
turtle.abort = false -- should be part of state
--turtle.status = 'idle' -- should be part of state
turtle.resetState() turtle.resetState()
return true return true
end end
@@ -124,7 +121,31 @@ function turtle.getAction(direction)
return actions[direction] return actions[direction]
end end
-- [[ Heading data ]] --
local headings = {
[ 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' }
}
local namedHeadings = {
east = headings[0],
south = headings[1],
west = headings[2],
north = headings[3],
up = headings[4],
down = headings[5]
}
function turtle.getHeadings() return headings end
function turtle.getHeadingInfo(heading) function turtle.getHeadingInfo(heading)
if heading and type(heading) == 'string' then
return namedHeadings[heading]
end
heading = heading or turtle.point.heading heading = heading or turtle.point.heading
return headings[heading] return headings[heading]
end end
@@ -153,7 +174,6 @@ local function inventoryAction(fn, name, qty)
return s return s
end end
-- [[ Attack ]] --
local function _attack(action) local function _attack(action)
if action.attack() then if action.attack() then
repeat until not action.attack() repeat until not action.attack()
@@ -162,21 +182,10 @@ local function _attack(action)
return false return false
end end
turtle.attackPolicies = {
none = noop,
attack = function(action)
return _attack(action)
end,
}
function turtle.attack() return _attack(actions.forward) end function turtle.attack() return _attack(actions.forward) end
function turtle.attackUp() return _attack(actions.up) end function turtle.attackUp() return _attack(actions.up) end
function turtle.attackDown() return _attack(actions.down) end function turtle.attackDown() return _attack(actions.down) end
function turtle.setAttackPolicy(policy) state.attackPolicy = policy end
-- [[ Place ]] --
local function _place(action, indexOrId) local function _place(action, indexOrId)
local slot local slot
@@ -229,11 +238,25 @@ function turtle.refuel(qtyOrName, qty)
return inventoryAction(turtle.native.refuel, qtyOrName, qty or 64) return inventoryAction(turtle.native.refuel, qtyOrName, qty or 64)
end end
--[[
function turtle.dig() return state.dig(actions.forward) end
function turtle.digUp() return state.dig(actions.up) end
function turtle.digDown() return state.dig(actions.down) end
--]]
function turtle.isTurtleAtSide(side) function turtle.isTurtleAtSide(side)
local sideType = peripheral.getType(side) local sideType = peripheral.getType(side)
return sideType and sideType == 'turtle' return sideType and sideType == 'turtle'
end end
turtle.attackPolicies = {
none = noop,
attack = function(action)
return _attack(action)
end,
}
turtle.digPolicies = { turtle.digPolicies = {
none = noop, none = noop,
@@ -279,13 +302,13 @@ turtle.movePolicies = {
if action.side == 'back' then if action.side == 'back' then
return false return false
end end
local oldStatus = state.status local oldStatus = turtle.status
print('assured move: stuck') print('assured move: stuck')
state.status = 'stuck' turtle.status = 'stuck'
repeat repeat
os.sleep(1) os.sleep(1)
until _defaultMove(action) until _defaultMove(action)
state.status = oldStatus turtle.status = oldStatus
end end
return true return true
end, end,
@@ -328,6 +351,7 @@ function turtle.setPolicy(...)
end end
function turtle.setDigPolicy(policy) state.digPolicy = policy end function turtle.setDigPolicy(policy) state.digPolicy = policy end
function turtle.setAttackPolicy(policy) state.attackPolicy = policy end
function turtle.setMoveCallback(cb) state.moveCallback = cb end function turtle.setMoveCallback(cb) state.moveCallback = cb end
function turtle.clearMoveCallback() state.moveCallback = noop end function turtle.clearMoveCallback() state.moveCallback = noop end
function turtle.getMoveCallback() return state.moveCallback end function turtle.getMoveCallback() return state.moveCallback end
@@ -338,31 +362,35 @@ function turtle.getHeading()
end end
function turtle.turnRight() function turtle.turnRight()
turtle.setHeading((turtle.point.heading + 1) % 4) turtle.setHeading(turtle.point.heading + 1)
return turtle.point return turtle.point
end end
function turtle.turnLeft() function turtle.turnLeft()
turtle.setHeading((turtle.point.heading - 1) % 4) turtle.setHeading(turtle.point.heading - 1)
return turtle.point return turtle.point
end end
function turtle.turnAround() function turtle.turnAround()
turtle.setHeading((turtle.point.heading + 2) % 4) turtle.setHeading(turtle.point.heading + 2)
return turtle.point return turtle.point
end end
-- combine with setHeading
function turtle.setNamedHeading(headingName)
local headingInfo = namedHeadings[headingName]
if headingInfo then
return turtle.setHeading(headingInfo.heading)
end
return false, 'Invalid heading'
end
function turtle.setHeading(heading) function turtle.setHeading(heading)
if not heading then if not heading then
return false, 'Invalid heading' return
end end
local fi = Point.facings[heading] heading = heading % 4
if not fi then
return false, 'Invalid heading'
end
heading = fi.heading % 4
if heading ~= turtle.point.heading then if heading ~= turtle.point.heading then
while heading < turtle.point.heading do while heading < turtle.point.heading do
heading = heading + 4 heading = heading + 4
@@ -450,14 +478,15 @@ function turtle.back()
end end
end end
local function moveTowardsX(dx) function turtle.moveTowardsX(dx)
local direction = dx - turtle.point.x local direction = dx - turtle.point.x
local move local move
if direction == 0 then if direction == 0 then
return true return true
end end
if direction > 0 and turtle.point.heading == 0 or if direction > 0 and turtle.point.heading == 0 or
direction < 0 and turtle.point.heading == 2 then direction < 0 and turtle.point.heading == 2 then
move = turtle.forward move = turtle.forward
@@ -473,14 +502,15 @@ local function moveTowardsX(dx)
return true return true
end end
local function moveTowardsZ(dz) function turtle.moveTowardsZ(dz)
local direction = dz - turtle.point.z local direction = dz - turtle.point.z
local move local move
if direction == 0 then if direction == 0 then
return true return true
end end
if direction > 0 and turtle.point.heading == 1 or if direction > 0 and turtle.point.heading == 1 or
direction < 0 and turtle.point.heading == 3 then direction < 0 and turtle.point.heading == 3 then
move = turtle.forward move = turtle.forward
@@ -498,14 +528,13 @@ end
-- [[ go ]] -- -- [[ go ]] --
-- 1 turn goto (going backwards if possible) -- 1 turn goto (going backwards if possible)
function turtle.gotoSingleTurn(dx, dy, dz, dh) function turtle.gotoSingleTurn(dx, dz, dy, dh)
dx = dx or turtle.point.x
dy = dy or turtle.point.y dy = dy or turtle.point.y
dz = dz or turtle.point.z
local function gx() local function gx()
if turtle.point.x ~= dx then if turtle.point.x ~= dx then
moveTowardsX(dx) turtle.moveTowardsX(dx)
end end
if turtle.point.z ~= dz then if turtle.point.z ~= dz then
if dh and dh % 2 == 1 then if dh and dh % 2 == 1 then
@@ -518,7 +547,7 @@ function turtle.gotoSingleTurn(dx, dy, dz, dh)
local function gz() local function gz()
if turtle.point.z ~= dz then if turtle.point.z ~= dz then
moveTowardsZ(dz) turtle.moveTowardsZ(dz)
end end
if turtle.point.x ~= dx then if turtle.point.x ~= dx then
if dh and dh % 2 == 0 then if dh and dh % 2 == 0 then
@@ -558,16 +587,17 @@ function turtle.gotoSingleTurn(dx, dy, dz, dh)
return false return false
end end
local function gotoEx(dx, dy, dz) local function gotoEx(dx, dz, dy)
-- determine the heading to ensure the least amount of turns -- determine the heading to ensure the least amount of turns
-- first check is 1 turn needed - remaining require 2 turns -- first check is 1 turn needed - remaining require 2 turns
if turtle.point.heading == 0 and turtle.point.x <= dx or if turtle.point.heading == 0 and turtle.point.x <= dx or
turtle.point.heading == 2 and turtle.point.x >= dx or turtle.point.heading == 2 and turtle.point.x >= dx or
turtle.point.heading == 1 and turtle.point.z <= dz or turtle.point.heading == 1 and turtle.point.z <= dz or
turtle.point.heading == 3 and turtle.point.z >= dz then turtle.point.heading == 3 and turtle.point.z >= dz then
-- maintain current heading -- maintain current heading
-- nop -- nop
elseif dz > turtle.point.z and turtle.point.heading == 0 or elseif dz > turtle.point.z and turtle.point.heading == 0 or
dz < turtle.point.z and turtle.point.heading == 2 or dz < turtle.point.z and turtle.point.heading == 2 or
dx < turtle.point.x and turtle.point.heading == 1 or dx < turtle.point.x and turtle.point.heading == 1 or
dx > turtle.point.x and turtle.point.heading == 3 then dx > turtle.point.x and turtle.point.heading == 3 then
@@ -592,8 +622,9 @@ local function gotoEx(dx, dy, dz)
end end
-- fallback goto - will turn around if was previously moving backwards -- fallback goto - will turn around if was previously moving backwards
local function gotoMultiTurn(dx, dy, dz) local function gotoMultiTurn(dx, dz, dy)
if gotoEx(dx, dy, dz) then
if gotoEx(dx, dz, dy) then
return true return true
end end
@@ -622,20 +653,19 @@ local function gotoMultiTurn(dx, dy, dz)
return false return false
end end
function turtle.gotoPoint(pt)
return turtle.goto(pt.x, pt.z, pt.y, pt.heading)
end
-- go backwards - turning around if necessary to fight mobs / break blocks -- go backwards - turning around if necessary to fight mobs / break blocks
function turtle.goback() function turtle.goback()
local hi = headings[turtle.point.heading] local hi = headings[turtle.point.heading]
return turtle._goto({ return turtle.goto(turtle.point.x - hi.xd, turtle.point.z - hi.zd, turtle.point.y, turtle.point.heading)
x = turtle.point.x - hi.xd,
y = turtle.point.y,
z = turtle.point.z - hi.zd,
heading = turtle.point.heading,
})
end end
function turtle.gotoYfirst(pt) function turtle.gotoYfirst(pt)
if turtle._gotoY(pt.y) then if turtle.gotoY(pt.y) then
if turtle._goto(pt) then if turtle.goto(pt.x, pt.z, nil, pt.heading) then
turtle.setHeading(pt.heading) turtle.setHeading(pt.heading)
return true return true
end end
@@ -643,7 +673,7 @@ function turtle.gotoYfirst(pt)
end end
function turtle.gotoYlast(pt) function turtle.gotoYlast(pt)
if turtle._goto({ x = pt.x, z = pt.z, heading = pt.heading }) then if turtle.goto(pt.x, pt.z, nil, pt.heading) then
if turtle.gotoY(pt.y) then if turtle.gotoY(pt.y) then
turtle.setHeading(pt.heading) turtle.setHeading(pt.heading)
return true return true
@@ -651,10 +681,9 @@ function turtle.gotoYlast(pt)
end end
end end
function turtle._goto(pt) function turtle.goto(dx, dz, dy, dh)
local dx, dy, dz, dh = pt.x, pt.y, pt.z, pt.heading if not turtle.gotoSingleTurn(dx, dz, dy, dh) then
if not turtle.gotoSingleTurn(dx, dy, dz, dh) then if not gotoMultiTurn(dx, dz, dy) then
if not gotoMultiTurn(dx, dy, dz) then
return false return false
end end
end end
@@ -662,9 +691,6 @@ function turtle._goto(pt)
return true return true
end end
-- avoid lint errors
turtle['goto'] = turtle._goto
function turtle.gotoX(dx) function turtle.gotoX(dx)
turtle.headTowardsX(dx) turtle.headTowardsX(dx)
@@ -693,7 +719,7 @@ function turtle.gotoY(dy)
return false return false
end end
end end
while turtle.point.y < dy do while turtle.point.y < dy do
if not turtle.up() then if not turtle.up() then
return false return false
@@ -704,6 +730,7 @@ end
-- [[ Slot management ]] -- -- [[ Slot management ]] --
function turtle.getSlot(indexOrId, slots) function turtle.getSlot(indexOrId, slots)
if type(indexOrId) == 'string' then if type(indexOrId) == 'string' then
slots = slots or turtle.getInventory() slots = slots or turtle.getInventory()
local _,c = string.gsub(indexOrId, ':', '') local _,c = string.gsub(indexOrId, ':', '')
@@ -739,6 +766,7 @@ function turtle.getSlot(indexOrId, slots)
end end
function turtle.select(indexOrId) function turtle.select(indexOrId)
if type(indexOrId) == 'number' then if type(indexOrId) == 'number' then
return turtle.native.select(indexOrId) return turtle.native.select(indexOrId)
end end
@@ -786,11 +814,6 @@ function turtle.getSummedInventory()
return t return t
end end
function turtle.has(item, count)
local slot = turtle.getSummedInventory()[item]
return slot and slot.count >= (count or 1)
end
function turtle.getFilledSlots(startSlot) function turtle.getFilledSlots(startSlot)
startSlot = startSlot or 1 startSlot = startSlot or 1
@@ -894,6 +917,7 @@ function turtle.getItemCount(idOrName)
end end
function turtle.equip(side, item) function turtle.equip(side, item)
if item then if item then
if not turtle.select(item) then if not turtle.select(item) then
return false, 'Unable to equip ' .. item return false, 'Unable to equip ' .. item
@@ -906,15 +930,6 @@ function turtle.equip(side, item)
return turtle.equipRight() return turtle.equipRight()
end end
function turtle.isEquipped(item)
if peripheral.getType('left') == item then
return 'left'
elseif peripheral.getType('right') == item then
return 'right'
end
end
-- [[ ]] --
function turtle.run(fn, ...) function turtle.run(fn, ...)
local args = { ... } local args = { ... }
local s, m local s, m
@@ -924,51 +939,40 @@ function turtle.run(fn, ...)
end end
synchronized(turtle, function() synchronized(turtle, function()
turtle.abort = false
turtle.status = 'busy'
turtle.resetState() turtle.resetState()
s, m = pcall(function() fn(unpack(args)) end) s, m = pcall(function() fn(unpack(args)) end)
turtle.resetState() turtle.abort = false
turtle.status = 'idle'
if not s and m then if not s and m then
_G.printError(m) printError(m)
end end
end) end)
return s, m return s, m
end end
function turtle.abort(abort) function turtle.abortAction()
state.abort = abort if turtle.status ~= 'idle' then
if abort then turtle.abort = true
os.queueEvent('turtle_abort') os.queueEvent('turtle_abort')
end end
end end
-- [[ Pathing ]] -- -- [[ Pathing ]] --
function turtle.setPersistent(isPersistent)
if isPersistent then
Pathing.setBlocks({ })
else
Pathing.setBlocks()
end
end
function turtle.setPathingBox(box)
Pathing.setBox(box)
end
function turtle.addWorldBlock(pt)
Pathing.addBlock(pt)
end
function turtle.faceAgainst(pt, options) -- 4 sided function turtle.faceAgainst(pt, options) -- 4 sided
options = options or { } options = options or { }
options.dest = { } options.dest = { }
for i = 0, 3 do for i = 0, 3 do
local hi = Point.facings[i] local hi = turtle.getHeadingInfo(i)
table.insert(options.dest, {
x = pt.x + hi.xd, table.insert(options.dest, {
z = pt.z + hi.zd, x = pt.x + hi.xd,
y = pt.y + hi.yd, z = pt.z + hi.zd,
y = pt.y + hi.yd,
heading = (hi.heading + 2) % 4, heading = (hi.heading + 2) % 4,
}) })
end end
@@ -976,11 +980,8 @@ function turtle.faceAgainst(pt, options) -- 4 sided
return turtle.pathfind(Point.closest(turtle.point, options.dest), options) return turtle.pathfind(Point.closest(turtle.point, options.dest), options)
end end
-- move against this point
-- if the point does not contain a heading, then the turtle
-- will face the block (if on same plane)
-- if above or below, the heading is undetermined unless specified
function turtle.moveAgainst(pt, options) -- 6 sided function turtle.moveAgainst(pt, options) -- 6 sided
options = options or { } options = options or { }
options.dest = { } options.dest = { }
@@ -997,11 +998,11 @@ function turtle.moveAgainst(pt, options) -- 6 sided
end end
table.insert(options.dest, { table.insert(options.dest, {
x = pt.x + hi.xd, x = pt.x + hi.xd,
z = pt.z + hi.zd, z = pt.z + hi.zd,
y = pt.y + hi.yd, y = pt.y + hi.yd,
direction = direction, direction = direction,
heading = pt.heading or heading, heading = heading,
}) })
end end
@@ -1056,114 +1057,67 @@ local actionsAt = {
}, },
} }
-- pt = { x,y,z,heading,direction }
-- direction should only be up or down if provided
-- heading can be provided to tell which way to face during action
-- ex: place a block at the point from above facing east
local function _actionAt(action, pt, ...) local function _actionAt(action, pt, ...)
if not pt.heading and not pt.direction then local pt = turtle.moveAgainst(pt)
local msg if pt then
pt, msg = turtle.moveAgainst(pt) return action[pt.direction](...)
if pt then
return action[pt.direction](...)
end
return pt, msg
end
local reversed =
{ [0] = 2, [1] = 3, [2] = 0, [3] = 1, [4] = 5, [5] = 4, }
local dir = reversed[headings[pt.direction or pt.heading].heading]
local apt = { x = pt.x + headings[dir].xd,
y = pt.y + headings[dir].yd,
z = pt.z + headings[dir].zd, }
local direction
-- ex: place a block at this point, from above, facing east
if dir < 4 then
apt.heading = (dir + 2) % 4
direction = 'forward'
elseif dir == 4 then
apt.heading = pt.heading
direction = 'down'
elseif dir == 5 then
apt.heading = pt.heading
direction = 'up'
end
if turtle.pathfind(apt) then
return action[direction](...)
end end
end end
local function _actionDownAt(action, pt, ...) function _actionDownAt(action, pt, ...)
pt = Util.shallowCopy(pt) if turtle.pathfind(Point.above(pt)) then
pt.direction = Point.DOWN return action.down(...)
return _actionAt(action, pt, ...) end
end end
local function _actionUpAt(action, pt, ...) function _actionForwardAt(action, pt, ...)
pt = Util.shallowCopy(pt)
pt.direction = Point.UP
return _actionAt(action, pt, ...)
end
local function _actionForwardAt(action, pt, ...)
if turtle.faceAgainst(pt) then if turtle.faceAgainst(pt) then
return action.forward(...) return action.forward(...)
end end
end end
function turtle.detectAt(pt) return _actionAt(actionsAt.detect, pt) end function _actionUpAt(action, pt, ...)
function turtle.detectDownAt(pt) return _actionDownAt(actionsAt.detect, pt) end if turtle.pathfind(Point.below(pt)) then
function turtle.detectForwardAt(pt) return _actionForwardAt(actionsAt.detect, pt) end return action.up(...)
function turtle.detectUpAt(pt) return _actionUpAt(actionsAt.detect, pt) end
function turtle.digAt(pt) return _actionAt(actionsAt.dig, pt) end
function turtle.digDownAt(pt) return _actionDownAt(actionsAt.dig, pt) end
function turtle.digForwardAt(pt) return _actionForwardAt(actionsAt.dig, pt) end
function turtle.digUpAt(pt) return _actionUpAt(actionsAt.dig, pt) end
function turtle.attackAt(pt) return _actionAt(actionsAt.attack, pt) end
function turtle.attackDownAt(pt) return _actionDownAt(actionsAt.attack, pt) end
function turtle.attackForwardAt(pt) return _actionForwardAt(actionsAt.attack, pt) end
function turtle.attackUpAt(pt) return _actionUpAt(actionsAt.attack, pt) end
function turtle.placeAt(pt, arg, dir) return _actionAt(actionsAt.place, pt, arg, dir) end
function turtle.placeDownAt(pt, arg) return _actionDownAt(actionsAt.place, pt, arg) end
function turtle.placeForwardAt(pt, arg) return _actionForwardAt(actionsAt.place, pt, arg) end
function turtle.placeUpAt(pt, arg) return _actionUpAt(actionsAt.place, pt, arg) end
function turtle.dropAt(pt, ...) return _actionAt(actionsAt.drop, pt, ...) end
function turtle.dropDownAt(pt, ...) return _actionDownAt(actionsAt.drop, pt, ...) end
function turtle.dropForwardAt(pt, ...) return _actionForwardAt(actionsAt.drop, pt, ...) end
function turtle.dropUpAt(pt, ...) return _actionUpAt(actionsAt.drop, pt, ...) end
function turtle.suckAt(pt, qty) return _actionAt(actionsAt.suck, pt, qty or 64) end
function turtle.suckDownAt(pt, qty) return _actionDownAt(actionsAt.suck, pt, qty or 64) end
function turtle.suckForwardAt(pt, qty) return _actionForwardAt(actionsAt.suck, pt, qty or 64) end
function turtle.suckUpAt(pt, qty) return _actionUpAt(actionsAt.suck, pt, qty or 64) end
function turtle.compareAt(pt) return _actionAt(actionsAt.compare, pt) end
function turtle.compareDownAt(pt) return _actionDownAt(actionsAt.compare, pt) end
function turtle.compareForwardAt(pt) return _actionForwardAt(actionsAt.compare, pt) end
function turtle.compareUpAt(pt) return _actionUpAt(actionsAt.compare, pt) end
function turtle.inspectAt(pt) return _actionAt(actionsAt.inspect, pt) end
function turtle.inspectDownAt(pt) return _actionDownAt(actionsAt.inspect, pt) end
function turtle.inspectForwardAt(pt) return _actionForwardAt(actionsAt.inspect, pt) end
function turtle.inspectUpAt(pt) return _actionUpAt(actionsAt.inspect, pt) end
-- [[ GPS ]] --
function turtle.enableGPS(timeout)
local pt = GPS.getPointAndHeading(timeout)
if pt then
turtle.setPoint(pt, true)
return turtle.point
end end
end end
function turtle.addFeatures(...) function turtle.detectAt(pt) return _actionAt(actionsAt.detect, pt) end
for _,feature in pairs({ ... }) do function turtle.detectDownAt(pt) return _actionDownAt(actionsAt.detect, pt) end
require('turtle.' .. feature) function turtle.detectForwardAt(pt) return _actionForwardAt(actionsAt.detect, pt) end
end function turtle.detectUpAt(pt) return _actionUpAt(actionsAt.detect, pt) end
end
function turtle.digAt(pt) return _actionAt(actionsAt.dig, pt) end
function turtle.digDownAt(pt) return _actionDownAt(actionsAt.dig, pt) end
function turtle.digForwardAt(pt) return _actionForwardAt(actionsAt.dig, pt) end
function turtle.digUpAt(pt) return _actionUpAt(actionsAt.dig, pt) end
function turtle.attackAt(pt) return _actionAt(actionsAt.attack, pt) end
function turtle.attackDownAt(pt) return _actionDownAt(actionsAt.attack, pt) end
function turtle.attackForwardAt(pt) return _actionForwardAt(actionsAt.attack, pt) end
function turtle.attackUpAt(pt) return _actionUpAt(actionsAt.attack, pt) end
function turtle.placeAt(pt, arg) return _actionAt(actionsAt.place, pt, arg) end
function turtle.placeDownAt(pt, arg) return _actionDownAt(actionsAt.place, pt, arg) end
function turtle.placeForwardAt(pt, arg) return _actionForwardAt(actionsAt.place, pt, arg) end
function turtle.placeUpAt(pt, arg) return _actionUpAt(actionsAt.place, pt, arg) end
function turtle.dropAt(pt, ...) return _actionAt(actionsAt.drop, pt, ...) end
function turtle.dropDownAt(pt, ...) return _actionDownAt(actionsAt.drop, pt, ...) end
function turtle.dropForwardAt(pt, ...) return _actionForwardAt(actionsAt.drop, pt, ...) end
function turtle.dropUpAt(pt, ...) return _actionUpAt(actionsAt.drop, pt, ...) end
function turtle.suckAt(pt, qty) return _actionAt(actionsAt.suck, pt, qty or 64) end
function turtle.suckDownAt(pt, qty) return _actionDownAt(actionsAt.suck, pt, qty or 64) end
function turtle.suckForwardAt(pt, qty) return _actionForwardAt(actionsAt.suck, pt, qty or 64) end
function turtle.suckUpAt(pt, qty) return _actionUpAt(actionsAt.suck, pt, qty or 64) end
function turtle.compareAt(pt) return _actionAt(actionsAt.compare, pt) end
function turtle.compareDownAt(pt) return _actionDownAt(actionsAt.compare, pt) end
function turtle.compareForwardAt(pt) return _actionForwardAt(actionsAt.compare, pt) end
function turtle.compareUpAt(pt) return _actionUpAt(actionsAt.compare, pt) end
function turtle.inspectAt(pt) return _actionAt(actionsAt.inspect, pt) end
function turtle.inspectDownAt(pt) return _actionDownAt(actionsAt.inspect, pt) end
function turtle.inspectForwardAt(pt) return _actionForwardAt(actionsAt.inspect, pt) end
function turtle.inspectUpAt(pt) return _actionUpAt(actionsAt.inspect, pt) end

View File

@@ -2,11 +2,9 @@ if fs.native then
return return
end end
_G.requireInjector() requireInjector(getfenv(1))
local Util = require('util') local Util = require('util')
local fs = _G.fs
fs.native = Util.shallowCopy(fs) fs.native = Util.shallowCopy(fs)
local fstypes = { } local fstypes = { }
@@ -20,7 +18,7 @@ for k,fn in pairs(fs) do
end end
end end
function nativefs.list(node, dir) function nativefs.list(node, dir, full)
local files local files
if fs.native.isDir(dir) then if fs.native.isDir(dir) then
@@ -45,7 +43,7 @@ function nativefs.list(node, dir)
end end
if not files then if not files then
error('Not a directory', 2) error('Not a directory')
end end
return files return files
@@ -133,7 +131,7 @@ local methods = { 'delete', 'getFreeSpace', 'exists', 'isDir', 'getSize',
for _,m in pairs(methods) do for _,m in pairs(methods) do
fs[m] = function(dir, ...) fs[m] = function(dir, ...)
dir = fs.combine(dir or '', '') dir = fs.combine(dir, '')
local node = getNode(dir) local node = getNode(dir)
return node.fs[m](node, dir, ...) return node.fs[m](node, dir, ...)
end end
@@ -316,14 +314,28 @@ local function getNodeByParts(parts)
end end
function fs.unmount(path) function fs.unmount(path)
local parts = splitpath(path) local parts = splitpath(path)
local targetName = table.remove(parts, #parts) local targetName = table.remove(parts, #parts)
local node = getNodeByParts(parts) local node = getNodeByParts(parts)
if node and node.nodes[targetName] then if not node or not node.nodes[targetName] then
error('Invalid node')
end
node.nodes[targetName] = nil
--[[
-- remove the shadow directories
while #parts > 0 do
targetName = table.remove(parts, #parts)
node = getNodeByParts(parts)
if not Util.empty(node.nodes[targetName].nodes) then
break
end
node.nodes[targetName] = nil node.nodes[targetName] = nil
end end
--]]
end end
function fs.registerType(name, fs) function fs.registerType(name, fs)

View File

@@ -1,70 +0,0 @@
--[[
Allow sharing of local peripherals.
]]--
local Event = require('event')
local Peripheral = require('peripheral')
local Socket = require('socket')
local Util = require('util')
Event.addRoutine(function()
print('peripheral: listening on port 189')
while true do
local socket = Socket.server(189)
print('peripheral: connection from ' .. socket.dhost)
Event.addRoutine(function()
local uri = socket:read(2)
if uri then
local peripheral = Peripheral.lookup(uri)
if not peripheral then
print('peripheral: invalid peripheral ' .. uri)
else
print('peripheral: proxing ' .. uri)
local proxy = {
methods = { }
}
if peripheral.blit then
peripheral = Util.shallowCopy(peripheral)
peripheral.fastBlit = function(data)
for _,v in ipairs(data) do
peripheral[v.fn](unpack(v.args))
end
end
end
for k,v in pairs(peripheral) do
if type(v) == 'function' then
table.insert(proxy.methods, k)
else
proxy[k] = v
end
end
socket:write(proxy)
if proxy.type == 'monitor' then
local h
h = Event.on('monitor_touch', function(...)
if not socket:write({ ... }) then
Event.off(h)
end
end)
end
while true do
local data = socket:read()
if not data then
print('peripheral: lost connection from ' .. socket.dhost)
break
end
socket:write({ peripheral[data.fn](table.unpack(data.args)) })
end
end
end
end)
end
end)

View File

@@ -1,35 +0,0 @@
local Event = require('event')
local Socket = require('socket')
Event.addRoutine(function()
while true do
print('proxy: listening on port 188')
local socket = Socket.server(188)
print('proxy: connection from ' .. socket.dhost)
Event.addRoutine(function()
local api = socket:read(2)
if api then
local proxy = _G[api]
local methods = { }
for k,v in pairs(proxy) do
if type(v) == 'function' then
table.insert(methods, k)
end
end
socket:write(methods)
while true do
local data = socket:read()
if not data then
print('proxy: lost connection from ' .. socket.dhost)
break
end
socket:write({ proxy[data.fn](table.unpack(data.args)) })
end
end
end)
end
end)

View File

@@ -3,12 +3,6 @@ local GPS = require('gps')
local Socket = require('socket') local Socket = require('socket')
local Util = require('util') local Util = require('util')
local device = _G.device
local multishell = _ENV.multishell
local network = _G.network
local os = _G.os
local turtle = _G.turtle
-- move this into gps api -- move this into gps api
local gpsRequested local gpsRequested
local gpsLastPoint local gpsLastPoint
@@ -23,7 +17,7 @@ local function snmpConnection(socket)
end end
if msg.type == 'reboot' then if msg.type == 'reboot' then
os.reboot() os.reboot()
elseif msg.type == 'shutdown' then elseif msg.type == 'shutdown' then
os.shutdown() os.shutdown()
@@ -32,19 +26,20 @@ local function snmpConnection(socket)
socket:write('pong') socket:write('pong')
elseif msg.type == 'script' then elseif msg.type == 'script' then
local fn, err = loadstring(msg.args, 'script') local fn, msg = loadstring(msg.args, 'script')
if fn then if fn then
multishell.openTab({ multishell.openTab({
fn = fn, fn = fn,
env = getfenv(1),
title = 'script', title = 'script',
}) })
else else
_G.printError(err) printError(msg)
end end
elseif msg.type == 'scriptEx' then elseif msg.type == 'scriptEx' then
local s, m = pcall(function() local s, m = pcall(function()
local env = setmetatable(Util.shallowCopy(_ENV), { __index = _G }) local env = setmetatable(Util.shallowCopy(getfenv(1)), { __index = _G })
local fn, m = load(msg.args, 'script', nil, env) local fn, m = load(msg.args, 'script', nil, env)
if not fn then if not fn then
error(m) error(m)
@@ -90,7 +85,7 @@ local function snmpConnection(socket)
} }
if turtle then if turtle then
info.fuel = turtle.getFuelLevel() info.fuel = turtle.getFuelLevel()
info.status = turtle.getStatus() info.status = turtle.status
end end
socket:write(info) socket:write(info)
end end
@@ -115,7 +110,8 @@ end)
device.wireless_modem.open(999) device.wireless_modem.open(999)
print('discovery: listening on port 999') print('discovery: listening on port 999')
Event.on('modem_message', function(_, _, sport, id, info, distance) Event.on('modem_message', function(e, s, sport, id, info, distance)
if sport == 999 and tonumber(id) and type(info) == 'table' then if sport == 999 and tonumber(id) and type(info) == 'table' then
if not network[id] then if not network[id] then
network[id] = { } network[id] = { }
@@ -144,7 +140,7 @@ local function sendInfo()
info.uptime = math.floor(os.clock()) info.uptime = math.floor(os.clock())
if turtle then if turtle then
info.fuel = turtle.getFuelLevel() info.fuel = turtle.getFuelLevel()
info.status = turtle.getStatus() info.status = turtle.status
info.point = turtle.point info.point = turtle.point
info.inventory = turtle.getInventory() info.inventory = turtle.getInventory()
info.slotIndex = turtle.getSelectedSlot() info.slotIndex = turtle.getSelectedSlot()
@@ -166,7 +162,7 @@ Event.onInterval(10, function()
end) end)
Event.on('turtle_response', function() Event.on('turtle_response', function()
if turtle.getStatus() ~= info.status or if turtle.status ~= info.status or
turtle.fuel ~= info.fuel then turtle.fuel ~= info.fuel then
sendInfo() sendInfo()
end end

View File

@@ -2,12 +2,9 @@ local Event = require('event')
local Socket = require('socket') local Socket = require('socket')
local Util = require('util') local Util = require('util')
local multishell = _ENV.multishell
local os = _G.os
local term = _G.term
local function telnetHost(socket) local function telnetHost(socket)
_G.requireInjector()
requireInjector(getfenv(1))
local Event = require('event') local Event = require('event')
@@ -17,7 +14,7 @@ local function telnetHost(socket)
local termInfo = socket:read(5) local termInfo = socket:read(5)
if not termInfo then if not termInfo then
_G.printError('read failed') printtError('read failed')
return return
end end
@@ -48,7 +45,7 @@ local function telnetHost(socket)
end end
local shellThread = Event.addRoutine(function() local shellThread = Event.addRoutine(function()
os.run(_ENV, 'sys/apps/shell') os.run(getfenv(1), 'sys/apps/shell')
Event.exitPullEvents() Event.exitPullEvents()
end) end)
@@ -70,6 +67,7 @@ local function telnetHost(socket)
end end
Event.addRoutine(function() Event.addRoutine(function()
print('telnet: listening on port 23') print('telnet: listening on port 23')
while true do while true do
local socket = Socket.server(23) local socket = Socket.server(23)
@@ -79,6 +77,7 @@ Event.addRoutine(function()
multishell.openTab({ multishell.openTab({
fn = telnetHost, fn = telnetHost,
args = { socket }, args = { socket },
env = getfenv(1),
title = 'Telnet Client', title = 'Telnet Client',
hidden = true, hidden = true,
}) })

View File

@@ -1,15 +1,9 @@
_G.requireInjector() requireInjector(getfenv(1))
local Event = require('event') local Event = require('event')
local Peripheral = require('peripheral') local Peripheral = require('peripheral')
local Util = require('util') local Util = require('util')
local colors = _G.colors
local device = _G.device
local multishell = _ENV.multishell
local os = _G.os
local term = _G.term
multishell.setTitle(multishell.getCurrent(), 'Devices') multishell.setTitle(multishell.getCurrent(), 'Devices')
local attachColor = colors.green local attachColor = colors.green
@@ -20,7 +14,7 @@ if not term.isColor() then
detachColor = colors.lightGray detachColor = colors.lightGray
end end
Event.on('peripheral', function(_, side) Event.on('peripheral', function(event, side)
if side then if side then
local dev = Peripheral.addDevice(device, side) local dev = Peripheral.addDevice(device, side)
if dev then if dev then
@@ -31,7 +25,7 @@ Event.on('peripheral', function(_, side)
end end
end) end)
Event.on('peripheral_detach', function(_, side) Event.on('peripheral_detach', function(event, side)
if side then if side then
local dev = Util.find(device, 'side', side) local dev = Util.find(device, 'side', side)
if dev then if dev then

View File

@@ -1,6 +1,6 @@
if device.wireless_modem then if device.wireless_modem then
_G.requireInjector() requireInjector(getfenv(1))
local Config = require('config') local Config = require('config')
local config = { } local config = { }

View File

@@ -1,17 +1,14 @@
_G.requireInjector() requireInjector(getfenv(1))
local Terminal = require('terminal') local Terminal = require('terminal')
local Util = require('util') local Util = require('util')
local multishell = _ENV.multishell
local os = _G.os
local term = _G.term
multishell.setTitle(multishell.getCurrent(), 'Debug') multishell.setTitle(multishell.getCurrent(), 'Debug')
term.redirect(Terminal.scrollable(term.current(), 50)) term.redirect(Terminal.scrollable(term.current(), 50))
local tabId = multishell.getCurrent() local tabId = multishell.getCurrent()
local tab = multishell.getTab(tabId)
local terminal = term.current() local terminal = term.current()
local previousId local previousId
@@ -25,7 +22,7 @@ end
print('Debug started') print('Debug started')
print('Press ^d to activate debug window') print('Press ^d to activate debug window')
multishell.addHotkey('control-d', function() multishell.addHotkey(32, function()
local currentId = multishell.getFocus() local currentId = multishell.getFocus()
if currentId ~= tabId then if currentId ~= tabId then
previousId = currentId previousId = currentId
@@ -40,4 +37,4 @@ os.pullEventRaw('terminate')
print('Debug stopped') print('Debug stopped')
_G.debug = function() end _G.debug = function() end
multishell.removeHotkey('control-d') multishell.removeHotkey(32)

View File

@@ -1,25 +1,17 @@
_G.requireInjector() requireInjector(getfenv(1))
local Util = require('util') local Util = require('util')
local device = _G.device
local fs = _G.fs
local multishell = _ENV.multishell
local os = _G.os
local printError = _G.printError
local network = { }
_G.network = network
multishell.setTitle(multishell.getCurrent(), 'Net Daemon') multishell.setTitle(multishell.getCurrent(), 'Net Daemon')
local function netUp() _G.network = { }
_G.requireInjector()
local function netUp()
requireInjector(getfenv(1))
local Event = require('event') local Event = require('event')
for _,file in pairs(fs.list('sys/network')) do for _,file in pairs(fs.list('sys/network')) do
local fn, msg = Util.run(_ENV, 'sys/network/' .. file) local fn, msg = Util.run(getfenv(1), 'sys/network/' .. file)
if not fn then if not fn then
printError(msg) printError(msg)
end end
@@ -49,7 +41,7 @@ local function startNetwork()
print('Starting network services') print('Starting network services')
local success, msg = Util.runFunction( local success, msg = Util.runFunction(
Util.shallowCopy(_ENV), netUp) Util.shallowCopy(getfenv(1)), netUp)
if not success and msg then if not success and msg then
printError(msg) printError(msg)
@@ -64,7 +56,7 @@ else
end end
while true do while true do
local _, deviceName = os.pullEvent('device_attach') local e, deviceName = os.pullEvent('device_attach')
if deviceName == 'wireless_modem' then if deviceName == 'wireless_modem' then
startNetwork() startNetwork()
end end

View File

@@ -6,18 +6,14 @@
* background read buffering * background read buffering
]]-- ]]--
local multishell = _ENV.multishell
local os = _G.os
multishell.setTitle(multishell.getCurrent(), 'Net transport') multishell.setTitle(multishell.getCurrent(), 'Net transport')
local computerId = os.getComputerID() local computerId = os.getComputerID()
local transport = { _G.transport = {
timers = { }, timers = { },
sockets = { }, sockets = { },
} }
_G.transport = transport
function transport.open(socket) function transport.open(socket)
transport.sockets[socket.sport] = socket transport.sockets[socket.sport] = socket
@@ -31,10 +27,11 @@ function transport.read(socket)
end end
function transport.write(socket, data) function transport.write(socket, data)
--debug('>> ' .. Util.tostring({ type = 'DATA', seq = socket.wseq })) --debug('>> ' .. Util.tostring({ type = 'DATA', seq = socket.wseq }))
socket.transmit(socket.dport, socket.dhost, data) socket.transmit(socket.dport, socket.dhost, data)
local timerId = os.startTimer(3) local timerId = os.startTimer(2)
transport.timers[timerId] = socket transport.timers[timerId] = socket
socket.timers[socket.wseq] = timerId socket.timers[socket.wseq] = timerId
@@ -42,18 +39,6 @@ function transport.write(socket, data)
socket.wseq = socket.wseq + 1 socket.wseq = socket.wseq + 1
end end
function transport.ping(socket)
--debug('>> ' .. Util.tostring({ type = 'DATA', seq = socket.wseq }))
socket.transmit(socket.dport, socket.dhost, {
type = 'PING',
seq = -1,
})
local timerId = os.startTimer(3)
transport.timers[timerId] = socket
socket.timers[-1] = timerId
end
function transport.close(socket) function transport.close(socket)
transport.sockets[socket.sport] = nil transport.sockets[socket.sport] = nil
end end
@@ -65,7 +50,6 @@ while true do
if e == 'timer' then if e == 'timer' then
local socket = transport.timers[timerId] local socket = transport.timers[timerId]
if socket and socket.connected then if socket and socket.connected then
print('transport timeout - closing socket ' .. socket.sport) print('transport timeout - closing socket ' .. socket.sport)
socket:close() socket:close()
@@ -84,12 +68,11 @@ while true do
socket:close() socket:close()
elseif msg.type == 'ACK' then elseif msg.type == 'ACK' then
local ackTimerId = socket.timers[msg.seq] local timerId = socket.timers[msg.seq]
if ackTimerId then
os.cancelTimer(ackTimerId) os.cancelTimer(timerId)
socket.timers[msg.seq] = nil socket.timers[msg.seq] = nil
transport.timers[ackTimerId] = nil transport.timers[timerId] = nil
end
elseif msg.type == 'PING' then elseif msg.type == 'PING' then
socket.transmit(socket.dport, socket.dhost, { socket.transmit(socket.dport, socket.dhost, {