40 Commits

Author SHA1 Message Date
kepler155c@gmail.com
13ec8ea04f peripheral overhaul 2018-01-06 22:25:33 -05:00
kepler155c@gmail.com
911fec9118 peripheral overhaul 2018-01-06 06:07:49 -05:00
kepler155c@gmail.com
1528bab3ac open editor for config on first run 2018-01-04 03:30:59 -05:00
kepler155c@gmail.com
dc9d174085 shadow text color override 2018-01-03 02:43:17 -05:00
kepler155c@gmail.com
1108c173d7 pass raw values for grid overrides 2017-12-27 23:55:30 -05:00
kepler155c@gmail.com
0830e46fb8 fix slideout 2017-12-23 01:47:54 -05:00
kepler155c@gmail.com
743959c1fa better emits 2017-12-16 00:07:22 -05:00
kepler155c@gmail.com
dd4211745e modal windows 2017-12-13 01:37:31 -05:00
kepler155c@gmail.com
0df22efdc2 slide out for UI + turtle.has 2017-12-11 11:31:41 -05:00
kepler155c@gmail.com
a8a4ceb85d api cleanup 2017-11-15 00:08:42 -05:00
kepler155c@gmail.com
f533e42c0c turtle api 2017-10-31 02:01:18 -04:00
kepler155c@gmail.com
1b9450017d refactor + cleanup 2017-10-27 20:24:48 -04:00
kepler155c@gmail.com
cac15722b8 proxy + pathfinding optimization 2017-10-26 18:56:55 -04:00
kepler155c@gmail.com
7fd93e8a8b proxy apis over wireless 2017-10-24 23:01:40 -04:00
kepler155c@gmail.com
84b2b8ce63 cleanup 2017-10-23 19:33:53 -04:00
kepler155c@gmail.com
22a432492c naming 2017-10-22 19:10:29 -04:00
kepler155c@gmail.com
8d3f5329f2 misc 2017-10-22 01:30:26 -04:00
kepler155c@gmail.com
8e9ff9c626 input redo 2017-10-20 13:57:24 -04:00
kepler155c@gmail.com
31b3787695 input redo + env pollution 2017-10-20 04:23:17 -04:00
kepler155c@gmail.com
fb0f3e567a redo tabs + wizard 2017-10-18 19:51:55 -04:00
kepler155c@gmail.com
f6d1cfc7ee grid event 2017-10-17 22:30:23 -04:00
kepler155c@gmail.com
39ba226a82 cleanup 2017-10-16 16:54:50 -04:00
kepler155c@gmail.com
fe0ca72b8b redo input translation -round 2 2017-10-16 00:02:42 -04:00
kepler155c@gmail.com
2721840596 redo input translation + shift-paste 2017-10-15 19:55:05 -04:00
kepler155c@gmail.com
9b8b5238b0 multishell hooks 2017-10-15 02:36:54 -04:00
kepler155c@gmail.com
8b187f2813 multishell hooks 2017-10-14 03:41:54 -04:00
kepler155c@gmail.com
153b0b86ff autocomplete 2017-10-13 16:30:47 -04:00
kepler155c@gmail.com
a9634cb438 version checking + ui cleanup 2017-10-12 17:58:35 -04:00
kepler155c@gmail.com
3460dd68b2 cleanup + global clipboard 2017-10-11 22:39:04 -04:00
kepler155c@gmail.com
f9221e67be cleanup 2017-10-11 16:31:48 -04:00
kepler155c@gmail.com
852ad193f0 simplify ui 2017-10-11 11:37:52 -04:00
kepler155c@gmail.com
05c99b583a friendlier networking + adding tabs 2017-10-09 13:08:38 -04:00
kepler155c@gmail.com
f5b99d91e5 tabs update + object identity 2017-10-09 00:26:19 -04:00
kepler155c@gmail.com
955f11042b http fixes 2017-10-08 20:03:01 -04:00
kepler155c@gmail.com
a625b52bad lint 2017-10-08 17:45:01 -04:00
kepler155c@gmail.com
98ec840db1 UI improvements 2017-10-07 23:03:18 -04:00
kepler155c@gmail.com
af981dd1f8 transition refactor + inactive elements 2017-10-07 00:27:41 -04:00
kepler155c@gmail.com
91a05c07dd drop menus 2017-10-06 13:39:47 -04:00
kepler155c@gmail.com
fc69d4be83 canvas palette 2017-10-06 03:07:24 -04:00
kepler155c@gmail.com
f0846c8daa id upgrade fixes 2017-10-05 18:05:57 -04:00
72 changed files with 3635 additions and 3935 deletions

View File

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

View File

@@ -19,7 +19,7 @@ return function(base)
-- expose a constructor which can be called by <classname>(<args>)
setmetatable(c, {
__call = function(class_tbl, ...)
local obj = {}
local obj = { }
setmetatable(obj,c)
if class_tbl.init then
class_tbl.init(obj, ...)

View File

@@ -1,8 +1,11 @@
local Util = require('util')
local fs = _G.fs
local shell = _ENV.shell
local Config = { }
Config.load = function(fname, data)
function Config.load(fname, data)
local filename = 'usr/config/' .. fname
if not fs.exists('usr/config') then
@@ -16,7 +19,24 @@ Config.load = function(fname, data)
end
end
Config.update = function(fname, data)
function Config.loadWithCheck(fname, data)
local filename = 'usr/config/' .. fname
if not fs.exists(filename) then
Config.load(fname, data)
print()
print('The configuration file has been created.')
print('The file name is: ' .. filename)
print()
_G.printError('Press enter to configure')
_G.read()
shell.run('edit ' .. filename)
end
Config.load(fname, data)
end
function Config.update(fname, data)
local filename = 'usr/config/' .. fname
Util.writeTable(filename, data)
end

View File

@@ -1,3 +1,5 @@
local os = _G.os
local Event = {
uid = 1, -- unique id for handlers
routines = { }, -- coroutines
@@ -22,6 +24,9 @@ function Routine:terminate()
end
function Routine:resume(event, ...)
--if coroutine.status(self.co) == 'running' then
--return
--end
if not self.co then
error('Cannot resume a dead routine')

View File

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

View File

@@ -1,3 +1,5 @@
local fs = _G.fs
local linkfs = { }
local methods = { 'exists', 'getFreeSpace', 'getSize',
@@ -10,7 +12,7 @@ for _,m in pairs(methods) do
end
end
function linkfs.mount(dir, source)
function linkfs.mount(_, source)
if not source then
error('Source is required')
end

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,9 @@
local GPS = { }
local device = _G.device
local gps = _G.gps
local turtle = _G.turtle
function GPS.locate(timeout, debug)
local pt = { }
timeout = timeout or 10
@@ -14,7 +18,6 @@ function GPS.isAvailable()
end
function GPS.getPoint(timeout, debug)
local pt = GPS.locate(timeout, debug)
if not pt then
return
@@ -24,7 +27,7 @@ function GPS.getPoint(timeout, debug)
pt.y = math.floor(pt.y)
pt.z = math.floor(pt.z)
if pocket then
if _G.pocket then
pt.y = pt.y - 1
end
@@ -47,7 +50,7 @@ function GPS.getHeading(timeout)
while not turtle.forward() do
turtle.turnRight()
if turtle.getHeading() == heading then
printError('GPS.getPoint: Unable to move forward')
_G.printError('GPS.getPoint: Unable to move forward')
return
end
end
@@ -79,12 +82,12 @@ function GPS.getPointAndHeading(timeout)
end
-- from stock gps API
local function trilaterate( A, B, C )
local function trilaterate(A, B, C)
local a2b = B.position - A.position
local a2c = C.position - A.position
if math.abs( a2b:normalize():dot( a2c:normalize() ) ) > 0.999 then
return nil
return
end
local d = a2b:length()

View File

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

150
sys/apis/input.lua Normal file
View File

@@ -0,0 +1,150 @@
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 = {}
while str:sub(1, 1) ~= "}" do
local k, v = nil, nil
local k, v
k, v, str = json.parseMember(str)
val[k] = v
str = removeWhite(str)

View File

@@ -1,105 +0,0 @@
-- 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,98 +0,0 @@
--- 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

@@ -1,32 +0,0 @@
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

@@ -7,85 +7,26 @@
-- made with regards of their `f` cost. From a given node being examined, the `pathfinder` will expand the search
-- to the next neighbouring node having the lowest `f` cost. See `core.bheap` for more details.
--
if (...) then
local 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 = {}
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)
return setmetatable({_x = x, _y = y, _z = z, _clearance = {}}, Node)
return setmetatable({x = x, y = y, z = z }, Node)
end
-- Enables the use of operator '<' to compare nodes.
-- Will be used to sort a collection of nodes in a binary heap on the basis of their F-cost
function Node.__lt(A,B) return (A._f < B._f) end
--- Returns x-coordinate of a `node`
-- @class function
-- @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
function Node:getX() return self.x end
function Node:getY() return self.y end
function Node:getZ() return self.z end
--- Clears temporary cached attributes of a `node`.
-- Deletes the attributes cached within a given node after a pathfinding call.
-- This function is internally used by the search algorithms, so you should not use it explicitely.
-- @class function
-- @treturn node self (the calling `node` itself, can be chained)
-- @usage
-- local thisNode = Node(1,2)
-- thisNode:reset()
function Node:reset()
self._g, self._h, self._f = nil, nil, nil
self._opened, self._closed, self._parent = nil, nil, nil
@@ -93,7 +34,7 @@ if (...) then
end
return setmetatable(Node,
{__call = function(self,...)
{__call = function(_,...)
return Node:new(...)
end}
)

View File

@@ -6,44 +6,25 @@
-- It should normally not be used explicitely, yet it remains fully accessible.
--
if (...) then
-- Dependencies
local _PATH = (...):match('(.+)%.path$')
local Heuristic = require (_PATH .. '.heuristics')
local t_remove = table.remove
-- 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 = {}
Path.__index = Path
--- Inits a new `path`.
-- @class function
-- @treturn path a `path`
-- @usage local p = Path()
function Path:new()
return setmetatable({_nodes = {}}, Path)
end
--- Iterates on each single `node` along a `path`. At each step of iteration,
-- returns the `node` plus a count value. Aliased as @{Path:nodes}
-- @class function
-- @treturn node a `node`
-- @treturn int the count for the number of nodes
-- @see Path:nodes
-- @usage
-- for node, count in p:iter() do
-- ...
-- end
function Path:iter()
local i,pathLen = 1,#self._nodes
function Path:nodes()
local i = 1
return function()
if self._nodes[i] then
i = i+1
@@ -52,149 +33,34 @@ if (...) then
end
end
--- Iterates on each single `node` along a `path`. At each step of iteration,
-- returns a `node` plus a count value. Alias for @{Path:iter}
-- @class function
-- @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}
-- consisting of straight moves. Does the opposite of @{Path:fill}
-- @class function
-- @treturn path self (the calling `path` itself, can be chained)
-- @treturn path self (the calling `path` itself, can be chained)
-- @see Path:fill
-- @usage p:filter()
-- @usage p:filter()
function Path:filter()
local i = 2
local xi,yi,dx,dy, olddx, olddy
xi,yi = self._nodes[i]._x, self._nodes[i]._y
dx, dy = xi - self._nodes[i-1]._x, yi-self._nodes[i-1]._y
local xi,yi,zi,dx,dy,dz, olddx, olddy, olddz
xi,yi,zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z
dx, dy,dz = xi - self._nodes[i-1].x, yi-self._nodes[i-1].y, zi-self._nodes[i-1].z
while true do
olddx, olddy = dx, dy
olddx, olddy, olddz = dx, dy, dz
if self._nodes[i+1] then
i = i+1
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 olddx == dx and olddy == dy then
xi, yi, zi = self._nodes[i].x, self._nodes[i].y, self._nodes[i].z
dx, dy, dz = xi - self._nodes[i-1].x, yi - self._nodes[i-1].y, zi - self._nodes[i-1].z
if olddx == dx and olddy == dy and olddz == dz then
t_remove(self._nodes, i-1)
i = i - 1
end
else break end
end
return self
return self
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,
{__call = function(self,...)
{__call = function(_,...)
return Path:new(...)
end
})

View File

@@ -5,125 +5,20 @@ if (...) then
-- Dependencies
local _PATH = (...):gsub('%.utils$','')
local Path = require (_PATH .. '.path')
local Node = require (_PATH .. '.node')
-- Local references
local pairs = pairs
local type = type
local t_insert = table.insert
local assert = assert
local coroutine = coroutine
-- Raw array items count
local function arraySize(t)
local count = 0
for k,v in pairs(t) do
for _ in pairs(t) do
count = count+1
end
return count
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
local function traceBackPath(finder, node, startNode)
local path = Path:new()
@@ -154,14 +49,8 @@ if (...) then
return {
arraySize = arraySize,
getKeys = getKeys,
indexOf = indexOf,
outOfRange = outOfRange,
getArrayBounds = getArrayBounds,
arrayToNodes = arrayToNodes,
strToMap = stringMapToArray,
around = around,
drAround = drAround,
traceBackPath = traceBackPath
}

View File

@@ -2,7 +2,8 @@
-- Implementation of the `grid` class.
-- The `grid` is a implicit graph which represents the 2D
-- world map layout on which the `pathfinder` object will run.
-- During a search, the `pathfinder` object needs to save some critical values. These values are cached within each `node`
-- During a search, the `pathfinder` object needs to save some critical values.
-- These values are cached within each `node`
-- object, and the whole set of nodes are tight inside the `grid` object itself.
if (...) then
@@ -12,16 +13,10 @@ if (...) then
-- Local references
local Utils = require (_PATH .. '.core.utils')
local Assert = require (_PATH .. '.core.assert')
local Node = require (_PATH .. '.core.node')
-- Local references
local pairs = pairs
local assert = assert
local next = next
local setmetatable = setmetatable
local floor = math.floor
local coroutine = coroutine
-- Offsets for straights moves
local straightOffsets = {
@@ -30,390 +25,67 @@ if (...) then
{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 = {}
Grid.__index = Grid
-- Specialized grids
local PreProcessGrid = setmetatable({},Grid)
local PostProcessGrid = setmetatable({},Grid)
PreProcessGrid.__index = PreProcessGrid
PostProcessGrid.__index = PostProcessGrid
PreProcessGrid.__call = function (self,x,y,z)
return self:getNodeAt(x,y,z)
end
PostProcessGrid.__call = function (self,x,y,z,create)
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]
function Grid:new(dim)
local newGrid = { }
newGrid._min_x, newGrid._max_x = dim.x, dim.ex
newGrid._min_y, newGrid._max_y = dim.y, dim.ey
newGrid._min_z, newGrid._max_z = dim.z, dim.ez
newGrid._nodes = { }
newGrid._width = (newGrid._max_x-newGrid._min_x)+1
newGrid._height = (newGrid._max_y-newGrid._min_y)+1
newGrid._length = (newGrid._max_z-newGrid._min_z)+1
return setmetatable(newGrid,Grid)
end
--- Inits a new `grid`
-- @class function
-- @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)
function Grid:isWalkableAt(x, y, z)
local node = self:getNodeAt(x,y,z)
return node and node.walkable ~= 1
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()
return self._width
end
--- Returns the `grid` height.
-- @class function
-- @treturn int the `grid` height
-- @usage print(myGrid:getHeight())
function Grid:getHeight()
return self._height
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()
return self._nodes
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()
return self._min_x, self._min_y, self._min_z, self._max_x, self._max_y, self._max_z
end
--- Returns neighbours. The returned value is an array of __walkable__ nodes neighbouring a given `node`.
-- @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
-- @usage
-- local aNode = myGrid:getNodeAt(5,6)
-- local neighbours = myGrid:getNeighbours(aNode, 0, true)
function Grid:getNeighbours(node, walkable, allowDiagonal, tunnel, clearance)
function Grid:getNeighbours(node)
local neighbours = {}
for i = 1,#straightOffsets do
local n = self:getNodeAt(
node._x + straightOffsets[i].x,
node._y + straightOffsets[i].y,
node._z + straightOffsets[i].z
node.x + straightOffsets[i].x,
node.y + straightOffsets[i].y,
node.z + straightOffsets[i].z
)
if n and self:isWalkableAt(n._x, n._y, n._z, walkable, clearance) then
if n and self:isWalkableAt(n.x, n.y, n.z) then
neighbours[#neighbours+1] = n
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
end
--- 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)
function Grid:getNodeAt(x,y,z)
if not x or not y or not z then return end
if Utils.outOfRange(x,self._min_x,self._max_x) then return end
if Utils.outOfRange(y,self._min_y,self._max_y) then return end
if Utils.outOfRange(z,self._min_z,self._max_z) then return end
-- inefficient
if not self._nodes[y] then self._nodes[y] = {} end
if not self._nodes[y][x] then self._nodes[y][x] = {} end
if not self._nodes[y][x][z] then self._nodes[y][x][z] = Node:new(x,y,z) end

View File

@@ -28,11 +28,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--]]
--- The Pathfinder class
--
-- Implementation of the `pathfinder` class.
local _VERSION = ""
local _RELEASEDATE = ""
@@ -41,333 +36,46 @@ if (...) then
-- Dependencies
local _PATH = (...):gsub('%.pathfinder$','')
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
local t_insert, t_remove = table.insert, table.remove
local floor = math.floor
local pairs = pairs
local assert = assert
local type = type
local setmetatable, getmetatable = setmetatable, getmetatable
local setmetatable = setmetatable
--- Finders (search algorithms implemented). Refers to the search algorithms actually implemented in Jumper.
--
-- <li>[A*](http://en.wikipedia.org/wiki/A*_search_algorithm)</li>
-- <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 = {
['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
-- to easily reset their properties for the next pathfinding call
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
-- 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 = {}
Pathfinder.__index = Pathfinder
--- 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)
function Pathfinder:new(heuristic)
local newPathfinder = {}
setmetatable(newPathfinder, Pathfinder)
--newPathfinder:setGrid(grid)
newPathfinder:setFinder(finderName)
--newPathfinder:setWalkable(walkable)
newPathfinder:setMode('DIAGONAL')
newPathfinder:setHeuristic('MANHATTAN')
newPathfinder:setTunnelling(false)
self._finder = Finders.ASTAR
self._heuristic = heuristic
return newPathfinder
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)
assert(Assert.inherits(grid, Grid), 'Wrong argument #1. Expected a \'grid\' object')
self._grid = grid
self._grid._eval = self._walkable and type(self._walkable) == 'function'
return self
end
--- 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]__.
--- Calculates a `path`. Returns the `path` from start to end location
-- Both locations must exist on the collision map. The starting location can be unwalkable.
-- @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
-- @usage local path = myFinder:getPath(1,1,5,5)
function Pathfinder:getPath(startX, startY, startZ, ih, endX, endY, endZ, oh, clearance)
function Pathfinder:getPath(startX, startY, startZ, ih, endX, endY, endZ, oh)
self:reset()
local startNode = self._grid:getNodeAt(startX, startY, startZ)
local endNode = self._grid:getNodeAt(endX, endY, endZ)
@@ -375,20 +83,21 @@ if (...) then
return nil
end
startNode._heading = ih
endNode._heading = oh
startNode.heading = ih
endNode.heading = oh
assert(startNode, ('Invalid location [%d, %d, %d]'):format(startX, startY, startZ))
assert(endNode and self._grid:isWalkableAt(endX, endY, endZ),
('Invalid or unreachable location [%d, %d, %d]'):format(endX, endY, endZ))
local _endNode = Finders[self._finder](self, startNode, endNode, clearance, toClear)
local _endNode = self._finder(self, startNode, endNode, toClear)
if _endNode then
return Utils.traceBackPath(self, _endNode, startNode)
end
return nil
end
--- Resets the `pathfinder`. This function is called internally between successive pathfinding calls, so you should not
--- Resets the `pathfinder`. This function is called internally between
-- successive pathfinding calls, so you should not
-- use it explicitely, unless under specific circumstances.
-- @class function
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
@@ -399,7 +108,6 @@ if (...) then
return self
end
-- Returns Pathfinder class
Pathfinder._VERSION = _VERSION
Pathfinder._RELEASEDATE = _RELEASEDATE
@@ -408,5 +116,4 @@ if (...) then
return self:new(...)
end
})
end

View File

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

View File

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

View File

@@ -1,49 +0,0 @@
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 Util = require('util')
local Event = require('event')
local Socket = require('socket')
local Util = require('util')
local Peripheral = { }
local function getDeviceList()
local Peripheral = Util.shallowCopy(_G.peripheral)
function Peripheral.getList()
if _G.device then
return _G.device
end
local deviceList = { }
for _,side in pairs(peripheral.getNames()) do
for _,side in pairs(Peripheral.getNames()) do
Peripheral.addDevice(deviceList, side)
end
@@ -19,14 +19,14 @@ end
function Peripheral.addDevice(deviceList, side)
local name = side
local ptype = peripheral.getType(side)
local ptype = Peripheral.getType(side)
if not ptype then
return
end
if ptype == 'modem' then
if peripheral.call(name, 'isWireless') then
if Peripheral.call(name, 'isWireless') then
ptype = 'wireless_modem'
else
ptype = 'wired_modem'
@@ -52,10 +52,10 @@ function Peripheral.addDevice(deviceList, side)
name = uniqueName
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
printError('wrap failed')
printError(m)
_G.printError('wrap failed')
_G.printError(m)
end
if deviceList[name] then
@@ -70,15 +70,15 @@ function Peripheral.addDevice(deviceList, side)
end
function Peripheral.getBySide(side)
return Util.find(getDeviceList(), 'side', side)
return Util.find(Peripheral.getList(), 'side', side)
end
function Peripheral.getByType(typeName)
return Util.find(getDeviceList(), 'type', typeName)
return Util.find(Peripheral.getList(), 'type', typeName)
end
function Peripheral.getByMethod(method)
for _,p in pairs(getDeviceList()) do
for _,p in pairs(Peripheral.getList()) do
if p[method] then
return p
end
@@ -92,7 +92,9 @@ function Peripheral.get(args)
args = { type = args }
end
args = args or { type = pType }
if args.name then
return _G.device[args.name]
end
if args.type then
local p = Peripheral.getByType(args.type)
@@ -116,4 +118,113 @@ function Peripheral.get(args)
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

View File

@@ -2,6 +2,48 @@ local Util = require('util')
local Point = { }
Point.directions = {
[ 0 ] = { xd = 1, zd = 0, yd = 0, heading = 0, direction = 'east' },
[ 1 ] = { xd = 0, zd = 1, yd = 0, heading = 1, direction = 'south' },
[ 2 ] = { xd = -1, zd = 0, yd = 0, heading = 2, direction = 'west' },
[ 3 ] = { xd = 0, zd = -1, yd = 0, heading = 3, direction = 'north' },
[ 4 ] = { xd = 0, zd = 0, yd = 1, heading = 4, direction = 'up' },
[ 5 ] = { xd = 0, zd = 0, yd = -1, heading = 5, direction = 'down' },
}
Point.facings = {
[ 0 ] = Point.directions[0],
[ 1 ] = Point.directions[1],
[ 2 ] = Point.directions[2],
[ 3 ] = Point.directions[3],
east = Point.directions[0],
south = Point.directions[1],
west = Point.directions[2],
north = Point.directions[3],
}
Point.headings = {
[ 0 ] = Point.directions[0],
[ 1 ] = Point.directions[1],
[ 2 ] = Point.directions[2],
[ 3 ] = Point.directions[3],
[ 4 ] = Point.directions[4],
[ 5 ] = Point.directions[5],
east = Point.directions[0],
south = Point.directions[1],
west = Point.directions[2],
north = Point.directions[3],
up = Point.directions[4],
down = Point.directions[5],
}
Point.EAST = 0
Point.SOUTH = 1
Point.WEST = 2
Point.NORTH = 3
Point.UP = 4
Point.DOWN = 5
function Point.copy(pt)
return { x = pt.x, y = pt.y, z = pt.z }
end
@@ -27,7 +69,7 @@ function Point.subtract(a, b)
end
-- Euclidian distance
function Point.pythagoreanDistance(a, b)
function Point.distance(a, b)
return math.sqrt(
math.pow(a.x - b.x, 2) +
math.pow(a.y - b.y, 2) +
@@ -57,28 +99,20 @@ function Point.calculateTurns(ih, oh)
end
function Point.calculateHeading(pta, ptb)
local heading
local xd, zd = pta.x - ptb.x, pta.z - ptb.z
if (pta.heading % 2) == 0 and pta.z ~= ptb.z then
if ptb.z > pta.z 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
if (pta.heading % 2) == 0 and zd ~= 0 then
heading = zd < 0 and 1 or 3
elseif (pta.heading % 2) == 1 and xd ~= 0 then
heading = xd < 0 and 0 or 2
elseif pta.heading == 0 and xd > 0 then
heading = 2
elseif pta.heading == 2 and pta.x < ptb.x then
elseif pta.heading == 2 and xd < 0 then
heading = 0
elseif pta.heading == 1 and pta.z > ptb.z then
elseif pta.heading == 1 and zd > 0 then
heading = 3
elseif pta.heading == 3 and pta.z < ptb.z then
elseif pta.heading == 3 and zd < 0 then
heading = 1
end
@@ -122,19 +156,25 @@ end
-- given a set of points, find the one taking the least moves
function Point.closest(reference, pts)
local lpt, lm -- lowest
if #pts == 1 then
return pts[1]
end
local lm, lpt = math.huge
for _,pt in pairs(pts) do
local m = Point.calculateMoves(reference, pt)
if not lm or m < lm then
lpt = pt
lm = m
local distance = Point.turtleDistance(reference, pt)
if distance < lm then
local m = Point.calculateMoves(reference, pt, distance)
if m < lm then
lpt = pt
lm = m
end
end
end
return lpt
end
function Point.eachClosest(spt, ipts, fn)
local pts = Util.shallowCopy(ipts)
while #pts > 0 do
local pt = Point.closest(spt, pts)
@@ -149,13 +189,87 @@ end
function Point.adjacentPoints(pt)
local pts = { }
for _, hi in pairs(turtle.getHeadings()) do
for i = 0, 5 do
local hi = Point.headings[i]
table.insert(pts, { x = pt.x + hi.xd, y = pt.y + hi.yd, z = pt.z + hi.zd })
end
return pts
end
-- get the point nearest A that is in the direction of B
function Point.nearestTo(pta, ptb)
local heading
if pta.x < ptb.x then
heading = 0
elseif pta.z < ptb.z then
heading = 1
elseif pta.x > ptb.x then
heading = 2
elseif pta.z > ptb.z then
heading = 3
elseif pta.y < ptb.y then
heading = 4
elseif pta.y > ptb.y then
heading = 5
end
if heading then
return {
x = pta.x + Point.headings[heading].xd,
y = pta.y + Point.headings[heading].yd,
z = pta.z + Point.headings[heading].zd,
}
end
return pta -- error ?
end
function Point.rotate(pt, facing)
local x, z = pt.x, pt.z
if facing == 1 then
pt.x = z
pt.z = -x
elseif facing == 2 then
pt.x = -x
pt.z = -z
elseif facing == 3 then
pt.x = -z
pt.z = x
end
end
function Point.makeBox(pt1, pt2)
return {
x = pt1.x,
y = pt1.y,
z = pt1.z,
ex = pt2.x,
ey = pt2.y,
ez = pt2.z,
}
end
-- expand box to include point
function Point.expandBox(box, pt)
if pt.x < box.x then
box.x = pt.x
elseif pt.x > box.ex then
box.ex = pt.x
end
if pt.y < box.y then
box.y = pt.y
elseif pt.y > box.ey then
box.ey = pt.y
end
if pt.z < box.z then
box.z = pt.z
elseif pt.z > box.ez then
box.ez = pt.z
end
end
function Point.normalizeBox(box)
return {
x = math.min(box.x, box.ex),
@@ -176,33 +290,17 @@ function Point.inBox(pt, box)
pt.z <= box.ez
end
return Point
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
--[[
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
return cpt
end
--]]
return Point

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,17 @@
requireInjector(getfenv(1))
_G.requireInjector()
local Event = require('event')
local Socket = require('socket')
local UI = require('ui')
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')
UI:configure('Network', ...)
@@ -22,10 +29,18 @@ end
local page = UI.Page {
menuBar = UI.MenuBar {
buttons = {
{ text = 'Telnet', event = 'telnet' },
{ text = 'VNC', event = 'vnc' },
{ text = 'Trust', event = 'trust' },
{ text = 'Reboot', event = 'reboot' },
{ text = 'Connect', dropdown = {
{ text = 'Telnet t', event = 'telnet' },
{ text = 'VNC v', event = 'vnc' },
UI.MenuBar.spacer,
{ 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 {
@@ -37,13 +52,15 @@ local page = UI.Page {
},
notification = UI.Notification { },
accelerators = {
t = 'telnet',
v = 'vnc',
r = 'reboot',
q = 'quit',
c = 'clear',
},
}
local function sendCommand(host, command)
if not device.wireless_modem then
page.notification:error('Wireless modem not present')
return
@@ -65,7 +82,7 @@ end
function page:eventHandler(event)
local t = self.grid:getSelected()
if t then
if event.type == 'telnet' or event.type == 'grid_select' then
if event.type == 'telnet' then
multishell.openTab({
path = 'sys/apps/telnet.lua',
focused = true,
@@ -79,20 +96,69 @@ function page:eventHandler(event)
args = { t.id },
title = t.label,
})
elseif event.type == 'clear' then
Util.clear(network)
page.grid:update()
page.grid:draw()
elseif event.type == 'trust' then
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
sendCommand(t.id, 'reboot')
elseif event.type == 'shutdown' then
sendCommand(t.id, 'shutdown')
end
end
if event.type == 'quit' then
if event.type == 'help' 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()
end
UI.Page.eventHandler(self, event)
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)
if not row.active then
return colors.orange
@@ -124,14 +190,14 @@ Event.onInterval(1, function()
page:sync()
end)
Event.on('device_attach', function(h, deviceName)
Event.on('device_attach', function(_, deviceName)
if deviceName == 'wireless_modem' then
page.notification:success('Modem connected')
page:sync()
end
end)
Event.on('device_detach', function(h, deviceName)
Event.on('device_detach', function(_, deviceName)
if deviceName == 'wireless_modem' then
page.notification:error('Wireless modem not attached')
page:sync()

View File

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

View File

@@ -1,9 +1,17 @@
requireInjector(getfenv(1))
_G.requireInjector()
local Config = require('config')
local Event = require('event')
local UI = require('ui')
local Util = require('util')
local Config = require('config')
local Security = require('security')
local SHA1 = require('sha1')
local UI = require('ui')
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')
UI:configure('System', ...)
@@ -11,7 +19,7 @@ UI:configure('System', ...)
local env = {
path = shell.path(),
aliases = shell.aliases(),
lua_path = LUA_PATH,
lua_path = _ENV.LUA_PATH,
}
Config.load('shell', env)
@@ -30,7 +38,6 @@ local systemPage = UI.Page {
},
grid = UI.Grid {
y = 4,
values = paths,
disableHeader = true,
columns = { { key = 'value' } },
autospace = true,
@@ -38,7 +45,7 @@ local systemPage = UI.Page {
},
aliasTab = UI.Window {
tabTitle = 'Aliases',
tabTitle = 'Alias',
alias = UI.TextEntry {
x = 2, y = 2, ex = -2,
limit = 32,
@@ -54,8 +61,6 @@ local systemPage = UI.Page {
},
grid = UI.Grid {
y = 5,
values = aliases,
autospace = true,
sortColumn = 'alias',
columns = {
{ heading = 'Alias', key = 'alias' },
@@ -67,6 +72,37 @@ 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 {
tabTitle = 'Info',
labelText = UI.Text {
@@ -87,12 +123,12 @@ local systemPage = UI.Page {
{ name = '', value = '' },
{ name = 'CC version', value = Util.getVersion() },
{ name = 'Lua version', value = _VERSION },
{ name = 'MC version', value = _MC_VERSION or 'unknown' },
{ name = 'MC version', value = Util.getMinecraftVersion() },
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
{ name = 'Day', value = tostring(os.day()) },
},
selectable = false,
inactive = true,
columns = {
{ key = 'name', width = 12 },
{ key = 'value' },
@@ -106,6 +142,99 @@ 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()
self.values = { }
for _,v in ipairs(Util.split(env.path, '(.-):')) do
@@ -116,7 +245,6 @@ function systemPage.tabs.pathTab.grid:draw()
end
function systemPage.tabs.pathTab:eventHandler(event)
if event.type == 'update_path' then
env.path = self.entry.value
self.grid:setIndex(self.grid:getIndex())
@@ -129,7 +257,6 @@ end
function systemPage.tabs.aliasTab.grid:draw()
self.values = { }
local aliases = { }
for k,v in pairs(env.aliases) do
table.insert(self.values, { alias = k, path = v })
end
@@ -138,7 +265,6 @@ function systemPage.tabs.aliasTab.grid:draw()
end
function systemPage.tabs.aliasTab:eventHandler(event)
if event.type == 'delete_alias' then
env.aliases[self.grid:getSelected().alias] = nil
self.grid:setIndex(self.grid:getIndex())
@@ -159,6 +285,22 @@ function systemPage.tabs.aliasTab:eventHandler(event)
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)
if event.type == 'update_label' then
os.setComputerLabel(self.label.value)
@@ -168,9 +310,8 @@ function systemPage.tabs.infoTab:eventHandler(event)
end
function systemPage:eventHandler(event)
if event.type == 'quit' then
Event.exitPullEvents()
UI:exitPullEvents()
elseif event.type == 'tab_activate' then
event.activated:focusFirst()
else
@@ -180,5 +321,4 @@ function systemPage:eventHandler(event)
end
UI:setPage(systemPage)
Event.pullEvents()
UI.term:reset()
UI:pullEvents()

View File

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

View File

@@ -1,43 +1,54 @@
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
if not os.getComputerLabel() then
local id = os.getComputerID()
if turtle then
if _G.turtle then
os.setComputerLabel('turtle_' .. id)
elseif pocket then
elseif _G.pocket then
os.setComputerLabel('pocket_' .. id)
elseif commands then
elseif _G.commands then
os.setComputerLabel('command_' .. id)
else
os.setComputerLabel('computer_' .. id)
end
end
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
if Util.getVersion() >= 1.76 then
closeInd = '\215'
end
@@ -59,7 +70,6 @@ local config = {
focusBackgroundColor = colors.gray,
},
}
Config.load('multishell', config)
local _colors = config.standard
@@ -69,92 +79,11 @@ end
local function redrawMenu()
if not tabsDirty then
os.queueEvent('multishell', 'draw')
os.queueEvent('multishell_redraw')
tabsDirty = true
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)
if not tab or coroutine.status(tab.co) == 'dead' then
return
@@ -179,18 +108,50 @@ local function resumeTab(tab, event, eventData)
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()
_tabId = _tabId + 1
return _tabId
end
local function launchProcess(tab)
tab.tabId = nextTabId()
tab.timestamp = os.clock()
tab.window = window.create(parentTerm, 1, 2, w, h - 1, false)
tab.terminal = tab.window
tab.env = Util.shallowCopy(tab.env or defaultEnv)
tab.env = Util.shallowCopy(tab.env or sandboxEnv)
tab.co = coroutine.create(function()
@@ -213,9 +174,6 @@ local function launchProcess(tab)
while true do
local e, code = os.pullEventRaw('key')
if e == 'terminate' or e == 'key' and code == keys.enter then
if tab.isOverview then
os.queueEvent('multishell', 'terminate')
end
break
end
end
@@ -225,78 +183,20 @@ local function launchProcess(tab)
local previousTab
if tab.previousTabId then
previousTab = tabs[tab.previousTabId]
if previousTab and previousTab.hidden then
previousTab = nil
end
end
selectTab(previousTab)
end
redrawMenu()
saveSession()
end)
tabs[tab.tabId] = tab
resumeTab(tab)
return tab
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)
hotkeys[code] = fn
end
@@ -326,10 +226,12 @@ function multishell.getTitle(tabId)
end
end
function multishell.setTitle(tabId, sTitle)
function multishell.setTitle(tabId, title)
local tab = tabs[tabId]
if tab then
tab.title = sTitle or ''
if not tab.isOverview then
tab.title = title or ''
end
redrawMenu()
end
end
@@ -345,23 +247,7 @@ function multishell.getTab(tabId)
end
function 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
os.queueEvent('multishell_terminate', tabId)
end
function multishell.getTabs()
@@ -378,11 +264,9 @@ function multishell.launch( tProgramEnv, sProgramPath, ... )
end
function multishell.openTab(tab)
if not tab.title and tab.path then
tab.title = fs.getName(tab.path)
end
tab.title = tab.title or 'untitled'
local previousTerm = term.current()
@@ -399,10 +283,6 @@ function multishell.openTab(tab)
redrawMenu()
end
if not tab.hidden then
saveSession()
end
return tab.tabId
end
@@ -410,6 +290,9 @@ function multishell.hideTab(tabId)
local tab = tabs[tabId]
if tab then
tab.hidden = true
if currentTab.tabId == tabId then
selectTab(tabs[currentTab.previousTabId])
end
redrawMenu()
end
end
@@ -423,202 +306,311 @@ function multishell.unhideTab(tabId)
end
function multishell.getCount()
local count
for _,tab in pairs(tabs) do
count = count + 1
end
return count
return Util.size(tabs)
end
-- control-o - overview
multishell.addHotkey(24, function()
multishell.setFocus(overviewTab.tabId)
end)
-- control-backspace
multishell.addHotkey(14, function()
local tabId = multishell.getFocus()
local tab = tabs[tabId]
if not tab.isOverview then
os.queueEvent('multishell', 'terminateTab', tabId)
tab = Util.shallowCopy(tab)
tab.isDead = false
tab.focused = true
multishell.openTab(tab)
function multishell.hook(event, fn)
if type(event) == 'table' then
for _,v in pairs(event) do
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)
-- control-tab - next tab
multishell.addHotkey(15, function()
multishell.hook('multishell_redraw', function()
tabsDirty = false
local function write(x, text, bg, fg)
parentTerm.setBackgroundColor(bg)
parentTerm.setTextColor(fg)
parentTerm.setCursorPos(x, 1)
parentTerm.write(text)
end
local bg = _colors.tabBarBackgroundColor
parentTerm.setBackgroundColor(bg)
parentTerm.setCursorPos(1, 1)
parentTerm.clearLine()
local function compareTab(a, b)
return a.tabId < b.tabId
end
local visibleTabs = { }
for _,tab in Util.spairs(tabs, compareTab) do
if not tab.hidden then
table.insert(visibleTabs, tab)
for _,tab in pairs(tabs) do
if tab.hidden and tab ~= currentTab then
tab.width = 0
else
tab.width = #tab.title + 1
end
end
for k,tab in ipairs(visibleTabs) do
if tab.tabId == currentTab.tabId then
if k < #visibleTabs then
multishell.setFocus(visibleTabs[k + 1].tabId)
return
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
if tab.width > 0 then
tab.sx = tabX + 1
tab.ex = tabX + tab.width
tabX = tabX + tab.width
if tab ~= currentTab then
write(tab.sx, tab.title:sub(1, tab.width - 1),
_colors.backgroundColor, _colors.textColor)
end
end
end
if #visibleTabs > 0 then
multishell.setFocus(visibleTabs[1].tabId)
end
end)
local function startup()
local hasError
local session = Util.readTable(SESSION_FILE)
local overviewId = multishell.openTab({
path = 'sys/apps/Overview.lua',
focused = true,
hidden = true,
isOverview = true,
})
overviewTab = tabs[overviewId]
if not Opus.loadServices() then
hasError = true
end
if not Opus.autorun() then
hasError = true
end
if session then
for _,v in pairs(session) do
multishell.openTab(v)
if currentTab then
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
if hasError then
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)
local function startup()
local success = true
local function runDir(directory, open)
if not fs.exists(directory) then
return true
end
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
_G.printError(err)
end
success = false
end
print()
end
end
runDir('sys/services', shell.openHiddenTab)
runDir('sys/autorun', shell.run)
runDir('usr/autorun', shell.run)
if not success then
print()
error('An autorun program has errored')
end
end
-- Begin
parentTerm.clear()
overviewId = multishell.openTab({
path = 'sys/apps/Overview.lua',
isOverview = true,
})
tabs[overviewId].title = '+'
multishell.openTab({
focused = true,
fn = startup,
env = defaultEnv,
title = 'Autorun',
})
if not overviewTab or coroutine.status(overviewTab.co) == 'dead' then
--error('Overview aborted')
end
if not currentTab then
multishell.setFocus(overviewTab.tabId)
end
draw()
local lastClicked
local currentTabEvents = Util.transpose {
'char', 'key', 'key_up',
'mouse_click', 'mouse_drag', 'mouse_scroll', 'mouse_up',
'paste', 'terminate',
}
while true do
-- Get the event
local tEventData = { os.pullEventRaw() }
local sEvent = table.remove(tEventData, 1)
local stopPropagation
if sEvent == 'key_up' then
processKeyEvent(sEvent, tEventData[1])
local eventHooks = hooks[sEvent]
if eventHooks then
for i = #eventHooks, 1, -1 do
stopPropagation = eventHooks[i](sEvent, tEventData)
if stopPropagation then
break
end
end
end
if sEvent == "term_resize" then
-- Resize event
w,h = parentTerm.getSize()
resizeWindows()
redrawMenu()
if not stopPropagation then
if currentTabEvents[sEvent] then
resumeTab(currentTab, sEvent, tEventData)
elseif sEvent == 'multishell' then
local action = tEventData[1]
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
else
-- Passthrough to all processes
for _,key in pairs(Util.keys(tabs)) do
resumeTab(tabs[key], sEvent, tEventData)
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

View File

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

View File

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

View File

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

View File

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

View File

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

21
sys/autorun/clipboard.lua Normal file
View File

@@ -0,0 +1,21 @@
_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)

56
sys/autorun/hotkeys.lua Normal file
View File

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

View File

@@ -1,37 +1,15 @@
local pullEvent = os.pullEventRaw
local redirect = term.redirect
local current = term.current
local shutdown = os.shutdown
local cos = { }
os.pullEventRaw = function(...)
local co = coroutine.running()
if not cos[co] then
cos[co] = true
error('die')
end
return pullEvent(...)
end
os.shutdown = function()
end
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
os.pullEventRaw = function()
error('die')
end
os.shutdown = function()
os.pullEventRaw = pullEvent
os.shutdown = shutdown
os.run(getfenv(1), 'sys/boot/multishell.boot')
end
os.queueEvent('modem_message')

View File

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

View File

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

View File

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

View File

@@ -1,58 +0,0 @@
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,24 +1,31 @@
if not turtle or turtle.getPoint then
if not _G.turtle then
return
end
requireInjector(getfenv(1))
_G.requireInjector()
local Pathing = require('turtle.pathfind')
local GPS = require('gps')
local Point = require('point')
local synchronized = require('sync')
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 headings = Point.headings
local state = { }
turtle.pathfind = Pathing.pathfind
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.getState() return state end
function turtle.getPoint() return turtle.point 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)
while not action.move() do
@@ -41,14 +48,13 @@ function turtle.setPoint(pt, isGPS)
end
function turtle.resetState()
--turtle.abort = false -- should be part of state
--turtle.status = 'idle' -- should be part of state
state.abort = false
state.status = 'idle'
state.attackPolicy = noop
state.digPolicy = noop
state.movePolicy = _defaultMove
state.moveCallback = noop
Pathing.reset()
return true
end
@@ -56,13 +62,10 @@ function turtle.reset()
turtle.point.x = 0
turtle.point.y = 0
turtle.point.z = 0
turtle.point.heading = 0
turtle.point.heading = 0 -- should be facing
turtle.point.gps = false
turtle.abort = false -- should be part of state
--turtle.status = 'idle' -- should be part of state
turtle.resetState()
return true
end
@@ -121,31 +124,7 @@ function turtle.getAction(direction)
return actions[direction]
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)
if heading and type(heading) == 'string' then
return namedHeadings[heading]
end
heading = heading or turtle.point.heading
return headings[heading]
end
@@ -174,6 +153,7 @@ local function inventoryAction(fn, name, qty)
return s
end
-- [[ Attack ]] --
local function _attack(action)
if action.attack() then
repeat until not action.attack()
@@ -182,10 +162,21 @@ local function _attack(action)
return false
end
turtle.attackPolicies = {
none = noop,
attack = function(action)
return _attack(action)
end,
}
function turtle.attack() return _attack(actions.forward) end
function turtle.attackUp() return _attack(actions.up) end
function turtle.attackDown() return _attack(actions.down) end
function turtle.setAttackPolicy(policy) state.attackPolicy = policy end
-- [[ Place ]] --
local function _place(action, indexOrId)
local slot
@@ -238,25 +229,11 @@ function turtle.refuel(qtyOrName, qty)
return inventoryAction(turtle.native.refuel, qtyOrName, qty or 64)
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)
local sideType = peripheral.getType(side)
return sideType and sideType == 'turtle'
end
turtle.attackPolicies = {
none = noop,
attack = function(action)
return _attack(action)
end,
}
turtle.digPolicies = {
none = noop,
@@ -302,13 +279,13 @@ turtle.movePolicies = {
if action.side == 'back' then
return false
end
local oldStatus = turtle.status
local oldStatus = state.status
print('assured move: stuck')
turtle.status = 'stuck'
state.status = 'stuck'
repeat
os.sleep(1)
until _defaultMove(action)
turtle.status = oldStatus
state.status = oldStatus
end
return true
end,
@@ -351,7 +328,6 @@ function turtle.setPolicy(...)
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.clearMoveCallback() state.moveCallback = noop end
function turtle.getMoveCallback() return state.moveCallback end
@@ -362,35 +338,31 @@ function turtle.getHeading()
end
function turtle.turnRight()
turtle.setHeading(turtle.point.heading + 1)
turtle.setHeading((turtle.point.heading + 1) % 4)
return turtle.point
end
function turtle.turnLeft()
turtle.setHeading(turtle.point.heading - 1)
turtle.setHeading((turtle.point.heading - 1) % 4)
return turtle.point
end
function turtle.turnAround()
turtle.setHeading(turtle.point.heading + 2)
turtle.setHeading((turtle.point.heading + 2) % 4)
return turtle.point
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)
if not heading then
return
return false, 'Invalid heading'
end
heading = heading % 4
local fi = Point.facings[heading]
if not fi then
return false, 'Invalid heading'
end
heading = fi.heading % 4
if heading ~= turtle.point.heading then
while heading < turtle.point.heading do
heading = heading + 4
@@ -478,8 +450,7 @@ function turtle.back()
end
end
function turtle.moveTowardsX(dx)
local function moveTowardsX(dx)
local direction = dx - turtle.point.x
local move
@@ -502,8 +473,7 @@ function turtle.moveTowardsX(dx)
return true
end
function turtle.moveTowardsZ(dz)
local function moveTowardsZ(dz)
local direction = dz - turtle.point.z
local move
@@ -528,13 +498,14 @@ end
-- [[ go ]] --
-- 1 turn goto (going backwards if possible)
function turtle.gotoSingleTurn(dx, dz, dy, dh)
function turtle.gotoSingleTurn(dx, dy, dz, dh)
dx = dx or turtle.point.x
dy = dy or turtle.point.y
dz = dz or turtle.point.z
local function gx()
if turtle.point.x ~= dx then
turtle.moveTowardsX(dx)
moveTowardsX(dx)
end
if turtle.point.z ~= dz then
if dh and dh % 2 == 1 then
@@ -547,7 +518,7 @@ function turtle.gotoSingleTurn(dx, dz, dy, dh)
local function gz()
if turtle.point.z ~= dz then
turtle.moveTowardsZ(dz)
moveTowardsZ(dz)
end
if turtle.point.x ~= dx then
if dh and dh % 2 == 0 then
@@ -587,8 +558,7 @@ function turtle.gotoSingleTurn(dx, dz, dy, dh)
return false
end
local function gotoEx(dx, dz, dy)
local function gotoEx(dx, dy, dz)
-- determine the heading to ensure the least amount of turns
-- first check is 1 turn needed - remaining require 2 turns
if turtle.point.heading == 0 and turtle.point.x <= dx or
@@ -622,9 +592,8 @@ local function gotoEx(dx, dz, dy)
end
-- fallback goto - will turn around if was previously moving backwards
local function gotoMultiTurn(dx, dz, dy)
if gotoEx(dx, dz, dy) then
local function gotoMultiTurn(dx, dy, dz)
if gotoEx(dx, dy, dz) then
return true
end
@@ -653,19 +622,20 @@ local function gotoMultiTurn(dx, dz, dy)
return false
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
function turtle.goback()
local hi = headings[turtle.point.heading]
return turtle.goto(turtle.point.x - hi.xd, turtle.point.z - hi.zd, turtle.point.y, turtle.point.heading)
return turtle._goto({
x = turtle.point.x - hi.xd,
y = turtle.point.y,
z = turtle.point.z - hi.zd,
heading = turtle.point.heading,
})
end
function turtle.gotoYfirst(pt)
if turtle.gotoY(pt.y) then
if turtle.goto(pt.x, pt.z, nil, pt.heading) then
if turtle._gotoY(pt.y) then
if turtle._goto(pt) then
turtle.setHeading(pt.heading)
return true
end
@@ -673,7 +643,7 @@ function turtle.gotoYfirst(pt)
end
function turtle.gotoYlast(pt)
if turtle.goto(pt.x, pt.z, nil, pt.heading) then
if turtle._goto({ x = pt.x, z = pt.z, heading = pt.heading }) then
if turtle.gotoY(pt.y) then
turtle.setHeading(pt.heading)
return true
@@ -681,9 +651,10 @@ function turtle.gotoYlast(pt)
end
end
function turtle.goto(dx, dz, dy, dh)
if not turtle.gotoSingleTurn(dx, dz, dy, dh) then
if not gotoMultiTurn(dx, dz, dy) then
function turtle._goto(pt)
local dx, dy, dz, dh = pt.x, pt.y, pt.z, pt.heading
if not turtle.gotoSingleTurn(dx, dy, dz, dh) then
if not gotoMultiTurn(dx, dy, dz) then
return false
end
end
@@ -691,6 +662,9 @@ function turtle.goto(dx, dz, dy, dh)
return true
end
-- avoid lint errors
turtle['goto'] = turtle._goto
function turtle.gotoX(dx)
turtle.headTowardsX(dx)
@@ -730,7 +704,6 @@ end
-- [[ Slot management ]] --
function turtle.getSlot(indexOrId, slots)
if type(indexOrId) == 'string' then
slots = slots or turtle.getInventory()
local _,c = string.gsub(indexOrId, ':', '')
@@ -766,7 +739,6 @@ function turtle.getSlot(indexOrId, slots)
end
function turtle.select(indexOrId)
if type(indexOrId) == 'number' then
return turtle.native.select(indexOrId)
end
@@ -814,6 +786,11 @@ function turtle.getSummedInventory()
return t
end
function turtle.has(item, count)
local slot = turtle.getSummedInventory()[item]
return slot and slot.count >= (count or 1)
end
function turtle.getFilledSlots(startSlot)
startSlot = startSlot or 1
@@ -917,7 +894,6 @@ function turtle.getItemCount(idOrName)
end
function turtle.equip(side, item)
if item then
if not turtle.select(item) then
return false, 'Unable to equip ' .. item
@@ -930,6 +906,15 @@ function turtle.equip(side, item)
return turtle.equipRight()
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, ...)
local args = { ... }
local s, m
@@ -939,36 +924,47 @@ function turtle.run(fn, ...)
end
synchronized(turtle, function()
turtle.abort = false
turtle.status = 'busy'
turtle.resetState()
s, m = pcall(function() fn(unpack(args)) end)
turtle.abort = false
turtle.status = 'idle'
turtle.resetState()
if not s and m then
printError(m)
_G.printError(m)
end
end)
return s, m
end
function turtle.abortAction()
if turtle.status ~= 'idle' then
turtle.abort = true
function turtle.abort(abort)
state.abort = abort
if abort then
os.queueEvent('turtle_abort')
end
end
-- [[ Pathing ]] --
function turtle.faceAgainst(pt, options) -- 4 sided
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
options = options or { }
options.dest = { }
for i = 0, 3 do
local hi = turtle.getHeadingInfo(i)
local hi = Point.facings[i]
table.insert(options.dest, {
x = pt.x + hi.xd,
z = pt.z + hi.zd,
@@ -980,8 +976,11 @@ function turtle.faceAgainst(pt, options) -- 4 sided
return turtle.pathfind(Point.closest(turtle.point, options.dest), options)
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
options = options or { }
options.dest = { }
@@ -1002,7 +1001,7 @@ function turtle.moveAgainst(pt, options) -- 6 sided
z = pt.z + hi.zd,
y = pt.y + hi.yd,
direction = direction,
heading = heading,
heading = pt.heading or heading,
})
end
@@ -1057,67 +1056,114 @@ 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 pt = turtle.moveAgainst(pt)
if pt then
return action[pt.direction](...)
if not pt.heading and not pt.direction then
local msg
pt, msg = turtle.moveAgainst(pt)
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
function _actionDownAt(action, pt, ...)
if turtle.pathfind(Point.above(pt)) then
return action.down(...)
end
local function _actionDownAt(action, pt, ...)
pt = Util.shallowCopy(pt)
pt.direction = Point.DOWN
return _actionAt(action, pt, ...)
end
function _actionForwardAt(action, pt, ...)
local function _actionUpAt(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
return action.forward(...)
end
end
function _actionUpAt(action, pt, ...)
if turtle.pathfind(Point.below(pt)) then
return action.up(...)
function turtle.detectAt(pt) return _actionAt(actionsAt.detect, pt) end
function turtle.detectDownAt(pt) return _actionDownAt(actionsAt.detect, pt) end
function turtle.detectForwardAt(pt) return _actionForwardAt(actionsAt.detect, pt) end
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
function turtle.detectAt(pt) return _actionAt(actionsAt.detect, pt) end
function turtle.detectDownAt(pt) return _actionDownAt(actionsAt.detect, pt) end
function turtle.detectForwardAt(pt) return _actionForwardAt(actionsAt.detect, pt) end
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) 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
function turtle.addFeatures(...)
for _,feature in pairs({ ... }) do
require('turtle.' .. feature)
end
end

View File

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

View File

@@ -0,0 +1,70 @@
--[[
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)

35
sys/network/proxy.lua Normal file
View File

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

View File

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

View File

@@ -1,9 +1,15 @@
requireInjector(getfenv(1))
_G.requireInjector()
local Event = require('event')
local Peripheral = require('peripheral')
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')
local attachColor = colors.green
@@ -14,7 +20,7 @@ if not term.isColor() then
detachColor = colors.lightGray
end
Event.on('peripheral', function(event, side)
Event.on('peripheral', function(_, side)
if side then
local dev = Peripheral.addDevice(device, side)
if dev then
@@ -25,7 +31,7 @@ Event.on('peripheral', function(event, side)
end
end)
Event.on('peripheral_detach', function(event, side)
Event.on('peripheral_detach', function(_, side)
if side then
local dev = Util.find(device, 'side', side)
if dev then

View File

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

View File

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

View File

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

View File

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