Initial commit

This commit is contained in:
kepler155c@gmail.com 2016-12-11 14:24:52 -05:00
commit fc243a9c12
110 changed files with 25577 additions and 0 deletions

1
README.md Normal file
View File

@ -0,0 +1 @@
# test

386
apps/Appstore.lua Normal file
View File

@ -0,0 +1,386 @@
require = requireInjector(getfenv(1))
local Util = require('util')
local class = require('class')
local UI = require('ui')
local Event = require('event')
local sandboxEnv = Util.shallowCopy(getfenv(1))
setmetatable(sandboxEnv, { __index = _G })
multishell.setTitle(multishell.getCurrent(), 'App Store')
UI:configure('Appstore', ...)
local sources = {
{ text = "STD Default",
event = 'source',
url = "http://pastebin.com/raw/zVws7eLq" }, --stock
{ text = "Discover",
event = 'source',
generateName = true,
url = "http://pastebin.com/raw/9bXfCz6M" }, --owned by dannysmc95
{ text = "Opus",
event = 'source',
url = "http://pastebin.com/raw/ajQ91Rmn" },
}
shell.setDir('/apps')
function downloadApp(app)
local h
if type(app.url) == "table" then
h = contextualGet(app.url[1])
else
h = http.get(app.url)
end
if h then
local contents = h.readAll()
h:close()
return contents
end
end
function runApp(app, checkExists, ...)
local path, fn
local args = { ... }
if checkExists and fs.exists(fs.combine('/apps', app.name)) then
path = fs.combine('/apps', app.name)
else
local program = downloadApp(app)
fn = function()
if not program then
error('Failed to download')
end
local fn = loadstring(program, app.name)
if not fn then
error('Failed to download')
end
setfenv(fn, sandboxEnv)
fn(unpack(args))
end
end
multishell.openTab({
title = app.name,
env = sandboxEnv,
path = path,
fn = fn,
focused = true,
})
return true, 'Running program'
end
local installApp = function(app)
local program = downloadApp(app)
if not program then
return false, "Failed to download"
end
local fullPath = fs.combine('/apps', app.name)
Util.writeFile(fullPath, program)
return true, 'Installed as ' .. fullPath
end
local viewApp = function(app)
local program = downloadApp(app)
if not program then
return false, "Failed to download"
end
Util.writeFile('/.source', program)
shell.openForegroundTab('edit /.source')
return true
end
local getSourceListing = function(source)
local contents = http.get(source.url)
if contents then
local fn = loadstring(contents.readAll(), source.text)
contents.close()
local env = { std = { } }
setmetatable(env, { __index = _G })
setfenv(fn, env)
fn()
if env.contextualGet then
contextualGet = env.contextualGet
end
source.storeURLs = env.std.storeURLs
source.storeCatagoryNames = env.std.storeCatagoryNames
if source.storeURLs and source.storeCatagoryNames then
for k,v in pairs(source.storeURLs) do
if source.generateName then
v.name = v.title:match('(%w+)')
if not v.name or #v.name == 0 then
v.name = tostring(k)
else
v.name = v.name:lower()
end
else
v.name = k
end
v.categoryName = source.storeCatagoryNames[v.catagory]
v.ltitle = v.title:lower()
end
end
end
end
local appPage = UI.Page({
backgroundColor = UI.ViewportWindow.defaults.backgroundColor,
menuBar = UI.MenuBar({
showBackButton = not os.isPocket(),
buttons = {
{ text = 'Install', event = 'install' },
{ text = 'Run', event = 'run' },
{ text = 'View', event = 'view' },
{ text = 'Remove', event = 'uninstall', name = 'removeButton' },
},
}),
container = UI.Window({
x = 2,
y = 3,
height = UI.term.height - 3,
width = UI.term.width - 2,
viewport = UI.ViewportWindow(),
}),
notification = UI.Notification(),
accelerators = {
q = 'back',
backspace = 'back',
},
})
function appPage.container.viewport:draw()
local app = self.parent.parent.app
local str = string.format(
'By: %s\nCategory: %s\nFile name: %s\n\n%s',
app.creator, app.categoryName, app.name, app.description)
self:clear()
local y = self:wrappedWrite(1, 1, app.title, self.width, nil, colors.yellow)
self.height = self:wrappedWrite(1, y, str, self.width)
if appPage.notification.enabled then
appPage.notification:draw()
end
end
function appPage:enable(source, app)
self.source = source
self.app = app
UI.Page.enable(self)
self.container.viewport:setScrollPosition(0)
if fs.exists(fs.combine('/apps', app.name)) then
self.menuBar.removeButton:enable('Remove')
else
self.menuBar.removeButton:disable('Remove')
end
end
function appPage:eventHandler(event)
if event.type == 'back' then
UI:setPreviousPage()
elseif event.type == 'run' then
self.notification:info('Running program', 3)
self:sync()
runApp(self.app, true)
elseif event.type == 'view' then
self.notification:info('Downloading program', 3)
self:sync()
viewApp(self.app)
elseif event.type == 'uninstall' then
if self.app.runOnly then
s,m = runApp(self.app, false, 'uninstall')
else
fs.delete(fs.combine('/apps', self.app.name))
self.notification:success("Uninstalled " .. self.app.name, 3)
self:focusFirst(self)
self.menuBar.removeButton:disable('Remove')
self.menuBar:draw()
os.unregisterApp(fs.combine('/apps', self.app.name))
end
elseif event.type == 'install' then
self.notification:info("Installing", 3)
self:sync()
local s, m
if self.app.runOnly then
s,m = runApp(self.app, false)
else
s,m = installApp(self.app)
end
if s then
self.notification:success(m, 3)
if not self.app.runOnly then
self.menuBar.removeButton:enable('Remove')
self.menuBar:draw()
local category = 'Apps'
if self.app.catagoryName == 'Game' then
category = 'Games'
end
os.registerApp({
run = fs.combine('/apps', self.app.name),
title = self.app.title,
category = category,
icon = self.app.icon,
})
end
else
self.notification:error(m, 3)
end
else
return UI.Page.eventHandler(self, event)
end
return true
end
local categoryPage = UI.Page({
menuBar = UI.MenuBar({
buttons = {
{ text = 'Catalog', event = 'dropdown', dropdown = 'sourceMenu' },
{ text = 'Category', event = 'dropdown', dropdown = 'categoryMenu' },
},
}),
sourceMenu = UI.DropMenu({
buttons = sources,
}),
grid = UI.ScrollingGrid({
y = 2,
height = UI.term.height - 2,
columns = {
{ heading = 'Title', key = 'title' },
},
sortColumn = 'title',
autospace = true,
}),
statusBar = UI.StatusBar(),
accelerators = {
l = 'lua',
q = 'quit',
},
})
function categoryPage:setCategory(source, name, index)
self.grid.values = { }
for k,v in pairs(source.storeURLs) do
if index == 0 or index == v.catagory then
table.insert(self.grid.values, v)
end
end
self.statusBar:setStatus(string.format('%s: %s', source.text, name))
self.grid:update()
self.grid:setIndex(1)
end
function categoryPage:setSource(source)
if not source.categoryMenu then
self.statusBar:setStatus('Loading...')
self.statusBar:draw()
self:sync()
getSourceListing(source)
if not source.storeURLs then
error('Unable to download application list')
end
local buttons = { }
for k,v in Util.spairs(source.storeCatagoryNames,
function(a, b) return a:lower() < b:lower() end) do
if v ~= 'Operating System' then
table.insert(buttons, {
text = v,
event = 'category',
index = k,
})
end
end
source.categoryMenu = UI.DropMenu({
y = 2,
x = 1,
buttons = buttons,
})
source.index, source.name = Util.first(source.storeCatagoryNames)
categoryPage:add({
categoryMenu = source.categoryMenu
})
end
self.source = source
self.categoryMenu = source.categoryMenu
categoryPage:setCategory(source, source.name, source.index)
end
function categoryPage.grid:sortCompare(a, b)
return a.ltitle < b.ltitle
end
function categoryPage.grid:getRowTextColor(row, selected)
if fs.exists(fs.combine('/apps', row.name)) then
return colors.orange
end
return UI.Grid:getRowTextColor(row, selected)
end
function categoryPage:eventHandler(event)
if event.type == 'grid_select' or event.type == 'select' then
UI:setPage(appPage, self.source, self.grid:getSelected())
elseif event.type == 'category' then
self:setCategory(self.source, event.button.text, event.button.index)
self:setFocus(self.grid)
self:draw()
elseif event.type == 'source' then
self:setFocus(self.grid)
self:setSource(event.button)
self:draw()
elseif event.type == 'quit' then
Event.exitPullEvents()
else
return UI.Page.eventHandler(self, event)
end
return true
end
print("Retrieving catalog list")
categoryPage:setSource(sources[1])
UI:setPage(categoryPage)
Event.pullEvents()
UI.term:reset()

118
apps/Events.lua Normal file
View File

@ -0,0 +1,118 @@
require = requireInjector(getfenv(1))
local Event = require('event')
local UI = require('ui')
multishell.setTitle(multishell.getCurrent(), 'Events')
UI:configure('Events', ...)
local page = UI.Page({
menuBar = UI.MenuBar({
buttons = {
{ text = 'Filter', event = 'filter' },
{ text = 'Reset', event = 'reset' },
{ text = 'Pause ', event = 'toggle', name = 'pauseButton' },
},
}),
grid = UI.Grid({
y = 2,
columns = {
{ heading = 'Event', key = 'event' },
{ key = 'p1' },
{ key = 'p2' },
{ key = 'p3' },
{ key = 'p4' },
{ key = 'p5' },
},
autospace = true,
}),
accelerators = {
f = 'filter',
p = 'toggle',
r = 'reset',
c = 'clear',
q = 'quit',
},
filtered = { },
})
function page:eventHandler(event)
if event.type == 'filter' then
local entry = self.grid:getSelected()
self.filtered[entry.event] = true
elseif event.type == 'toggle' then
self.paused = not self.paused
if self.paused then
self.menuBar.pauseButton.text = 'Resume'
else
self.menuBar.pauseButton.text = 'Pause '
end
self.menuBar:draw()
elseif event.type == 'reset' then
self.filtered = { }
self.grid:setValues({ })
self.grid:draw()
if self.paused then
self:emit({ type = 'toggle' })
end
elseif event.type == 'clear' then
self.grid:setValues({ })
self.grid:draw()
elseif event.type == 'quit' then
Event.exitPullEvents()
elseif event.type == 'focus_change' then
if event.focused == self.grid then
if not self.paused then
self:emit({ type = 'toggle' })
end
end
else
return UI.Page.eventHandler(self, event)
end
return true
end
function page.grid:draw()
self:adjustWidth()
UI.Grid.draw(self)
end
function eventLoop()
local function tovalue(s)
if type(s) == 'table' then
return 'table'
end
return s
end
while true do
local e = { os.pullEvent() }
if not page.paused and not page.filtered[e[1]] then
table.insert(page.grid.values, 1, {
event = e[1],
p1 = tovalue(e[2]),
p2 = tovalue(e[3]),
p3 = tovalue(e[4]),
p4 = tovalue(e[5]),
p5 = tovalue(e[6]),
})
if #page.grid.values > page.grid.height - 1 then
table.remove(page.grid.values, #page.grid.values)
end
page.grid:update()
page.grid:draw()
page:sync()
end
end
end
UI:setPage(page)
Event.pullEvents(eventLoop)
UI.term:reset()

375
apps/Files.lua Normal file
View File

@ -0,0 +1,375 @@
require = requireInjector(getfenv(1))
local Util = require('util')
local Event = require('event')
local UI = require('ui')
local cleanEnv = Util.shallowCopy(getfenv(1))
cleanEnv.require = nil
multishell.setTitle(multishell.getCurrent(), 'Files')
UI:configure('Files', ...)
local copied = { }
local marked = { }
local directories = { }
local hidden = true
local cutMode = false
function formatSize(size)
if size >= 1000000 then
return string.format('%dM', math.floor(size/1000000, 2))
elseif size >= 1000 then
return string.format('%dK', math.floor(size/1000, 2))
end
return size
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' },
},
},
fileMenu = UI.DropMenu {
buttons = {
{ text = 'Run', event = 'run' },
{ text = 'Edit e', event = 'edit' },
{ text = 'Shell s', event = 'shell' },
{ text = 'Quit q', event = 'quit' },
}
},
editMenu = UI.DropMenu {
buttons = {
{ text = 'Mark m', event = 'mark' },
{ text = 'Cut ^x', event = 'cut' },
{ text = 'Copy ^c', event = 'copy' },
{ text = 'Paste ^v', event = 'paste' },
{ text = 'Delete del', event = 'delete' },
{ text = 'Unmark all u', event = 'unmark' },
}
},
viewMenu = UI.DropMenu {
buttons = {
{ text = 'Refresh r', event = 'refresh' },
{ text = 'Hidden ^h', event = 'toggle_hidden' },
}
},
grid = UI.ScrollingGrid {
columns = {
{ heading = 'Name', key = 'name', width = UI.term.width-11 },
{ key = 'flags', width = 2 },
{ heading = 'Size', key = 'fsize', width = 6 },
},
sortColumn = 'name',
y = 2,
height = UI.term.height-2,
},
statusBar = UI.StatusBar {
columns = {
{ '', 'status', UI.term.width - 19 },
{ '', 'info', 10 },
{ 'Size: ', 'totalSize', 8 },
},
},
accelerators = {
q = 'quit',
e = 'edit',
s = 'shell',
r = 'refresh',
space = 'mark',
backspace = 'updir',
m = 'move',
u = 'unmark',
d = 'delete',
delete = 'delete',
[ 'control-h' ] = 'toggle_hidden',
[ 'control-x' ] = 'cut',
[ 'control-c' ] = 'copy',
paste = 'paste',
},
}
function Browser:enable()
UI.Page.enable(self)
self:setFocus(self.grid)
end
function Browser.grid:sortCompare(a, b)
if self.sortColumn == 'fsize' then
return a.size < b.size
elseif self.sortColumn == 'flags' then
return a.flags < b.flags
end
if a.isDir == b.isDir then
return a.name:lower() < b.name:lower()
end
return a.isDir
end
function Browser.grid:getRowTextColor(file, selected)
if file.marked then
return colors.green
end
if file.isDir then
return colors.cyan
end
if file.isReadOnly then
return colors.pink
end
return colors.white
end
function Browser.grid:getRowBackgroundColorX(file, selected)
if selected then
return colors.gray
end
return self.backgroundColor
end
function Browser.statusBar:draw()
if self.parent.dir then
local info = '#:' .. Util.size(self.parent.dir.files)
local numMarked = Util.size(marked)
if numMarked > 0 then
info = info .. ' M:' .. numMarked
end
self:setValue('info', info)
self:setValue('totalSize', formatSize(self.parent.dir.totalSize))
UI.StatusBar.draw(self)
end
end
function Browser:setStatus(status, ...)
self.statusBar:timedStatus(string.format(status, ...))
end
function Browser:unmarkAll()
for k,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]
if not dir then
dir = {
name = directory,
size = 0,
files = { },
totalSize = 0,
index = 1
}
directories[directory] = dir
end
self:updateDirectory(dir)
return dir
end)
return s, dir
end
function Browser:updateDirectory(dir)
dir.size = 0
dir.totalSize = 0
Util.clear(dir.files)
local files = fs.list(dir.name, true)
if files then
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
file.fsize = formatSize(file.size)
else
file.flags = 'D'
end
if file.isReadOnly then
file.flags = file.flags .. 'R'
end
if not hidden or file.name:sub(1, 1) ~= '.' then
dir.files[file.fullName] = file
end
end
end
-- self.grid:update()
-- self.grid:setIndex(dir.index)
self.grid:setValues(dir.files)
end
function Browser:setDir(dirName, noStatus)
self:unmarkAll()
if self.dir then
self.dir.index = self.grid:getIndex()
end
DIR = fs.combine('', dirName)
shell.setDir(DIR)
local s, dir = self:getDirectory(DIR)
if s then
self.dir = dir
elseif noStatus then
error(dir)
else
self:setStatus(dir)
self:setDir('', true)
return
end
if not noStatus then
self.statusBar:setValue('status', '/' .. self.dir.name)
self.statusBar:draw()
end
self.grid:setIndex(self.dir.index)
end
function Browser:run(path, ...)
local tabId = multishell.launch(cleanEnv, path, ...)
multishell.setFocus(tabId)
end
function Browser:hasMarked()
if Util.size(marked) == 0 then
local file = self.grid:getSelected()
if file then
file.marked = true
marked[file.fullName] = file
self.grid:draw()
end
end
return Util.size(marked) > 0
end
function Browser:eventHandler(event)
local file = self.grid:getSelected()
if event.type == 'quit' then
Event.exitPullEvents()
elseif event.type == 'edit' and file then
self:run('/apps/shell', 'edit', file.name)
elseif event.type == 'shell' then
self:run('/apps/shell')
elseif event.type == 'refresh' then
self:updateDirectory(self.dir)
self.grid:draw()
self:setStatus('Refreshed')
elseif event.type == 'toggle_hidden' then
hidden = not hidden
self:updateDirectory(self.dir)
self.grid:draw()
if hidden then
self:setStatus('Hiding hidden')
else
self:setStatus('Displaying hidden')
end
elseif event.type == 'mark' and file then
file.marked = not file.marked
if file.marked then
marked[file.fullName] = file
else
marked[file.fullName] = nil
end
self.grid:draw()
self.statusBar:draw()
elseif event.type == 'unmark' then
self:unmarkAll()
self.grid:draw()
self:setStatus('Marked files cleared')
elseif event.type == 'grid_select' or event.type == 'run' then
if file then
if file.isDir then
self:setDir(file.fullName)
else
self:run('/apps/shell', file.name)
end
end
elseif event.type == 'updir' then
local dir = (self.dir.name:match("(.*/)"))
self:setDir(dir or '/')
elseif event.type == 'delete' then
if self:hasMarked() then
local width = self.statusBar:getColumnWidth('status')
self.statusBar:setColumnWidth('status', UI.term.width)
self.statusBar:setValue('status', 'Delete marked? (y/n)')
self.statusBar:draw()
self.statusBar:sync()
local _, ch = os.pullEvent('char')
if ch == 'y' or ch == 'Y' then
for k,m in pairs(marked) do
pcall(function()
fs.delete(m.fullName)
end)
end
end
marked = { }
self.statusBar:setColumnWidth('status', width)
self.statusBar:setValue('status', '/' .. self.dir.name)
self:updateDirectory(self.dir)
self.statusBar:draw()
self.grid:draw()
self:setFocus(self.grid)
end
elseif event.type == 'copy' or event.type == 'cut' then
if self:hasMarked() then
cutMode = event.type == 'cut'
Util.clear(copied)
Util.merge(copied, marked)
--self:unmarkAll()
self.grid:draw()
self:setStatus('Copied %d file(s)', Util.size(copied))
end
elseif event.type == 'paste' then
for k,m in pairs(copied) do
local s, m = pcall(function()
if cutMode then
fs.move(m.fullName, fs.combine(self.dir.name, m.name))
else
fs.copy(m.fullName, fs.combine(self.dir.name, m.name))
end
end)
end
self:updateDirectory(self.dir)
self.grid:draw()
self:setStatus('Pasted ' .. Util.size(copied) .. ' file(s)')
else
return UI.Page.eventHandler(self, event)
end
self:setFocus(self.grid)
return true
end
--[[-- Startup logic --]]--
local args = { ... }
Browser:setDir(args[1] or shell.dir())
UI:setPage(Browser)
Event.pullEvents()
UI.term:reset()

84
apps/Help.lua Normal file
View File

@ -0,0 +1,84 @@
require = requireInjector(getfenv(1))
local Event = require('event')
local UI = require('ui')
multishell.setTitle(multishell.getCurrent(), 'Help')
UI:configure('Help', ...)
local files = { }
for _,f in pairs(fs.list('/rom/help')) do
table.insert(files, { name = f })
end
local page = UI.Page({
labelText = UI.Text({
y = 2,
x = 3,
value = 'Search',
}),
filter = UI.TextEntry({
y = 2,
x = 10,
width = UI.term.width - 13,
limit = 32,
}),
grid = UI.ScrollingGrid({
y = 4,
height = UI.term.height - 4,
values = files,
columns = {
{ heading = 'Name', key = 'name', width = 12 },
},
sortColumn = 'name',
}),
statusBar = UI.StatusBar(),
accelerators = {
q = 'quit',
},
})
local function showHelp(name)
UI.term:reset()
shell.run('help ' .. name)
print('Press enter to return')
read()
end
function page:eventHandler(event)
if event.type == 'quit' then
Event.exitPullEvents()
elseif event.type == 'key' and event.key == 'enter' then
showHelp(self.grid:getSelected().name)
self:setFocus(self.filter)
self:draw()
elseif event.type == 'grid_select' then
showHelp(event.selected.name)
self:setFocus(self.filter)
self:draw()
elseif event.type == 'text_change' then
local text = event.text
if #text == 0 then
self.grid.values = files
else
self.grid.values = { }
for _,f in pairs(files) do
if string.find(f.name, text) then
table.insert(self.grid.values, f)
end
end
end
self.grid:update()
self.grid:setIndex(1)
self.grid:draw()
else
UI.Page.eventHandler(self, event)
end
end
UI:setPage(page)
Event.pullEvents()
UI.term:reset()

247
apps/Lua.lua Normal file
View File

@ -0,0 +1,247 @@
require = requireInjector(getfenv(1))
local Util = require('util')
local UI = require('ui')
local Event = require('event')
local History = require('history')
local sandboxEnv = Util.shallowCopy(getfenv(1))
sandboxEnv.exit = function() Event.exitPullEvents() end
sandboxEnv.require = requireInjector(sandboxEnv)
setmetatable(sandboxEnv, { __index = _G })
multishell.setTitle(multishell.getCurrent(), 'Lua')
UI:configure('Lua', ...)
local command = ''
local history = History.load('.lua_history', 25)
local resultsPage = UI.Page({
menuBar = UI.MenuBar({
buttons = {
{ text = 'Local', event = 'local' },
{ text = 'Global', event = 'global' },
{ text = 'Device', event = 'device' },
},
}),
prompt = UI.TextEntry({
y = 2,
shadowText = 'enter command',
backgroundFocusColor = colors.black,
limit = 256,
accelerators = {
enter = 'command_enter',
up = 'history_back',
down = 'history_forward',
mouse_rightclick = 'clear_prompt',
},
}),
grid = UI.ScrollingGrid({
y = 3,
columns = {
{ heading = 'Key', key = 'name' },
{ heading = 'Value', key = 'value' },
},
sortColumn = 'name',
autospace = true,
}),
notification = UI.Notification(),
})
function resultsPage:setPrompt(value, focus)
self.prompt:setValue(value)
self.prompt.scroll = 0
self.prompt:setPosition(#value)
self.prompt:updateScroll()
if value:sub(-1) == ')' then
self.prompt:setPosition(#value - 1)
end
self.prompt:draw()
if focus then
resultsPage:setFocus(self.prompt)
end
end
function resultsPage:enable()
self:setFocus(self.prompt)
UI.Page.enable(self)
end
function resultsPage:eventHandler(event)
if event.type == 'global' then
resultsPage:setPrompt('', true)
self:executeStatement('getfenv(0)')
command = nil
elseif event.type == 'local' then
resultsPage:setPrompt('', true)
self:executeStatement('getfenv(1)')
command = nil
elseif event.type == 'device' then
resultsPage:setPrompt('device', true)
self:executeStatement('device')
elseif event.type == 'history_back' then
local value = history.back()
if value then
self:setPrompt(value)
end
elseif event.type == 'history_forward' then
self:setPrompt(history.forward() or '')
elseif event.type == 'clear_prompt' then
self:setPrompt('')
history.setPosition(#history.entries + 1)
elseif event.type == 'command_enter' then
local s = tostring(self.prompt.value)
if #s > 0 then
history.add(s)
self:executeStatement(s)
else
local t = { }
for k = #history.entries, 1, -1 do
table.insert(t, {
name = #t + 1,
value = history.entries[k],
isHistory = true,
pos = k,
})
end
history.setPosition(#history.entries + 1)
command = nil
self.grid:setValues(t)
self.grid:setIndex(1)
self.grid:adjustWidth()
self:draw()
end
return true
else
return UI.Page.eventHandler(self, event)
end
return true
end
function resultsPage:setResult(result)
local t = { }
local function safeValue(v)
local t = type(v)
if t == 'string' or t == 'number' then
return v
end
return tostring(v)
end
if type(result) == 'table' then
for k,v in pairs(result) do
local entry = {
name = safeValue(k),
rawName = k,
value = safeValue(v),
rawValue = v,
}
if type(v) == 'table' then
if Util.size(v) == 0 then
entry.value = 'table: (empty)'
else
entry.value = 'table'
end
end
table.insert(t, entry)
end
else
table.insert(t, {
name = type(result),
value = tostring(result),
rawValue = result,
})
end
self.grid:setValues(t)
self.grid:setIndex(1)
self.grid:adjustWidth()
self:draw()
end
function resultsPage.grid:eventHandler(event)
local entry = self:getSelected()
local function commandAppend()
if entry.isHistory then
history.setPosition(entry.pos)
return entry.value
end
if type(entry.rawValue) == 'function' then
if command then
return command .. '.' .. entry.name .. '()'
end
return entry.name .. '()'
end
if command then
if type(entry.rawName) == 'number' then
return command .. '[' .. entry.name .. ']'
end
if entry.name:match("%W") or
entry.name:sub(1, 1):match("%d") then
return command .. "['" .. tostring(entry.name) .. "']"
end
return command .. '.' .. entry.name
end
return entry.name
end
if event.type == 'grid_focus_row' then
if self.focused then
resultsPage:setPrompt(commandAppend())
end
elseif event.type == 'grid_select' then
resultsPage:setPrompt(commandAppend(), true)
resultsPage:executeStatement(commandAppend())
else
return UI.Grid.eventHandler(self, event)
end
return true
end
function resultsPage:rawExecute(s)
local fn, m = loadstring("return (" .. s .. ')', 'lua')
if not fn then
fn, m = loadstring(s, 'lua')
end
if fn then
setfenv(fn, sandboxEnv)
fn, m = pcall(fn)
end
return fn, m
end
function resultsPage:executeStatement(statement)
command = statement
local s, m = self:rawExecute(command)
if s and m then
self:setResult(m)
else
self.grid:setValues({ })
self.grid:draw()
if m then
self.notification:error(m, 5)
end
end
end
UI:setPage(resultsPage)
Event.pullEvents()
UI.term:reset()

153
apps/Network.lua Normal file
View File

@ -0,0 +1,153 @@
require = requireInjector(getfenv(1))
local Event = require('event')
local UI = require('ui')
local Socket = require('socket')
multishell.setTitle(multishell.getCurrent(), 'Network')
UI:configure('Network', ...)
local gridColumns = {
{ heading = 'Label', key = 'label' },
{ heading = 'Dist', key = 'distance' },
{ heading = 'Status', key = 'status' },
}
if UI.term.width >= 30 then
table.insert(gridColumns, { heading = 'Fuel', key = 'fuel' })
table.insert(gridColumns, { heading = 'Uptime', key = 'uptime' })
end
local page = UI.Page({
menuBar = UI.MenuBar({
buttons = {
{ text = 'Telnet', event = 'telnet' },
{ text = 'VNC', event = 'vnc' },
{ text = 'Reboot', event = 'reboot' },
},
}),
grid = UI.ScrollingGrid({
y = 2,
values = network,
columns = gridColumns,
sortColumn = 'label',
autospace = true,
}),
notification = UI.Notification(),
accelerators = {
q = 'quit',
c = 'clear',
},
})
function sendCommand(host, command)
if not device.wireless_modem then
page.notification:error('Wireless modem not present')
return
end
page.notification:info('Connecting')
page:sync()
local socket = Socket.connect(host, 161)
if socket then
socket:write({ type = command })
socket:close()
page.notification:success('Command sent')
else
page.notification:error('Failed to connect')
end
end
function page:eventHandler(event)
local t = self.grid.selected
if t then
if event.type == 'telnet' or event.type == 'grid_select' then
multishell.openTab({
path = '/apps/telnet.lua',
focused = true,
args = { t.id },
title = t.label,
})
elseif event.type == 'vnc' then
multishell.openTab({
path = '/apps/vnc.lua',
focused = true,
args = { t.id },
title = t.label,
})
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
Event.exitPullEvents()
end
UI.Page.eventHandler(self, event)
end
function page.grid:getRowTextColor(row, selected)
if not row.active then
return colors.orange
end
return UI.Grid.getRowTextColor(self, row, selected)
end
function page.grid:getDisplayValues(row)
row = Util.shallowCopy(row)
if row.uptime then
if row.uptime < 60 then
row.uptime = string.format("%ds", math.floor(row.uptime))
else
row.uptime = string.format("%sm", math.floor(row.uptime/6)/10)
end
end
if row.fuel then
row.fuel = Util.toBytes(row.fuel)
end
if row.distance then
row.distance = Util.round(row.distance, 1)
end
return row
end
function page.grid:draw()
self:adjustWidth()
UI.Grid.draw(self)
if page.notification.enabled then
page.notification:draw()
end
end
function updateComputers()
while true do
page.grid:update()
page.grid:draw()
page:sync()
os.sleep(1)
end
end
Event.addHandler('device_attach', function(h, deviceName)
if deviceName == 'wireless_modem' then
page.notification:success('Modem connected')
page:sync()
end
end)
Event.addHandler('device_detach', function(h, deviceName)
if deviceName == 'wireless_modem' then
page.notification:error('Wireless modem not attached')
page:sync()
end
end)
if not device.wireless_modem then
page.notification:error('Wireless modem not attached')
end
UI:setPage(page)
Event.pullEvents(updateComputers)
UI.term:reset()

445
apps/Overview.lua Normal file
View File

@ -0,0 +1,445 @@
require = requireInjector(getfenv(1))
local Util = require('util')
local Event = require('event')
local UI = require('ui')
local Config = require('config')
local NFT = require('nft')
local class = require('class')
local FileUI = require('fileui')
multishell.setTitle(multishell.getCurrent(), 'Overview')
UI:configure('Overview', ...)
local config = {
Recent = { },
currentCategory = 'Apps',
}
local applications = { }
Config.load('Overview', config)
Config.load('apps', applications)
local defaultIcon = NFT.parse([[
8071180
8007180
7180071]])
local sx, sy = term.current().getSize()
local maxRecent = math.ceil(sx * sy / 62)
local function elipse(s, len)
if #s > len then
s = s:sub(1, len - 2) .. '..'
end
return s
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' })
end
end
table.insert(buttons, { text = '+', event = 'new' })
local function parseIcon(iconText)
local icon
local s, m = pcall(function()
icon = NFT.parse(iconText)
if icon then
if icon.height > 3 or icon.width > 8 then
error('Invalid size')
end
end
return icon
end)
if s then
return icon
end
return s, m
end
local page = UI.Page {
tabBar = UI.TabBar {
buttons = buttons,
},
container = UI.ViewportWindow {
y = 2,
},
notification = UI.Notification(),
accelerators = {
r = 'refresh',
e = 'edit',
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
function UI.Icon:eventHandler(event)
if event.type == 'mouse_click' then
self:setFocus(self.button)
return true
elseif event.type == 'mouse_doubleclick' then
self:emit({ type = self.button.event, button = self.button })
elseif event.type == 'mouse_rightclick' then
self:setFocus(self.button)
self:emit({ type = 'edit', button = self.button })
end
return UI.Window.eventHandler(self, event)
end
function page.container:setCategory(categoryName)
-- reset the viewport window
self.children = { }
self.offy = 0
local function filter(it, f)
local ot = { }
for _,v in pairs(it) do
if f(v) then
table.insert(ot, v)
end
end
return ot
end
local filtered
if categoryName == 'Recent' then
filtered = { }
for _,v in ipairs(config.Recent) do
local app = Util.find(applications, 'run', v)
if app and fs.exists(app.run) then
table.insert(filtered, app)
end
end
else
filtered = filter(applications, function(a)
return a.category == categoryName -- and fs.exists(a.run)
end)
table.sort(filtered, function(a, b) return a.title < b.title end)
end
for _,program in ipairs(filtered) do
local icon
if program.icon then
icon = parseIcon(program.icon)
end
if not icon then
icon = defaultIcon
end
local title = elipse(program.title, 8)
local width = math.max(icon.width + 2, #title + 2)
table.insert(self.children, UI.Icon({
width = width,
image = UI.NftImage({
x = math.floor((width - icon.width) / 2) + 1,
image = icon,
width = 5,
height = 3,
}),
button = UI.Button({
x = math.floor((width - #title - 2) / 2) + 1,
y = 4,
text = title,
backgroundColor = self.backgroundColor,
width = #title + 2,
event = 'button',
app = program,
}),
}))
end
local gutter = 2
if UI.term.width <= 26 then
gutter = 1
end
local col, row = gutter, 2
local count = #self.children
-- reposition all children
for k,child in ipairs(self.children) do
child.x = col
child.y = row
if k < count then
col = col + child.width
if col + self.children[k + 1].width + gutter - 2 > UI.term.width then
col = gutter
row = row + 5
end
end
end
self:initChildren()
end
function page:refresh()
local pos = self.container.offy
self:focusFirst(self)
self.container:setCategory(config.currentCategory)
self.container:setScrollPosition(pos)
end
function page:resize()
self:refresh()
UI.Page.resize(self)
end
function page:eventHandler(event)
if event.type == 'category' then
self.tabBar:selectTab(event.button.text)
self.container:setCategory(event.button.text)
self.container:draw()
config.currentCategory = event.button.text
Config.update('Overview', config)
elseif event.type == 'button' then
for k,v in ipairs(config.Recent) do
if v == event.button.app.run then
table.remove(config.Recent, k)
break
end
end
table.insert(config.Recent, 1, event.button.app.run)
if #config.Recent > maxRecent then
table.remove(config.Recent, maxRecent + 1)
end
Config.update('Overview', config)
multishell.openTab({
path = '/apps/shell',
args = { event.button.app.run },
focused = true,
})
elseif event.type == 'shell' then
multishell.openTab({
path = '/apps/shell',
focused = true,
})
elseif event.type == 'lua' then
multishell.openTab({
path = '/apps/Lua.lua',
focused = true,
})
elseif event.type == 'focus_change' then
if event.focused.parent.UIElement == 'Icon' then
event.focused.parent:scrollIntoView()
end
elseif event.type == 'refresh' then
applications = { }
Config.load('apps', applications)
self:refresh()
self:draw()
self.notification:success('Refreshed')
elseif event.type == 'delete' then
local focused = page:getFocused()
if focused.app then
local _,k = Util.find(applications, 'run', focused.app.run)
if k then
table.remove(applications, k)
Config.update('apps', applications)
page:refresh()
page:draw()
self.notification:success('Removed')
end
end
elseif event.type == 'new' then
local category = 'Apps'
if config.currentCategory ~= 'Recent' then
category = config.currentCategory or 'Apps'
end
UI:setPage('editor', { category = category })
elseif event.type == 'edit' then
local focused = page:getFocused()
if focused.app then
UI:setPage('editor', focused.app)
end
else
UI.Page.eventHandler(self, event)
end
return true
end
local formWidth = math.max(UI.term.width - 14, 26)
local gutter = math.floor((UI.term.width - formWidth) / 2) + 1
local editor = UI.Page({
backgroundColor = colors.blue,
form = UI.Form({
fields = {
{ label = 'Title', key = 'title', width = 15, limit = 11, display = UI.Form.D.entry,
help = 'Application title' },
{ label = 'Run', key = 'run', width = formWidth - 11, limit = 100, display = UI.Form.D.entry,
help = 'Full path to application' },
{ label = 'Category', key = 'category', width = 15, limit = 11, display = UI.Form.D.entry,
help = 'Category of application' },
{ text = 'Accept', event = 'accept', display = UI.Form.D.button,
x = 1, y = 9, width = 10 },
{ text = 'Cancel', event = 'cancel', display = UI.Form.D.button,
x = formWidth - 11, y = 9, width = 10 },
},
labelWidth = 8,
x = gutter + 1,
y = math.max(2, math.floor((UI.term.height - 9) / 2)),
height = 9,
width = UI.term.width - (gutter * 2),
image = UI.NftImage({
y = 5,
x = 1,
height = 3,
width = 8,
}),
button = UI.Button({
x = 10,
y = 6,
text = 'Load icon',
width = 11,
event = 'loadIcon',
}),
}),
statusBar = UI.StatusBar(),
notification = UI.Notification(),
iconFile = '',
})
function editor:enable(app)
if app then
self.original = app
self.form:setValues(Util.shallowCopy(app))
local icon
if app.icon then
icon = parseIcon(app.icon)
end
self.form.image:setImage(icon)
self:setFocus(self.form.children[1])
end
UI.Page.enable(self)
end
function editor.form.image:draw()
self:clear()
UI.NftImage.draw(self)
end
function editor:updateApplications(app, original)
if original.run then
local _,k = Util.find(applications, 'run', original.run)
if k then
applications[k] = nil
end
end
table.insert(applications, app)
Config.update('apps', applications)
end
function editor:eventHandler(event)
if event.type == 'cancel' then
UI:setPreviousPage()
elseif event.type == 'focus_change' then
self.statusBar:setStatus(event.focused.help or '')
self.statusBar:draw()
elseif event.type == 'loadIcon' then
UI:setPage(FileUI(), fs.getDir(self.iconFile), function(fileName)
if fileName then
self.iconFile = fileName
local s, m = pcall(function()
local iconLines = Util.readFile(fileName)
if not iconLines then
error('Unable to load file')
end
local icon, m = parseIcon(iconLines)
if not icon then
error(m)
end
self.form.values.icon = iconLines
self.form.image:setImage(icon)
self.form.image:draw()
end)
if not s and m then
self.notification:error(m:gsub('.*: (.*)', '%1'))
end
end
end)
elseif event.type == 'accept' then
local values = self.form.values
if #values.run > 0 and #values.title > 0 and #values.category > 0 then
UI:setPreviousPage()
self:updateApplications(values, self.original)
page:refresh()
page:draw()
else
self.notification:error('Require fields missing')
--self.statusBar:setStatus('Require fields missing')
--self.statusBar:draw()
end
else
return UI.Page.eventHandler(self, event)
end
return true
end
UI:setPages({
editor = editor,
main = page,
})
Event.addHandler('os_register_app', function()
applications = { }
Config.load('apps', applications)
page:refresh()
page:draw()
page:sync()
end)
page.tabBar:selectTab(config.currentCategory or 'Apps')
page.container:setCategory(config.currentCategory or 'Apps')
UI:setPage(page)
Event.pullEvents()
UI.term:reset()

213
apps/Peripherals.lua Normal file
View File

@ -0,0 +1,213 @@
require = requireInjector(getfenv(1))
local Util = require('util')
local Event = require('event')
local UI = require('ui')
multishell.setTitle(multishell.getCurrent(), 'Devices')
--[[ -- PeripheralsPage -- ]] --
local peripheralsPage = UI.Page({
grid = UI.ScrollingGrid({
columns = {
{ heading = 'Type', key = 'type' },
{ heading = 'Side', key = 'side' }
},
sortColumn = 'type',
height = UI.term.height - 1,
autospace = true,
}),
statusBar = UI.StatusBar({
status = 'Select peripheral'
}),
accelerators = {
q = 'quit',
},
})
function peripheralsPage.grid:draw()
local sides = peripheral.getNames()
Util.clear(self.values)
for _,side in pairs(sides) do
table.insert(self.values, {
type = peripheral.getType(side),
side = side
})
end
self:update()
self:adjustWidth()
UI.Grid.draw(self)
end
function peripheralsPage:updatePeripherals()
if UI:getCurrentPage() == self then
self.grid:draw()
self:sync()
end
end
function peripheralsPage:eventHandler(event)
if event.type == 'quit' then
Event.exitPullEvents()
elseif event.type == 'grid_select' then
UI:setPage('methods', event.selected)
end
return UI.Page.eventHandler(self, event)
end
--[[ -- MethodsPage -- ]] --
local methodsPage = UI.Page({
grid = UI.ScrollingGrid({
columns = {
{ heading = 'Name', key = 'name', width = UI.term.width }
},
sortColumn = 'name',
height = 7,
}),
container = UI.Window({
y = 8,
height = UI.term.height-8,
viewportConsole = UI.ViewportWindow({
backgroundColor = colors.brown
}),
}),
statusBar = UI.StatusBar({
status = 'q to return',
}),
accelerators = {
q = 'back',
backspace = 'back',
},
})
function methodsPage:enable(p)
self.peripheral = p or self.peripheral
local p = peripheral.wrap(self.peripheral.side)
if not p.getAdvancedMethodsData then
self.grid.values = { }
for name,f in pairs(p) do
table.insert(self.grid.values, {
name = name,
noext = true,
})
end
else
self.grid.values = p.getAdvancedMethodsData()
for name,f in pairs(self.grid.values) do
f.name = name
end
end
self.grid:update()
self.grid:setIndex(1)
self.statusBar:setStatus(self.peripheral.type)
UI.Page.enable(self)
end
function methodsPage.container.viewportConsole:draw()
if methodsPage.grid:getSelected() then
methodsPage:drawMethodInfo(self, methodsPage.grid:getSelected())
end
end
function methodsPage:eventHandler(event)
if event.type == 'back' then
UI:setPage(peripheralsPage)
return true
elseif event.type == 'grid_focus_row' then
self.container.viewportConsole.height = 1
self.container.viewportConsole.offset = 0
self.container.viewportConsole.y = 1
self:drawMethodInfo(self.container.viewportConsole, event.selected)
end
return UI.Page.eventHandler(self, event)
end
function methodsPage:drawMethodInfo(c, method)
c:clear()
c:setCursorPos(1, 1)
if method.noext then
c:print('No extended Information')
return 2
end
if method.description then
c:print(method.description)
end
c.cursorY = c.cursorY + 2
c.cursorX = 1
if method.returnTypes ~= '()' then
c:print(method.returnTypes .. ' ', nil, colors.yellow)
end
c:print(method.name, nil, colors.black)
c:print('(')
local maxArgLen = 1
for k,arg in ipairs(method.args) do
if #arg.description > 0 then
maxArgLen = math.max(#arg.name, maxArgLen)
end
local argName = arg.name
local fg = colors.green
if arg.optional then
argName = string.format('[%s]', arg.name)
fg = colors.orange
end
c:print(argName, nil, fg)
if k < #method.args then
c:print(', ')
end
end
c:print(')')
c.cursorY = c.cursorY + 1
if #method.args > 0 then
for _,arg in ipairs(method.args) do
if #arg.description > 0 then
c.cursorY = c.cursorY + 1
c.cursorX = 1
local fg = colors.green
if arg.optional then
fg = colors.orange
end
c:print(arg.name .. ': ', nil, fg)
c.cursorX = maxArgLen + 3
c:print(arg.description, nil, nil, maxArgLen + 3)
end
end
end
c.height = c.cursorY + 1
term.setBackgroundColor(colors.black)
return y
end
Event.addHandler('peripheral', function()
peripheralsPage:updatePeripherals()
end)
Event.addHandler('peripheral_detach', function()
peripheralsPage:updatePeripherals()
end)
UI:setPage(peripheralsPage)
UI:setPages({
methods = methodsPage,
})
Event.pullEvents()
UI.term:reset()

105
apps/Pim.lua Normal file
View File

@ -0,0 +1,105 @@
require = requireInjector(getfenv(1))
local Event = require('event')
local UI = require('ui')
local Config = require('config')
multishell.setTitle(multishell.getCurrent(), 'PIM')
local inventory = { }
local mode = 'sync'
if not device.pim then
error('PIM not attached')
end
local page = UI.Page({
menu = UI.Menu({
centered = true,
y = 2,
menuItems = {
{ prompt = 'Learn', event = 'learn', help = '' },
},
}),
statusBar = UI.StatusBar({
columns = {
{ 'Status', 'status', UI.term.width - 7 },
{ 'Mode', 'mode', 7 }
}
}),
accelerators = {
q = 'quit',
},
})
local function learn()
if device.pim.getInventorySize() > 0 then
local stacks = device.pim.getAllStacks(false)
Config.update('pim', stacks)
mode = 'sync'
page.statusBar:setValue('status', 'Learned inventory')
end
page.statusBar:setValue('mode', mode)
page.statusBar:draw()
end
function page:eventHandler(event)
if event.type == 'learn' then
mode = 'learn'
learn()
elseif event.type == 'quit' then
Event.exitPullEvents()
end
return UI.Page.eventHandler(self, event)
end
local function inInventory(s)
for _,i in pairs(inventory) do
if i.id == s.id then
return true
end
end
end
local function pimWatcher()
local playerOn = false
while true do
if device.pim.getInventorySize() > 0 and not playerOn then
playerOn = true
if mode == 'learn' then
learn()
else
local stacks = device.pim.getAllStacks(false)
for k,stack in pairs(stacks) do
if not inInventory(stack) then
device.pim.pushItem('down', k, stack.qty)
end
end
page.statusBar:setValue('status', 'Synchronized')
page.statusBar:draw()
end
elseif device.pim.getInventorySize() == 0 and playerOn then
page.statusBar:setValue('status', 'No player')
page.statusBar:draw()
playerOn = false
end
os.sleep(1)
end
end
Config.load('pim', inventory)
if Util.empty(inventory) then
mode = 'learn'
end
page.statusBar:setValue('mode', mode)
UI:setPage(page)
Event.pullEvents(pimWatcher)
UI.term:reset()

561
apps/Script.lua Normal file
View File

@ -0,0 +1,561 @@
require = requireInjector(getfenv(1))
local Event = require('event')
local UI = require('ui')
local Socket = require('socket')
local Config = require('config')
local GROUPS_PATH = '/apps/groups'
local SCRIPTS_PATH = '/apps/scripts'
multishell.setTitle(multishell.getCurrent(), 'Script')
UI:configure('Script', ...)
local config = {
showGroups = false,
variables = [[{
COMPUTER_ID = os.getComputerID(),
}]],
}
Config.load('Script', config)
local width = math.floor(UI.term.width / 2) - 1
if UI.term.width % 2 ~= 0 then
width = width + 1
end
function processVariables(script)
local fn = loadstring('return ' .. config.variables)
if fn then
local variables = fn()
for k,v in pairs(variables) do
local token = string.format('{%s}', k)
script = script:gsub(token, v)
end
end
return script
end
function invokeScript(computer, scriptName)
local script = Util.readFile(scriptName)
if not script then
print('Unable to read script file')
end
local socket = Socket.connect(computer.id, 161)
if not socket then
print('Unable to connect to ' .. computer.id)
return
end
script = processVariables(script)
Util.print('Running %s on %s', scriptName, computer.label)
socket:write({ type = 'script', args = script })
--[[
local response = socket:read(2)
if response and response.result then
if type(response.result) == 'table' then
print(textutils.serialize(response.result))
else
print(tostring(response.result))
end
else
printError('No response')
end
--]]
socket:close()
end
function runScript(computerOrGroup, scriptName)
if computerOrGroup.id then
invokeScript(computerOrGroup, scriptName)
else
local list = computerOrGroup.list
if computerOrGroup.path then
list = Util.readTable(computerOrGroup.path)
end
if list then
for _,computer in pairs(list) do
invokeScript(computer, scriptName)
end
end
end
end
local function getActiveComputers(t)
t = t or { }
Util.clear(t)
for k,computer in pairs(_G.network) do
if computer.active then
t[k] = computer
end
end
return t
end
local function getTurtleList()
local turtles = {
label = 'Turtles',
list = { },
}
for k,computer in pairs(getActiveComputers()) do
if computer.fuel then
turtles.list[k] = computer
end
end
return turtles
end
local args = { ... }
if #args == 2 then
local key = args[1]
local script = args[2]
local target
if tonumber(key) then
target = _G.network[tonumber(key)]
elseif key == 'All' then
target = {
list = Util.shallowCopy(getActiveComputers()),
}
elseif key == 'Localhost' then
target = { id = os.getComputerID() }
elseif key == 'Turtles' then
target = getTurtleList()
else
target = Util.readTable(fs.combine(GROUPS_PATH, key))
end
if not target then
error('Syntax: Script <ID or group> <script>')
end
runScript(target, fs.combine(SCRIPTS_PATH, script))
return
end
local function getListing(t, path)
Util.clear(t)
local files = fs.list(path)
for _,f in pairs(files) do
table.insert(t, { label = f, path = fs.combine(path, f) })
end
end
local mainPage = UI.Page({
menuBar = UI.MenuBar({
buttons = {
{ text = 'Groups', event = 'groups' },
{ text = 'Scripts', event = 'scripts' },
{ text = 'Toggle', event = 'toggle' },
},
}),
computers = UI.ScrollingGrid({
y = 2,
height = UI.term.height-3,
columns = {
{ heading = 'Label', key = 'label', width = width },
},
width = width,
sortColumn = 'label',
}),
scripts = UI.ScrollingGrid({
columns = {
{ heading = 'Name', key = 'label', width = width },
},
sortColumn = 'label',
height = UI.term.height - 3,
width = width,
x = UI.term.width - width + 1,
y = 2,
}),
statusBar = UI.StatusBar({
columns = {
{ '', 'status', 4 },
{ '', 'fuelF', 5 },
{ '', 'distanceF', 4 },
},
autospace = true,
}),
accelerators = {
q = 'quit',
},
})
local editorPage = UI.Page({
menuBar = UI.MenuBar({
showBackButton = true,
buttons = {
{ text = 'Save', event = 'save', help = 'Save this group' },
},
}),
grid1 = UI.ScrollingGrid({
columns = {
{ heading = 'Name', key = 'label', width = width },
},
sortColumn = 'label',
height = UI.term.height - 4,
width = width,
y = 3,
}),
right = UI.Button({
text = '>',
event = 'right',
x = width - 2,
y = 2,
width = 3,
}),
left = UI.Button({
text = '<',
event = 'left',
x = UI.term.width - width + 1,
y = 2,
width = 3,
}),
grid2 = UI.ScrollingGrid({
columns = {
{ heading = 'Name', key = 'label', width = width },
},
sortColumn = 'label',
height = UI.term.height - 4,
width = width,
x = UI.term.width - width + 1,
y = 3,
}),
statusBar = UI.StatusBar(),
accelerators = {
q = 'back',
},
})
local groupsPage = UI.Page({
menuBar = UI.MenuBar({
showBackButton = true,
buttons = {
{ text = 'Add', event = 'add' },
{ text = 'Edit', event = 'edit' },
{ text = 'Delete', event = 'delete' },
},
}),
grid = UI.ScrollingGrid({
y = 2,
height = UI.term.height-2,
columns = {
{ heading = 'Name', key = 'label' },
},
sortColumn = 'label',
autospace = true,
}),
statusBar = UI.StatusBar(),
accelerators = {
q = 'back',
},
})
local scriptsPage = UI.Page({
menuBar = UI.MenuBar({
showBackButton = true,
buttons = {
{ text = 'Add', event = 'add' },
{ text = 'Edit', event = 'edit' },
{ text = 'Delete', event = 'delete' },
},
}),
grid = UI.ScrollingGrid({
y = 2,
height = UI.term.height-2,
columns = {
{ heading = 'Name', key = 'label' },
},
sortColumn = 'label',
autospace = true,
}),
statusBar = UI.StatusBar(),
accelerators = {
a = 'add',
e = 'edit',
delete = 'delete',
q = 'back',
},
})
function editorPage:enable()
self:focusFirst()
local groupPath = fs.combine(GROUPS_PATH, self.groupName)
if fs.exists(groupPath) then
self.grid1.values = Util.readTable(groupPath)
else
Util.clear(self.grid1.values)
end
self.grid1:update()
UI.Page.enable(self)
end
function editorPage.grid2:draw()
getActiveComputers(self.values)
for k in pairs(editorPage.grid1.values) do
self.values[k] = nil
end
self:update()
UI.ScrollingGrid.draw(self)
end
function editorPage:eventHandler(event)
if event.type == 'back' then
UI:setPage(groupsPage)
elseif event.type == 'left' then
local computer = self.grid2:getSelected()
self.grid1.values[computer.id] = computer
self.grid1:update()
self.grid1:draw()
self.grid2:draw()
elseif event.type == 'right' then
local computer = self.grid1:getSelected()
self.grid1.values[computer.id] = nil
self.grid1:update()
self.grid1:draw()
self.grid2:draw()
elseif event.type == 'save' then
Util.writeTable(fs.combine(GROUPS_PATH, self.groupName), self.grid1.values)
UI:setPage(groupsPage)
end
return UI.Page.eventHandler(self, event)
end
local function nameDialog(f)
local dialog = UI.Dialog({
x = (UI.term.width - 28) / 2,
width = 28,
textEntry = UI.TextEntry({ x = 4, y = 3, width = 20, limit = 20 })
})
dialog.titleBar.title = 'Enter Name'
dialog.eventHandler = function(self, event)
if event.type == 'accept' then
local name = self.textEntry.value
if name then
f(name)
else
self.statusBar:timedStatus('Invalid Name', 3)
end
return true
end
return UI.Dialog.eventHandler(self, event)
end
dialog:setFocus(dialog.textEntry)
UI:setPage(dialog)
end
function groupsPage:draw()
getListing(self.grid.values, GROUPS_PATH)
self.grid:update()
UI.Page.draw(self)
end
function groupsPage:enable()
self:focusFirst()
UI.Page.enable(self)
end
function groupsPage:eventHandler(event)
if event.type == 'back' then
UI:setPage(mainPage)
elseif event.type == 'add' then
nameDialog(function(name)
editorPage.groupName = name
UI:setPage(editorPage)
end)
elseif event.type == 'delete' then
fs.delete(fs.combine(GROUPS_PATH, self.grid:getSelected().label))
self:draw()
elseif event.type == 'edit' then
editorPage.groupName = self.grid:getSelected().label
UI:setPage(editorPage)
end
return UI.Page.eventHandler(self, event)
end
function scriptsPage:draw()
getListing(self.grid.values, SCRIPTS_PATH)
self.grid:update()
UI.Page.draw(self)
end
function scriptsPage:enable()
self:focusFirst()
UI.Page.enable(self)
end
function scriptsPage:eventHandler(event)
if event.type == 'back' then
UI:setPreviousPage()
elseif event.type == 'add' then
nameDialog(function(name)
shell.run('edit ' .. fs.combine(SCRIPTS_PATH, name))
UI:setPreviousPage()
end)
elseif event.type == 'edit' then
local name = fs.combine(SCRIPTS_PATH, self.grid:getSelected().label)
shell.run('edit ' .. name)
self:draw()
elseif event.type == 'delete' then
local name = fs.combine(SCRIPTS_PATH, self.grid:getSelected().label)
fs.delete(name)
self:draw()
end
return UI.Page.eventHandler(self, event)
end
function mainPage:eventHandler(event)
if event.type == 'quit' then
Event.exitPullEvents()
elseif event.type == 'groups' then
UI:setPage(groupsPage)
elseif event.type == 'scripts' then
UI:setPage(scriptsPage)
elseif event.type == 'toggle' then
config.showGroups = not config.showGroups
local text = 'Computers'
if config.showGroups then
text = 'Groups'
end
-- self.statusBar.toggleButton.text = text
self:draw()
Config.update('Script', config)
elseif event.type == 'grid_focus_row' then
local computer = self.computers:getSelected()
self.statusBar.values = { computer }
self.statusBar:draw()
elseif event.type == 'grid_select' then
local script = self.scripts:getSelected()
local computer = self.computers:getSelected()
self:clear()
self:sync()
self.enabled = false
runScript(computer, script.path)
print()
print('Press any key to continue...')
while true do
local e = os.pullEvent()
if e == 'char' or e == 'key' or e == 'mouse_click' then
break
end
end
self.enabled = true
self:draw()
end
return UI.Page.eventHandler(self, event)
end
function mainPage.statusBar:draw()
local computer = self.values[1]
if computer then
if computer.fuel then
computer.fuelF = string.format("%dk", math.floor(computer.fuel/1000))
end
if computer.distance then
computer.distanceF = Util.round(computer.distance, 1)
end
mainPage.statusBar:adjustWidth()
end
UI.StatusBar.draw(self)
end
function mainPage:draw()
getListing(self.scripts.values, SCRIPTS_PATH)
if config.showGroups then
getListing(self.computers.values, GROUPS_PATH)
table.insert(self.computers.values, {
label = 'All',
list = getActiveComputers(),
})
table.insert(self.computers.values, getTurtleList())
table.insert(self.computers.values, {
label = 'Localhost',
id = os.getComputerID(),
})
else
getActiveComputers(self.computers.values)
end
self.scripts:update()
self.computers:update()
UI.Page.draw(self)
end
if not fs.exists(SCRIPTS_PATH) then
fs.makeDir(SCRIPTS_PATH)
end
if not fs.exists(GROUPS_PATH) then
fs.makeDir(GROUPS_PATH)
end
Event.addHandler('network_attach', function()
if mainPage.enabled then
mainPage:draw()
end
end)
Event.addHandler('network_detach', function()
if mainPage.enabled then
mainPage:draw()
end
end)
function statusUpdate()
while true do
if mainPage.enabled then
local selected = mainPage.computers:getSelected()
if selected then
local computer = _G.network[selected.id]
mainPage.statusBar.values = { computer }
mainPage.statusBar:draw()
mainPage:sync()
end
end
os.sleep(1)
end
end
UI:setPage(mainPage)
Event.pullEvents(statusUpdate)
UI.term:reset()

190
apps/System.lua Normal file
View File

@ -0,0 +1,190 @@
require = requireInjector(getfenv(1))
local Event = require('event')
local UI = require('ui')
local Config = require('config')
multishell.setTitle(multishell.getCurrent(), 'System')
UI:configure('System', ...)
local env = {
path = shell.path(),
aliases = shell.aliases(),
lua_path = LUA_PATH,
}
Config.load('multishell', env)
UI.TextEntry.defaults.backgroundFocusColor = colors.black
local systemPage = UI.Page({
backgroundColor = colors.blue,
tabs = UI.Tabs({
pathTab = UI.Window({
tabTitle = 'Path',
entry = UI.TextEntry({
y = 2, x = 2, limit = 256,
width = UI.term.width - 2,
value = shell.path(),
shadowText = 'enter system path',
accelerators = {
enter = 'update_path',
},
}),
grid = UI.Grid({
y = 4,
values = paths,
disableHeader = true,
columns = { { key = 'value' } },
autospace = true,
}),
}),
aliasTab = UI.Window({
tabTitle = 'Aliases',
alias = UI.TextEntry({
y = 2, x = 2, width = UI.term.width - 2,
limit = 32,
shadowText = 'Alias',
}),
path = UI.TextEntry({
y = 3, x = 2, width = UI.term.width - 2, limit = 256,
shadowText = 'Program path',
accelerators = {
enter = 'new_alias',
},
}),
grid = UI.Grid({
y = 5, values = aliases, autospace = true,
columns = {
{ heading = 'Alias', key = 'alias' },
{ heading = 'Program', key = 'path' },
},
sortColumn = 'alias',
accelerators = {
delete = 'delete_alias',
},
}),
}),
infoTab = UI.Window({
tabTitle = 'Info',
labelText = UI.Text({ y = 2, x = 3, value = 'Label' }),
label = UI.TextEntry({
y = 2, x = 9, width = UI.term.width - 12,
limit = 32, value = os.getComputerLabel(),
backgroundFocusColor = colors.black,
accelerators = {
enter = 'update_label',
},
}),
grid = UI.ScrollingGrid({
y = 4,
values = {
{ name = 'CC version', value = os.version() },
{ name = 'Lua version', value = _VERSION },
{ name = 'MC version', value = _MC_VERSION or 'unknown' },
{ name = 'Disk free', value = Util.toBytes(fs.getFreeSpace('/')) },
{ name = 'Computer ID', value = tostring(os.getComputerID()) },
{ name = 'Day', value = tostring(os.day()) },
},
selectable = false,
backgroundColor = colors.blue,
columns = {
{ key = 'name', width = 12 },
{ key = 'value', width = UI.term.width - 15 },
},
}),
}),
}),
-- statusBar = UI.StatusBar(),
notification = UI.Notification(),
accelerators = {
q = 'quit',
},
})
function systemPage.tabs.pathTab.grid:draw()
self.values = { }
for _,v in ipairs(Util.split(env.path, '(.-):')) do
table.insert(self.values, { value = v })
end
self:update()
UI.Grid.draw(self)
end
function systemPage.tabs.pathTab:eventHandler(event)
if event.type == 'update_path' then
env.path = self.entry.value
self.grid:setIndex(self.grid:getIndex())
self.grid:draw()
Config.update('multishell', env)
systemPage.notification:success('reboot to take effect')
else
return UI.Window.eventHandler(self, event)
end
return true
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
self:update()
UI.Grid.draw(self)
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())
self.grid:draw()
Config.update('multishell', env)
systemPage.notification:success('reboot to take effect')
elseif event.type == 'new_alias' then
env.aliases[self.alias.value] = self.path.value
self.alias:reset()
self.path:reset()
self:draw()
self:setFocus(self.alias)
Config.update('multishell', env)
systemPage.notification:success('reboot to take effect')
else
return UI.Window.eventHandler(self, event)
end
return true
end
function systemPage.tabs.infoTab:eventHandler(event)
if event.type == 'update_label' then
os.setComputerLabel(self.label.value)
systemPage.notification:success('Label updated')
return true
else
return UI.Window.eventHandler(self, event)
end
return true
end
function systemPage:eventHandler(event)
if event.type == 'quit' then
Event.exitPullEvents()
elseif event.type == 'tab_activate' then
event.activated:focusFirst()
--self.statusBar:setValue('')
--self.statusBar:draw()
else
return UI.Page.eventHandler(self, event)
end
return true
end
UI:setPage(systemPage)
Event.pullEvents()
UI.term:reset()

78
apps/Tabs.lua Normal file
View File

@ -0,0 +1,78 @@
require = requireInjector(getfenv(1))
local UI = require('ui')
local Event = require('event')
multishell.setTitle(multishell.getCurrent(), 'Tabs')
UI:configure('Tabs', ...)
local page = UI.Page {
menuBar = UI.MenuBar {
buttons = {
{ text = 'Activate', event = 'activate' },
{ text = 'Terminate', event = 'terminate' },
},
},
grid = UI.ScrollingGrid {
y = 2,
columns = {
{ heading = 'ID', key = 'tabId' },
{ heading = 'Title', key = 'title' },
{ heading = 'Status', key = 'status' },
{ heading = 'Time', key = 'timestamp' },
},
values = multishell.getTabs(),
sortColumn = 'title',
autospace = true,
},
accelerators = {
q = 'quit',
space = 'activate',
t = 'terminate',
},
}
function page:eventHandler(event)
local t = self.grid:getSelected()
if t then
if event.type == 'activate' or event.type == 'grid_select' then
multishell.setFocus(t.tabId)
elseif event.type == 'terminate' then
multishell.terminate(t.tabId)
end
end
if event.type == 'quit' then
Event.exitPullEvents()
end
UI.Page.eventHandler(self, event)
end
function page.grid:getDisplayValues(row)
row = Util.shallowCopy(row)
local elapsed = os.clock()-row.timestamp
if elapsed < 60 then
row.timestamp = string.format("%ds", math.floor(elapsed))
else
row.timestamp = string.format("%fm", math.floor(elapsed/6)/10)
end
if row.isDead then
row.status = 'error'
else
row.status = coroutine.status(row.co)
end
return row
end
function page.grid:draw()
self:adjustWidth()
UI.Grid.draw(self)
end
Event.addTimer(1, true, function()
page.grid:update()
page.grid:draw()
page:sync()
end)
UI:setPage(page)
Event.pullEvents()
UI.term:reset()

2064
apps/builder.lua Normal file

File diff suppressed because it is too large Load Diff

24
apps/cat.lua Normal file
View File

@ -0,0 +1,24 @@
local args = { ... }
if #args < 1 then
error('cat <filename>')
end
local fileName = shell.resolve(args[1])
if not fs.exists(fileName) then
error('not a file: ' .. args[1])
end
local file = fs.open(fileName, 'r')
if not file then
error('unable to open ' .. args[1])
end
while true do
local line = file.readLine()
if not line then
break
end
print(line)
end
file.close()

1184
apps/edit.lua Normal file

File diff suppressed because it is too large Load Diff

102
apps/logMonitor.lua Normal file
View File

@ -0,0 +1,102 @@
require = requireInjector(getfenv(1))
local Event = require('event')
local Message = require('message')
local UI = require('ui')
multishell.setTitle(multishell.getCurrent(), 'Log Monitor')
if not device.wireless_modem then
error('Wireless modem is required')
end
device.wireless_modem.open(59998)
local ids = { }
local messages = { }
local terminal = UI.term
if device.openperipheral_bridge then
UI.Glasses = require('glasses')
terminal = UI.Glasses({
x = 4,
y = 175,
height = 40,
width = 64,
textScale = .5,
backgroundOpacity = .65,
})
elseif device.monitor then
terminal = UI.Device({
deviceType = 'monitor',
textScale = .5
})
end
terminal:clear()
function getClient(id)
if not ids[id] then
ids[id] = {
titleBar = UI.TitleBar({ title = 'ID: ' .. id, parent = terminal }),
scrollingText = UI.ScrollingText({ parent = terminal })
}
local clientCount = Util.size(ids)
local clientHeight = math.floor((terminal.height - clientCount) / clientCount)
terminal:clear()
local y = 1
for k,v in pairs(ids) do
v.titleBar.y = y
y = y + 1
v.scrollingText.height = clientHeight
v.scrollingText.y = y
y = y + clientHeight
v.scrollingText:clear()
v.titleBar:draw()
v.scrollingText:draw()
end
end
return ids[id]
end
local function logWriter()
while true do
os.pullEvent('logMessage')
local t = { }
while #messages > 0 do
local msg = messages[1]
table.remove(messages, 1)
local client = getClient(msg.id)
client.scrollingText:appendLine(string.format('%d %s', math.floor(os.clock()), msg.text))
t[msg.id] = client
end
for _,client in pairs(t) do
client.scrollingText:draw()
end
terminal:sync()
end
end
Message.addHandler('log', function(h, id, msg)
table.insert(messages, { id = id, text = msg.contents })
os.queueEvent('logMessage')
end)
Event.addHandler('monitor_touch', function()
terminal:reset()
ids = { }
end)
Event.addHandler('mouse_click', function()
terminal:reset()
ids = { }
end)
Event.addHandler('char', function()
Event.exitPullEvents()
end)
Event.pullEvents(logWriter)
terminal:reset()

84
apps/mirror.lua Normal file
View File

@ -0,0 +1,84 @@
require = requireInjector(getfenv(1))
local Socket = require('socket')
local Terminal = require('terminal')
local Logger = require('logger')
local process = require('process')
Logger.setScreenLogging()
local remoteId
local args = { ... }
if #args == 1 then
remoteId = tonumber(args[1])
else
print('Enter host ID')
remoteId = tonumber(read())
end
if not remoteId then
error('Syntax: telnet <host ID>')
end
print('connecting...')
local socket
for i = 1,3 do
socket = Socket.connect(remoteId, 5901)
if socket then
break
end
os.sleep(3)
end
if not socket then
error('Unable to connect to ' .. remoteId .. ' on port 5901')
end
print('connected')
local function wrapTerm(socket)
local methods = { 'blit', 'clear', 'clearLine', 'setCursorPos', 'write',
'setTextColor', 'setTextColour', 'setBackgroundColor',
'setBackgroundColour', 'scroll', 'setCursorBlink', }
socket.term = multishell.term
socket.oldTerm = Util.shallowCopy(socket.term)
for _,k in pairs(methods) do
socket.term[k] = function(...)
if not socket.queue then
socket.queue = { }
os.queueEvent('mirror_flush')
end
table.insert(socket.queue, {
f = k,
args = { ... },
})
socket.oldTerm[k](...)
end
end
end
wrapTerm(socket)
os.queueEvent('term_resize')
while true do
local e = process:pullEvent('mirror_flush')
if e == 'terminate' then
break
end
if not socket.connected then
break
end
if socket.queue then
socket:write(socket.queue)
socket.queue = nil
end
end
for k,v in pairs(socket.oldTerm) do
socket.term[k] = v
end
socket:close()

45
apps/mirrorHost.lua Normal file
View File

@ -0,0 +1,45 @@
require = requireInjector(getfenv(1))
local Socket = require('socket')
local Logger = require('logger')
local process = require('process')
Logger.setScreenLogging()
local args = { ... }
local mon = device[args[1] or 'monitor']
if not mon then
error('Monitor not attached')
end
mon.setBackgroundColor(colors.black)
mon.clear()
while true do
local socket = Socket.server(5901, true)
print('mirror: connection from ' .. socket.dhost)
local updateThread = process:newThread('updateThread', function()
while true do
local data = socket:read()
if not data then
break
end
for _,v in ipairs(data) do
mon[v.f](unpack(v.args))
end
end
end)
while true do
process:pullEvent('modem_message')
if updateThread:isDead() then
break
end
end
print('connection lost')
socket:close()
end

600
apps/multishell Normal file
View File

@ -0,0 +1,600 @@
-- Default label
if not os.getComputerLabel() then
local id = os.getComputerID()
if turtle then
os.setComputerLabel('turtle_' .. id)
elseif pocket then
os.setComputerLabel('pocket_' .. id)
else
os.setComputerLabel('computer_' .. id)
end
end
multishell.term = term.current()
local defaultEnv = Util.shallowCopy(getfenv(1))
require = requireInjector(getfenv(1))
local Config = require('config')
-- Begin multishell
local parentTerm = term.current()
local w,h = parentTerm.getSize()
local tabs = {}
local currentTab
local _tabId = 0
local overviewTab
local runningTab
local tabsDirty = false
local config = {
standard = {
focusTextColor = colors.lightGray,
focusBackgroundColor = colors.gray,
textColor = colors.black,
backgroundColor = colors.lightGray,
tabBarTextColor = colors.black,
tabBarBackgroundColor = colors.lightGray,
},
color = {
focusTextColor = colors.white,
focusBackgroundColor = colors.brown,
textColor = colors.gray,
backgroundColor = colors.brown,
tabBarTextColor = colors.lightGray,
tabBarBackgroundColor = colors.brown,
},
-- path = '.:/apps:' .. shell.path():sub(3),
path = '/apps:' .. shell.path(),
}
Config.load('multishell', config)
shell.setPath(config.path)
if config.aliases then
for k in pairs(shell.aliases()) do
shell.clearAlias(k)
end
for k,v in pairs(config.aliases) do
shell.setAlias(k, v)
end
end
local _colors = config.standard
if parentTerm.isColor() then
_colors = config.color
end
local function redrawMenu()
if not tabsDirty then
os.queueEvent('multishell', 'draw')
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.textColor)
parentTerm.setBackgroundColor(_colors.backgroundColor)
parentTerm.setCursorPos( w, 1 )
parentTerm.write('*')
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
end
if not tab.filter or tab.filter == event or event == "terminate" then
eventData = eventData or { }
term.redirect(tab.terminal)
local previousTab = runningTab
runningTab = tab
local ok, result = coroutine.resume(tab.co, event, unpack(eventData))
tab.terminal = term.current()
if ok then
tab.filter = result
else
printError(result)
end
runningTab = previousTab
return ok, result
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.co = coroutine.create(function()
local result, err
if tab.fn then
result, err = Util.runFunction(tab.env, tab.fn, table.unpack(tab.args or { } ))
elseif tab.path then
result, err = os.run(tab.env, tab.path, table.unpack(tab.args or { } ))
else
err = 'multishell: invalid tab'
end
if not result and err ~= 'Terminated' then
if err then
printError(tostring(err))
end
printError('Press enter to exit')
tab.isDead = true
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
end
tabs[tab.tabId] = nil
if tab == currentTab then
local previousTab
if tab.previousTabId then
previousTab = tabs[tab.previousTabId]
end
selectTab(previousTab)
end
redrawMenu()
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 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
function multishell.removeHotkey(code)
hotkeys[code] = nil
end
function multishell.getFocus()
return currentTab.tabId
end
function multishell.setFocus(tabId)
local tab = tabs[tabId]
if tab then
selectTab(tab)
redrawMenu()
return true
end
return false
end
function multishell.getTitle(tabId)
local tab = tabs[tabId]
if tab then
return tab.title
end
end
function multishell.setTitle(tabId, sTitle)
local tab = tabs[tabId]
if tab then
tab.title = sTitle or ''
redrawMenu()
end
end
function multishell.getCurrent()
if runningTab then
return runningTab.tabId
end
end
function multishell.getTab(tabId)
return tabs[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
end
function multishell.getTabs()
return tabs
end
function multishell.launch( tProgramEnv, sProgramPath, ... )
-- backwards compatibility
return multishell.openTab({
env = tProgramEnv,
path = sProgramPath,
args = { ... },
})
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()
launchProcess(tab)
term.redirect(previousTerm)
if tab.hidden then
if coroutine.status(tab.co) == 'dead' or tab.isDead then
tab.hidden = false
end
elseif tab.focused then
multishell.setFocus(tab.tabId)
else
redrawMenu()
end
return tab.tabId
end
function multishell.hideTab(tabId)
local tab = tabs[tabId]
if tab then
tab.hidden = true
redrawMenu()
end
end
function multishell.unhideTab(tabId)
local tab = tabs[tabId]
if tab then
tab.hidden = false
redrawMenu()
end
end
function multishell.getCount()
local count
for _,tab in pairs(tabs) do
count = count + 1
end
return count
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)
end
end)
-- control-tab - next tab
multishell.addHotkey(15, function()
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)
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
end
end
end
if #visibleTabs > 0 then
multishell.setFocus(visibleTabs[1].tabId)
end
end)
local function startup()
local hasError
local function runDir(directory, open)
if not fs.exists(directory) then
return
end
local files = fs.list(directory)
table.sort(files)
for _,file in ipairs(files) do
print('Autorunning: ' .. file)
local result, err = open(directory .. '/' .. file)
if not result then
printError(err)
hasError = true
end
end
end
runDir('/sys/extensions', shell.run)
local overviewId = multishell.openTab({
path = '/apps/Overview.lua',
focused = true,
hidden = true,
isOverview = true,
})
overviewTab = tabs[overviewId]
runDir('/sys/services', shell.openHiddenTab)
runDir('/autorun', shell.run)
if hasError then
error('An autorun program has errored')
end
end
-- Begin
parentTerm.clear()
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()
while true do
-- Get the event
local tEventData = { os.pullEventRaw() }
local sEvent = table.remove(tEventData, 1)
if sEvent == "term_resize" then
-- Resize event
w,h = parentTerm.getSize()
resizeWindows()
redrawMenu()
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]
if y == 1 and os.locked then
-- ignore
elseif y == 1 then
-- Switch process
local w, h = parentTerm.getSize()
if x == 1 then
multishell.setFocus(overviewTab.tabId)
elseif x == w then
if currentTab then
multishell.terminate(currentTab.tabId)
end
else
for _,tab in pairs(tabs) do
if not tab.hidden and tab.sx then
if x >= tab.sx and x <= tab.ex then
multishell.setFocus(tab.tabId)
break
end
end
end
end
elseif currentTab then
-- Passthrough to current process
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

343
apps/pickup.lua Normal file
View File

@ -0,0 +1,343 @@
require = requireInjector(getfenv(1))
local GPS = require('gps')
local Socket = require('socket')
local MEProvider = require('meProvider')
local Logger = require('logger')
local Point = require('point')
local process = require('process')
if not device.wireless_modem then
error('Modem is required')
end
Logger.setWirelessLogging()
if not turtle then
error('Can only be run on a turtle')
end
turtle.clearMoveCallback()
local gps = GPS.getPointAndHeading()
if not gps then
error('could not get gps location')
end
turtle.setPoint(gps)
local blocks = { }
local meProvider = MEProvider()
local items = { }
local pickups = Util.readTable('pickup.tbl') or { }
local cells = Util.readTable('cells.tbl') or { }
local refills = Util.readTable('refills.tbl') or { }
local fluids = Util.readTable('fluids.tbl') or { }
local chestPt = turtle.loadLocation('chest')
local chargePt = turtle.loadLocation('charge')
local fuel = {
item = {
id = 'minecraft:coal',
dmg = 0,
},
qty = 64
}
local slots
turtle.setMoveCallback(function(action, pt)
if slots then
for _,slot in pairs(slots) do
if turtle.getItemCount(slot.index) ~= slot.qty then
printError('Slots changed')
process:terminate()
end
end
end
end)
function refuel()
if turtle.getFuelLevel() < 5000 then
print('refueling')
turtle.status = 'refueling'
gotoPoint(chestPt, true)
dropOff(chestPt)
while turtle.getFuelLevel() < 5000 do
turtle.select(1)
meProvider:provide(fuel.item, fuel.qty, 1)
turtle.refuel(64)
print(turtle.getFuelLevel())
os.sleep(1)
end
end
end
function pickUp(pt)
turtle.status = 'picking up'
gotoPoint(pt, true)
while true do
if not turtle.selectOpenSlot() then
dropOff(chestPt)
gotoPoint(pt, true)
end
turtle.select(1)
if not turtle.suckDown(64) then
return
end
end
end
function dropOff(pt)
if turtle.selectSlotWithItems() then
gotoPoint(pt, true)
turtle.emptyInventory(turtle.dropDown)
if pt == chestPt then
print('refreshing items')
items = meProvider:refresh()
end
end
end
function gotoPoint(pt, doDetect)
slots = turtle.getInventory()
while not turtle.pathfind(pt, blocks) do
if turtle.abort then
error('aborted')
end
turtle.status = 'blocked'
os.sleep(5)
end
if doDetect and not turtle.detectDown() then
error('Missing target')
end
end
function checkCell(pt)
if not turtle.selectOpenSlot() then
dropOff(chestPt)
end
print('checking cell')
turtle.status = 'recharging'
gotoPoint(pt, true)
local c = peripheral.wrap('bottom')
local energy = c.getMaxEnergyStored() -
c.getEnergyStored()
if energy > 20000 then
print('charging cell')
turtle.selectOpenSlot()
turtle.digDown()
gotoPoint(chargePt, true)
turtle.dropDown()
os.sleep(energy / 20000)
turtle.suckDown()
print('replacing cell')
gotoPoint(pt)
if not turtle.placeDown() then
error('could not place down cell')
end
end
end
function fluid(points)
print('checking fluid')
turtle.status = 'fluiding'
gotoPoint(points.source, true)
turtle.select(1)
turtle.digDown()
gotoPoint(points.target)
if not turtle.placeDown() then
error('could not place fluid container')
end
os.sleep(5)
turtle.digDown()
gotoPoint(points.source)
turtle.placeDown()
end
function refill(entry)
dropOff(chestPt)
turtle.status = 'refilling'
gotoPoint(chestPt)
for _,item in pairs(entry.items) do
meProvider:provide(item, tonumber(item.qty), turtle.selectOpenSlot())
end
if turtle.selectSlotWithItems() then
if entry.point then
dropOff(entry.point)
end
end
end
function oldRefill(points)
gotoPoint(points.source)
repeat until not turtle.suckDown(64)
if points.target then
dropOff(points.target)
end
if points.targets then
for k,target in pairs(points.targets) do
dropOff(target)
end
end
dropOff(points.source)
dropOff(chestPt)
end
local function makeKey(pt)
return string.format('%d:%d:%d', pt.x, pt.y, pt.z)
end
local function pickupHost(socket)
while true do
local data = socket:read()
if not data then
print('pickup: closing connection to ' .. socket.dhost)
return
end
print('command: ' .. data.type)
if data.type == 'pickup' then
local key = makeKey(data.point)
pickups[key] = data.point
Util.writeTable('pickup.tbl', pickups)
socket:write( { type = "response", response = 'added' })
elseif data.type == 'items' then
socket:write( { type = "response", response = items })
elseif data.type == 'refill' then
local key = makeKey(data.entry.point)
refills[key] = data.entry
Util.writeTable('refills.tbl', refills)
socket:write( { type = "response", response = 'added' })
elseif data.type == 'setPickup' then
chestPt = data.point
turtle.storeLocation('chest', chestPt)
socket:write( { type = "response", response = 'Location set' })
elseif data.type == 'setRecharge' then
chargePt = data.point
turtle.storeLocation('charge', chargePt)
socket:write( { type = "response", response = 'Location set' })
elseif data.type == 'charge' then
local key = makeKey(data.point)
cells[key] = data.point
Util.writeTable('cells.tbl', cells)
socket:write( { type = "response", response = 'added' })
elseif data.type == 'fluid' then
elseif data.type == 'clear' then
local key = makeKey(data.point)
refills[key] = nil
cells[key] = nil
fluids[key] = nil
pickups[key] = nil
Util.writeTable('refills.tbl', refills)
Util.writeTable('cells.tbl', cells)
Util.writeTable('fluids.tbl', fluids)
Util.writeTable('pickup.tbl', pickups)
socket:write( { type = "response", response = 'cleared' })
else
print('unknown command')
end
end
end
process:newThread('pickup', function()
while true do
print('waiting for connection on port 5222')
local socket = Socket.server(5222)
print('pickup: connection from ' .. socket.dhost)
process:newThread('pickup_connection', function() pickupHost(socket) end)
end
end)
local function eachEntry(t, fn)
local keys = Util.keys(t)
for _,key in pairs(keys) do
if t[key] then
if turtle.abort then
return
end
fn(t[key])
end
end
end
local function eachClosestEntry(t, fn)
local points = { }
for k,v in pairs(t) do
v = Util.shallowCopy(v)
v.key = k
table.insert(points, v)
end
while not Util.empty(points) do
local closest = Point.closest(turtle.point, points)
if turtle.abort then
return
end
if t[closest.key] then
fn(closest)
end
for k,v in pairs(points) do
if v.key == closest.key then
table.remove(points, k)
break
end
end
end
end
refuel()
turtle.abort = false
local deliveryThread = process:newThread('deliveries', function()
while true do
if chestPt then
eachClosestEntry(pickups, pickUp)
eachEntry(refills, refill)
refuel()
end
eachEntry(fluids, fluid)
if chargePt then
eachEntry(cells, checkCell)
end
print('sleeping')
turtle.status = 'sleeping'
if turtle.abort then
printError('aborted')
break
end
os.sleep(60)
end
end)
turtle.run(function()
while true do
local e = process:pullEvent()
if e == 'terminate' or deliveryThread:isDead() then
break
end
end
end)
process:threadEvent('terminate')

229
apps/pickupRemote.lua Normal file
View File

@ -0,0 +1,229 @@
if not device.wireless_modem then
error('Wireless modem is required')
end
require = requireInjector(getfenv(1))
local GPS = require('gps')
local Event = require('event')
local UI = require('ui')
local Socket = require('socket')
multishell.setTitle(multishell.getCurrent(), 'Pickup Remote')
local id
local mainPage = UI.Page({
menu = UI.Menu({
centered = true,
y = 2,
menuItems = {
{ prompt = 'Pickup', event = 'pickup', help = 'Pickup items from this location' },
{ prompt = 'Charge cell', event = 'charge', help = 'Recharge this cell' },
{ prompt = 'Refill', event = 'refill', help = 'Recharge this cell' },
{ prompt = 'Set pickup location', event = 'setPickup', help = 'Recharge this cell' },
{ prompt = 'Set recharge location', event = 'setRecharge', help = 'Recharge this cell' },
{ prompt = 'Clear', event = 'clear', help = 'Remove this location' },
},
}),
statusBar = UI.StatusBar(),
accelerators = {
q = 'quit',
},
})
local refillPage = UI.Page({
menuBar = UI.MenuBar({
y = 1,
buttons = {
{ text = 'Done', event = 'done', help = 'Pickup items from this location' },
{ text = 'Back', event = 'back', help = 'Recharge this cell' },
},
}),
grid1 = UI.ScrollingGrid({
columns = {
{ heading = 'Name', key = 'name', width = UI.term.width-9 },
{ heading = 'Qty', key = 'fQty', width = 5 },
},
sortColumn = 'name',
height = 8,
y = 3,
}),
grid2 = UI.ScrollingGrid({
columns = {
{ heading = 'Name', key = 'name', width = UI.term.width-9 },
{ heading = 'Qty', key = 'qty', width = 5 },
},
sortColumn = 'name',
height = 4,
y = 12,
}),
statusBar = UI.StatusBar(),
accelerators = {
q = 'quit',
},
})
refillPage.menuBar:add({
filter = UI.TextEntry({
x = UI.term.width-10,
width = 10,
})
})
local function sendCommand(cmd)
local socket = Socket.connect(id, 5222)
if not socket then
mainPage.statusBar:timedStatus('Unable to connect', 3)
return
end
socket:write(cmd)
local m = socket:read(3)
socket:close()
if m then
return m.response
end
mainPage.statusBar:timedStatus('No response', 3)
end
local function getPoint()
local gpt = GPS.getPoint()
if not gpt then
mainPage.statusBar:timedStatus('Unable to get location', 3)
end
return gpt
end
function refillPage:eventHandler(event)
if event.type == 'grid_select' then
local item = {
name = event.selected.name,
id = event.selected.id,
dmg = event.selected.dmg,
qty = 0,
}
local dialog = UI.Dialog({
x = 1,
width = UI.term.width,
text = UI.Text({ x = 3, y = 3, value = 'Quantity' }),
textEntry = UI.TextEntry({ x = 14, y = 3 })
})
dialog.eventHandler = function(self, event)
if event.type == 'accept' then
local l = tonumber(self.textEntry.value)
if l and l <= 1024 and l > 0 then
item.qty = self.textEntry.value
table.insert(refillPage.grid2.values, item)
refillPage.grid2:update()
UI:setPreviousPage()
else
self.statusBar:timedStatus('Invalid Quantity', 3)
end
return true
end
return UI.Dialog.eventHandler(self, event)
end
dialog.titleBar.title = item.name
dialog:setFocus(dialog.textEntry)
UI:setPage(dialog)
elseif event.type == 'text_change' then
local text = event.text
if #text == 0 then
self.grid1.values = self.allItems
else
self.grid1.values = { }
for _,item in pairs(self.allItems) do
if string.find(item.lname, text) then
table.insert(self.grid1.values, item)
end
end
end
--self.grid:adjustWidth()
self.grid1:update()
self.grid1:setIndex(1)
self.grid1:draw()
elseif event.type == 'back' then
UI:setPreviousPage()
elseif event.type == 'done' then
UI:setPage(mainPage)
local pt = getPoint()
if pt then
local response = sendCommand({ type = 'refill', entry = { point = pt, items = self.grid2.values } })
if response then
mainPage.statusBar:timedStatus(response, 3)
end
end
elseif event.type == 'grid_focus_row' then
self.statusBar:setStatus(event.selected.id .. ':' .. event.selected.dmg)
self.statusBar:draw()
end
return UI.Page.eventHandler(self, event)
end
function refillPage:enable()
for _,item in pairs(self.allItems) do
item.lname = string.lower(item.name)
item.fQty = Util.toBytes(item.qty)
end
self.grid1:setValues(self.allItems)
self.menuBar.filter.value = ''
self.menuBar.filter.pos = 1
self:setFocus(self.menuBar.filter)
UI.Page.enable(self)
end
function mainPage:eventHandler(event)
if event.type == 'quit' then
Event.exitPullEvents()
elseif event.type == 'refill' then
local response = sendCommand({ type = 'items' })
if response then
refillPage.allItems = response
refillPage.grid2:setValues({ })
UI:setPage(refillPage)
end
elseif event.type == 'pickup' or event.type == 'setPickup' or
event.type == 'setRecharge' or event.type == 'charge' or
event.type == 'clear' then
local pt = getPoint()
if pt then
local response = sendCommand({ type = event.type, point = pt })
if response then
self.statusBar:timedStatus(response, 3)
end
end
end
return UI.Page.eventHandler(self, event)
end
local args = { ... }
if #args == 1 then
id = tonumber(args[1])
end
if not id then
error('Syntax: pickupRemote <turtle ID>')
end
UI:setPage(mainPage)
Event.pullEvents()
UI.term:reset()

529
apps/recorder.lua Normal file
View File

@ -0,0 +1,529 @@
-- +---------------------+------------+---------------------+
-- | | | |
-- | | RecGif | |
-- | | | |
-- +---------------------+------------+---------------------+
local version = "Version 1.1.6"
-- Records your terminal and saves the result as an animating GIF.
-- http://www.computercraft.info/forums2/index.php?/topic/24840-recgif/
-- ----------------------------------------------------------
-- Original code by Bomb Bloke
-- Modified to integrate with opus os
local calls, recTerm, oldTerm, arg, showInput, skipLast, lastDelay, curInput, callCount, callListCount = {{["delay"] = 0}}, {}, term.current(), {...}, false, false, 2, "", 1, 2
local curBlink, oldBlink, curCalls, tTerm, buffer, colourNum, xPos, yPos, oldXPos, oldYPos, tCol, bCol, xSize, ySize = false, false, calls[1], {}, {}, {}, 1, 1, 1, 1, colours.white, colours.black, term.getSize()
local greys, buttons = {["0"] = true, ["7"] = true, ["8"] = true, ["f"] = true}, {"l", "r", "m"}
local charW, charH, chars, resp
local filename
local function showSyntax()
print('Gif Recorder by Bomb Bloke\n')
print('Syntax: recGif [-i] [-s] [-ld:<delay>] filename')
print(' -i : show input')
print(' -s : skip last')
print(' -ld : last delay')
end
for i = #arg, 1, -1 do
local curArg = arg[i]:lower()
if curArg == "-i" then
showInput, ySize = true, ySize + 1
table.remove(arg, i)
elseif curArg == "-s" then
skipLast = true
table.remove(arg, i)
elseif curArg:sub(1, 4) == "-ld:" then
curArg = tonumber(curArg:sub(5))
if curArg then lastDelay = curArg end
table.remove(arg, i)
elseif curArg == '-?' then
showSyntax()
return
elseif i ~= #arg then
showSyntax()
printError('\nInvalid argument')
return
end
end
print('Press control-p to stop recording')
local filename = arg[#arg]
if not filename then
print('Enter file name:')
filename = read()
end
if #filename == 0 then
showSyntax()
print()
error('Invalid file name')
end
print('Initializing...')
fs.mount('.recGif', 'ramfs', 'directory')
fs.mount('.recGif/GIF', 'urlfs', 'http://pastebin.com/raw/5uk9uRjC')
fs.mount('.recGif/package', 'urlfs', 'http://pastebin.com/raw/cUYTGbpb')
-- don't pollute global env
local function loadAPI(filename, env)
local apiEnv = Util.shallowCopy(env)
apiEnv.shell = nil
apiEnv.multishell = nil
setmetatable(apiEnv, { __index = _G })
local fn = loadfile(filename, apiEnv)
fn()
return apiEnv
end
package = loadAPI('.recGif/package', getfenv(1))
GIF = loadAPI('.recGif/GIF', getfenv(1))
oldTerm = Util.shallowCopy(multishell.term)
local oldDir = shell.dir()
shell.setDir('.recGif')
shell.run("package get Y0eLUPtr")
shell.setDir(oldDir)
local function snooze()
local myEvent = tostring({})
os.queueEvent(myEvent)
os.pullEvent(myEvent)
end
local function safeString(text)
local newText = {}
for i = 1, #text do
local val = text:byte(i)
newText[i] = (val > 31 and val < 127) and val or 63
end
return string.char(unpack(newText))
end
local function safeCol(text, subst)
local newText = {}
for i = 1, #text do
local val = text:sub(i, i)
newText[i] = greys[val] and val or subst
end
return table.concat(newText)
end
-- Build a terminal that records stuff:
recTerm = multishell.term
for key, func in pairs(oldTerm) do
recTerm[key] = function(...)
local result = {pcall(func, ...)}
if result[1] then
curCalls[callCount] = {key, ...}
callCount = callCount + 1
return unpack(result, 2)
else error(result[2], 2) end
end
end
local tabId = multishell.getCurrent()
multishell.addHotkey(25, function()
os.queueEvent('recorder_stop')
end)
local tabs = multishell.getTabs()
for _,tab in pairs(tabs) do
if tab.isOverview then
multishell.hideTab(tabId)
multishell.setFocus(tab.tabId)
os.queueEvent('term_resize')
break
end
end
do
local curTime = os.clock() - 1
while true do
local event = { os.pullEventRaw() }
if event[1] == 'recorder_stop' or event[1] == 'terminate' then
break
end
local newTime = os.clock()
if newTime ~= curTime then
local delay = curCalls.delay + (newTime - curTime)
curTime = newTime
if callCount > 1 then
curCalls.delay = curCalls.delay + delay
curCalls, callCount = {["delay"] = 0}, 1
calls[callListCount] = curCalls
callListCount = callListCount + 1
elseif callListCount > 2 then
calls[callListCount - 2].delay = calls[callListCount - 2].delay + delay
end
end
if showInput and (event[1] == "key" or event[1] == "mouse_click") then
curCalls[callCount] = {unpack(event)}
callCount = callCount + 1
end
end
end
multishell.removeHotkey(25)
for k,fn in pairs(oldTerm) do
multishell.term[k] = fn
end
multishell.unhideTab(tabId)
multishell.setFocus(tabId)
if #calls[#calls] == 0 then calls[#calls] = nil end
if skipLast and #calls > 1 then calls[#calls] = nil end
calls[#calls].delay = lastDelay
-- Recording done, bug user as to whether to encode it:
print(string.format("Encoding %d frames...", #calls))
--Util.writeTable('tmp/raw.txt', calls)
-- Perform a quick re-parse of the recorded data (adding frames for when the cursor blinks):
do
local callListCount, tempCalls, blink, oldBlink, curBlink, blinkDelay = 1, {}, false, false, true, 0
for i = 1, #calls - 1 do
curCalls = calls[i]
tempCalls[callListCount] = curCalls
for j = 1, #curCalls do if curCalls[j][1] == "setCursorBlink" then blink = curCalls[j][2] end end
if blink then
if blinkDelay == 0 then
curCalls[#curCalls + 1] = {"toggleCur", curBlink}
blinkDelay, curBlink = 0.4, not curBlink
end
while tempCalls[callListCount].delay > blinkDelay do
local remainder = tempCalls[callListCount].delay - blinkDelay
tempCalls[callListCount].delay = blinkDelay
callListCount = callListCount + 1
tempCalls[callListCount] = {{"toggleCur", curBlink}, ["delay"] = remainder}
blinkDelay, curBlink = 0.4, not curBlink
end
blinkDelay = blinkDelay - tempCalls[callListCount].delay
else
if oldBlink then curCalls[#curCalls + 1] = {"toggleCur", false} end
blinkDelay = (curCalls.delay - blinkDelay) % 0.4
end
callListCount, oldBlink = callListCount + 1, blink
end
tempCalls[callListCount] = calls[#calls]
tempCalls[callListCount][#tempCalls[callListCount] + 1] = {"toggleCur", false}
calls, curCalls = tempCalls, nil
end
snooze()
-- Load font data:
do
local ascii, counter = GIF.toPaintutils(GIF.flattenGIF(GIF.loadGIF(".recGif/ascii.gif"))), 0
local newFont, ybump, xbump = #ascii ~= #ascii[1], 0, 0
charW, charH, chars = newFont and #ascii[1] / 16 or #ascii[1] * 3 / 64, #ascii / 16, {}
for yy = 0, newFont and 15 or 7 do
for xx = 0, 15 do
local newChar, length = {}, 0
-- Place in 2d grid of bools:
for y = 1, charH do
local newRow = {}
for x = 1, charW do
local set = ascii[y + ybump][x + xbump] == 1
if set and x > length then length = x end
newRow[x] = set
end
newChar[y] = newRow
end
-- Center:
if not newFont then for y = 1, charH do for x = 1, math.floor((charW - length) / 2) do table.insert(newChar[y], 1, false) end end end
chars[counter] = newChar
counter, xbump = counter + 1, xbump + (newFont and charW or charH)
end
xbump, ybump = 0, ybump + charH
end
end
snooze()
-- Terminal data translation:
do
local hex, counter = "0123456789abcdef", 1
for i = 1, 16 do
colourNum[counter] = hex:sub(i, i)
counter = counter * 2
end
end
for y = 1, ySize do
buffer[y] = {}
for x = 1, xSize do buffer[y][x] = {" ", colourNum[tCol], colourNum[bCol]} end
end
if showInput then for x = 1, xSize do buffer[ySize][x][3] = colourNum[colours.lightGrey] end end
tTerm.blit = function(text, fgCol, bgCol)
if xPos > xSize or xPos + #text - 1 < 1 or yPos < 1 or yPos > ySize then return end
if not _HOST then text = safeString(text) end
if not term.isColour() then
fgCol = safeCol(fgCol, "0")
bgCol = safeCol(bgCol, "f")
end
if xPos < 1 then
text = text:sub(2 - xPos)
fgCol = fgCol:sub(2 - xPos)
bgCol = bgCol:sub(2 - xPos)
xPos = 1
end
if xPos + #text - 1 > xSize then
text = text:sub(1, xSize - xPos + 1)
fgCol = fgCol:sub(1, xSize - xPos + 1)
bgCol = bgCol:sub(1, xSize - xPos + 1)
end
for x = 1, #text do
buffer[yPos][xPos + x - 1][1] = text:sub(x, x)
buffer[yPos][xPos + x - 1][2] = fgCol:sub(x, x)
buffer[yPos][xPos + x - 1][3] = bgCol:sub(x, x)
end
xPos = xPos + #text
end
tTerm.write = function(text)
text = tostring(text)
tTerm.blit(text, string.rep(colourNum[tCol], #text), string.rep(colourNum[bCol], #text))
end
tTerm.clearLine = function()
local oldXPos = xPos
xPos = 1
tTerm.write(string.rep(" ", xSize))
xPos = oldXPos
end
tTerm.clear = function()
local oldXPos, oldYPos = xPos, yPos
for y = 1, ySize do
xPos, yPos = 1, y
tTerm.write(string.rep(" ", xSize))
end
xPos, yPos = oldXPos, oldYPos
end
tTerm.setCursorPos = function(x, y)
xPos, yPos = math.floor(x), math.floor(y)
end
tTerm.setTextColour = function(col)
tCol = col
end
tTerm.setTextColor = function(col)
tCol = col
end
tTerm.setBackgroundColour = function(col)
bCol = col
end
tTerm.setBackgroundColor = function(col)
bCol = col
end
tTerm.scroll = function(lines)
if math.abs(lines) < ySize then
local oldXPos, oldYPos = xPos, yPos
for y = 1, ySize do
if y + lines > 0 and y + lines <= ySize then
for x = 1, xSize do
xPos, yPos = x, y
tTerm.blit(buffer[y + lines][x][1], buffer[y + lines][x][2], buffer[y + lines][x][3])
end
else
yPos = y
tTerm.clearLine()
end
end
xPos, yPos = oldXPos, oldYPos
else tTerm.clear() end
end
tTerm.toggleCur = function(newBlink)
curBlink = newBlink
end
tTerm.newInput = function(input)
local oldTC, oldBC, oldX, oldY = tCol, bCol, xPos, yPos
tCol, bCol, xPos, yPos, ySize, input = colours.grey, colours.lightGrey, 1, ySize + 1, ySize + 1, input .. " "
while #curInput + #input + 1 > xSize do curInput = curInput:sub(curInput:find(" ") + 1) end
curInput = curInput .. input .. " "
tTerm.clearLine()
tTerm.write(curInput)
tCol, bCol, xPos, yPos, ySize = oldTC, oldBC, oldX, oldY, ySize - 1
end
tTerm.key = function(key)
tTerm.newInput((not keys.getName(key)) and "unknownKey" or keys.getName(key))
end
tTerm.mouse_click = function(button, x, y)
tTerm.newInput(buttons[button] .. "C@" .. tostring(x) .. "x" .. tostring(y))
end
local image = {["width"] = xSize * charW, ["height"] = ySize * charH}
for i = 1, #calls do
local xMin, yMin, xMax, yMax, oldBuffer, curCalls, changed = xSize + 1, ySize + 1, 0, 0, {}, calls[i], false
calls[i] = nil
for y = 1, ySize do
oldBuffer[y] = {}
for x = 1, xSize do oldBuffer[y][x] = {buffer[y][x][1], buffer[y][x][2], buffer[y][x][3], buffer[y][x][4]} end
end
snooze()
if showInput then ySize = ySize - 1 end
for j = 1, #curCalls do if tTerm[curCalls[j][1]] then tTerm[curCalls[j][1]](unpack(curCalls[j], 2)) end end
if showInput then ySize = ySize + 1 end
if i > 1 then
for yy = 1, ySize do for xx = 1, xSize do if buffer[yy][xx][1] ~= oldBuffer[yy][xx][1] or (buffer[yy][xx][2] ~= oldBuffer[yy][xx][2] and buffer[yy][xx][1] ~= " ") or buffer[yy][xx][3] ~= oldBuffer[yy][xx][3] then
changed = true
if xx < xMin then xMin = xx end
if xx > xMax then xMax = xx end
if yy < yMin then yMin = yy end
if yy > yMax then yMax = yy end
end end end
else xMin, yMin, xMax, yMax, changed = 1, 1, xSize, ySize, true end
if oldBlink and (xPos ~= oldXPos or yPos ~= oldYPos or not curBlink) and oldXPos > 0 and oldYPos > 0 and oldXPos <= xSize and oldYPos <= ySize then
changed = true
if oldXPos < xMin then xMin = oldXPos end
if oldXPos > xMax then xMax = oldXPos end
if oldYPos < yMin then yMin = oldYPos end
if oldYPos > yMax then yMax = oldYPos end
buffer[oldYPos][oldXPos][4] = false
end
if curBlink and (xPos ~= oldXPos or yPos ~= oldYPos or not oldBlink) and xPos > 0 and yPos > 0 and xPos <= xSize and yPos <= ySize then
changed = true
if xPos < xMin then xMin = xPos end
if xPos > xMax then xMax = xPos end
if yPos < yMin then yMin = yPos end
if yPos > yMax then yMax = yPos end
buffer[yPos][xPos][4] = true
end
oldBlink, oldXPos, oldYPos = curBlink, xPos, yPos
local thisFrame = {["xstart"] = (xMin - 1) * charW, ["ystart"] = (yMin - 1) * charH, ["xend"] = (xMax - xMin + 1) * charW, ["yend"] = (yMax - yMin + 1) * charH, ["delay"] = curCalls.delay, ["disposal"] = 1}
for y = 1, (yMax - yMin + 1) * charH do
local row = {}
for x = 1, (xMax - xMin + 1) * charW do row[x] = " " end
thisFrame[y] = row
end
snooze()
for yy = yMin, yMax do
local yBump = (yy - yMin) * charH
for xx = xMin, xMax do if buffer[yy][xx][1] ~= oldBuffer[yy][xx][1] or (buffer[yy][xx][2] ~= oldBuffer[yy][xx][2] and buffer[yy][xx][1] ~= " ") or buffer[yy][xx][3] ~= oldBuffer[yy][xx][3] or buffer[yy][xx][4] ~= oldBuffer[yy][xx][4] or i == 1 then
local thisChar, thisT, thisB, xBump = chars[buffer[yy][xx][1]:byte()], buffer[yy][xx][2], buffer[yy][xx][3], (xx - xMin) * charW
for y = 1, charH do for x = 1, charW do thisFrame[y + yBump][x + xBump] = thisChar[y][x] and thisT or thisB end end
if buffer[yy][xx][4] then
thisT, thisChar = colourNum[tCol], chars[95]
for y = 1, charH do for x = 1, charW do if thisChar[y][x] then thisFrame[y + yBump][x + xBump] = thisT end end end
end
end end
for y = yBump + 1, yBump + charH do
local skip, chars, row = 0, {}, {}
for x = 1, #thisFrame[y] do
if thisFrame[y][x] == " " then
if #chars > 0 then
row[#row + 1] = table.concat(chars)
chars = {}
end
skip = skip + 1
else
if skip > 0 then
row[#row + 1] = skip
skip = 0
end
chars[#chars + 1] = thisFrame[y][x]
end
end
if #chars > 0 then row[#row + 1] = table.concat(chars) end
thisFrame[y] = row
end
snooze()
end
if changed then image[#image + 1] = thisFrame else image[#image].delay = image[#image].delay + curCalls.delay end
end
buffer = nil
GIF.saveGIF(image, filename)
fs.unmount('.recGif')
print("Encode complete")

1
apps/scripts/abort Normal file
View File

@ -0,0 +1 @@
turtle.abortAction()

120
apps/scripts/follow Normal file
View File

@ -0,0 +1,120 @@
local function follow(id)
local GPS = require('gps')
local Socket = require('socket')
local Point = require('point')
local process = require('process')
turtle.status = 'follow ' .. id
local pt = GPS.getPointAndHeading()
if not pt or not pt.heading then
error('turtle: No GPS found')
end
turtle.setPoint(pt)
local socket = Socket.connect(id, 161)
if not socket then
error('turtle: Unable to connect to ' .. id)
return
end
local lastPoint
local following = false
local followThread = process:newThread('follower', function()
while true do
local function getRemotePoint()
if not turtle.abort then
if socket:write({ type = 'gps' }) then
return socket:read(3)
end
end
end
-- sometimes gps will fail if moving
local pt, d
for i = 1, 3 do
pt, d = getRemotePoint()
if pt then
break
end
os.sleep(.5)
end
if not pt or turtle.abort then
error('Did not receive GPS location')
end
if not lastPoint or (lastPoint.x ~= pt.x or lastPoint.y ~= pt.y or lastPoint.z ~= pt.z) then
if following then
turtle.abort = true
while following do
os.sleep(.1)
end
turtle.abort = false
end
-- check if gps is inaccurate (player moving too fast)
if d < Point.pythagoreanDistance(turtle.point, pt) + 10 then
lastPoint = Point.copy(pt)
following = true
process:newThread('turtle_follow', function()
local pts = {
{ x = pt.x + 2, z = pt.z, y = pt.y },
{ x = pt.x - 2, z = pt.z, y = pt.y },
{ x = pt.x, z = pt.z + 2, y = pt.y },
{ x = pt.x, z = pt.z - 2, y = pt.y },
}
local cpt = Point.closest(turtle.point, pts)
local blocks = { }
local function addBlocks(tpt)
table.insert(blocks, tpt)
local apts = Point.adjacentPoints(tpt)
for _,apt in pairs(apts) do
table.insert(blocks, apt)
end
end
-- don't run into player
addBlocks(pt)
addBlocks({ x = pt.x, z = pt.z, y = pt.y + 1 })
if turtle.pathfind(cpt, blocks) then
turtle.headTowards(pt)
end
following = false
end)
end
end
os.sleep(.5)
end
end)
while true do
local e = process:pullEvent()
if e == 'terminate' or followThread:isDead() or e =='turtle_abort' then
process:threadEvent('terminate')
break
end
end
socket:close()
return true
end
local s, m = turtle.run(function() follow({COMPUTER_ID}) end)
if not s and m then
error(m)
end

1
apps/scripts/goHome Normal file
View File

@ -0,0 +1 @@
turtle.run(turtle.gotoGPSHome)

29
apps/scripts/moveTo Normal file
View File

@ -0,0 +1,29 @@
turtle.run(function()
local GPS = require('gps')
local Socket = require('socket')
local id = {COMPUTER_ID}
local pt = GPS.getPointAndHeading()
if not pt or not pt.heading then
error('turtle: No GPS found')
end
turtle.setPoint(pt)
local socket = Socket.connect(id, 161)
if not socket then
error('turtle: Unable to connect to ' .. id)
end
socket:write({ type = 'gps' })
local pt = socket:read(3)
if not pt then
error('turtle: No GPS response')
end
if not turtle.pathfind(pt, nil, 64) then
error('Unable to go to location')
end
end)

1
apps/scripts/reboot Normal file
View File

@ -0,0 +1 @@
os.reboot()

1
apps/scripts/setHome Normal file
View File

@ -0,0 +1 @@
turtle.run(turtle.setGPSHome)

1
apps/scripts/shutdown Normal file
View File

@ -0,0 +1 @@
os.shutdown()

72
apps/scripts/summon Normal file
View File

@ -0,0 +1,72 @@
local function summon(id)
local GPS = require('gps')
local Socket = require('socket')
local Point = require('point')
turtle.status = 'GPSing'
turtle.setPoint({ x = 0, y = 0, z = 0, heading = 0 })
local pts = {
[ 1 ] = { x = 0, z = 0, y = 0 },
[ 2 ] = { x = 4, z = 0, y = 0 },
[ 3 ] = { x = 2, z = -2, y = 2 },
[ 4 ] = { x = 2, z = 2, y = 2 },
}
local tFixes = { }
local socket = Socket.connect(id, 161)
if not socket then
error('turtle: Unable to connect to ' .. id)
end
local function getDistance()
socket:write({ type = 'ping' })
local _, d = socket:read(5)
return d
end
local function doGPS()
tFixes = { }
for i = 1, 4 do
if not turtle.gotoPoint(pts[i]) then
error('turtle: Unable to perform GPS maneuver')
end
local distance = getDistance()
if not distance then
error('turtle: No response from ' .. id)
end
table.insert(tFixes, {
position = vector.new(turtle.point.x, turtle.point.y, turtle.point.z),
distance = distance
})
end
return true
end
if not doGPS() then
turtle.turnAround()
turtle.setPoint({ x = 0, y = 0, z = 0, heading = 0})
if not doGPS() then
socket:close()
return false
end
end
socket:close()
local pos = GPS.trilaterate(tFixes)
if pos then
local pt = { x = pos.x, y = pos.y, z = pos.z }
local _, h = Point.calculateMoves(turtle.getPoint(), pt)
local hi = turtle.getHeadingInfo(h)
turtle.status = 'recalling'
turtle.pathfind({ x = pt.x - hi.xd, z = pt.z - hi.zd, y = pt.y - hi.yd, heading = h })
else
error("turtle: Could not determine position")
end
end
turtle.run(function() summon({COMPUTER_ID}) end)

1
apps/scripts/update Normal file
View File

@ -0,0 +1 @@
shell.run('/apps/update.lua')

623
apps/shell Normal file
View File

@ -0,0 +1,623 @@
local parentShell = shell
shell = { }
local sandboxEnv = Util.shallowCopy(getfenv(1))
setmetatable(sandboxEnv, { __index = _G })
local DIR = (parentShell and parentShell.dir()) or ""
local PATH = (parentShell and parentShell.path()) or ".:/rom/programs"
local ALIASES = (parentShell and parentShell.aliases()) or {}
local tCompletionInfo = (parentShell and parentShell.getCompletionInfo()) or {}
local bExit = false
local tProgramStack = {}
local function parseCommandLine( ... )
local sLine = table.concat( { ... }, " " )
local tWords = {}
local bQuoted = false
for match in string.gmatch( sLine .. "\"", "(.-)\"" ) do
if bQuoted then
table.insert( tWords, match )
else
for m in string.gmatch( match, "[^ \t]+" ) do
table.insert( tWords, m )
end
end
bQuoted = not bQuoted
end
return table.remove(tWords, 1), tWords
end
-- Install shell API
function shell.run(...)
local path, args = parseCommandLine(...)
path = shell.resolveProgram(path)
if path then
tProgramStack[#tProgramStack + 1] = path
local oldTitle
if multishell and multishell.getTitle then
oldTitle = multishell.getTitle(multishell.getCurrent())
multishell.setTitle(multishell.getCurrent(), fs.getName(path))
end
local result, err = os.run(Util.shallowCopy(sandboxEnv), path, unpack(args))
if multishell then
local title = 'shell'
if #tProgramStack > 0 then
title = fs.getName(tProgramStack[#tProgramStack])
end
multishell.setTitle(multishell.getCurrent(), oldTitle or 'shell')
end
return result, err
end
return false, 'No such program'
end
function shell.exit()
bExit = true
end
function shell.dir() return DIR end
function shell.setDir(d) DIR = d end
function shell.path() return PATH end
function shell.setPath(p) PATH = p end
function shell.resolve( _sPath )
local sStartChar = string.sub( _sPath, 1, 1 )
if sStartChar == "/" or sStartChar == "\\" then
return fs.combine( "", _sPath )
else
return fs.combine(DIR, _sPath )
end
end
function shell.resolveProgram( _sCommand )
local sPath = PATH or ''
if ALIASES[ _sCommand ] ~= nil then
_sCommand = ALIASES[ _sCommand ]
end
local path = shell.resolve(_sCommand)
if fs.exists(path) and not fs.isDir(path) then
return path
end
if fs.exists(path .. '.lua') then
return path .. '.lua'
end
-- If the path is a global path, use it directly
local sStartChar = string.sub( _sCommand, 1, 1 )
if sStartChar == "/" or sStartChar == "\\" then
local sPath = fs.combine( "", _sCommand )
if fs.exists( sPath ) and not fs.isDir( sPath ) then
return sPath
end
return nil
end
-- Otherwise, look on the path variable
for sPath in string.gmatch(sPath, "[^:]+") do
sPath = fs.combine( shell.resolve(sPath), _sCommand )
if fs.exists( sPath ) and not fs.isDir( sPath ) then
return sPath
end
if fs.exists(sPath .. '.lua') then
return sPath .. '.lua'
end
end
-- Not found
return nil
end
function shell.programs( _bIncludeHidden )
local tItems = {}
-- Add programs from the path
for sPath in string.gmatch(PATH, "[^:]+") do
sPath = shell.resolve(sPath)
if fs.isDir( sPath ) then
local tList = fs.list( sPath )
for n,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
end
end
end
end
-- Sort and return
local tItemList = {}
for sItem, b 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
function shell.setCompletionFunction(sProgram, fnComplete)
tCompletionInfo[sProgram] = { fnComplete = fnComplete }
end
function shell.getCompletionInfo()
return tCompletionInfo
end
function shell.getRunningProgram()
return tProgramStack[#tProgramStack]
end
function shell.set(name, value)
getfenv(1)[name] = value
end
function shell.get(name)
return getfenv(1)[name]
end
function shell.setAlias( _sCommand, _sProgram )
ALIASES[ _sCommand ] = _sProgram
end
function shell.clearAlias( _sCommand )
ALIASES[ _sCommand ] = nil
end
function shell.aliases()
local tCopy = {}
for sAlias, sCommand in pairs(ALIASES) do
tCopy[sAlias] = sCommand
end
return tCopy
end
function shell.newTab(tabInfo, ...)
local path, args = parseCommandLine(...)
path = shell.resolveProgram(path)
if path then
tabInfo.path = path
tabInfo.env = sandboxEnv
tabInfo.args = args
tabInfo.title = fs.getName(path)
return multishell.openTab(tabInfo)
end
return nil, 'No such program'
end
function shell.openTab( ... )
return shell.newTab({ }, ...)
end
function shell.openForegroundTab( ... )
return shell.newTab({ focused = true }, ...)
end
function shell.openHiddenTab( ... )
return shell.newTab({ hidden = true }, ...)
end
function shell.switchTab(tabId)
multishell.setFocus(tabId)
end
local tArgs = { ... }
if #tArgs > 0 then
-- "shell x y z"
-- Run the program specified in this new shell
local s, m = shell.run( ... )
if not s and m ~= 'Terminated' then
error(m or '')
end
return s, m
end
require = requireInjector(getfenv(1))
local Config = require('config')
local History = require('history')
local config = {
standard = {
textColor = colors.white,
commandTextColor = colors.lightGray,
directoryTextColor = colors.gray,
directoryBackgroundColor = colors.black,
promptTextColor = colors.gray,
promptBackgroundColor = colors.black,
directoryColor = colors.gray,
},
color = {
textColor = colors.white,
commandTextColor = colors.yellow,
directoryTextColor = colors.orange,
directoryBackgroundColor = colors.black,
promptTextColor = colors.blue,
promptBackgroundColor = colors.black,
directoryColor = colors.green,
},
displayDirectory = true,
}
--Config.load('shell', 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)
debug({ rawPath, startDir })
for _,f in pairs(files) do
local path = fs.combine(rawPath, f)
if fs.isDir(fs.combine(startDir, f)) then
results[path .. '/'] = 'directory'
else
results[path .. ' '] = 'program'
end
end
end
end
local function autocompleteProgram(results, words)
if #words == 1 then
local files = shell.programs(true)
for _,f in ipairs(files) do
results[f .. ' '] = 'program'
end
for f in pairs(ALIASES) do
results[f .. ' '] = 'program'
end
end
end
local function autocompleteArgument(results, program, words)
local word = ''
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
end
local function autocomplete(line, suggestions)
local words = { }
for word in line:gmatch("%S+") do
table.insert(words, word)
end
if line:match(' $') then
table.insert(words, '')
end
local results = { }
if #words == 0 then
files = autocompleteFile(results, words)
else
local program = shell.resolveProgram(words[1])
if tCompletionInfo[program] then
autocompleteArgument(results, program, words)
else
autocompleteProgram(results, words)
autocompleteFile(results, words)
end
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
end
if #files == 1 then
words[#words] = files[1]
return table.concat(words, ' ')
elseif #files > 1 and suggestions then
print()
local word = words[#words] or ''
local prefix = word:match("(.*/)") or ''
if #prefix > 0 then
for _,f in ipairs(files) do
if f:match("^" .. prefix) ~= prefix then
prefix = ''
break
end
end
end
local tDirs, tFiles = { }, { }
for _,f in ipairs(files) do
if results[f] == 'directory' then
f = f:gsub(prefix, '', 1)
table.insert(tDirs, f)
else
f = f:gsub(prefix, '', 1)
table.insert(tFiles, f)
end
end
table.sort(tDirs)
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
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
table.insert(tDirs, '')
end
end
end
if #tDirs > 0 then
textutils.tabulate(_colors.directoryColor, tDirs, colors.white, tFiles)
else
textutils.tabulate(colors.white, tFiles)
end
term.setTextColour(_colors.promptTextColor)
term.setBackgroundColor(_colors.promptBackgroundColor)
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
local function shellRead(_tHistory )
term.setCursorBlink( true )
local sLine = ""
local nHistoryPos
local nPos = 0
local lastPattern
local w = term.getSize()
local sx = term.getCursorPos()
local function redraw( sReplace )
local nScroll = 0
if sx + nPos >= w then
nScroll = (sx + nPos) - w
end
local cx,cy = term.getCursorPos()
term.setCursorPos( sx, cy )
if sReplace then
term.write( string.rep( sReplace, math.max( string.len(sLine) - nScroll, 0 ) ) )
else
term.write( string.sub( sLine, nScroll + 1 ) )
end
term.setCursorPos( sx + nPos - nScroll, cy )
end
while true do
local sEvent, param, param2 = os.pullEventRaw()
if sEvent == "char" then
sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 )
nPos = nPos + 1
redraw()
elseif sEvent == "paste" then
sLine = string.sub( sLine, 1, nPos ) .. param .. string.sub( sLine, nPos + 1 )
nPos = nPos + string.len( param )
redraw()
elseif sEvent == 'mouse_click' and param == 2 then
redraw(string.rep(' ', #sLine))
sLine = ''
nPos = 0
redraw()
elseif sEvent == 'terminate' then
bExit = true
break
elseif sEvent == "key" then
if param == keys.enter then
-- Enter
break
elseif param == keys.tab then
if nPos == #sLine then
local showSuggestions = lastPattern == sLine
lastPattern = sLine
local cline = autocomplete(sLine, showSuggestions)
if cline then
sLine = cline
nPos = #sLine
redraw()
end
end
elseif param == keys.left then
if nPos > 0 then
nPos = nPos - 1
redraw()
end
elseif param == keys.right then
if nPos < string.len(sLine) then
redraw(" ")
nPos = nPos + 1
redraw()
end
elseif param == keys.up or param == keys.down then
if _tHistory then
redraw(" ")
if param == keys.up then
if nHistoryPos == nil then
if #_tHistory > 0 then
nHistoryPos = #_tHistory
end
elseif nHistoryPos > 1 then
nHistoryPos = nHistoryPos - 1
end
else
if nHistoryPos == #_tHistory then
nHistoryPos = nil
elseif nHistoryPos ~= nil then
nHistoryPos = nHistoryPos + 1
end
end
if nHistoryPos then
sLine = _tHistory[nHistoryPos]
nPos = string.len( sLine )
else
sLine = ""
nPos = 0
end
redraw()
end
elseif param == keys.backspace then
if nPos > 0 then
redraw(" ")
sLine = string.sub( sLine, 1, nPos - 1 ) .. string.sub( sLine, nPos + 1 )
nPos = nPos - 1
redraw()
end
elseif param == keys.home then
redraw(" ")
nPos = 0
redraw()
elseif param == keys.delete then
if nPos < string.len(sLine) then
redraw(" ")
sLine = string.sub( sLine, 1, nPos ) .. string.sub( sLine, nPos + 2 )
redraw()
end
elseif param == keys["end"] then
redraw(" ")
nPos = string.len(sLine)
redraw()
end
elseif sEvent == "term_resize" then
w = term.getSize()
redraw()
end
end
local cx, cy = term.getCursorPos()
term.setCursorPos( w + 1, cy )
print()
term.setCursorBlink( false )
return sLine
end
local history = History.load('.shell_history', 25)
while not bExit do
if config.displayDirectory then
term.setTextColour(_colors.directoryTextColor)
term.setBackgroundColor(_colors.directoryBackgroundColor)
print('==' .. os.getComputerLabel() .. ':/' .. DIR)
end
term.setTextColour(_colors.promptTextColor)
term.setBackgroundColor(_colors.promptBackgroundColor)
write("$ " )
term.setTextColour(_colors.commandTextColor)
term.setBackgroundColor(colors.black)
local sLine = shellRead(history.entries)
if bExit then -- terminated
break
end
sLine = Util.trim(sLine)
if #sLine > 0 and sLine ~= 'exit' then
history.add(sLine)
end
term.setTextColour(_colors.textColor)
if #sLine > 0 then
local result, err = shell.run( sLine )
if not result then
printError(err)
end
end
end

570
apps/simpleMiner.lua Normal file
View File

@ -0,0 +1,570 @@
require = requireInjector(getfenv(1))
local Point = require('point')
local Logger = require('logger')
if device and device.wireless_modem then
Logger.setWirelessLogging()
end
local args = { ... }
local options = {
chunks = { arg = 'c', type = 'number', value = -1,
desc = 'Number of chunks to mine' },
depth = { arg = 'd', type = 'number', value = 9000,
desc = 'Mining depth' },
-- enderChest = { arg = 'e', type = 'flag', value = false,
-- desc = 'Use ender chest' },
resume = { arg = 'r', type = 'flag', value = false,
desc = 'Resume mining' },
setTrash = { arg = 's', type = 'flag', value = false,
desc = 'Set trash items' },
help = { arg = 'h', type = 'flag', value = false,
desc = 'Displays the options' },
}
local MIN_FUEL = 7500
local LOW_FUEL = 1500
local mining = {
diameter = 1,
chunkIndex = 0,
chunks = -1,
}
local trash
local boreDirection
function getChunkCoordinates(diameter, index, x, z)
local dirs = { -- circumference of grid
{ xd = 0, zd = 1, heading = 1 }, -- south
{ xd = -1, zd = 0, heading = 2 },
{ xd = 0, zd = -1, heading = 3 },
{ xd = 1, zd = 0, heading = 0 } -- east
}
-- always move east when entering the next diameter
if index == 0 then
dirs[4].x = x + 16
dirs[4].z = z
return dirs[4]
end
dir = dirs[math.floor(index / (diameter - 1)) + 1]
dir.x = x + dir.xd * 16
dir.z = z + dir.zd * 16
return dir
end
function getBoreLocations(x, z)
local locations = {}
while true do
local a = math.abs(z)
local b = math.abs(x)
if x > 0 and z > 0 or
x < 0 and z < 0 then
-- rotate coords
a = math.abs(x)
b = math.abs(z)
end
if (a % 5 == 0 and b % 5 == 0) or
(a % 5 == 2 and b % 5 == 1) or
(a % 5 == 4 and b % 5 == 2) or
(a % 5 == 1 and b % 5 == 3) or
(a % 5 == 3 and b % 5 == 4) then
table.insert(locations, { x = x, z = z, y = 0 })
end
if z % 2 == 0 then -- forward dir
if (x + 1) % 16 == 0 then
z = z + 1
else
x = x + 1
end
else
if (x - 1) % 16 == 15 then
if (z + 1) % 16 == 0 then
break
end
z = z + 1
else
x = x - 1
end
end
end
return locations
end
-- get the bore location closest to the miner
local function getClosestLocation(points, b)
local key = 1
local leastMoves = 9000
for k,pt in pairs(points) do
local moves = Point.calculateMoves(turtle.point, pt)
if moves < leastMoves then
key = k
leastMoves = moves
if leastMoves == 0 then
break
end
end
end
return table.remove(points, key)
end
function getCornerOf(c)
return math.floor(c.x / 16) * 16, math.floor(c.z / 16) * 16
end
function nextChunk()
local x, z = getCornerOf({ x = mining.x, z = mining.z })
local points = math.pow(mining.diameter, 2) - math.pow(mining.diameter-2, 2)
mining.chunkIndex = mining.chunkIndex + 1
if mining.chunkIndex >= points then
mining.diameter = mining.diameter + 2
mining.chunkIndex = 0
end
if mining.chunks ~= -1 then
local chunks = math.pow(mining.diameter-2, 2) + mining.chunkIndex
if chunks >= mining.chunks then
return false
end
end
local nc = getChunkCoordinates(mining.diameter, mining.chunkIndex, x, z)
mining.locations = getBoreLocations(nc.x, nc.z)
-- enter next chunk
mining.x = nc.x
mining.z = nc.z
Util.writeTable('mining.progress', mining)
return true
end
function addTrash()
if not trash then
trash = { }
end
local slots = turtle.getFilledSlots()
for k,slot in pairs(slots) do
if slot.iddmg ~= 'minecraft:bucket:0' then
trash[slot.iddmg] = true
end
end
Util.writeTable('mining.trash', trash)
end
function log(text)
print(text)
Logger.log('mineWorker', text)
end
function status(status)
turtle.status = status
log(status)
end
function refuel()
if turtle.getFuelLevel() < MIN_FUEL then
status('refueling')
if turtle.selectSlot('minecraft:coal:0') then
local qty = turtle.getItemCount()
print('refueling ' .. qty)
turtle.refuel(qty)
end
if turtle.getFuelLevel() < MIN_FUEL then
log('desperate fueling')
turtle.eachFilledSlot(function(slot)
if turtle.getFuelLevel() < MIN_FUEL then
turtle.select(slot.index)
turtle.refuel(64)
end
end)
end
log('Fuel: ' .. turtle.getFuelLevel())
status('boring')
end
turtle.select(1)
end
function enderChestUnload()
log('unloading')
turtle.select(1)
if not Util.tryTimed(5, function()
turtle.digDown()
return turtle.placeDown()
end) then
log('placedown failed')
else
turtle.reconcileInventory(slots, turtle.dropDown)
turtle.select(1)
turtle.drop(64)
turtle.digDown()
end
end
function safeGoto(x, z, y, h)
local oldStatus = turtle.status
while not turtle.pathfind({ x = x, z = z, y = y, heading = h }) do
--status('stuck')
if turtle.abort then
return false
end
--os.sleep(1)
end
turtle.status = oldStatus
return true
end
function safeGotoY(y)
local oldStatus = turtle.status
while not turtle.gotoY(y) do
status('stuck')
if turtle.abort then
return false
end
os.sleep(1)
end
turtle.status = oldStatus
return true
end
function makeWalkableTunnel(action, tpt, pt)
if action ~= 'turn' and not Point.compare(tpt, { x = 0, z = 0 }) then -- not at source
if not Point.compare(tpt, pt) then -- not at dest
local r, block = turtle.inspectUp()
if r and block.name ~= 'minecraft:cobblestone' then
if block.name ~= 'minecraft:chest' then
turtle.digUp()
end
end
end
end
end
function normalChestUnload()
local oldStatus = turtle.status
status('unloading')
local pt = Util.shallowCopy(turtle.point)
safeGotoY(0)
turtle.setMoveCallback(function(action, tpt)
makeWalkableTunnel(action, tpt, { x = pt.x, z = pt.z })
end)
safeGoto(0, 0)
if not turtle.detectUp() then
error('no chest')
end
local slots = turtle.getFilledSlots()
for _,slot in pairs(slots) do
if not trash[slot.iddmg] and slot.iddmg ~= 'minecraft:bucket:0' then
turtle.select(slot.index)
turtle.dropUp(64)
end
end
turtle.select(1)
safeGoto(pt.x, pt.z, 0, pt.heading)
turtle.clearMoveCallback()
safeGotoY(pt.y)
status(oldStatus)
end
function ejectTrash()
local cobbleSlotCount = 0
turtle.eachFilledSlot(function(slot)
if slot.iddmg == 'minecraft:cobblestone:0' then
cobbleSlotCount = cobbleSlotCount + 1
end
if trash[slot.iddmg] then
-- retain 1 slot with cobble in order to indicate active mining
if slot.iddmg ~= 'minecraft:cobblestone:0' or cobbleSlotCount > 1 then
turtle.select(slot.index)
turtle.dropDown(64)
end
end
end)
end
function mineable(action)
local r, block = action.inspect()
if not r then
return false
end
if block.name == 'minecraft:chest' then
collectDrops(action.suck)
end
if turtle.getFuelLevel() < 99000 then
if block.name == 'minecraft:lava' or block.name == 'minecraft:flowing_lava' then
if turtle.selectSlot('minecraft:bucket:0') then
if action.place() then
log('Lava! ' .. turtle.getFuelLevel())
turtle.refuel()
log(turtle.getFuelLevel())
end
turtle.select(1)
end
return false
end
end
if action.side == 'bottom' then
return true
end
return not trash[block.name .. ':0']
end
function mine(action)
if mineable(action) then
checkSpace()
--collectDrops(action.suck)
action.dig()
end
end
function bore()
local loc = turtle.point
local level = loc.y
turtle.select(1)
status('boring down')
boreDirection = 'down'
while true do
if turtle.abort then
status('aborting')
return false
end
if loc.y <= -mining.depth then
break
end
if turtle.point.y < -2 then
-- turtle.setDigPolicy(turtle.digPolicies.turtleSafe)
end
mine(turtle.getAction('down'))
if not Util.tryTimed(3, turtle.down) then
break
end
mine(turtle.getAction('forward'))
turtle.turnRight()
mine(turtle.getAction('forward'))
end
boreDirection = 'up'
status('boring up')
turtle.turnRight()
mine(turtle.getAction('forward'))
turtle.turnRight()
mine(turtle.getAction('forward'))
turtle.turnLeft()
while true do
if turtle.abort then
status('aborting')
return false
end
if turtle.point.y > -2 then
-- turtle.setDigPolicy(turtle.digPolicies.turtleSafe)
end
while not Util.tryTimed(3, turtle.up) do
status('stuck')
end
if turtle.status == 'stuck' then
status('boring up')
end
if loc.y >= level - 1 then
break
end
mine(turtle.getAction('forward'))
turtle.turnLeft()
mine(turtle.getAction('forward'))
end
if turtle.getFuelLevel() < LOW_FUEL then
refuel()
local veryMinFuel = Point.turtleDistance(turtle.point, { x = 0, y = 0, z = 0}) + 512
if turtle.getFuelLevel() < veryMinFuel then
log('Not enough fuel to continue')
return false
end
end
return true
end
function checkSpace()
if turtle.getItemCount(16) > 0 then
refuel()
local oldStatus = turtle.status
status('condensing')
ejectTrash()
turtle.condense()
local lastSlot = 16
if boreDirection == 'down' then
lastSlot = 15
end
if turtle.getItemCount(lastSlot) > 0 then
unload()
end
status(oldStatus)
turtle.select(1)
end
end
function collectDrops(suckAction)
for i = 1, 50 do
if not suckAction() then
break
end
checkSpace()
end
end
function Point.compare(pta, ptb)
if pta.x == ptb.x and pta.z == ptb.z then
if pta.y and ptb.y then
return pta.y == ptb.y
end
return true
end
return false
end
function inspect(action, name)
local r, block = action.inspect()
if r and block.name == name then
return true
end
end
function boreCommand()
local pt = getClosestLocation(mining.locations, turtle.point)
turtle.setMoveCallback(function(action, tpt)
makeWalkableTunnel(action, tpt, pt)
end)
safeGotoY(0)
safeGoto(pt.x, pt.z, 0)
turtle.clearMoveCallback()
if inspect(turtle.getAction('up'), 'minecraft:cobblestone') or
inspect(turtle.getAction('down'), 'minecraft:cobblestone') then
return true
end
turtle.digUp()
turtle.placeUp('minecraft:cobblestone:0')
local success = bore()
safeGotoY(0) -- may have aborted
turtle.digUp()
if success then
turtle.placeDown('minecraft:cobblestone:0') -- cap with cobblestone to indicate this spot was mined out
end
return success
end
if not Util.getOptions(options, args) then
return
end
mining.depth = options.depth.value
mining.chunks = options.chunks.value
unload = normalChestUnload
--if options.enderChest.value then
-- unload = enderChestUnload
--end
mining.x = 0
mining.z = 0
mining.locations = getBoreLocations(0, 0)
trash = Util.readTable('mining.trash')
if options.resume.value then
mining = Util.readTable('mining.progress')
elseif fs.exists('mining.progress') then
print('use -r to resume')
read()
end
if not trash or options.setTrash.value then
print('Add trash blocks, press enter when ready')
read()
addTrash()
end
if not turtle.getSlot('minecraft:bucket:0') or
not turtle.getSlot('minecraft:cobblestone:0') then
print('Add bucket and cobblestone, press enter when ready')
read()
end
local function main()
repeat
while #mining.locations > 0 do
if not boreCommand() then
return
end
Util.writeTable('mining.progress', mining)
end
until not nextChunk()
end
turtle.run(function()
turtle.reset()
turtle.setPolicy(turtle.policies.digAttack)
turtle.setDigPolicy(turtle.digPolicies.turtleSafe)
unload()
status('mining')
local s, m = pcall(function() main() end)
if not s and m then
printError(m)
end
safeGotoY(0)
safeGoto(0, 0, 0, 0)
unload()
turtle.reset()
end)

194
apps/storageActivity.lua Normal file
View File

@ -0,0 +1,194 @@
require = requireInjector(getfenv(1))
local Event = require('event')
local UI = require('ui')
local Peripheral = require('peripheral')
if not device.monitor then
error('Monitor not found')
end
local me = Peripheral.getByMethod('getAvailableItems')
if not me then
error("No ME peripheral attached")
end
local monitor = UI.Device({
deviceType = 'monitor',
textScale = .5
})
UI:setDefaultDevice(monitor)
multishell.setTitle(multishell.getCurrent(), 'Storage Activity')
UI:configure('StorageActivity', ...)
-- Strip off color prefix
local function safeString(text)
local val = text:byte(1)
if val < 32 or val > 128 then
local newText = {}
for i = 4, #text do
local val = text:byte(i)
newText[i - 3] = (val > 31 and val < 127) and val or 63
end
return string.char(unpack(newText))
end
return text
end
local changedPage = UI.Page({
grid = UI.Grid({
columns = {
{ heading = 'Qty', key = 'dispQty', width = 5 },
{ heading = 'Change', key = 'change', width = 6 },
{ heading = 'Name', key = 'name', width = monitor.width - 15 },
},
sortColumn = 'name',
height = monitor.height - 6,
}),
buttons = UI.Window({
y = monitor.height - 5,
height = 5,
backgroundColor = colors.gray,
prevButton = UI.Button({
event = 'previous',
backgroundColor = colors.lightGray,
x = 2,
y = 2,
height = 3,
width = 5,
text = ' < '
}),
resetButton = UI.Button({
event = 'reset',
backgroundColor = colors.lightGray,
x = 8,
y = 2,
height = 3,
width = monitor.width - 14,
text = 'Reset'
}),
nextButton = UI.Button({
event = 'next',
backgroundColor = colors.lightGray,
x = monitor.width - 5,
y = 2,
height = 3,
width = 5,
text = ' > '
})
}),
statusBar = UI.StatusBar({
columns = {
{ '', 'slots', 18 },
{ '', 'spacer', monitor.width-36 },
{ '', 'space', 15 }
}
}),
accelerators = {
q = 'quit',
}
})
function changedPage:eventHandler(event)
if event.type == 'reset' then
self.lastItems = nil
self.grid:setValues({ })
self.grid:clear()
self.grid:draw()
elseif event.type == 'next' then
self.grid:nextPage()
elseif event.type == 'previous' then
self.grid:previousPage()
elseif event.type == 'quit' then
Event.exitPullEvents()
else
return UI.Page.eventHandler(self, event)
end
return true
end
function changedPage:refresh()
local t = me.getAvailableItems('all')
if not t or Util.empty(t) then
self:clear()
self:centeredWrite(math.ceil(self.height/2), 'Communication failure')
return
end
for k,v in pairs(t) do
v.id = v.item.id
v.dmg = v.item.dmg
v.name = safeString(v.item.display_name)
v.qty = v.size
end
if not self.lastItems then
self.lastItems = t
self.grid:setValues({ })
else
local changedItems = {}
for _,v in pairs(self.lastItems) do
found = false
for k2,v2 in pairs(t) do
if v.id == v2.id and
v.dmg == v2.dmg then
if v.qty ~= v2.qty then
local c = Util.shallowCopy(v2)
c.lastQty = v.qty
table.insert(changedItems, c)
end
table.remove(t, k2)
found = true
break
end
end
-- New item
if not found then
local c = Util.shallowCopy(v)
c.lastQty = v.qty
c.qty = 0
table.insert(changedItems, c)
end
end
-- No items left
for k,v in pairs(t) do
v.lastQty = 0
table.insert(changedItems, v)
end
for k,v in pairs(changedItems) do
local diff = v.qty - v.lastQty
local ind = '+'
if v.qty < v.lastQty then
ind = ''
end
v.change = ind .. diff
v.dispQty = v.qty
if v.dispQty > 10000 then
v.dispQty = math.floor(v.qty / 1000) .. 'k'
end
v.iddmg = tostring(v.id) .. ':' .. tostring(v.dmg)
end
self.grid:setValues(changedItems)
end
self:draw()
end
Event.addTimer(5, true, function()
changedPage:refresh()
changedPage:sync()
end)
UI:setPage(changedPage)
Event.pullEvents()
UI.term:reset()

917
apps/storageManager.lua Normal file
View File

@ -0,0 +1,917 @@
require = requireInjector(getfenv(1))
local Event = require('event')
local UI = require('ui')
local ME = require('me')
local Config = require('config')
local Logger = require('logger')
-- Must be a crafty turtle with duck antenna !
-- 3 wide monitor (any side of turtle)
-- Config location is /sys/config/storageMonitor
-- adjust directions in that file if needed
local config = {
trashDirection = 'up', -- trash /chest in relation to interface
turtleDirection = 'down', -- turtle in relation to interface
noCraftingStorage = 'false' -- no ME crafting (or ability to tell if powered - use with caution)
}
Config.load('storageMonitor', config)
if not device.tileinterface then
error('ME interface not found')
end
local duckAntenna
if device.workbench then
local oppositeSide = {
[ 'left' ] = 'right',
[ 'right' ] = 'left'
}
local duckAntennaSide = oppositeSide[device.workbench.side]
duckAntenna = peripheral.wrap(duckAntennaSide)
end
--if not device.monitor then
-- error('Monitor not found')
--end
ME.setDevice(device.tileinterface)
local jobListGrid
local craftingPaused = false
multishell.setTitle(multishell.getCurrent(), 'Storage Manager')
Logger.disable()
function getItem(items, inItem, ignore_dmg)
for _,item in pairs(items) do
if item.id == inItem.id then
if ignore_dmg and ignore_dmg == 'yes' then
return item
elseif item.dmg == inItem.dmg and item.nbt_hash == inItem.nbt_hash then
return item
end
end
end
end
local function uniqueKey(item)
local key = item.id .. ':' .. item.dmg
if item.nbt_hash then
key = key .. ':' .. item.nbt_hash
end
return key
end
function mergeResources(t)
local resources = Util.readTable('resource.limits')
resources = resources or { }
for _,item in pairs(t) do
item.has_recipe = false
end
for _,v in pairs(resources) do
local item = getItem(t, v)
if item then
item.limit = tonumber(v.limit)
item.low = tonumber(v.low)
item.auto = v.auto
item.ignore_dmg = v.ignore_dmg
else
v.qty = 0
v.limit = tonumber(v.limit)
v.low = tonumber(v.low)
v.auto = v.auto
v.ignore_dmg = v.ignore_dmg
table.insert(t, v)
end
end
recipes = Util.readTable('recipes') or { }
for _,v in pairs(recipes) do
local item = getItem(t, v)
if item then
item.has_recipe = true
else
v.qty = 0
v.limit = nil
v.low = nil
v.has_recipe = true
v.auto = 'no'
v.ignore_dmg = 'no'
v.has_recipe = 'true'
table.insert(t, v)
end
end
end
function filterItems(t, filter)
local r = {}
if filter then
filter = filter:lower()
for k,v in pairs(t) do
if string.find(v.lname, filter) then
table.insert(r, v)
end
end
else
return t
end
return r
end
function sumItems(items)
local t = {}
for _,item in pairs(items) do
local key = uniqueKey(item)
local summedItem = t[key]
if summedItem then
summedItem.qty = summedItem.qty + item.qty
else
summedItem = Util.shallowCopy(item)
t[key] = summedItem
end
end
return t
end
function isGridClear()
for i = 1, 16 do
if turtle.getItemCount(i) ~= 0 then
return false
end
end
return true
end
local function clearGrid()
for i = 1, 16 do
local count = turtle.getItemCount(i)
if count > 0 then
ME.insert(i, count, config.turtleDirection)
if turtle.getItemCount(i) ~= 0 then
return false
end
end
end
return true
end
function turtleCraft(recipe, originalItem)
for k,v in pairs(recipe.ingredients) do
-- ugh
local dmg = v.dmg
if v.max_dmg and v.max_dmg > 0 then
local item = ME.getItemDetail({ id = v.id, nbt_hash = v.nbt_hash }, false)
if item then
dmg = item.dmg
end
end
if not ME.extract(v.id, dmg, v.nbt_hash, v.qty, config.turtleDirection, k) then
clearGrid()
originalItem.status = v.name .. ' (extract failed)'
return false
end
end
if not turtle.craft() then
clearGrid()
return false
end
clearGrid()
return true
end
function craftItem(items, recipes, item, originalItem, itemList)
local key = uniqueKey(item)
local recipe = recipes[key]
if recipe then
if not isGridClear() then
return
end
local summedItems = sumItems(recipe.ingredients)
for i = 1, math.ceil(item.qty / recipe.qty) do
local failed = false -- try to craft all components (use all CPUs available)
for _,ingredient in pairs(summedItems) do
local ignore_dmg = 'no'
if ingredient.max_dmg and ingredient.max_dmg > 0 then
ignore_dmg = 'yes'
end
local qty = ME.getItemCount(ingredient.id, ingredient.dmg, ingredient.nbt_hash, ignore_dmg)
if qty < ingredient.qty then
originalItem.status = ingredient.name .. ' (crafting)'
ingredient.qty = ingredient.qty - qty
if not craftItem(items, recipes, ingredient, originalItem, itemList) then
failed = true
end
end
end
if failed then
return false
end
if not failed and not turtleCraft(recipe, originalItem) then
Logger.debug('turtle failed to craft ' .. item.name)
return false
end
end
return true
else
local meItem = getItem(items, item)
if not meItem or not meItem.is_craftable then
if item.id == originalItem.id and item.dmg == originalItem.dmg then
originalItem.status = '(not craftable)'
else
originalItem.status = item.name .. ' (missing)'
end
else
if item.id == originalItem.id and item.dmg == originalItem.dmg then
item.meCraft = true
return false
end
-- find it in the list of items to be crafted
for _,v in pairs(itemList) do
if v.id == item.id and v.dmg == item.dmg and v.nbt_hash == item.nbt_hash then
v.qty = item.qty + v.qty
return false
end
end
-- add to the item list
table.insert(itemList, {
id = item.id,
dmg = item.dmg,
nbt_hash = item.nbt_hash,
qty = item.qty,
name = item.name,
meCraft = true,
status = ''
})
end
end
return false
end
function craftItems(itemList)
local recipes = Util.readTable('recipes') or { }
local items = ME.getAvailableItems()
-- turtle craft anything we can, build up list for ME items
local keys = Util.keys(itemList)
for _,key in pairs(keys) do
local item = itemList[key]
craftItem(items, recipes, item, item, itemList)
end
-- second pass is to request crafting from ME with aggregated items
for _,item in pairs(itemList) do
if item.meCraft then
local alreadyCrafting = false
local jobList = ME.getJobList()
for _,v in pairs(jobList) do
if v.id == item.id and v.dmg == item.dmg and v.nbt_hash == item.nbt_hash then
alreadyCrafting = true
end
end
if alreadyCrafting then
item.status = '(crafting)'
elseif not ME.isCPUAvailable() then
item.status = '(waiting)'
else
item.status = '(failed)'
local qty = item.qty
while qty >= 1 do -- try to request smaller quantities until successful
if ME.craft(item.id, item.dmg, item.nbt_hash, qty) then
item.status = '(crafting)'
break -- successfully requested crafting
end
qty = math.floor(qty / 2)
end
end
end
end
end
-- AE 1 (obsolete)
function isCrafting(jobList, id, dmg)
for _, job in pairs(jobList) do
if job.id == id and job.dmg == dmg then
return job
end
end
end
local nullDevice = {
setCursorPos = function(...) end,
write = function(...) end,
getSize = function() return 13, 20 end,
isColor = function() return false end,
setBackgroundColor = function(...) end,
setTextColor = function(...) end,
clear = function(...) end,
}
local function jobMonitor(jobList)
local mon
if device.monitor then
mon = UI.Device({
deviceType = 'monitor',
textScale = .5,
})
else
mon = UI.Device({
device = nullDevice
})
end
jobListGrid = UI.Grid({
parent = mon,
sortColumn = 'name',
columns = {
{ heading = 'Qty', key = 'qty', width = 6 },
{ heading = 'Crafting', key = 'name', width = mon.width / 2 - 10 },
{ heading = 'Status', key = 'status', width = mon.width - 10 },
},
})
end
function getAutocraftItems(items)
local t = Util.readTable('resource.limits') or { }
local itemList = { }
for _,res in pairs(t) do
if res.auto and res.auto == 'yes' then
res.qty = 4 -- this could be higher to increase autocrafting speed
table.insert(itemList, res)
end
end
return itemList
end
local function getItemWithQty(items, res, ignore_dmg)
local item = getItem(items, res, ignore_dmg)
if item then
if ignore_dmg and ignore_dmg == 'yes' then
local qty = 0
for _,v in pairs(items) do
if item.id == v.id and item.nbt_hash == v.nbt_hash then
if item.max_dmg > 0 or item.dmg == v.dmg then
qty = qty + v.qty
end
end
end
item.qty = qty
end
end
return item
end
function watchResources(items)
local itemList = { }
local t = Util.readTable('resource.limits') or { }
for k, res in pairs(t) do
local item = getItemWithQty(items, res, res.ignore_dmg)
res.limit = tonumber(res.limit)
res.low = tonumber(res.low)
if not item then
item = {
id = res.id,
dmg = res.dmg,
nbt_hash = res.nbt_hash,
name = res.name,
qty = 0
}
end
if res.limit and item.qty > res.limit then
Logger.debug("Purging " .. item.qty-res.limit .. " " .. res.name)
if not ME.extract(item.id, item.dmg, item.nbt_hash, item.qty - res.limit, config.trashDirection) then
Logger.debug('Failed to purge ' .. res.name)
end
elseif res.low and item.qty < res.low then
if res.ignore_dmg and res.ignore_dmg == 'yes' then
item.dmg = 0
end
table.insert(itemList, {
id = item.id,
dmg = item.dmg,
nbt_hash = item.nbt_hash,
qty = res.low - item.qty,
name = item.name,
status = ''
})
end
end
return itemList
end
itemPage = UI.Page({
backgroundColor = colors.lightGray,
titleBar = UI.TitleBar({
title = 'Limit Resource',
previousPage = true,
backgroundColor = colors.green
}),
idField = UI.Text({
x = 5,
y = 3,
width = UI.term.width - 10
}),
form = UI.Form({
fields = {
{ label = 'Min', key = 'low', width = 7, display = UI.Form.D.entry,
help = 'Craft if below min' },
{ label = 'Max', key = 'limit', width = 7, display = UI.Form.D.entry,
validation = UI.Form.V.number, dataType = UI.Form.T.number,
help = 'Eject if above max' },
{ label = 'Autocraft', key = 'auto', width = 7, display = UI.Form.D.chooser,
nochoice = 'No',
choices = {
{ name = 'Yes', value = 'yes' },
{ name = 'No', value = 'no' },
},
help = 'Craft until out of ingredients' },
{ label = 'Ignore Dmg', key = 'ignore_dmg', width = 7, display = UI.Form.D.chooser,
nochoice = 'No',
choices = {
{ name = 'Yes', value = 'yes' },
{ name = 'No', value = 'no' },
},
help = 'Ignore damage of item' },
{ text = 'Accept', event = 'accept', display = UI.Form.D.button,
x = 1, y = 6, width = 10 },
{ text = 'Cancel', event = 'cancel', display = UI.Form.D.button,
x = 21, y = 6, width = 10 },
},
labelWidth = 10,
x = 5,
y = 5,
height = 6
}),
statusBar = UI.StatusBar()
})
function itemPage:enable()
UI.Page.enable(self)
self:focusFirst()
end
function itemPage:eventHandler(event)
if event.type == 'cancel' then
UI:setPreviousPage()
elseif event.type == 'focus_change' then
self.statusBar:setStatus(event.focused.help)
self.statusBar:draw()
elseif event.type == 'accept' then
local values = self.form.values
local t = Util.readTable('resource.limits') or { }
for k,v in pairs(t) do
if v.id == values.id and v.dmg == values.dmg then
table.remove(t, k)
break
end
end
local keys = { 'name', 'auto', 'id', 'low', 'dmg', 'max_dmg', 'nbt_hash', 'limit', 'ignore_dmg' }
local filtered = { }
for _,key in pairs(keys) do
filtered[key] = values[key]
end
table.insert(t, filtered)
Util.writeTable('resource.limits', t)
UI:setPreviousPage()
else
return UI.Page.eventHandler(self, event)
end
return true
end
listingPage = UI.Page({
menuBar = UI.MenuBar({
buttons = {
{ text = 'Learn', event = 'learn' },
{ text = 'Forget', event = 'forget' },
},
}),
grid = UI.Grid({
columns = {
{ heading = 'Name', key = 'name' , width = 22 },
{ heading = 'Qty', key = 'qty' , width = 5 },
{ heading = 'Min', key = 'low' , width = 4 },
{ heading = 'Max', key = 'limit', width = 4 },
},
y = 2,
sortColumn = 'name',
height = UI.term.height-2,
}),
statusBar = UI.StatusBar({
backgroundColor = colors.gray,
width = UI.term.width,
filterText = UI.Text({
value = 'Filter',
x = 2,
width = 6,
}),
filter = UI.TextEntry({
width = 19,
limit = 50,
x = 9,
}),
refresh = UI.Button({
text = 'Refresh',
event = 'refresh',
x = 31,
width = 8
}),
}),
accelerators = {
r = 'refresh',
q = 'quit',
}
})
function listingPage.grid:getRowTextColor(row, selected)
if row.is_craftable then
return colors.yellow
end
if row.has_recipe then
if selected then
return colors.blue
end
return colors.lightBlue
end
return UI.Grid:getRowTextColor(row, selected)
end
function listingPage.grid:getDisplayValues(row)
row = Util.shallowCopy(row)
row.qty = Util.toBytes(row.qty)
if row.low then
row.low = Util.toBytes(row.low)
end
if row.limit then
row.limit = Util.toBytes(row.limit)
end
return row
end
function listingPage.statusBar:draw()
return UI.Window.draw(self)
end
function listingPage.statusBar.filter:eventHandler(event)
if event.type == 'mouse_rightclick' then
self.value = ''
self:draw()
local page = UI:getCurrentPage()
page.filter = nil
page:applyFilter()
page.grid:draw()
page:setFocus(self)
end
return UI.TextEntry.eventHandler(self, event)
end
function listingPage:eventHandler(event)
if event.type == 'quit' then
Event.exitPullEvents()
elseif event.type == 'grid_select' then
local selected = event.selected
itemPage.form:setValues(selected)
itemPage.titleBar.title = selected.name
itemPage.idField.value = selected.id
UI:setPage('item')
elseif event.type == 'refresh' then
self:refresh()
self.grid:draw()
elseif event.type == 'learn' then
if not duckAntenna then
self.statusBar:timedStatus('Missing peripherals', 3)
else
UI:getPage('craft').form:setValues( { ignore_dmg = 'no' } )
UI:setPage('craft')
end
elseif event.type == 'forget' then
local item = self.grid:getSelected()
if item then
local recipes = Util.readTable('recipes') or { }
local key = uniqueKey(item)
local recipe = recipes[key]
if recipe then
recipes[key] = nil
Util.writeTable('recipes', recipes)
end
local resources = Util.readTable('resource.limits') or { }
for k,v in pairs(resources) do
if v.id == item.id and v.dmg == item.dmg then
table.remove(resources, k)
Util.writeTable('resource.limits', resources)
break
end
end
self.statusBar:timedStatus('Forgot: ' .. item.name, 3)
self:refresh()
self.grid:draw()
end
elseif event.type == 'text_change' then
self.filter = event.text
if #self.filter == 0 then
self.filter = nil
end
self:applyFilter()
self.grid:draw()
self.statusBar.filter:focus()
else
UI.Page.eventHandler(self, event)
end
return true
end
function listingPage:enable()
self:refresh()
self:setFocus(self.statusBar.filter)
UI.Page.enable(self)
end
function listingPage:refresh()
self.allItems = ME.getAvailableItems('all')
mergeResources(self.allItems)
Util.each(self.allItems, function(item)
item.lname = item.name:lower()
end)
self:applyFilter()
end
function listingPage:applyFilter()
local t = filterItems(self.allItems, self.filter)
self.grid:setValues(t)
end
-- without duck antenna
local function getTurtleInventory()
local inventory = { }
for i = 1,16 do
if turtle.getItemCount(i) > 0 then
turtle.select(i)
local item = turtle.getItemDetail()
inventory[i] = {
id = item.name,
dmg = item.damage,
qty = item.count,
name = item.name,
}
end
end
return inventory
end
-- Strip off color prefix
local function safeString(text)
local val = text:byte(1)
if val < 32 or val > 128 then
local newText = {}
for i = 4, #text do
local val = text:byte(i)
newText[i - 3] = (val > 31 and val < 127) and val or 63
end
return string.char(unpack(newText))
end
return text
end
local function filter(t, filter)
local keys = Util.keys(t)
for _,key in pairs(keys) do
if not Util.key(filter, key) then
t[key] = nil
end
end
end
local function learnRecipe(page, ignore_dmg)
local t = Util.readTable('recipes') or { }
local recipe = { }
local ingredients = duckAntenna.getAllStacks(false) -- getTurtleInventory()
if ingredients then
turtle.select(1)
if turtle.craft() then
recipe = duckAntenna.getAllStacks(false) -- getTurtleInventory()
if recipe and recipe[1] then
recipe = recipe[1]
local key = uniqueKey(recipe)
clearGrid()
recipe.name = safeString(recipe.display_name)
filter(recipe, { 'name', 'id', 'dmg', 'nbt_hash', 'qty', 'max_size' })
for _,ingredient in pairs(ingredients) do
ingredient.name = safeString(ingredient.display_name)
filter(ingredient, { 'name', 'id', 'dmg', 'nbt_hash', 'qty', 'max_size', 'max_dmg' })
if ingredient.max_dmg > 0 then -- let's try this...
ingredient.dmg = 0
end
end
recipe.ingredients = ingredients
recipe.ignore_dmg = 'no' -- ignore_dmg
t[key] = recipe
Util.writeTable('recipes', t)
listingPage.statusBar.filter:setValue(recipe.name)
listingPage.statusBar:timedStatus('Learned: ' .. recipe.name, 3)
listingPage.filter = recipe.name
listingPage:refresh()
listingPage.grid:draw()
return true
end
else
page.statusBar:timedStatus('Failed to craft', 3)
end
else
page.statusBar:timedStatus('No recipe defined', 3)
end
end
craftPage = UI.Page({
x = 4,
y = math.floor((UI.term.height - 8) / 2) + 1,
height = 7,
width = UI.term.width - 6,
backgroundColor = colors.lightGray,
titleBar = UI.TitleBar({
title = 'Learn Recipe',
previousPage = true,
}),
idField = UI.Text({
x = 5,
y = 3,
width = UI.term.width - 10,
value = 'Place recipe in turtle'
}),
form = UI.Form({
fields = {
--[[
{ label = 'Ignore Damage', key = 'ignore_dmg', width = 7, display = UI.Form.D.chooser,
nochoice = 'No',
choices = {
{ name = 'Yes', value = 'yes' },
{ name = 'No', value = 'no' },
},
help = 'Ignore damage of ingredients' },
--]]
{ text = 'Accept', event = 'accept', display = UI.Form.D.button,
x = 1, y = 1, width = 10 },
{ text = 'Cancel', event = 'cancel', display = UI.Form.D.button,
x = 16, y = 1, width = 10 },
},
labelWidth = 13,
x = 5,
y = 5,
height = 2
}),
statusBar = UI.StatusBar({
status = 'Crafting paused'
})
})
function craftPage:enable()
craftingPaused = true
self:focusFirst()
UI.Page.enable(self)
end
function craftPage:disable()
craftingPaused = false
end
function craftPage:eventHandler(event)
if event.type == 'cancel' then
UI:setPreviousPage()
elseif event.type == 'accept' then
local values = self.form.values
if learnRecipe(self, values.ignore_dmg) then
UI:setPreviousPage()
end
else
return UI.Page.eventHandler(self, event)
end
return true
end
UI:setPages({
listing = listingPage,
item = itemPage,
craft = craftPage,
})
UI:setPage(listingPage)
listingPage:setFocus(listingPage.statusBar.filter)
clearGrid()
jobMonitor()
jobListGrid:draw()
jobListGrid:sync()
function craftingThread()
while true do
os.sleep(5)
if not craftingPaused then
local items = ME.getAvailableItems()
if Util.size(items) == 0 then
jobListGrid.parent:clear()
jobListGrid.parent:centeredWrite(math.ceil(jobListGrid.parent.height/2), 'No items in system')
jobListGrid:sync()
elseif config.noCraftingStorage ~= 'true' and #ME.getCraftingCPUs() <= 0 then -- only way to determine if AE is online
jobListGrid.parent:clear()
jobListGrid.parent:centeredWrite(math.ceil(jobListGrid.parent.height/2), 'Power failure')
jobListGrid:sync()
else
local itemList = watchResources(items)
jobListGrid:setValues(itemList)
jobListGrid:draw()
jobListGrid:sync()
craftItems(itemList)
jobListGrid:update()
jobListGrid:draw()
jobListGrid:sync()
itemList = getAutocraftItems(items) -- autocrafted items don't show on job monitor
craftItems(itemList)
end
end
end
end
Event.pullEvents(craftingThread)
UI.term:reset()
jobListGrid.parent:reset()

392
apps/supplier.lua Normal file
View File

@ -0,0 +1,392 @@
require = requireInjector(getfenv(1))
local Logger = require('logger')
local Message = require('message')
local Event = require('event')
local Point = require('point')
local TableDB = require('tableDB')
local MEProvider = require('meProvider')
if not device.wireless_modem then
error('No wireless modem detected')
end
Logger.filter('modem_send', 'event', 'ui')
Logger.setWirelessLogging()
local __BUILDER_ID = 6
local Builder = {
version = '1.70',
ccVersion = nil,
slots = { },
index = 1,
fuelItem = { id = 'minecraft:coal', dmg = 0 },
resupplying = true,
ready = true,
}
--[[-- maxStackDB --]]--
local maxStackDB = TableDB({
fileName = 'maxstack.db',
tabledef = {
autokeys = false,
type = 'simple',
columns = {
{ label = 'Key', type = 'key', length = 8 },
{ label = 'Quantity', type = 'number', length = 2 }
}
}
})
function maxStackDB:get(id, dmg)
return self.data[id .. ':' .. dmg] or 64
end
function Builder:dumpInventory()
local success = true
for i = 1, 16 do
local qty = turtle.getItemCount(i)
if qty > 0 then
self.itemProvider:insert(i, qty)
end
if turtle.getItemCount(i) ~= 0 then
success = false
end
end
turtle.select(1)
return success
end
function Builder:dumpInventoryWithCheck()
while not self:dumpInventory() do
Builder:log('Unable to dump inventory')
print('Provider is full or missing - make space or replace')
print('Press enter to continue')
turtle.setHeading(0)
self.ready = false
read()
end
self.ready = true
end
function Builder:autocraft(supplies)
local t = { }
for i,s in pairs(supplies) do
local key = s.id .. ':' .. s.dmg
local item = t[key]
if not item then
item = {
id = s.id,
dmg = s.dmg,
qty = 0,
}
t[key] = item
end
item.qty = item.qty + (s.need-s.qty)
end
Builder.itemProvider:craftItems(t)
end
function Builder:refuel()
while turtle.getFuelLevel() < 4000 and self.fuelItem do
Builder:log('Refueling')
turtle.select(1)
self.itemProvider:provide(self.fuelItem, 64, 1)
if turtle.getItemCount(1) == 0 then
Builder:log('Out of fuel, add coal to chest/ME system')
turtle.setHeading(0)
os.sleep(5)
else
turtle.refuel(64)
end
end
end
function Builder:log(...)
Logger.log('supplier', ...)
Util.print(...)
end
function Builder:getSupplies()
Builder.itemProvider:refresh()
local t = { }
for _,s in ipairs(self.slots) do
if s.need > 0 then
local item = Builder.itemProvider:getItemInfo(s.id, s.dmg)
if item then
if item.name then
s.name = item.name
end
local qty = math.min(s.need-s.qty, item.qty)
if qty + s.qty > item.max_size then
maxStackDB:add({ s.id, s.dmg }, item.max_size)
maxStackDB.dirty = true
maxStackDB:flush()
qty = item.max_size
s.need = qty
end
if qty > 0 then
self.itemProvider:provide(item, qty, s.index)
s.qty = turtle.getItemCount(s.index)
end
end
end
if s.qty < s.need then
table.insert(t, s)
local name = s.name or s.id .. ':' .. s.dmg
Builder:log('Need %d %s', s.need - s.qty, name)
end
end
return t
end
local function moveTowardsX(dx)
local direction = dx - turtle.point.x
local move
if direction == 0 then
return false
end
if direction > 0 and turtle.point.heading == 0 or
direction < 0 and turtle.point.heading == 2 then
move = turtle.forward
else
move = turtle.back
end
return move()
end
local function moveTowardsZ(dz)
local direction = dz - turtle.point.z
local move
if direction == 0 then
return false
end
if direction > 0 and turtle.point.heading == 1 or
direction < 0 and turtle.point.heading == 3 then
move = turtle.forward
else
move = turtle.back
end
return move()
end
function Builder:finish()
Builder.resupplying = true
Builder.ready = false
if turtle.gotoLocation('supplies') then
turtle.setHeading(0)
os.sleep(.1) -- random 'Computer is not connected' error...
Builder:dumpInventory()
Event.exitPullEvents()
print('Finished')
end
end
function Builder:gotoBuilder()
if Builder.lastPoint then
turtle.status = 'tracking'
while true do
local pt = Point.copy(Builder.lastPoint)
pt.y = pt.y + 3
if turtle.point.y ~= pt.y then
turtle.gotoY(pt.y)
else
local distance = Point.turtleDistance(turtle.point, pt)
if distance <= 3 then
Builder:log('Synchronized')
break
end
if turtle.point.heading % 2 == 0 then
if turtle.point.x == pt.x then
turtle.headTowardsZ(pt.z)
moveTowardsZ(pt.z)
else
moveTowardsX(pt.x)
end
elseif turtle.point.z ~= pt.z then
moveTowardsZ(pt.z)
else
turtle.headTowardsX(pt.x)
moveTowardsX(pt.x)
end
end
end
end
end
Message.addHandler('builder',
function(h, id, msg, distance)
if not id or id ~= __BUILDER_ID then
return
end
if not Builder.resupplying then
local pt = msg.contents
pt.y = pt.y + 3
turtle.status = 'supervising'
turtle.gotoYfirst(pt)
end
end)
Message.addHandler('supplyList',
function(h, id, msg, distance)
if not id or id ~= __BUILDER_ID then
return
end
turtle.status = 'resupplying'
Builder.resupplying = true
Builder.slots = msg.contents.slots
Builder.slotUid = msg.contents.uid
Builder:log('Received supply list ' .. Builder.slotUid)
os.sleep(0)
if not turtle.gotoLocation('supplies') then
Builder:log('Failed to go to supply location')
self.ready = false
Event.exitPullEvents()
end
os.sleep(.2) -- random 'Computer is not connected' error...
Builder:dumpInventoryWithCheck()
Builder:refuel()
while true do
local supplies = Builder:getSupplies()
if #supplies == 0 then
break
end
turtle.setHeading(0)
Builder:autocraft(supplies)
turtle.status = 'waiting'
os.sleep(5)
end
Builder:log('Got all supplies')
os.sleep(0)
Builder:gotoBuilder()
Builder.resupplying = false
end)
Message.addHandler('needSupplies',
function(h, id, msg, distance)
if not id or id ~= __BUILDER_ID then
return
end
if Builder.resupplying or msg.contents.uid ~= Builder.slotUid then
Builder:log('No supplies ready')
Message.send(__BUILDER_ID, 'gotSupplies')
else
turtle.status = 'supplying'
Builder:log('Supplying')
os.sleep(0)
local pt = msg.contents.point
pt.y = turtle.getPoint().y
pt.heading = nil
if not turtle.gotoYfirst(pt) then -- location of builder
Builder.resupplying = true
Message.send(__BUILDER_ID, 'gotSupplies')
os.sleep(0)
if not turtle.gotoLocation('supplies') then
Builder:log('failed to go to supply location')
self.ready = false
Event.exitPullEvents()
end
return
end
pt.y = pt.y - 2 -- location where builder should go for the chest to be above
turtle.select(15)
turtle.placeDown()
os.sleep(.1) -- random computer not connected error
local p = peripheral.wrap('bottom')
for i = 1, 16 do
p.pullItem('up', i, 64)
end
Message.send(__BUILDER_ID, 'gotSupplies', { supplies = true, point = pt })
Message.waitForMessage('thanks', 5, __BUILDER_ID)
--os.sleep(0)
p.condenseItems()
for i = 1, 16 do
p.pushItem('up', i, 64)
end
turtle.digDown()
turtle.status = 'waiting'
end
end)
Message.addHandler('finished',
function(h, id)
if not id or id ~= __BUILDER_ID then
return
end
Builder:finish()
end)
Event.addHandler('turtle_abort',
function()
turtle.abort = false
turtle.status = 'aborting'
Builder:finish()
end)
local function onTheWay() -- parallel routine
while true do
local e, side, _id, id, msg, distance = os.pullEvent('modem_message')
if Builder.ready then
if id == __BUILDER_ID and msg and msg.type then
if msg.type == 'needSupplies' then
Message.send(__BUILDER_ID, 'gotSupplies', { supplies = true })
elseif msg.type == 'builder' then
Builder.lastPoint = msg.contents
end
end
end
end
end
local args = {...}
if #args < 1 then
error('Supply id for builder')
end
__BUILDER_ID = tonumber(args[1])
maxStackDB:load()
turtle.setPoint({ x = -1, z = -2, y = 0, heading = 0 })
turtle.saveLocation('supplies')
Builder.itemProvider = MEProvider()
turtle.run(function()
Event.pullEvents(onTheWay)
end)

98
apps/t.lua Normal file
View File

@ -0,0 +1,98 @@
function doCommand(command, moves)
--[[
if command == 'sl' then
local pt = GPS.getPoint()
if pt then
turtle.storeLocation(moves, pt)
end
return
end
--]]
local function format(value)
if type(value) == 'boolean' then
if value then return 'true' end
return 'false'
end
if type(value) ~= 'table' then
return value
end
local str
for k,v in pairs(value) do
if not str then
str = '{ '
else
str = str .. ', '
end
str = str .. k .. '=' .. tostring(v)
end
if str then
str = str .. ' }'
else
str = '{ }'
end
return str
end
local function runCommand(fn, arg)
local r = { fn(arg) }
if r[2] then
print(format(r[1]) .. ': ' .. format(r[2]))
elseif r[1] then
print(format(r[1]))
end
return r[1]
end
local cmds = {
[ 's' ] = turtle.select,
[ 'rf' ] = turtle.refuel,
[ 'gh' ] = function() turtle.pathfind({ x = 0, y = 0, z = 0, heading = 0}) end,
}
local repCmds = {
[ 'u' ] = turtle.up,
[ 'd' ] = turtle.down,
[ 'f' ] = turtle.forward,
[ 'r' ] = turtle.turnRight,
[ 'l' ] = turtle.turnLeft,
[ 'ta' ] = turtle.turnAround,
[ 'DD' ] = turtle.digDown,
[ 'DU' ] = turtle.digUp,
[ 'D' ] = turtle.dig,
[ 'p' ] = turtle.place,
[ 'pu' ] = turtle.placeUp,
[ 'pd' ] = turtle.placeDown,
[ 'b' ] = turtle.back,
[ 'gfl' ] = turtle.getFuelLevel,
[ 'gp' ] = turtle.getPoint,
[ 'R' ] = function() turtle.setPoint({x = 0, y = 0, z = 0, heading = 0}) return turtle.point end
}
if cmds[command] then
runCommand(cmds[command], moves)
elseif repCmds[command] then
for i = 1, moves do
if not runCommand(repCmds[command]) then
break
end
end
end
end
local args = {...}
if #args > 0 then
doCommand(args[1], args[2] or 1)
else
print('Enter command (q to quit):')
while true do
local cmd = read()
if cmd == 'q' then break
end
args = { }
cmd:gsub('%w+', function(w) table.insert(args, w) end)
doCommand(args[1], args[2] or 1)
end
end

82
apps/telnet.lua Normal file
View File

@ -0,0 +1,82 @@
require = requireInjector(getfenv(1))
local process = require('process')
local Socket = require('socket')
local Terminal = require('terminal')
local remoteId
local args = { ... }
if #args == 1 then
remoteId = tonumber(args[1])
else
print('Enter host ID')
remoteId = tonumber(read())
end
if not remoteId then
error('Syntax: telnet <host ID>')
end
print('connecting...')
local socket = Socket.connect(remoteId, 23)
if not socket then
error('Unable to connect to ' .. remoteId .. ' on port 23')
end
local ct = Util.shallowCopy(term.current())
if not ct.isColor() then
Terminal.toGrayscale(ct)
end
local w, h = ct.getSize()
socket:write({
type = 'termInfo',
width = w,
height = h,
isColor = ct.isColor(),
})
process:newThread('telnet_read', function()
while true do
local data = socket:read()
if not data then
break
end
for _,v in ipairs(data) do
ct[v.f](unpack(v.args))
end
end
end)
ct.clear()
ct.setCursorPos(1, 1)
while true do
local e = { process:pullEvent() }
local event = e[1]
if not socket.connected then
print()
print('Connection lost')
print('Press enter to exit')
read()
break
end
if event == 'char' or
event == 'paste' or
event == 'key' or
event == 'key_up' or
event == 'mouse_scroll' or
event == 'mouse_click' or
event == 'mouse_drag' then
socket:write({
type = 'shellRemote',
event = e,
})
elseif event == 'terminate' then
socket:close()
break
end
end

52
apps/trace.lua Normal file
View File

@ -0,0 +1,52 @@
local args = {...}
if not args[1] then
print("Usage:")
print(shell.getRunningProgram() .. " <program> [program arguments, ...]")
return
end
local path = shell.resolveProgram(args[1]) or shell.resolve(args[1])
-- here be dragons
if fs.exists(path) then
local eshell = setmetatable({getRunningProgram=function() return path end}, {__index = shell})
local env = setmetatable({shell=eshell}, {__index=_ENV})
local f = fs.open(path, "r")
local d = f.readAll()
f.close()
local func, e = load(d, fs.getName(path), nil, env)
if not func then
printError("Syntax error:")
printError(" " .. e)
else
table.remove(args, 1)
xpcall(function() func(unpack(args)) end, function(err)
local trace = {}
local i, hitEnd, _, e = 4, false
repeat
_, e = pcall(function() error("<tracemarker>", i) end)
i = i + 1
if e == "xpcall: <tracemarker>" then
hitEnd = true
break
end
table.insert(trace, e)
until i > 10
table.remove(trace)
if err:match("^" .. trace[1]:match("^(.-:%d+)")) then table.remove(trace, 1) end
printError("\nProgram has crashed! Stack trace:")
printError(err)
for i, v in ipairs(trace) do
printError(" at " .. v:match("^(.-:%d+)"))
end
if not hitEnd then
printError(" ...")
end
end)
end
else
printError("program not found")
end

6
apps/update.lua Normal file
View File

@ -0,0 +1,6 @@
local args = { ... }
local options = ''
for _,v in pairs(args) do
options = options .. ' ' .. v
end
shell.run('pastebin run sj4VMVJj' .. options)

88
apps/vnc.lua Normal file
View File

@ -0,0 +1,88 @@
require = requireInjector(getfenv(1))
local process = require('process')
local Socket = require('socket')
local Terminal = require('terminal')
local remoteId
local args = { ... }
if #args == 1 then
remoteId = tonumber(args[1])
else
print('Enter host ID')
remoteId = tonumber(read())
end
if not remoteId then
error('Syntax: vnc <host ID>')
end
multishell.setTitle(multishell.getCurrent(), 'VNC-' .. remoteId)
print('connecting...')
local socket = Socket.connect(remoteId, 5900)
if not socket then
error('Unable to connect to ' .. remoteId .. ' on port 5900')
end
local w, h = term.getSize()
socket:write({
type = 'termInfo',
width = w,
height = h,
isColor = term.isColor(),
})
local ct = Util.shallowCopy(term.current())
if not ct.isColor() then
Terminal.toGrayscale(ct)
end
process:newThread('vnc_read', function()
while true do
local data = socket:read()
if not data then
break
end
for _,v in ipairs(data) do
ct[v.f](unpack(v.args))
end
end
end)
ct.clear()
ct.setCursorPos(1, 1)
while true do
local e = { process:pullEvent() }
local event = e[1]
if not socket.connected then
print()
print('Connection lost')
print('Press enter to exit')
read()
break
end
if event == 'char' or
event == 'paste' or
event == 'key' or
event == 'key_up' or
event == 'mouse_scroll' or
event == 'mouse_click' or
event == 'mouse_drag' then
socket:write({
type = 'shellRemote',
event = e,
})
elseif event == 'terminate' then
socket:close()
ct.setBackgroundColor(colors.black)
ct.clear()
ct.setCursorPos(1, 1)
break
end
end

51
autorun/gps.lua Normal file
View File

@ -0,0 +1,51 @@
if turtle and device.wireless_modem then
local s, m = turtle.run(function()
local homePt = turtle.loadLocation('gpsHome')
if homePt then
require = requireInjector(getfenv(1))
local Config = require('config')
local config = {
destructive = false,
}
Config.load('gps', config)
local GPS = require('gps')
local pt
for i = 1, 3 do
pt = GPS.getPointAndHeading(2)
if pt then
break
end
end
if not pt and config.destructive then
turtle.setPolicy('turtleSafe')
pt = GPS.getPointAndHeading(2)
end
if not pt then
error('Unable to get GPS position')
end
if config.destructive then
turtle.setPolicy('turtleSafe')
end
Util.print('Setting turtle point to %d %d %d', pt.x, pt.y, pt.z)
turtle.setPoint(pt)
if not turtle.pathfind(homePt) then
error('Failed to return home')
end
end
end)
turtle.setPolicy('none')
if not s and m then
error(m)
end
end

45
startup Normal file
View File

@ -0,0 +1,45 @@
local bootOptions = {
{ prompt = 'Default Shell', file = '/sys/boot/default.boot' },
{ prompt = 'Multishell' , file = '/sys/boot/multishell.boot' },
{ prompt = 'TLCO' , file = '/sys/boot/tlco.boot' },
}
local bootOption = 2
local function startupMenu()
while true do
term.clear()
term.setCursorPos(1, 1)
print('Select startup mode')
print()
for k,option in pairs(bootOptions) do
print(k .. ' : ' .. option.prompt)
end
print('')
term.write('> ')
local ch = tonumber(read())
if ch and bootOptions[ch] then
return ch
end
end
term.clear()
term.setCursorPos(1, 1)
end
term.clear()
term.setCursorPos(1, 1)
print('Starting OS')
print()
print('Press any key for menu')
local timerId = os.startTimer(.75)
while true do
local e, id = os.pullEvent()
if e == 'timer' and id == timerId then
break
end
if e == 'char' then
bootOption = startupMenu()
break
end
end
os.run(getfenv(1), bootOptions[bootOption].file)

893
sys/apis/blocks.lua Normal file
View File

@ -0,0 +1,893 @@
local class = require('class')
local TableDB = require('tableDB')
local blockDB = TableDB({
fileName = 'block.db',
tabledef = {
autokeys = false,
columns = {
{ name = 'key', type = 'key', length = 8 },
{ name = 'id', type = 'number', length = 5 },
{ name = 'dmg', type = 'number', length = 2 },
{ name = 'name', type = 'string', length = 35 },
{ name = 'refname', type = 'string', length = 35 },
{ name = 'strId', type = 'string', length = 80 },
}
}
})
function blockDB:load(dir, sbDB, btDB)
self.fileName = fs.combine(dir, self.fileName)
if fs.exists(self.fileName) then
TableDB.load(self)
else
self:seedDB(dir)
end
end
function blockDB:seedDB(dir)
-- http://lua-users.org/wiki/LuaCsv
function ParseCSVLine (line,sep)
local res = {}
local pos = 1
sep = sep or ','
while true do
local c = string.sub(line,pos,pos)
if (c == "") then break end
if (c == '"') then
-- quoted value (ignore separator within)
local txt = ""
repeat
local startp,endp = string.find(line,'^%b""',pos)
txt = txt..string.sub(line,startp+1,endp-1)
pos = endp + 1
c = string.sub(line,pos,pos)
if (c == '"') then txt = txt..'"' end
-- check first char AFTER quoted string, if it is another
-- quoted string without separator, then append it
-- this is the way to "escape" the quote char in a quote. example:
-- value1,"blub""blip""boing",value3 will result in blub"blip"boing for the middle
until (c ~= '"')
table.insert(res,txt)
assert(c == sep or c == "")
pos = pos + 1
else
-- no quotes used, just look for the first separator
local startp,endp = string.find(line,sep,pos)
if (startp) then
table.insert(res,string.sub(line,pos,startp-1))
pos = endp + 1
else
-- no separator found -> use rest of string and terminate
table.insert(res,string.sub(line,pos))
break
end
end
end
return res
end
local f = fs.open(fs.combine(dir, 'blockIds.csv'), "r")
if not f then
error('unable to read blockIds.csv')
end
local lastID = nil
while true do
local data = f.readLine()
if not data then
break
end
local line = ParseCSVLine(data, ',')
local strId = line[3] -- string ID
local nid -- schematic ID
local id = line[3]
local dmg = 0
local name = line[1]
if not strId or #strId == 0 then
strId = lastID
end
lastID = strId
local sep = string.find(line[2], ':')
if not sep then
nid = tonumber(line[2])
dmg = 0
else
nid = tonumber(string.sub(line[2], 1, sep - 1))
dmg = tonumber(string.sub(line[2], sep + 1, #line[2]))
end
self:add(nid, dmg, name, strId)
end
f.close()
self.dirty = true
self:flush()
end
function blockDB:lookup(id, dmg)
if not id then
return
end
if not id or not dmg then error('blockDB:lookup: nil passed', 2) end
local key = id .. ':' .. dmg
return self.data[key]
end
function blockDB:getName(id, dmg)
return self:lookupName(id, dmg) or id .. ':' .. dmg
end
function blockDB:lookupName(id, dmg)
if not id or not dmg then error('blockDB:lookupName: nil passed', 2) end
for _,v in pairs(self.data) do
if v.strId == id and v.dmg == dmg then
return v.name
end
end
end
function blockDB:add(id, dmg, name, strId)
local key = id .. ':' .. dmg
TableDB.add(self, key, {
id = id,
dmg = dmg,
key = key,
name = name,
strId = strId,
})
end
--[[-- placementDB --]]--
-- in memory table that expands the standardBlock and blockType tables for each item/dmg/placement combination
local placementDB = TableDB({
fileName = 'placement.db'
})
function placementDB:load(dir, sbDB, btDB)
self.fileName = fs.combine(dir, self.fileName)
for k,blockType in pairs(sbDB.data) do
local bt = btDB.data[blockType]
if not bt then
error('missing block type: ' .. blockType)
end
local id, dmg = string.match(k, '(%d+):*(%d+)')
self:addSubsForBlockType(tonumber(id), tonumber(dmg), bt)
end
-- testing
-- self.dirty = true
-- self:flush()
end
function placementDB:addSubsForBlockType(id, dmg, bt)
for _,sub in pairs(bt) do
local odmg = sub.odmg
if type(sub.odmg) == 'string' then
odmg = dmg + tonumber(string.match(odmg, '+(%d+)'))
end
local b = blockDB:lookup(id, dmg)
local strId = tostring(id)
if b then
strId = b.strId
end
self:add(
id,
odmg,
sub.sid or strId,
sub.sdmg or dmg,
sub.dir)
end
end
function placementDB:add(id, dmg, sid, sdmg, direction)
if not id or not dmg then error('placementDB:add: nil passed', 2) end
local key = id .. ':' .. dmg
if direction and #direction == 0 then
direction = nil
end
self.data[key] = {
id = id, -- numeric ID
dmg = dmg, -- dmg with placement info
key = key,
sid = sid, -- string ID
sdmg = sdmg, -- dmg without placement info
direction = direction,
}
end
--[[-- StandardBlockDB --]]--
local standardBlockDB = TableDB({
fileName = 'standard.db',
tabledef = {
autokeys = false,
type = 'simple',
columns = {
{ label = 'Key', type = 'key', length = 8 },
{ label = 'Block Type', type = 'string', length = 20 }
}
}
})
function standardBlockDB:load(dir)
self.fileName = fs.combine(dir, self.fileName)
if fs.exists(self.fileName) then
TableDB.load(self)
else
self:seedDB()
end
end
function standardBlockDB:seedDB()
self.data = {
[ '6:0' ] = 'sapling',
[ '6:1' ] = 'sapling',
[ '6:2' ] = 'sapling',
[ '6:3' ] = 'sapling',
[ '6:4' ] = 'sapling',
[ '6:5' ] = 'sapling',
[ '8:0' ] = 'truncate',
[ '9:0' ] = 'truncate',
[ '17:0' ] = 'wood',
[ '17:1' ] = 'wood',
[ '17:2' ] = 'wood',
[ '17:3' ] = 'wood',
[ '18:0' ] = 'leaves',
[ '18:1' ] = 'leaves',
[ '18:2' ] = 'leaves',
[ '18:3' ] = 'leaves',
[ '23:0' ] = 'dispenser',
[ '26:0' ] = 'bed',
[ '27:0' ] = 'adp-rail',
[ '28:0' ] = 'adp-rail',
[ '29:0' ] = 'piston',
[ '33:0' ] = 'piston',
[ '34:0' ] = 'air',
[ '36:0' ] = 'air',
[ '44:0' ] = 'slab',
[ '44:1' ] = 'slab',
[ '44:2' ] = 'slab',
[ '44:3' ] = 'slab',
[ '44:4' ] = 'slab',
[ '44:5' ] = 'slab',
[ '44:6' ] = 'slab',
[ '44:7' ] = 'slab',
[ '50:0' ] = 'torch',
[ '51:0' ] = 'flatten',
[ '53:0' ] = 'stairs',
[ '54:0' ] = 'chest-furnace',
[ '55:0' ] = 'flatten',
[ '59:0' ] = 'flatten',
[ '60:0' ] = 'flatten',
[ '61:0' ] = 'chest-furnace',
[ '62:0' ] = 'chest-furnace',
[ '63:0' ] = 'signpost',
[ '64:0' ] = 'door',
[ '65:0' ] = 'wallsign-ladder',
[ '66:0' ] = 'rail',
[ '67:0' ] = 'stairs',
[ '68:0' ] = 'wallsign',
[ '69:0' ] = 'lever',
[ '71:0' ] = 'door',
[ '75:0' ] = 'torch',
[ '76:0' ] = 'torch',
[ '77:0' ] = 'button',
[ '78:0' ] = 'flatten',
[ '81:0' ] = 'flatten',
[ '83:0' ] = 'flatten',
[ '86:0' ] = 'pumpkin',
[ '90:0' ] = 'air',
[ '91:0' ] = 'pumpkin',
[ '93:0' ] = 'repeater',
[ '94:0' ] = 'repeater',
[ '96:0' ] = 'trapdoor',
[ '99:0' ] = 'flatten',
[ '100:0' ] = 'flatten',
[ '106:0' ] = 'vine',
[ '107:0' ] = 'gate',
[ '108:0' ] = 'stairs',
[ '109:0' ] = 'stairs',
[ '114:0' ] = 'stairs',
[ '115:0' ] = 'flatten',
[ '117:0' ] = 'flatten',
[ '118:0' ] = 'cauldron',
[ '126:0' ] = 'slab',
[ '126:1' ] = 'slab',
[ '126:2' ] = 'slab',
[ '126:3' ] = 'slab',
[ '126:4' ] = 'slab',
[ '126:5' ] = 'slab',
[ '127:0' ] = 'cocoa',
[ '128:0' ] = 'stairs',
[ '130:0' ] = 'chest-furnace',
[ '131:0' ] = 'tripwire',
[ '132:0' ] = 'flatten',
[ '134:0' ] = 'stairs',
[ '135:0' ] = 'stairs',
[ '136:0' ] = 'stairs',
[ '140:0' ] = 'flatten',
[ '141:0' ] = 'flatten',
[ '142:0' ] = 'flatten',
[ '143:0' ] = 'button',
[ '144:0' ] = 'mobhead',
[ '145:0' ] = 'anvil',
[ '146:0' ] = 'chest-furnace',
[ '149:0' ] = 'comparator',
[ '151:0' ] = 'flatten',
[ '154:0' ] = 'hopper',
[ '155:2' ] = 'quartz-pillar',
[ '156:0' ] = 'stairs',
[ '157:0' ] = 'adp-rail',
[ '158:0' ] = 'hopper',
[ '161:0' ] = 'leaves',
[ '161:1' ] = 'leaves',
[ '162:0' ] = 'wood',
[ '162:1' ] = 'wood',
[ '163:0' ] = 'stairs',
[ '164:0' ] = 'stairs',
[ '167:0' ] = 'trapdoor',
[ '170:0' ] = 'flatten',
[ '175:0' ] = 'largeplant',
[ '175:1' ] = 'largeplant',
[ '175:2' ] = 'largeplant', -- double tallgrass - an alternative would be to use grass as the bottom part, bonemeal as top part
[ '175:3' ] = 'largeplant',
[ '175:4' ] = 'largeplant',
[ '175:5' ] = 'largeplant',
[ '176:0' ] = 'banner',
[ '177:0' ] = 'wall_banner',
[ '178:0' ] = 'truncate',
[ '180:0' ] = 'stairs',
[ '182:0' ] = 'slab',
[ '183:0' ] = 'gate',
[ '184:0' ] = 'gate',
[ '185:0' ] = 'gate',
[ '186:0' ] = 'gate',
[ '187:0' ] = 'gate',
[ '193:0' ] = 'door',
[ '194:0' ] = 'door',
[ '195:0' ] = 'door',
[ '196:0' ] = 'door',
[ '197:0' ] = 'door',
[ '198:0' ] = 'flatten',
[ '205:0' ] = 'slab',
[ '210:0' ] = 'flatten',
[ '355:0' ] = 'bed',
[ '356:0' ] = 'repeater',
[ '404:0' ] = 'comparator',
}
self.dirty = true
self:flush()
end
--[[-- BlockTypeDB --]]--
local blockTypeDB = TableDB({
fileName = 'blocktype.db',
tabledef = {
autokeys = true,
columns = {
{ name = 'odmg', type = 'number', length = 2 },
{ name = 'sid', type = 'number', length = 5 },
{ name = 'sdmg', type = 'number', length = 2 },
{ name = 'dir', type = 'string', length = 20 },
}
}
})
function blockTypeDB:load(dir)
self.fileName = fs.combine(dir, self.fileName)
if fs.exists(self.fileName) then
TableDB.load(self)
else
self:seedDB()
end
end
function blockTypeDB:addTemp(blockType, subs)
local bt = self.data[blockType]
if not bt then
bt = { }
self.data[blockType] = bt
end
for _,sub in pairs(subs) do
table.insert(bt, {
odmg = sub[1],
sid = sub[2],
sdmg = sub[3],
dir = sub[4]
})
end
self.dirty = true
end
function blockTypeDB:seedDB()
blockTypeDB:addTemp('stairs', {
{ 0, nil, 0, 'east-up' },
{ 1, nil, 0, 'west-up' },
{ 2, nil, 0, 'south-up' },
{ 3, nil, 0, 'north-up' },
{ 4, nil, 0, 'east-down' },
{ 5, nil, 0, 'west-down' },
{ 6, nil, 0, 'south-down' },
{ 7, nil, 0, 'north-down' },
})
blockTypeDB:addTemp('gate', {
{ 0, nil, 0, 'north' },
{ 1, nil, 0, 'east' },
{ 2, nil, 0, 'south' },
{ 3, nil, 0, 'west' },
{ 4, nil, 0, 'north' },
{ 5, nil, 0, 'east' },
{ 6, nil, 0, 'south' },
{ 7, nil, 0, 'west' },
})
blockTypeDB:addTemp('pumpkin', {
{ 0, nil, 0, 'north-block' },
{ 1, nil, 0, 'east-block' },
{ 2, nil, 0, 'south-block' },
{ 3, nil, 0, 'west-block' },
{ 4, nil, 0, 'north-block' },
{ 5, nil, 0, 'east-block' },
{ 6, nil, 0, 'south-block' },
{ 7, nil, 0, 'west-block' },
})
blockTypeDB:addTemp('anvil', {
{ 0, nil, 0, 'south' },
{ 1, nil, 0, 'east' },
{ 2, nil, 0, 'south'},
{ 3, nil, 0, 'east' },
{ 4, nil, 0, 'south' },
{ 5, nil, 0, 'east' },
{ 6, nil, 0, 'east' },
{ 7, nil, 0, 'south' },
{ 8, nil, 0, 'south' },
{ 9, nil, 0, 'east' },
{ 10, nil, 0, 'east' },
{ 11, nil, 0, 'south' },
{ 12, nil, 0 },
{ 13, nil, 0 },
{ 14, nil, 0 },
{ 15, nil, 0 },
})
blockTypeDB:addTemp('bed', {
{ 0, nil, 0, 'south' },
{ 1, nil, 0, 'west' },
{ 2, nil, 0, 'north' },
{ 3, nil, 0, 'east' },
{ 4, nil, 0, 'south' },
{ 5, nil, 0, 'west' },
{ 6, nil, 0, 'north' },
{ 7, nil, 0, 'east' },
{ 8, 'minecraft:air', 0 },
{ 9, 'minecraft:air', 0 },
{ 10, 'minecraft:air', 0 },
{ 11, 'minecraft:air', 0 },
{ 12, 'minecraft:air', 0 },
{ 13, 'minecraft:air', 0 },
{ 14, 'minecraft:air', 0 },
{ 15, 'minecraft:air', 0 },
})
blockTypeDB:addTemp('comparator', {
{ 0, nil, 0, 'south' },
{ 1, nil, 0, 'west' },
{ 2, nil, 0, 'north' },
{ 3, nil, 0, 'east' },
{ 4, nil, 0, 'south' },
{ 5, nil, 0, 'west' },
{ 6, nil, 0, 'north' },
{ 7, nil, 0, 'east' },
{ 8, nil, 0, 'south' },
{ 9, nil, 0, 'west' },
{ 10, nil, 0, 'north' },
{ 11, nil, 0, 'east' },
{ 12, nil, 0, 'south' },
{ 13, nil, 0, 'west' },
{ 14, nil, 0, 'north' },
{ 15, nil, 0, 'east' },
})
blockTypeDB:addTemp('quartz-pillar', {
{ 2, nil, 2 },
{ 3, nil, 2, 'north-south-block' },
{ 4, nil, 2, 'east-west-block' }, -- should be east-west-block
})
blockTypeDB:addTemp('button', {
{ 1, nil, 0, 'west-block' },
{ 2, nil, 0, 'east-block' },
{ 3, nil, 0, 'north-block' },
{ 4, nil, 0, 'south-block' },
{ 5, nil, 0 }, -- block top
})
blockTypeDB:addTemp('cauldron', {
{ 0, nil, 0 },
{ 1, nil, 0 },
{ 2, nil, 0 },
{ 3, nil, 0 },
})
blockTypeDB:addTemp('dispenser', {
{ 0, nil, 0 },
{ 1, nil, 0 },
{ 2, nil, 0, 'south' },
{ 3, nil, 0, 'north' },
{ 4, nil, 0, 'east' },
{ 5, nil, 0, 'west' },
{ 9, nil, 0 },
})
blockTypeDB:addTemp('hopper', {
{ 0, nil, 0 },
{ 1, nil, 0 },
{ 2, nil, 0, 'south-block' },
{ 3, nil, 0, 'north-block' },
{ 4, nil, 0, 'east-block' },
{ 5, nil, 0, 'west-block' },
{ 9, nil, 0 },
{ 10, nil, 0 },
{ 11, nil, 0, 'south-block' },
{ 12, nil, 0, 'north-block' },
{ 13, nil, 0, 'east-block' },
{ 14, nil, 0, 'west-block' },
})
blockTypeDB:addTemp('mobhead', {
{ 0, nil, 0 },
{ 1, nil, 0 },
{ 2, nil, 0, 'south-block' },
{ 3, nil, 0, 'north-block' },
{ 4, nil, 0, 'west-block' },
{ 5, nil, 0, 'east-block' },
})
blockTypeDB:addTemp('rail', {
{ 0, nil, 0, 'south' },
{ 1, nil, 0, 'east' },
{ 2, nil, 0, 'east' },
{ 3, nil, 0, 'east' },
{ 4, nil, 0, 'south' },
{ 5, nil, 0, 'south' },
{ 6, nil, 0, 'east' },
{ 7, nil, 0, 'south' },
{ 8, nil, 0, 'east' },
{ 9, nil, 0, 'south' },
})
blockTypeDB:addTemp('adp-rail', {
{ 0, nil, 0, 'south' },
{ 1, nil, 0, 'east' },
{ 2, nil, 0, 'east' },
{ 3, nil, 0, 'east' },
{ 4, nil, 0, 'south' },
{ 5, nil, 0, 'south' },
{ 8, nil, 0, 'south' },
{ 9, nil, 0, 'east' },
{ 10, nil, 0, 'east' },
{ 11, nil, 0, 'east' },
{ 12, nil, 0, 'south' },
{ 13, nil, 0, 'south' },
})
blockTypeDB:addTemp('signpost', {
{ 0, nil, 0, 'south' },
{ 1, nil, 0, 'south' },
{ 2, nil, 0, 'south' },
{ 3, nil, 0, 'south' },
{ 4, nil, 0, 'west' },
{ 5, nil, 0, 'west' },
{ 6, nil, 0, 'west' },
{ 7, nil, 0, 'west' },
{ 8, nil, 0, 'north' },
{ 9, nil, 0, 'north' },
{ 10, nil, 0, 'north' },
{ 11, nil, 0, 'north' },
{ 12, nil, 0, 'east' },
{ 13, nil, 0, 'east' },
{ 14, nil, 0, 'east' },
{ 15, nil, 0, 'east' },
})
blockTypeDB:addTemp('vine', {
{ 0, nil, 0 },
{ 1, nil, 0, 'south-block-vine' },
{ 2, nil, 0, 'west-block-vine' },
{ 3, nil, 0, 'south-block-vine' },
{ 4, nil, 0, 'north-block-vine' },
{ 5, nil, 0, 'south-block-vine' },
{ 6, nil, 0, 'north-block-vine' },
{ 7, nil, 0, 'south-block-vine' },
{ 8, nil, 0, 'east-block-vine' },
{ 9, nil, 0, 'south-block-vine' },
{ 10, nil, 0, 'east-block-vine' },
{ 11, nil, 0, 'east-block-vine' },
{ 12, nil, 0, 'east-block-vine' },
{ 13, nil, 0, 'east-block-vine' },
{ 14, nil, 0, 'east-block-vine' },
{ 15, nil, 0, 'east-block-vine' },
})
blockTypeDB:addTemp('torch', {
{ 0, nil, 0 },
{ 1, nil, 0, 'west-block' },
{ 2, nil, 0, 'east-block' },
{ 3, nil, 0, 'north-block' },
{ 4, nil, 0, 'south-block' },
{ 5, nil, 0 },
})
blockTypeDB:addTemp('tripwire', {
{ 0, nil, 0, 'north-block' },
{ 1, nil, 0, 'east-block' },
{ 2, nil, 0, 'south-block' },
{ 3, nil, 0, 'west-block' },
})
blockTypeDB:addTemp('trapdoor', {
{ 0, nil, 0, 'south-block' },
{ 1, nil, 0, 'north-block' },
{ 2, nil, 0, 'east-block' },
{ 3, nil, 0, 'west-block' },
{ 4, nil, 0, 'south-block' },
{ 5, nil, 0, 'north-block' },
{ 6, nil, 0, 'east-block' },
{ 7, nil, 0, 'west-block' },
{ 8, nil, 0, 'south-block' },
{ 9, nil, 0, 'north-block' },
{ 10, nil, 0, 'east-block' },
{ 11, nil, 0, 'west-block' },
{ 12, nil, 0, 'south-block' },
{ 13, nil, 0, 'north-block' },
{ 14, nil, 0, 'east-block' },
{ 15, nil, 0, 'west-block' },
})
blockTypeDB:addTemp('piston', { -- piston placement is broken in 1.7 -- need to add work around
{ 0, nil, 0, 'piston-down' },
{ 1, nil, 0 },
{ 2, nil, 0, 'piston-north' },
{ 3, nil, 0, 'piston-south' },
{ 4, nil, 0, 'piston-west' },
{ 5, nil, 0, 'piston-east' },
{ 8, nil, 0, 'piston-down' },
{ 9, nil, 0 },
{ 10, nil, 0, 'piston-north' },
{ 11, nil, 0, 'piston-south' },
{ 12, nil, 0, 'piston-west' },
{ 13, nil, 0, 'piston-east' },
})
blockTypeDB:addTemp('lever', {
{ 0, nil, 0, 'up' },
{ 1, nil, 0, 'west-block' },
{ 2, nil, 0, 'east-block' },
{ 3, nil, 0, 'north-block' },
{ 4, nil, 0, 'south-block' },
{ 5, nil, 0, 'north' },
{ 6, nil, 0, 'west' },
{ 7, nil, 0, 'up' },
{ 8, nil, 0, 'up' },
{ 9, nil, 0, 'west-block' },
{ 10, nil, 0, 'east-block' },
{ 11, nil, 0, 'north-block' },
{ 12, nil, 0, 'south-block' },
{ 13, nil, 0, 'north' },
{ 14, nil, 0, 'west' },
{ 15, nil, 0, 'up' },
})
blockTypeDB:addTemp('wallsign-ladder', {
{ 0, nil, 0 },
{ 2, nil, 0, 'south-block' },
{ 3, nil, 0, 'north-block' },
{ 4, nil, 0, 'east-block' },
{ 5, nil, 0, 'west-block' },
})
blockTypeDB:addTemp('wallsign', {
{ 0, nil, 0 },
{ 2, 'minecraft:sign', 0, 'south-block' },
{ 3, 'minecraft:sign', 0, 'north-block' },
{ 4, 'minecraft:sign', 0, 'east-block' },
{ 5, 'minecraft:sign', 0, 'west-block' },
})
blockTypeDB:addTemp('chest-furnace', {
{ 0, nil, 0 },
{ 2, nil, 0, 'south' },
{ 3, nil, 0, 'north' },
{ 4, nil, 0, 'east' },
{ 5, nil, 0, 'west' },
})
blockTypeDB:addTemp('repeater', {
{ 0, nil, 0, 'north' },
{ 1, nil, 0, 'east' },
{ 2, nil, 0, 'south' },
{ 3, nil, 0, 'west' },
{ 4, nil, 0, 'north' },
{ 5, nil, 0, 'east' },
{ 6, nil, 0, 'south' },
{ 7, nil, 0, 'west' },
{ 8, nil, 0, 'north' },
{ 9, nil, 0, 'east' },
{ 10, nil, 0, 'south' },
{ 11, nil, 0, 'west' },
{ 12, nil, 0, 'north' },
{ 13, nil, 0, 'east' },
{ 14, nil, 0, 'south' },
{ 15, nil, 0, 'west' },
})
blockTypeDB:addTemp('flatten', {
{ 0, nil, 0 },
{ 1, nil, 0 },
{ 2, nil, 0 },
{ 3, nil, 0 },
{ 4, nil, 0 },
{ 5, nil, 0 },
{ 6, nil, 0 },
{ 7, nil, 0 },
{ 8, nil, 0 },
{ 9, nil, 0 },
{ 10, nil, 0 },
{ 11, nil, 0 },
{ 12, nil, 0 },
{ 13, nil, 0 },
{ 14, nil, 0 },
{ 15, nil, 0 },
})
blockTypeDB:addTemp('sapling', {
{ '+0', nil, nil },
{ '+8', nil, nil },
})
blockTypeDB:addTemp('leaves', {
{ '+0', nil, nil },
{ '+4', nil, nil },
{ '+8', nil, nil },
{ '+12', nil, nil },
})
blockTypeDB:addTemp('air', {
{ 0, 'minecraft:air', 0 },
{ 1, 'minecraft:air', 0 },
{ 2, 'minecraft:air', 0 },
{ 3, 'minecraft:air', 0 },
{ 4, 'minecraft:air', 0 },
{ 5, 'minecraft:air', 0 },
{ 6, 'minecraft:air', 0 },
{ 7, 'minecraft:air', 0 },
{ 8, 'minecraft:air', 0 },
{ 9, 'minecraft:air', 0 },
{ 10, 'minecraft:air', 0 },
{ 11, 'minecraft:air', 0 },
{ 12, 'minecraft:air', 0 },
{ 13, 'minecraft:air', 0 },
{ 14, 'minecraft:air', 0 },
{ 15, 'minecraft:air', 0 },
})
blockTypeDB:addTemp('truncate', {
{ 0, nil, 0 },
{ 1, 'minecraft:air', 0 },
{ 2, 'minecraft:air', 0 },
{ 3, 'minecraft:air', 0 },
{ 4, 'minecraft:air', 0 },
{ 5, 'minecraft:air', 0 },
{ 6, 'minecraft:air', 0 },
{ 7, 'minecraft:air', 0 },
{ 8, 'minecraft:air', 0 },
{ 9, 'minecraft:air', 0 },
{ 10, 'minecraft:air', 0 },
{ 11, 'minecraft:air', 0 },
{ 12, 'minecraft:air', 0 },
{ 13, 'minecraft:air', 0 },
{ 14, 'minecraft:air', 0 },
{ 15, 'minecraft:air', 0 },
})
blockTypeDB:addTemp('slab', {
{ '+0', nil, nil, 'bottom' },
{ '+8', nil, nil, 'top' },
})
blockTypeDB:addTemp('largeplant', {
{ '+0', nil, nil, 'east-door' }, -- should use a generic double tall keyword
{ '+8', 'minecraft:air', 0 },
})
blockTypeDB:addTemp('wood', {
{ '+0', nil, nil },
{ '+4', nil, nil, 'east-west-block' },
{ '+8', nil, nil, 'north-south-block' },
{ '+12', nil, nil },
})
blockTypeDB:addTemp('door', {
{ 0, nil, 0, 'east-door' },
{ 1, nil, 0, 'south-door' },
{ 2, nil, 0, 'west-door' },
{ 3, nil, 0, 'north-door' },
{ 4, nil, 0, 'east-door' },
{ 5, nil, 0, 'south-door' },
{ 6, nil, 0, 'west-door' },
{ 7, nil, 0, 'north-door' },
{ 8,'minecraft:air', 0 },
{ 9,'minecraft:air', 0 },
{ 10,'minecraft:air', 0 },
{ 11,'minecraft:air', 0 },
{ 12,'minecraft:air', 0 },
{ 13,'minecraft:air', 0 },
{ 14,'minecraft:air', 0 },
{ 15,'minecraft:air', 0 },
})
blockTypeDB:addTemp('banner', {
{ 0, nil, 0, 'north' },
{ 1, nil, 0, 'east' },
{ 2, nil, 0, 'east' },
{ 3, nil, 0, 'east' },
{ 4, nil, 0, 'east' },
{ 5, nil, 0, 'east' },
{ 6, nil, 0, 'east' },
{ 7, nil, 0, 'east' },
{ 8, nil, 0, 'south' },
{ 9, nil, 0, 'west' },
{ 10, nil, 0, 'west' },
{ 11, nil, 0, 'west' },
{ 12, nil, 0, 'west' },
{ 13, nil, 0, 'west' },
{ 14, nil, 0, 'west' },
{ 15, nil, 0, 'west' },
})
blockTypeDB:addTemp('wall_banner', {
{ 0, nil, 0 },
{ 1, nil, 0 },
{ 2, nil, 0, 'south-block' },
{ 3, nil, 0, 'north-block' },
{ 4, nil, 0, 'east-block' },
{ 5, nil, 0, 'west-block' },
})
blockTypeDB:addTemp('cocoa', {
{ 0, nil, 0, 'south-block' },
{ 1, nil, 0, 'west-block' },
{ 2, nil, 0, 'north-block' },
{ 3, nil, 0, 'east-block' },
{ 4, nil, 0, 'south-block' },
{ 5, nil, 0, 'west-block' },
{ 6, nil, 0, 'north-block' },
{ 7, nil, 0, 'east-block' },
{ 8, nil, 0, 'south-block' },
{ 9, nil, 0, 'west-block' },
{ 10, nil, 0, 'north-block' },
{ 11, nil, 0, 'east-block' },
})
self.dirty = true
self:flush()
end
local Blocks = class()
function Blocks:init(args)
Util.merge(self, args)
self.blockDB = blockDB
blockDB:load(self.dir)
standardBlockDB:load(self.dir)
blockTypeDB:load(self.dir)
placementDB:load(self.dir, standardBlockDB, blockTypeDB)
end
-- for an ID / dmg (with placement info) - return the correct block (without the placment info embedded in the dmg)
function Blocks:getRealBlock(id, dmg)
local p = placementDB:get({id, dmg})
if p then
return { id = p.sid, dmg = p.sdmg, direction = p.direction }
end
local b = blockDB:get({id, dmg})
if b then
return { id = b.strId, dmg = b.dmg }
end
return { id = id, dmg = dmg }
end
return Blocks

103
sys/apis/chestProvider.lua Normal file
View File

@ -0,0 +1,103 @@
local class = require('class')
local Logger = require('logger')
local ChestProvider = class()
function ChestProvider:init(args)
args = args or { }
self.stacks = {}
self.name = 'chest'
self.direction = args.direction or 'up'
self.wrapSide = args.wrapSide or 'bottom'
self.p = peripheral.wrap(self.wrapSide)
end
function ChestProvider:isValid()
return self.p and self.p.getAllStacks
end
function ChestProvider:refresh()
if self.p then
self.p.condenseItems()
self.stacks = self.p.getAllStacks(false)
local t = { }
for _,s in ipairs(self.stacks) do
local key = s.id .. ':' .. s.dmg
if t[key] and t[key].qty < 64 then
t[key].max_size = t[key].qty
else
t[key] = {
qty = s.qty
}
end
end
for _,s in ipairs(self.stacks) do
local key = s.id .. ':' .. s.dmg
if t[key].max_size then
s.max_size = t[key].qty
else
s.max_size = 64
end
end
end
return self.stacks
end
function ChestProvider:getItemInfo(id, dmg)
local item = { id = id, dmg = dmg, qty = 0, max_size = 64 }
for _,stack in pairs(self.stacks) do
if stack.id == id and stack.dmg == dmg then
item.name = stack.display_name
item.qty = item.qty + stack.qty
item.max_size = stack.max_size
end
end
return item
end
function ChestProvider:craft(id, dmg, qty)
return false
end
function ChestProvider:craftItems(items)
end
function ChestProvider:provide(item, qty, slot)
if self.p then
self.stacks = self.p.getAllStacks(false)
for key,stack in pairs(self.stacks) do
if stack.id == item.id and stack.dmg == item.dmg then
local amount = math.min(qty, stack.qty)
self.p.pushItemIntoSlot(self.direction, key, amount, slot)
qty = qty - amount
if qty <= 0 then
break
end
end
end
end
end
function ChestProvider:insert(slot, qty)
if self.p then
local s, m = pcall(function() self.p.pullItem(self.direction, slot, qty) end)
if not s and m then
print('chestProvider:pullItem')
print(m)
Logger.log('chestProvider', 'Insert failed, trying again')
sleep(1)
s, m = pcall(function() self.p.pullItem(self.direction, slot, qty) end)
if not s and m then
print('chestProvider:pullItem')
print(m)
Logger.log('chestProvider', 'Insert failed again')
else
Logger.log('chestProvider', 'Insert successful')
end
end
end
end
return ChestProvider

46
sys/apis/class.lua Normal file
View File

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

24
sys/apis/config.lua Normal file
View File

@ -0,0 +1,24 @@
local Util = require('util')
local Config = { }
Config.load = function(fname, data)
local filename = '/config/' .. fname
if not fs.exists('/config') then
fs.makeDir('/config')
end
if not fs.exists(filename) then
Util.writeTable(filename, data)
else
Util.merge(data, Util.readTable(filename) or { })
end
end
Config.update = function(fname, data)
local filename = '/config/' .. fname
Util.writeTable(filename, data)
end
return Config

870
sys/apis/deflatelua.lua Normal file
View File

@ -0,0 +1,870 @@
--[[
LUA MODULE
compress.deflatelua - deflate (and gunzip/zlib) implemented in Lua.
SYNOPSIS
local DEFLATE = require 'compress.deflatelua'
-- uncompress gzip file
local fh = assert(io.open'foo.txt.gz', 'rb')
local ofh = assert(io.open'foo.txt', 'wb')
DEFLATE.gunzip {input=fh, output=ofh}
fh:close(); ofh:close()
-- can also uncompress from string including zlib and raw DEFLATE formats.
DESCRIPTION
This is a pure Lua implementation of decompressing the DEFLATE format,
including the related zlib and gzip formats.
Note: This library only supports decompression.
Compression is not currently implemented.
API
Note: in the following functions, input stream `fh` may be
a file handle, string, or an iterator function that returns strings.
Output stream `ofh` may be a file handle or a function that
consumes one byte (number 0..255) per call.
DEFLATE.inflate {input=fh, output=ofh}
Decompresses input stream `fh` in the DEFLATE format
while writing to output stream `ofh`.
DEFLATE is detailed in http://tools.ietf.org/html/rfc1951 .
DEFLATE.gunzip {input=fh, output=ofh, disable_crc=disable_crc}
Decompresses input stream `fh` with the gzip format
while writing to output stream `ofh`.
`disable_crc` (defaults to `false`) will disable CRC-32 checking
to increase speed.
gzip is detailed in http://tools.ietf.org/html/rfc1952 .
DEFLATE.inflate_zlib {input=fh, output=ofh, disable_crc=disable_crc}
Decompresses input stream `fh` with the zlib format
while writing to output stream `ofh`.
`disable_crc` (defaults to `false`) will disable CRC-32 checking
to increase speed.
zlib is detailed in http://tools.ietf.org/html/rfc1950 .
DEFLATE.adler32(byte, crc) --> rcrc
Returns adler32 checksum of byte `byte` (number 0..255) appended
to string with adler32 checksum `crc`. This is internally used by
`inflate_zlib`.
ADLER32 in detailed in http://tools.ietf.org/html/rfc1950 .
COMMAND LINE UTILITY
A `gunziplua` command line utility (in folder `bin`) is also provided.
This mimicks the *nix `gunzip` utility but is a pure Lua implementation
that invokes this library. For help do
gunziplua -h
DEPENDENCIES
Requires 'digest.crc32lua' (used for optional CRC-32 checksum checks).
https://github.com/davidm/lua-digest-crc32lua
Will use a bit library ('bit', 'bit32', 'bit.numberlua') if available. This
is not that critical for this library but is required by digest.crc32lua.
'pythonic.optparse' is only required by the optional `gunziplua`
command-line utilty for command line parsing.
https://github.com/davidm/lua-pythonic-optparse
INSTALLATION
Copy the `compress` directory into your LUA_PATH.
REFERENCES
[1] DEFLATE Compressed Data Format Specification version 1.3
http://tools.ietf.org/html/rfc1951
[2] GZIP file format specification version 4.3
http://tools.ietf.org/html/rfc1952
[3] http://en.wikipedia.org/wiki/DEFLATE
[4] pyflate, by Paul Sladen
http://www.paul.sladen.org/projects/pyflate/
[5] Compress::Zlib::Perl - partial pure Perl implementation of
Compress::Zlib
http://search.cpan.org/~nwclark/Compress-Zlib-Perl/Perl.pm
LICENSE
(c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT).
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
(end license)
--]]
local M = {_TYPE='module', _NAME='compress.deflatelua', _VERSION='0.3.20111128'}
local assert = assert
local error = error
local ipairs = ipairs
local pairs = pairs
local print = print
local require = require
local tostring = tostring
local type = type
local setmetatable = setmetatable
local io = io
local math = math
local table_sort = table.sort
local math_max = math.max
local string_char = string.char
--[[
Requires the first module listed that exists, else raises like `require`.
If a non-string is encountered, it is returned.
Second return value is module name loaded (or '').
--]]
local function requireany(...)
local errs = {}
for i = 1, select('#', ...) do local name = select(i, ...)
if type(name) ~= 'string' then return name, '' end
local ok, mod = pcall(require, name)
if ok then return mod, name end
errs[#errs+1] = mod
end
error(table.concat(errs, '\n'), 2)
end
--local crc32 = require "digest.crc32lua" . crc32_byte
--local bit, name_ = requireany('bit', 'bit32', 'bit.numberlua', nil)
local bit
local crc32
local DEBUG = false
-- Whether to use `bit` library functions in current module.
-- Unlike the crc32 library, it doesn't make much difference in this module.
local NATIVE_BITOPS = (bit ~= nil)
local band, lshift, rshift
if NATIVE_BITOPS then
band = bit.band
lshift = bit.lshift
rshift = bit.rshift
end
local function warn(s)
io.stderr:write(s, '\n')
end
local function debug(...)
print('DEBUG', ...)
end
local function runtime_error(s, level)
level = level or 1
error({s}, level+1)
end
local function make_outstate(outbs)
local outstate = {}
outstate.outbs = outbs
outstate.window = {}
outstate.window_pos = 1
return outstate
end
local function output(outstate, byte)
-- debug('OUTPUT:', s)
local window_pos = outstate.window_pos
outstate.outbs(byte)
outstate.window[window_pos] = byte
outstate.window_pos = window_pos % 32768 + 1 -- 32K
end
local function noeof(val)
return assert(val, 'unexpected end of file')
end
local function hasbit(bits, bit)
return bits % (bit + bit) >= bit
end
local function memoize(f)
local mt = {}
local t = setmetatable({}, mt)
function mt:__index(k)
local v = f(k)
t[k] = v
return v
end
return t
end
-- small optimization (lookup table for powers of 2)
local pow2 = memoize(function(n) return 2^n end)
--local tbits = memoize(
-- function(bits)
-- return memoize( function(bit) return getbit(bits, bit) end )
-- end )
-- weak metatable marking objects as bitstream type
local is_bitstream = setmetatable({}, {__mode='k'})
-- DEBUG
-- prints LSB first
--[[
local function bits_tostring(bits, nbits)
local s = ''
local tmp = bits
local function f()
local b = tmp % 2 == 1 and 1 or 0
s = s .. b
tmp = (tmp - b) / 2
end
if nbits then
for i=1,nbits do f() end
else
while tmp ~= 0 do f() end
end
return s
end
--]]
local function bytestream_from_file(fh)
local o = {}
function o:read()
local sb = fh:read(1)
if sb then return sb:byte() end
end
return o
end
local function bytestream_from_string(s)
local i = 1
local o = {}
function o:read()
local by
if i <= #s then
by = s:byte(i)
i = i + 1
end
return by
end
return o
end
local function bytestream_from_function(f)
local i = 0
local buffer = ''
local o = {}
function o:read()
return f()
-- i = i + 1
-- if i > #buffer then
-- buffer = f()
-- if not buffer then return end
-- i = 1
-- end
-- return buffer:byte(i,i)
end
return o
end
local function bitstream_from_bytestream(bys)
local buf_byte = 0
local buf_nbit = 0
local o = {}
function o:nbits_left_in_byte()
return buf_nbit
end
if NATIVE_BITOPS then
function o:read(nbits)
nbits = nbits or 1
while buf_nbit < nbits do
local byte = bys:read()
if not byte then return end -- note: more calls also return nil
buf_byte = buf_byte + lshift(byte, buf_nbit)
buf_nbit = buf_nbit + 8
end
local bits
if nbits == 0 then
bits = 0
elseif nbits == 32 then
bits = buf_byte
buf_byte = 0
else
bits = band(buf_byte, rshift(0xffffffff, 32 - nbits))
buf_byte = rshift(buf_byte, nbits)
end
buf_nbit = buf_nbit - nbits
return bits
end
else
function o:read(nbits)
nbits = nbits or 1
while buf_nbit < nbits do
local byte = bys:read()
if not byte then return end -- note: more calls also return nil
buf_byte = buf_byte + pow2[buf_nbit] * byte
buf_nbit = buf_nbit + 8
end
local m = pow2[nbits]
local bits = buf_byte % m
buf_byte = (buf_byte - bits) / m
buf_nbit = buf_nbit - nbits
return bits
end
end
is_bitstream[o] = true
return o
end
local function get_bitstream(o)
local bs
if is_bitstream[o] then
return o
elseif io.type(o) == 'file' then
bs = bitstream_from_bytestream(bytestream_from_file(o))
elseif type(o) == 'string' then
bs = bitstream_from_bytestream(bytestream_from_string(o))
elseif type(o) == 'function' then
bs = bitstream_from_bytestream(bytestream_from_function(o))
else
runtime_error 'unrecognized type'
end
return bs
end
local function get_obytestream(o)
local bs
if io.type(o) == 'file' then
bs = function(sbyte) o:write(string_char(sbyte)) end
elseif type(o) == 'function' then
bs = o
else
runtime_error('unrecognized type: ' .. tostring(o))
end
return bs
end
local function HuffmanTable(init, is_full)
local t = {}
if is_full then
for val,nbits in pairs(init) do
if nbits ~= 0 then
t[#t+1] = {val=val, nbits=nbits}
--debug('*',val,nbits)
end
end
else
for i=1,#init-2,2 do
local firstval, nbits, nextval = init[i], init[i+1], init[i+2]
--debug(val, nextval, nbits)
if nbits ~= 0 then
for val=firstval,nextval-1 do
t[#t+1] = {val=val, nbits=nbits}
end
end
end
end
table_sort(t, function(a,b)
return a.nbits == b.nbits and a.val < b.val or a.nbits < b.nbits
end)
-- assign codes
local code = 1 -- leading 1 marker
local nbits = 0
for i,s in ipairs(t) do
if s.nbits ~= nbits then
code = code * pow2[s.nbits - nbits]
nbits = s.nbits
end
s.code = code
--debug('huffman code:', i, s.nbits, s.val, code, bits_tostring(code))
code = code + 1
end
local minbits = math.huge
local look = {}
for i,s in ipairs(t) do
minbits = math.min(minbits, s.nbits)
look[s.code] = s.val
end
--for _,o in ipairs(t) do
-- debug(':', o.nbits, o.val)
--end
-- function t:lookup(bits) return look[bits] end
local msb = NATIVE_BITOPS and function(bits, nbits)
local res = 0
for i=1,nbits do
res = lshift(res, 1) + band(bits, 1)
bits = rshift(bits, 1)
end
return res
end or function(bits, nbits)
local res = 0
for i=1,nbits do
local b = bits % 2
bits = (bits - b) / 2
res = res * 2 + b
end
return res
end
local tfirstcode = memoize(
function(bits) return pow2[minbits] + msb(bits, minbits) end)
function t:read(bs)
local code = 1 -- leading 1 marker
local nbits = 0
while 1 do
if nbits == 0 then -- small optimization (optional)
code = tfirstcode[noeof(bs:read(minbits))]
nbits = nbits + minbits
else
local b = noeof(bs:read())
nbits = nbits + 1
code = code * 2 + b -- MSB first
--[[NATIVE_BITOPS
code = lshift(code, 1) + b -- MSB first
--]]
end
--debug('code?', code, bits_tostring(code))
local val = look[code]
if val then
--debug('FOUND', val)
return val
end
end
end
return t
end
local function parse_gzip_header(bs)
-- local FLG_FTEXT = 2^0
local FLG_FHCRC = 2^1
local FLG_FEXTRA = 2^2
local FLG_FNAME = 2^3
local FLG_FCOMMENT = 2^4
local id1 = bs:read(8)
local id2 = bs:read(8)
if id1 ~= 31 or id2 ~= 139 then
runtime_error 'not in gzip format'
end
local cm = bs:read(8) -- compression method
local flg = bs:read(8) -- FLaGs
local mtime = bs:read(32) -- Modification TIME
local xfl = bs:read(8) -- eXtra FLags
local os = bs:read(8) -- Operating System
if DEBUG then
debug("CM=", cm)
debug("FLG=", flg)
debug("MTIME=", mtime)
-- debug("MTIME_str=",os.date("%Y-%m-%d %H:%M:%S",mtime)) -- non-portable
debug("XFL=", xfl)
debug("OS=", os)
end
if not os then runtime_error 'invalid header' end
if hasbit(flg, FLG_FEXTRA) then
local xlen = bs:read(16)
local extra = 0
for i=1,xlen do
extra = bs:read(8)
end
if not extra then runtime_error 'invalid header' end
end
local function parse_zstring(bs)
repeat
local by = bs:read(8)
if not by then runtime_error 'invalid header' end
until by == 0
end
if hasbit(flg, FLG_FNAME) then
parse_zstring(bs)
end
if hasbit(flg, FLG_FCOMMENT) then
parse_zstring(bs)
end
if hasbit(flg, FLG_FHCRC) then
local crc16 = bs:read(16)
if not crc16 then runtime_error 'invalid header' end
-- IMPROVE: check CRC. where is an example .gz file that
-- has this set?
if DEBUG then
debug("CRC16=", crc16)
end
end
end
local function parse_zlib_header(bs)
local cm = bs:read(4) -- Compression Method
local cinfo = bs:read(4) -- Compression info
local fcheck = bs:read(5) -- FLaGs: FCHECK (check bits for CMF and FLG)
local fdict = bs:read(1) -- FLaGs: FDICT (present dictionary)
local flevel = bs:read(2) -- FLaGs: FLEVEL (compression level)
local cmf = cinfo * 16 + cm -- CMF (Compresion Method and flags)
local flg = fcheck + fdict * 32 + flevel * 64 -- FLaGs
if cm ~= 8 then -- not "deflate"
runtime_error("unrecognized zlib compression method: " + cm)
end
if cinfo > 7 then
runtime_error("invalid zlib window size: cinfo=" + cinfo)
end
local window_size = 2^(cinfo + 8)
if (cmf*256 + flg) % 31 ~= 0 then
runtime_error("invalid zlib header (bad fcheck sum)")
end
if fdict == 1 then
runtime_error("FIX:TODO - FDICT not currently implemented")
local dictid_ = bs:read(32)
end
return window_size
end
local function parse_huffmantables(bs)
local hlit = bs:read(5) -- # of literal/length codes - 257
local hdist = bs:read(5) -- # of distance codes - 1
local hclen = noeof(bs:read(4)) -- # of code length codes - 4
local ncodelen_codes = hclen + 4
local codelen_init = {}
local codelen_vals = {
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}
for i=1,ncodelen_codes do
local nbits = bs:read(3)
local val = codelen_vals[i]
codelen_init[val] = nbits
end
local codelentable = HuffmanTable(codelen_init, true)
local function decode(ncodes)
local init = {}
local nbits
local val = 0
while val < ncodes do
local codelen = codelentable:read(bs)
--FIX:check nil?
local nrepeat
if codelen <= 15 then
nrepeat = 1
nbits = codelen
--debug('w', nbits)
elseif codelen == 16 then
nrepeat = 3 + noeof(bs:read(2))
-- nbits unchanged
elseif codelen == 17 then
nrepeat = 3 + noeof(bs:read(3))
nbits = 0
elseif codelen == 18 then
nrepeat = 11 + noeof(bs:read(7))
nbits = 0
else
error 'ASSERT'
end
for i=1,nrepeat do
init[val] = nbits
val = val + 1
end
end
local huffmantable = HuffmanTable(init, true)
return huffmantable
end
local nlit_codes = hlit + 257
local ndist_codes = hdist + 1
local littable = decode(nlit_codes)
local disttable = decode(ndist_codes)
return littable, disttable
end
local tdecode_len_base
local tdecode_len_nextrabits
local tdecode_dist_base
local tdecode_dist_nextrabits
local function parse_compressed_item(bs, outstate, littable, disttable)
local val = littable:read(bs)
--debug(val, val < 256 and string_char(val))
if val < 256 then -- literal
output(outstate, val)
elseif val == 256 then -- end of block
return true
else
if not tdecode_len_base then
local t = {[257]=3}
local skip = 1
for i=258,285,4 do
for j=i,i+3 do t[j] = t[j-1] + skip end
if i ~= 258 then skip = skip * 2 end
end
t[285] = 258
tdecode_len_base = t
--for i=257,285 do debug('T1',i,t[i]) end
end
if not tdecode_len_nextrabits then
local t = {}
if NATIVE_BITOPS then
for i=257,285 do
local j = math_max(i - 261, 0)
t[i] = rshift(j, 2)
end
else
for i=257,285 do
local j = math_max(i - 261, 0)
t[i] = (j - (j % 4)) / 4
end
end
t[285] = 0
tdecode_len_nextrabits = t
--for i=257,285 do debug('T2',i,t[i]) end
end
local len_base = tdecode_len_base[val]
local nextrabits = tdecode_len_nextrabits[val]
local extrabits = bs:read(nextrabits)
local len = len_base + extrabits
if not tdecode_dist_base then
local t = {[0]=1}
local skip = 1
for i=1,29,2 do
for j=i,i+1 do t[j] = t[j-1] + skip end
if i ~= 1 then skip = skip * 2 end
end
tdecode_dist_base = t
--for i=0,29 do debug('T3',i,t[i]) end
end
if not tdecode_dist_nextrabits then
local t = {}
if NATIVE_BITOPS then
for i=0,29 do
local j = math_max(i - 2, 0)
t[i] = rshift(j, 1)
end
else
for i=0,29 do
local j = math_max(i - 2, 0)
t[i] = (j - (j % 2)) / 2
end
end
tdecode_dist_nextrabits = t
--for i=0,29 do debug('T4',i,t[i]) end
end
local dist_val = disttable:read(bs)
local dist_base = tdecode_dist_base[dist_val]
local dist_nextrabits = tdecode_dist_nextrabits[dist_val]
local dist_extrabits = bs:read(dist_nextrabits)
local dist = dist_base + dist_extrabits
--debug('BACK', len, dist)
for i=1,len do
local pos = (outstate.window_pos - 1 - dist) % 32768 + 1 -- 32K
output(outstate, assert(outstate.window[pos], 'invalid distance'))
end
end
return false
end
local function parse_block(bs, outstate)
local bfinal = bs:read(1)
local btype = bs:read(2)
local BTYPE_NO_COMPRESSION = 0
local BTYPE_FIXED_HUFFMAN = 1
local BTYPE_DYNAMIC_HUFFMAN = 2
local BTYPE_RESERVED_ = 3
if DEBUG then
debug('bfinal=', bfinal)
debug('btype=', btype)
end
if btype == BTYPE_NO_COMPRESSION then
bs:read(bs:nbits_left_in_byte())
local len = bs:read(16)
local nlen_ = noeof(bs:read(16))
for i=1,len do
local by = noeof(bs:read(8))
output(outstate, by)
end
elseif btype == BTYPE_FIXED_HUFFMAN or btype == BTYPE_DYNAMIC_HUFFMAN then
local littable, disttable
if btype == BTYPE_DYNAMIC_HUFFMAN then
littable, disttable = parse_huffmantables(bs)
else
littable = HuffmanTable {0,8, 144,9, 256,7, 280,8, 288,nil}
disttable = HuffmanTable {0,5, 32,nil}
end
repeat
local is_done = parse_compressed_item(
bs, outstate, littable, disttable)
until is_done
else
runtime_error 'unrecognized compression type'
end
return bfinal ~= 0
end
function M.inflate(t)
local bs = get_bitstream(t.input)
local outbs = get_obytestream(t.output)
local outstate = make_outstate(outbs)
repeat
local is_final = parse_block(bs, outstate)
until is_final
end
local inflate = M.inflate
function M.gunzip(t)
local bs = get_bitstream(t.input)
local outbs = get_obytestream(t.output)
local disable_crc = t.disable_crc
if disable_crc == nil then disable_crc = false end
parse_gzip_header(bs)
local data_crc32 = 0
inflate{input=bs, output=
disable_crc and outbs or
function(byte)
data_crc32 = crc32(byte, data_crc32)
outbs(byte)
end
}
bs:read(bs:nbits_left_in_byte())
local expected_crc32 = bs:read(32)
local isize = bs:read(32) -- ignored
if DEBUG then
debug('crc32=', expected_crc32)
debug('isize=', isize)
end
if not disable_crc and data_crc32 then
if data_crc32 ~= expected_crc32 then
runtime_error('invalid compressed data--crc error')
end
end
if bs:read() then
warn 'trailing garbage ignored'
end
end
function M.adler32(byte, crc)
local s1 = crc % 65536
local s2 = (crc - s1) / 65536
s1 = (s1 + byte) % 65521
s2 = (s2 + s1) % 65521
return s2*65536 + s1
end -- 65521 is the largest prime smaller than 2^16
function M.inflate_zlib(t)
local bs = get_bitstream(t.input)
local outbs = get_obytestream(t.output)
local disable_crc = t.disable_crc
if disable_crc == nil then disable_crc = false end
local window_size_ = parse_zlib_header(bs)
local data_adler32 = 1
inflate{input=bs, output=
disable_crc and outbs or
function(byte)
data_adler32 = M.adler32(byte, data_adler32)
outbs(byte)
end
}
bs:read(bs:nbits_left_in_byte())
local b3 = bs:read(8)
local b2 = bs:read(8)
local b1 = bs:read(8)
local b0 = bs:read(8)
local expected_adler32 = ((b3*256 + b2)*256 + b1)*256 + b0
if DEBUG then
debug('alder32=', expected_adler32)
end
if not disable_crc then
if data_adler32 ~= expected_adler32 then
runtime_error('invalid compressed data--crc error')
end
end
if bs:read() then
warn 'trailing garbage ignored'
end
end
return M

177
sys/apis/event.lua Normal file
View File

@ -0,0 +1,177 @@
local Util = require('util')
local Event = {
uid = 1, -- unique id for handlers
}
local eventHandlers = {
namedTimers = {}
}
-- debug purposes
function Event.getHandlers()
return eventHandlers
end
function Event.addHandler(type, f)
local event = eventHandlers[type]
if not event then
event = {}
event.handlers = {}
eventHandlers[type] = event
end
local handler = {
uid = Event.uid,
event = type,
f = f,
}
Event.uid = Event.uid + 1
event.handlers[handler.uid] = handler
return handler
end
function Event.removeHandler(h)
if h and h.event then
eventHandlers[h.event].handlers[h.uid] = nil
end
end
function Event.queueTimedEvent(name, timeout, event, args)
Event.addNamedTimer(name, timeout, false,
function()
os.queueEvent(event, args)
end
)
end
function Event.addNamedTimer(name, interval, recurring, f)
Event.cancelNamedTimer(name)
eventHandlers.namedTimers[name] = Event.addTimer(interval, recurring, f)
end
function Event.getNamedTimer(name)
return eventHandlers.namedTimers[name]
end
function Event.cancelNamedTimer(name)
local timer = Event.getNamedTimer(name)
if timer then
timer.enabled = false
Event.removeHandler(timer)
end
end
function Event.isTimerActive(timer)
return timer.enabled and
os.clock() < timer.start + timer.interval
end
function Event.addTimer(interval, recurring, f)
local timer = Event.addHandler('timer',
function(t, id)
if t.timerId ~= id then
return
end
if t.enabled then
t.fired = true
t.cf(t, id)
end
if t.recurring then
t.fired = false
t.start = os.clock()
t.timerId = os.startTimer(t.interval)
else
Event.removeHandler(t)
end
end
)
timer.cf = f
timer.interval = interval
timer.recurring = recurring
timer.start = os.clock()
timer.enabled = true
timer.timerId = os.startTimer(interval)
return timer
end
function Event.removeTimer(h)
Event.removeHandler(h)
end
function Event.blockUntilEvent(event, timeout)
return Event.waitForEvent(event, timeout, os.pullEvent)
end
function Event.waitForEvent(event, timeout, pullEvent)
pullEvent = pullEvent or Event.pullEvent
local timerId = os.startTimer(timeout)
repeat
local e, p1, p2, p3, p4 = pullEvent()
if e == event then
return e, p1, p2, p3, p4
end
until e == 'timer' and p1 == timerId
end
local exitPullEvents = false
local function _pullEvents()
--exitPullEvents = false
while true do
local e = Event.pullEvent()
if exitPullEvents or e == 'terminate' then
break
end
end
end
function Event.sleep(t)
local timerId = os.startTimer(t or 0)
repeat
local event, id = Event.pullEvent()
until event == 'timer' and id == timerId
end
function Event.pullEvents(...)
local routines = { ... }
if #routines > 0 then
parallel.waitForAny(_pullEvents, ...)
else
_pullEvents()
end
end
function Event.exitPullEvents()
exitPullEvents = true
os.sleep(0)
end
function Event.pullEvent(eventType)
local e = { os.pullEventRaw(eventType) }
return Event.processEvent(e)
end
function Event.processEvent(pe)
local e, p1, p2, p3, p4, p5 = unpack(pe)
local event = eventHandlers[e]
if event then
local keys = Util.keys(event.handlers)
for _,key in pairs(keys) do
local h = event.handlers[key]
if h then
h.f(h, p1, p2, p3, p4, p5)
end
end
end
return e, p1, p2, p3, p4, p5
end
return Event

142
sys/apis/fileui.lua Normal file
View File

@ -0,0 +1,142 @@
local UI = require('ui')
return function()
local columns = {
{ heading = 'Name', key = 'name', width = UI.term.width - 9 },
}
if UI.term.width > 28 then
columns[1].width = UI.term.width - 16
table.insert(columns,
{ heading = 'Size', key = 'size', width = 6 }
)
end
local selectFile = UI.Page({
x = 3,
y = 3,
ex = -3,
ey = -3,
backgroundColor = colors.brown,
titleBar = UI.TitleBar({
title = 'Select file',
previousPage = true,
event = 'cancel',
}),
grid = UI.ScrollingGrid({
x = 2,
y = 2,
ex = -2,
ey = -4,
path = '',
sortColumn = 'name',
columns = columns,
}),
path = UI.TextEntry({
x = 2,
oy = -1,
ex = -11,
limit = 256,
accelerators = {
enter = 'path_enter',
}
}),
cancel = UI.Button({
text = 'Cancel',
ox = -8,
oy = -1,
event = 'cancel',
}),
})
function selectFile:enable(path, fn)
self:setPath(path)
self.fn = fn
UI.Page.enable(self)
end
function selectFile:setPath(path)
self.grid.dir = path
while not fs.isDir(self.grid.dir) do
self.grid.dir = fs.getDir(self.grid.dir)
end
self.path.value = self.grid.dir
end
function selectFile.grid:draw()
local files = fs.list(self.dir, true)
if #self.dir > 0 then
table.insert(files, {
name = '..',
isDir = true,
})
end
self:setValues(files)
self:setIndex(1)
UI.Grid.draw(self)
end
function selectFile.grid:getDisplayValues(row)
if row.size then
row = Util.shallowCopy(row)
row.size = Util.toBytes(row.size)
end
return row
end
function selectFile.grid:getRowTextColor(file, selected)
if file.isDir then
return colors.cyan
end
if file.isReadOnly then
return colors.pink
end
return colors.white
end
function selectFile.grid:sortCompare(a, b)
if self.sortColumn == 'size' then
return a.size < b.size
end
if a.isDir == b.isDir then
return a.name:lower() < b.name:lower()
end
return a.isDir
end
function selectFile:eventHandler(event)
if event.type == 'grid_select' then
self.grid.dir = fs.combine(self.grid.dir, event.selected.name)
self.path.value = self.grid.dir
if event.selected.isDir then
self.grid:draw()
self.path:draw()
else
UI:setPreviousPage()
self.fn(self.path.value)
end
elseif event.type == 'path_enter' then
if fs.isDir(self.path.value) then
self:setPath(self.path.value)
self.grid:draw()
self.path:draw()
else
UI:setPreviousPage()
self.fn(self.path.value)
end
elseif event.type == 'cancel' then
UI:setPreviousPage()
self.fn()
else
return UI.Page.eventHandler(self, event)
end
return true
end
return selectFile
end

16
sys/apis/fs/gitfs.lua Normal file
View File

@ -0,0 +1,16 @@
local git = require('git')
local gitfs = { }
function gitfs.mount(dir, user, repo, branch)
if not user or not repo then
error('gitfs syntax: user, repo, [branch]')
end
local list = git.list(user, repo, branch)
for path, url in pairs(list) do
fs.mount(fs.combine(dir, path), 'urlfs', url)
end
end
return gitfs

61
sys/apis/fs/linkfs.lua Normal file
View File

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

161
sys/apis/fs/netfs.lua Normal file
View File

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

153
sys/apis/fs/ramfs.lua Normal file
View File

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

78
sys/apis/fs/urlfs.lua Normal file
View File

@ -0,0 +1,78 @@
local synchronized = require('sync')
local urlfs = { }
function urlfs.mount(dir, url)
if not url then
error('URL is required')
end
return {
url = url,
}
end
function urlfs.delete(node, dir)
fs.unmount(dir)
end
function urlfs.exists()
return true
end
function urlfs.getSize(node)
return node.size or 0
end
function urlfs.isReadOnly()
return true
end
function urlfs.isDir()
return false
end
function urlfs.getDrive()
return 'url'
end
function urlfs.open(node, fn, fl)
if fl ~= 'r' then
error('Unsupported mode')
end
local c = node.cache
if not c then
synchronized(node.url, function()
c = Util.download(node.url)
end)
if c and #c > 0 then
node.cache = c
node.size = #c
end
end
if not c or #c == 0 then
return
end
local ctr = 0
local lines
return {
readLine = function()
if not lines then
lines = Util.split(c)
end
ctr = ctr + 1
return lines[ctr]
end,
readAll = function()
return c
end,
close = function()
lines = nil
end,
}
end
return urlfs

40
sys/apis/git.lua Normal file
View File

@ -0,0 +1,40 @@
local json = require('json')
local TREE_URL = 'https://api.github.com/repos/%s/%s/git/trees/%s?recursive=1'
local FILE_URL = 'https://raw.github.com/%s/%s/%s/%s'
local git = { }
function git.list(user, repo, branch)
branch = branch or 'master'
local dataUrl = string.format(TREE_URL, user, repo, branch)
local contents = Util.download(dataUrl)
if not contents then
error('Invalid repository')
end
local data = json.decode(contents)
if data.message and data.message:find("API rate limit exceeded") then
error("Out of API calls, try again later")
end
if data.message and data.message == "Not found" then
error("Invalid repository")
end
local list = { }
for k,v in pairs(data.tree) do
if v.type == "blob" then
v.path = v.path:gsub("%s","%%20")
list[v.path] = string.format(FILE_URL, user, repo, branch, v.path)
end
end
return list
end
return git

196
sys/apis/glasses.lua Normal file
View File

@ -0,0 +1,196 @@
local class = require('class')
local UI = require('ui')
local Event = require('event')
local Peripheral = require('peripheral')
--[[-- Glasses device --]]--
local Glasses = class()
function Glasses:init(args)
local defaults = {
backgroundColor = colors.black,
textColor = colors.white,
textScale = .5,
backgroundOpacity = .5,
multiplier = 2.6665,
-- multiplier = 2.333,
}
defaults.width, defaults.height = term.getSize()
UI.setProperties(defaults, args)
UI.setProperties(self, defaults)
self.bridge = Peripheral.get({
type = 'openperipheral_bridge',
method = 'addBox',
})
self.bridge.clear()
self.setBackgroundColor = function(...) end
self.setTextColor = function(...) end
self.t = { }
for i = 1, self.height do
self.t[i] = {
text = string.rep(' ', self.width+1),
--text = self.bridge.addText(0, 40+i*4, string.rep(' ', self.width+1), 0xffffff),
bg = { },
textFields = { },
}
end
end
function Glasses:setBackgroundBox(boxes, ax, bx, y, bgColor)
local colors = {
[ colors.black ] = 0x000000,
[ colors.brown ] = 0x7F664C,
[ colors.blue ] = 0x253192,
[ colors.red ] = 0xFF0000,
[ colors.gray ] = 0x272727,
[ colors.lime ] = 0x426A0D,
[ colors.green ] = 0x2D5628,
[ colors.white ] = 0xFFFFFF
}
local function overlap(box, ax, bx)
if bx < box.ax or ax > box.bx then
return false
end
return true
end
for _,box in pairs(boxes) do
if overlap(box, ax, bx) then
if box.bgColor == bgColor then
ax = math.min(ax, box.ax)
bx = math.max(bx, box.bx)
box.ax = box.bx + 1
elseif ax == box.ax then
box.ax = bx + 1
elseif ax > box.ax then
if bx < box.bx then
table.insert(boxes, { -- split
ax = bx + 1,
bx = box.bx,
bgColor = box.bgColor
})
box.bx = ax - 1
break
else
box.ax = box.bx + 1
end
elseif ax < box.ax then
if bx > box.bx then
box.ax = box.bx + 1 -- delete
else
box.ax = bx + 1
end
end
end
end
if bgColor ~= colors.black then
table.insert(boxes, {
ax = ax,
bx = bx,
bgColor = bgColor
})
end
local deleted
repeat
deleted = false
for k,box in pairs(boxes) do
if box.ax > box.bx then
if box.box then
box.box.delete()
end
table.remove(boxes, k)
deleted = true
break
end
if not box.box then
box.box = self.bridge.addBox(
math.floor(self.x + (box.ax - 1) * self.multiplier),
self.y + y * 4,
math.ceil((box.bx - box.ax + 1) * self.multiplier),
4,
colors[bgColor],
self.backgroundOpacity)
else
box.box.setX(self.x + math.floor((box.ax - 1) * self.multiplier))
box.box.setWidth(math.ceil((box.bx - box.ax + 1) * self.multiplier))
end
end
until not deleted
end
function Glasses:write(x, y, text, bg)
if x < 1 then
error(' less ', 6)
end
if y <= #self.t then
local line = self.t[y]
local str = line.text
str = str:sub(1, x-1) .. text .. str:sub(x + #text)
self.t[y].text = str
for _,tf in pairs(line.textFields) do
tf.delete()
end
line.textFields = { }
local function split(st)
local words = { }
local offset = 0
while true do
local b,e,w = st:find('(%S+)')
if not b then
break
end
table.insert(words, {
offset = b + offset - 1,
text = w,
})
offset = offset + e
st = st:sub(e + 1)
end
return words
end
local words = split(str)
for _,word in pairs(words) do
local tf = self.bridge.addText(self.x + word.offset * self.multiplier,
self.y+y*4, '', 0xffffff)
tf.setScale(self.textScale)
tf.setZ(1)
tf.setText(word.text)
table.insert(line.textFields, tf)
end
self:setBackgroundBox(line.bg, x, x + #text - 1, y, bg)
end
end
function Glasses:clear(bg)
for _,line in pairs(self.t) do
for _,tf in pairs(line.textFields) do
tf.delete()
end
line.textFields = { }
line.text = string.rep(' ', self.width+1)
-- self.t[i].text.setText('')
end
end
function Glasses:reset()
self:clear()
self.bridge.clear()
self.bridge.sync()
end
function Glasses:sync()
self.bridge.sync()
end
return Glasses

152
sys/apis/gps.lua Normal file
View File

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

47
sys/apis/history.lua Normal file
View File

@ -0,0 +1,47 @@
local Util = require('util')
local History = { }
function History.load(filename, limit)
local entries = Util.readLines(filename) or { }
local pos = #entries + 1
return {
entries = entries,
add = function(line)
local last = entries[pos] or entries[pos - 1]
if not last or line ~= last then
table.insert(entries, line)
if limit then
while #entries > limit do
table.remove(entries, 1)
end
end
Util.writeLines(filename, entries)
pos = #entries + 1
end
end,
setPosition = function(p)
pos = p
end,
back = function()
if pos > 1 then
pos = pos - 1
return entries[pos]
end
end,
forward = function()
if pos <= #entries then
pos = pos + 1
return entries[pos]
end
end,
}
end
return History

69
sys/apis/injector.lua Normal file
View File

@ -0,0 +1,69 @@
local resolver, loader
local function resolveFile(filename, dir, lua_path)
if filename:sub(1, 1) == "/" then
if not fs.exists(filename) then
error('Unable to load: ' .. filename, 2)
end
return filename
end
if dir then
local path = fs.combine(dir, filename)
if fs.exists(path) and not fs.isDir(path) then
return path
end
end
if lua_path then
for dir in string.gmatch(lua_path, "[^:]+") do
local path = fs.combine(dir, filename)
if fs.exists(path) and not fs.isDir(path) then
return path
end
end
end
error('Unable to load: ' .. filename, 2)
end
local function requireWrapper(env)
local modules = { }
return function(filename)
local dir = DIR
if not dir and shell and type(shell.dir) == 'function' then
dir = shell.dir()
end
local fname = resolver(filename:gsub('%.', '/') .. '.lua',
dir or '', LUA_PATH or '/sys/apis')
local rname = fname:gsub('%/', '.'):gsub('%.lua', '')
local module = modules[rname]
if not module then
local f, err = loader(fname, env)
if not f then
error(err)
end
module = f(rname)
modules[rname] = module
end
return module
end
end
local args = { ... }
resolver = args[1] or resolveFile
loader = args[2] or loadfile
return function(env)
setfenv(requireWrapper, env)
return requireWrapper(env)
end

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

@ -0,0 +1,112 @@
local input = { }
function input:translate(event, code)
if event == 'key' then
local ch = keys.getName(code)
if ch then
if code == keys.leftCtrl or code == keys.rightCtrl then
self.control = true
self.combo = false
return
end
if code == keys.leftShift or code == keys.rightShift then
self.shift = true
self.combo = false
return
end
if self.shift then
if #ch > 1 then
ch = 'shift-' .. ch
elseif self.control then
-- will create control-X
-- better than shift-control-x
ch = ch:upper()
end
self.combo = true
end
if self.control then
ch = 'control-' .. ch
self.combo = true
-- even return numbers such as
-- control-seven
return ch
end
-- filter out characters that will be processed in
-- the subsequent char event
if ch and #ch > 1 and (code < 2 or code > 11) then
return ch
end
end
elseif event == 'key_up' then
if code == keys.leftCtrl or code == keys.rightCtrl then
self.control = false
elseif code == keys.leftShift or code == keys.rightShift then
self.shift = false
else
return
end
-- only send through the shift / control event if it wasn't
-- used in combination with another event
if not self.combo then
return keys.getName(code)
end
elseif event == 'char' then
if not self.control then
self.combo = true
return event
end
elseif event == 'mouse_click' then
local buttons = { 'mouse_click', 'mouse_rightclick', 'mouse_doubleclick' }
self.combo = true
if self.shift then
return 'shift-' .. buttons[code]
end
return buttons[code]
elseif event == "mouse_scroll" then
local directions = {
[ -1 ] = 'scrollUp',
[ 1 ] = 'scrollDown'
}
return directions[code]
elseif event == 'paste' then
self.combo = true
return event
elseif event == 'mouse_drag' then
return event
end
end
-- can be useful for testing what keys are generated
function input:test()
print('press a key...')
while true do
local e, code = os.pullEvent()
if e == 'char' and code == 'q' then
break
end
local ch = input:translate(e, code)
if ch then
print(e .. ' ' .. code .. ' ' .. ch)
end
end
end
return input

213
sys/apis/json.lua Normal file
View File

@ -0,0 +1,213 @@
-- credit ElvishJerricco
-- http://pastebin.com/raw.php?i=4nRg9CHU
local json = { }
------------------------------------------------------------------ utils
local controls = {["\n"]="\\n", ["\r"]="\\r", ["\t"]="\\t", ["\b"]="\\b", ["\f"]="\\f", ["\""]="\\\"", ["\\"]="\\\\"}
local function isArray(t)
local max = 0
for k,v in pairs(t) do
if type(k) ~= "number" then
return false
elseif k > max then
max = k
end
end
return max == #t
end
local whites = {['\n']=true; ['\r']=true; ['\t']=true; [' ']=true; [',']=true; [':']=true}
local function removeWhite(str)
while whites[str:sub(1, 1)] do
str = str:sub(2)
end
return str
end
------------------------------------------------------------------ encoding
local function encodeCommon(val, pretty, tabLevel, tTracking)
local str = ""
-- Tabbing util
local function tab(s)
str = str .. ("\t"):rep(tabLevel) .. s
end
local function arrEncoding(val, bracket, closeBracket, iterator, loopFunc)
str = str .. bracket
if pretty then
str = str .. "\n"
tabLevel = tabLevel + 1
end
for k,v in iterator(val) do
tab("")
loopFunc(k,v)
str = str .. ","
if pretty then str = str .. "\n" end
end
if pretty then
tabLevel = tabLevel - 1
end
if str:sub(-2) == ",\n" then
str = str:sub(1, -3) .. "\n"
elseif str:sub(-1) == "," then
str = str:sub(1, -2)
end
tab(closeBracket)
end
-- Table encoding
if type(val) == "table" then
assert(not tTracking[val], "Cannot encode a table holding itself recursively")
tTracking[val] = true
if isArray(val) then
arrEncoding(val, "[", "]", ipairs, function(k,v)
str = str .. encodeCommon(v, pretty, tabLevel, tTracking)
end)
else
arrEncoding(val, "{", "}", pairs, function(k,v)
assert(type(k) == "string", "JSON object keys must be strings", 2)
str = str .. encodeCommon(k, pretty, tabLevel, tTracking)
str = str .. (pretty and ": " or ":") .. encodeCommon(v, pretty, tabLevel, tTracking)
end)
end
-- String encoding
elseif type(val) == "string" then
str = '"' .. val:gsub("[%c\"\\]", controls) .. '"'
-- Number encoding
elseif type(val) == "number" or type(val) == "boolean" then
str = tostring(val)
else
error("JSON only supports arrays, objects, numbers, booleans, and strings", 2)
end
return str
end
function json.encode(val)
return encodeCommon(val, false, 0, {})
end
function json.encodePretty(val)
return encodeCommon(val, true, 0, {})
end
------------------------------------------------------------------ decoding
local decodeControls = {}
for k,v in pairs(controls) do
decodeControls[v] = k
end
local function parseBoolean(str)
if str:sub(1, 4) == "true" then
return true, removeWhite(str:sub(5))
else
return false, removeWhite(str:sub(6))
end
end
local function parseNull(str)
return nil, removeWhite(str:sub(5))
end
local numChars = {['e']=true; ['E']=true; ['+']=true; ['-']=true; ['.']=true}
local function parseNumber(str)
local i = 1
while numChars[str:sub(i, i)] or tonumber(str:sub(i, i)) do
i = i + 1
end
local val = tonumber(str:sub(1, i - 1))
str = removeWhite(str:sub(i))
return val, str
end
local function parseString(str)
str = str:sub(2)
local s = ""
while str:sub(1,1) ~= "\"" do
local next = str:sub(1,1)
str = str:sub(2)
assert(next ~= "\n", "Unclosed string")
if next == "\\" then
local escape = str:sub(1,1)
str = str:sub(2)
next = assert(decodeControls[next..escape], "Invalid escape character")
end
s = s .. next
end
return s, removeWhite(str:sub(2))
end
function json.parseArray(str)
str = removeWhite(str:sub(2))
local val = {}
local i = 1
while str:sub(1, 1) ~= "]" do
local v
v, str = json.parseValue(str)
val[i] = v
i = i + 1
str = removeWhite(str)
end
str = removeWhite(str:sub(2))
return val, str
end
function json.parseValue(str)
local fchar = str:sub(1, 1)
if fchar == "{" then
return json.parseObject(str)
elseif fchar == "[" then
return json.parseArray(str)
elseif tonumber(fchar) ~= nil or numChars[fchar] then
return parseNumber(str)
elseif str:sub(1, 4) == "true" or str:sub(1, 5) == "false" then
return parseBoolean(str)
elseif fchar == "\"" then
return parseString(str)
elseif str:sub(1, 4) == "null" then
return parseNull(str)
end
end
function json.parseMember(str)
local k, val
k, str = json.parseValue(str)
val, str = json.parseValue(str)
return k, val, str
end
function json.parseObject(str)
str = removeWhite(str:sub(2))
local val = {}
while str:sub(1, 1) ~= "}" do
local k, v = nil, nil
k, v, str = json.parseMember(str)
val[k] = v
str = removeWhite(str)
end
str = removeWhite(str:sub(2))
return val, str
end
function json.decode(str)
str = removeWhite(str)
return json.parseValue(str)
end
function json.decodeFromFile(path)
local file = assert(fs.open(path, "r"))
local decoded = decode(file.readAll())
file.close()
return decoded
end
return json

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,100 @@
--- The Node class.
-- The `node` represents a cell (or a tile) on a collision map. Basically, for each single cell (tile)
-- in the collision map passed-in upon initialization, a `node` object will be generated
-- and then cached within the `grid`.
--
-- In the following implementation, nodes can be compared using the `<` operator. The comparison is
-- made with regards of their `f` cost. From a given node being examined, the `pathfinder` will expand the search
-- to the next neighbouring node having the lowest `f` cost. See `core.bheap` for more details.
--
if (...) then
local 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)
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
--- 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
return self
end
return setmetatable(Node,
{__call = function(self,...)
return Node:new(...)
end}
)
end

View File

@ -0,0 +1,201 @@
--- The Path class.
-- The `path` class is a structure which represents a path (ordered set of nodes) from a start location to a goal.
-- An instance from this class would be a result of a request addressed to `Pathfinder:getPath`.
--
-- This module is internally used by the library on purpose.
-- It should normally not be used explicitely, yet it remains fully accessible.
--
if (...) then
-- Dependencies
local _PATH = (...):match('(.+)%.path$')
local Heuristic = require (_PATH .. '.heuristics')
-- Local references
local abs, max = math.abs, math.max
local t_insert, t_remove = table.insert, table.remove
--- The `Path` class.<br/>
-- This class is callable.
-- Therefore, <em><code>Path(...)</code></em> acts as a shortcut to <em><code>Path:new(...)</code></em>.
-- @type Path
local Path = {}
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
return function()
if self._nodes[i] then
i = i+1
return self._nodes[i-1],i-1
end
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}
-- @class function
-- @treturn path self (the calling `path` itself, can be chained)
-- @see Path:fill
-- @usage p:filter()
function Path:filter()
local i = 2
local xi,yi,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
while true do
olddx, olddy = dx, dy
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
t_remove(self._nodes, i-1)
i = i - 1
end
else break end
end
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,...)
return Path:new(...)
end
})
end

View File

@ -0,0 +1,168 @@
-- Various utilities for Jumper top-level modules
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
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()
path._grid = finder._grid
while true do
if node._parent then
t_insert(path._nodes,1,node)
node = node._parent
else
t_insert(path._nodes,1,startNode)
return path
end
end
end
-- Lookup for value in a table
local indexOf = function(t,v)
for i = 1,#t do
if t[i] == v then return i end
end
return nil
end
-- Is i out of range
local function outOfRange(i,low,up)
return (i< low or i > up)
end
return {
arraySize = arraySize,
getKeys = getKeys,
indexOf = indexOf,
outOfRange = outOfRange,
getArrayBounds = getArrayBounds,
arrayToNodes = arrayToNodes,
strToMap = stringMapToArray,
around = around,
drAround = drAround,
traceBackPath = traceBackPath
}
end

429
sys/apis/jumper/grid.lua Normal file
View File

@ -0,0 +1,429 @@
--- The Grid class.
-- Implementation of the `grid` class.
-- The `grid` is a implicit graph which represents the 2D
-- world map layout on which the `pathfinder` object will run.
-- During a search, the `pathfinder` object needs to save some critical values. These values are cached within each `node`
-- object, and the whole set of nodes are tight inside the `grid` object itself.
if (...) then
-- Dependencies
local _PATH = (...):gsub('%.grid$','')
-- Local references
local Utils = require (_PATH .. '.core.utils')
local 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 = {
{x = 1, y = 0, z = 0} --[[W]], {x = -1, y = 0, z = 0}, --[[E]]
{x = 0, y = 1, z = 0} --[[S]], {x = 0, y = -1, z = 0}, --[[N]]
{x = 0, y = 0, z = 1} --[[U]], {x = 0, y = -0, z = -1}, --[[D]]
}
-- 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]
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)
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)
local neighbours = {}
for i = 1,#straightOffsets do
local n = self:getNodeAt(
node._x + straightOffsets[i].x,
node._y + straightOffsets[i].y,
node._z + straightOffsets[i].z
)
if n and self:isWalkableAt(n._x, n._y, n._z, walkable, clearance) 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)
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
if not self._nodes[y] then self._nodes[y] = {} end
if not self._nodes[y][x] then self._nodes[y][x] = {} end
if not self._nodes[y][x][z] then self._nodes[y][x][z] = Node:new(x,y,z) end
return self._nodes[y][x][z]
end
return setmetatable(Grid,{
__call = function(self,...)
return self:new(...)
end
})
end

View File

@ -0,0 +1,412 @@
--[[
The following License applies to all files within the jumper directory.
Note that this is only a partial copy of the full jumper code base. Also,
the code was modified to support 3D maps.
--]]
--[[
This work is under MIT-LICENSE
Copyright (c) 2012-2013 Roland Yonaba.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
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 = ""
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
--- 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)
local newPathfinder = {}
setmetatable(newPathfinder, Pathfinder)
--newPathfinder:setGrid(grid)
newPathfinder:setFinder(finderName)
--newPathfinder:setWalkable(walkable)
newPathfinder:setMode('DIAGONAL')
newPathfinder:setHeuristic('MANHATTAN')
newPathfinder:setTunnelling(false)
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]__.
-- 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)
self:reset()
local startNode = self._grid:getNodeAt(startX, startY, startZ)
local endNode = self._grid:getNodeAt(endX, endY, endZ)
if not startNode or not endNode then
return nil
end
startNode._heading = ih
endNode._heading = oh
assert(startNode, ('Invalid location [%d, %d, %d]'):format(startX, startY, startZ))
assert(endNode and self._grid:isWalkableAt(endX, endY, endZ),
('Invalid or unreachable location [%d, %d, %d]'):format(endX, endY, endZ))
local _endNode = Finders[self._finder](self, startNode, endNode, clearance, toClear)
if _endNode then
return Utils.traceBackPath(self, _endNode, startNode)
end
return nil
end
--- Resets the `pathfinder`. This function is called internally between successive pathfinding calls, so you should not
-- use it explicitely, unless under specific circumstances.
-- @class function
-- @treturn pathfinder self (the calling `pathfinder` itself, can be chained)
-- @usage local path, len = myFinder:getPath(1,1,5,5)
function Pathfinder:reset()
for node in pairs(toClear) do node:reset() end
toClear = {}
return self
end
-- Returns Pathfinder class
Pathfinder._VERSION = _VERSION
Pathfinder._RELEASEDATE = _RELEASEDATE
return setmetatable(Pathfinder,{
__call = function(self,...)
return self:new(...)
end
})
end

View File

@ -0,0 +1,88 @@
-- Astar algorithm
-- This actual implementation of A-star is based on
-- [Nash A. & al. pseudocode](http://aigamedev.com/open/tutorials/theta-star-any-angle-paths/)
if (...) then
-- Internalization
local 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 mCost, heading = heuristic(neighbour, node) -- Heuristics.EUCLIDIAN(neighbour, node)
if node._g + mCost < neighbour._g then
neighbour._parent = node
neighbour._g = node._g + mCost
neighbour._heading = heading
end
end
-- Updates vertex node-neighbour
local function updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval)
local oldG = neighbour._g
local cmpCost = overrideCostEval or computeCost
cmpCost(node, neighbour, finder, clearance, 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
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
local openList = Heap()
startNode._g = 0
startNode._h = heuristic(endNode, startNode)
startNode._f = startNode._g + startNode._h
openList:push(startNode)
toClear[startNode] = true
startNode._opened = true
while not openList:empty() do
local node = openList:pop()
node._closed = true
if node == endNode then return node end
local neighbours = finder._grid:getNeighbours(node, finder._walkable, finder._allowDiagonal, finder._tunnel)
for i = 1,#neighbours do
local neighbour = neighbours[i]
if not neighbour._closed then
toClear[neighbour] = true
if not neighbour._opened then
neighbour._g = huge
neighbour._parent = nil
end
updateVertex(finder, openList, node, neighbour, endNode, clearance, heuristic, overrideCostEval)
end
end
--[[
printf('x:%d y:%d z:%d g:%d', node._x, node._y, node._z, node._g)
for i = 1,#neighbours do
local n = neighbours[i]
printf('x:%d y:%d z:%d f:%f g:%f h:%d', n._x, n._y, n._z, n._f, n._g, n._heading or -1)
end
--]]
end
return nil
end
end

View File

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

133
sys/apis/logger.lua Normal file
View File

@ -0,0 +1,133 @@
local Logger = {
fn = function() end,
filteredEvents = { },
}
function Logger.setLogger(fn)
Logger.fn = fn
end
function Logger.disable()
Logger.setLogger(function() end)
end
function Logger.setDaemonLogging()
Logger.setLogger(function (text)
os.queueEvent('log', { text = text })
end)
end
function Logger.setMonitorLogging()
local debugMon = device.monitor
if not debugMon then
debugMon.setTextScale(.5)
debugMon.clear()
debugMon.setCursorPos(1, 1)
Logger.setLogger(function(text)
debugMon.write(text)
debugMon.scroll(-1)
debugMon.setCursorPos(1, 1)
end)
end
end
function Logger.setScreenLogging()
Logger.setLogger(function(text)
local x, y = term.getCursorPos()
if x ~= 1 then
local sx, sy = term.getSize()
term.setCursorPos(1, sy)
--term.scroll(1)
end
print(text)
end)
end
function Logger.setWirelessLogging()
if device.wireless_modem then
Logger.filter('modem_message')
Logger.filter('modem_receive')
Logger.filter('rednet_message')
Logger.setLogger(function(text)
device.wireless_modem.transmit(59998, os.getComputerID(), {
type = 'log', contents = text
})
end)
Logger.debug('Logging enabled')
return true
end
end
function Logger.setFileLogging(fileName)
fs.delete(fileName)
Logger.setLogger(function (text)
local logFile
local mode = 'w'
if fs.exists(fileName) then
mode = 'a'
end
local file = io.open(fileName, mode)
if file then
file:write(text)
file:write('\n')
file:close()
end
end)
end
function Logger.log(category, value, ...)
if Logger.filteredEvents[category] then
return
end
if type(value) == 'table' then
local str
for k,v in pairs(value) do
if not str then
str = '{ '
else
str = str .. ', '
end
str = str .. k .. '=' .. tostring(v)
end
if str then
value = str .. ' }'
else
value = '{ }'
end
elseif type(value) == 'string' then
local args = { ... }
if #args > 0 then
value = string.format(value, unpack(args))
end
else
value = tostring(value)
end
Logger.fn(category .. ': ' .. value)
end
function Logger.debug(value, ...)
Logger.log('debug', value, ...)
end
function Logger.logNestedTable(t, indent)
for _,v in ipairs(t) do
if type(v) == 'table' then
log('table')
logNestedTable(v) --, indent+1)
else
log(v)
end
end
end
function Logger.filter( ...)
local events = { ... }
for _,event in pairs(events) do
Logger.filteredEvents[event] = true
end
end
return Logger

165
sys/apis/me.lua Normal file
View File

@ -0,0 +1,165 @@
local ME = {
jobList = { }
}
function ME.setDevice(device)
ME.p = device
--Util.merge(ME, ME.p)
if not device then
error('ME device not attached')
end
for k,v in pairs(ME.p) do
if not ME[k] then
ME[k] = v
end
end
end
function ME.isAvailable()
return not Util.empty(ME.getAvailableItems())
end
-- Strip off color prefix
local function safeString(text)
local val = text:byte(1)
if val < 32 or val > 128 then
local newText = {}
for i = 4, #text do
local val = text:byte(i)
newText[i - 3] = (val > 31 and val < 127) and val or 63
end
return string.char(unpack(newText))
end
return text
end
function ME.getAvailableItems()
local items
pcall(function()
items = ME.p.getAvailableItems('all')
for k,v in pairs(items) do
v.id = v.item.id
v.name = safeString(v.item.display_name)
v.qty = v.item.qty
v.dmg = v.item.dmg
v.max_dmg = v.item.max_dmg
v.nbt_hash = v.item.nbt_hash
end
end)
return items or { }
end
function ME.getItemCount(id, dmg, nbt_hash, ignore_dmg)
local fingerprint = {
id = id,
nbt_hash = nbt_hash,
}
if not ignore_dmg or ignore_dmg ~= 'yes' then
fingerprint.dmg = dmg or 0
end
local item = ME.getItemDetail(fingerprint, false)
if item then
return item.qty
end
return 0
end
function ME.extract(id, dmg, nbt_hash, qty, direction, slot)
dmg = dmg or 0
qty = qty or 1
direction = direction or 'up'
return pcall(function()
local fingerprint = {
dmg = dmg,
id = id,
nbt_hash = nbt_hash
}
return ME.exportItem(fingerprint, direction, qty, slot)
end)
end
function ME.insert(slot, qty, direction)
direction = direction or 'up'
return ME.pullItem(direction, slot, qty)
end
function ME.isCrafting()
local cpus = ME.p.getCraftingCPUs() or { }
for k,v in pairs(cpus) do
if v.busy then
return true
end
end
end
function ME.isCPUAvailable()
local cpus = ME.p.getCraftingCPUs() or { }
local available = false
for cpu,v in pairs(cpus) do
if not v.busy then
available = true
elseif not ME.jobList[cpu] then -- something else is crafting something (don't know what)
return false -- return false since we are in an unknown state
end
end
return available
end
function ME.getJobList()
local cpus = ME.p.getCraftingCPUs() or { }
for cpu,v in pairs(cpus) do
if not v.busy then
ME.jobList[cpu] = nil
end
end
return ME.jobList
end
function ME.craft(id, dmg, nbt_hash, qty)
local cpus = ME.p.getCraftingCPUs() or { }
for cpu,v in pairs(cpus) do
if not v.busy then
ME.p.requestCrafting({
id = id,
dmg = dmg or 0,
nbt_hash = nbt_hash,
},
qty or 1,
cpu
)
os.sleep(0) -- tell it to craft, yet it doesn't show busy - try waiting a cycle...
cpus = ME.p.getCraftingCPUs() or { }
if not cpus[cpu].busy then
-- print('sleeping again')
os.sleep(.1) -- sigh
cpus = ME.p.getCraftingCPUs() or { }
end
-- not working :(
if cpus[cpu].busy then
ME.jobList[cpu] = { id = id, dmg = dmg, qty = qty, nbt_hash = nbt_hash }
return true
end
break -- only need to try the first available cpu
end
end
return false
end
return ME

137
sys/apis/meProvider.lua Normal file
View File

@ -0,0 +1,137 @@
local class = require('class')
local Logger = require('logger')
local MEProvider = class()
function MEProvider:init(args)
self.items = {}
self.name = 'ME'
end
function MEProvider:isValid()
local mep = peripheral.wrap('bottom')
return mep and mep.getAvailableItems and mep.getAvailableItems()
end
-- Strip off color prefix
local function safeString(text)
local val = text:byte(1)
if val < 32 or val > 128 then
local newText = {}
for i = 4, #text do
local val = text:byte(i)
newText[i - 3] = (val > 31 and val < 127) and val or 63
end
return string.char(unpack(newText))
end
return text
end
function MEProvider:refresh()
local mep = peripheral.wrap('bottom')
if mep then
self.items = mep.getAvailableItems('all')
for _,v in pairs(self.items) do
Util.merge(v, v.item)
v.name = safeString(v.display_name)
end
end
return self.items
end
function MEProvider:getItemInfo(id, dmg)
for key,item in pairs(self.items) do
if item.id == id and item.dmg == dmg then
return item
end
end
end
function MEProvider:craft(id, dmg, qty)
self:refresh()
local item = self:getItemInfo(id, dmg)
if item and item.is_craftable then
local mep = peripheral.wrap('bottom')
if mep then
Logger.log('meProvideer', 'requested crafting for: ' .. id .. ':' .. dmg .. ' qty: ' .. qty)
mep.requestCrafting({ id = id, dmg = dmg }, qty)
return true
end
end
return false
end
function MEProvider:craftItems(items)
local mep = peripheral.wrap('bottom')
local cpus = mep.getCraftingCPUs() or { }
local count = 0
for _,cpu in pairs(cpus) do
if cpu.busy then
return
end
end
for _,item in pairs(items) do
if count >= #cpus then
break
end
if self:craft(item.id, item.dmg, item.qty) then
count = count + 1
end
end
end
function MEProvider:provide(item, qty, slot)
local mep = peripheral.wrap('bottom')
if mep then
return pcall(function()
mep.exportItem({
id = item.id,
dmg = item.dmg
},
'up',
qty,
slot)
end)
--if item.qty then
-- item.qty = item.qty - extractedQty
--end
end
end
function MEProvider:insert(slot, qty)
local mep = peripheral.wrap('bottom')
if mep then
local s, m = pcall(function() mep.pullItem('up', slot, qty) end)
if not s and m then
print('meProvider:pullItem')
print(m)
Logger.log('meProvider', 'Insert failed, trying again')
sleep(1)
s, m = pcall(function() mep.pullItem('up', slot, qty) end)
if not s and m then
print('meProvider:pullItem')
print(m)
Logger.log('meProvider', 'Insert failed again')
read()
else
Logger.log('meProvider', 'Insert successful')
end
end
end
end
return MEProvider

106
sys/apis/message.lua Normal file
View File

@ -0,0 +1,106 @@
local Event = require('event')
local Logger = require('logger')
local Message = { }
local messageHandlers = {}
function Message.enable()
if not device.wireless_modem.isOpen(os.getComputerID()) then
device.wireless_modem.open(os.getComputerID())
end
if not device.wireless_modem.isOpen(60000) then
device.wireless_modem.open(60000)
end
end
if device and device.wireless_modem then
Message.enable()
end
Event.addHandler('device_attach', function(event, deviceName)
if deviceName == 'wireless_modem' then
Message.enable()
end
end)
function Message.addHandler(type, f)
table.insert(messageHandlers, {
type = type,
f = f,
enabled = true
})
end
function Message.removeHandler(h)
for k,v in pairs(messageHandlers) do
if v == h then
messageHandlers[k] = nil
break
end
end
end
Event.addHandler('modem_message',
function(event, side, sendChannel, replyChannel, msg, distance)
if msg and msg.type then -- filter out messages from other systems
local id = replyChannel
Logger.log('modem_receive', { id, msg.type })
--Logger.log('modem_receive', msg.contents)
for k,h in pairs(messageHandlers) do
if h.type == msg.type then
-- should provide msg.contents instead of message - type is already known
h.f(h, id, msg, distance)
end
end
end
end
)
function Message.send(id, msgType, contents)
if not device.wireless_modem then
error('No modem attached', 2)
end
if id then
Logger.log('modem_send', { tostring(id), msgType })
device.wireless_modem.transmit(id, os.getComputerID(), {
type = msgType, contents = contents
})
else
Logger.log('modem_send', { 'broadcast', msgType })
device.wireless_modem.transmit(60000, os.getComputerID(), {
type = msgType, contents = contents
})
end
end
function Message.broadcast(t, contents)
if not device.wireless_modem then
error('No modem attached', 2)
end
Message.send(nil, t, contents)
-- Logger.log('rednet_send', { 'broadcast', t })
-- rednet.broadcast({ type = t, contents = contents })
end
function Message.waitForMessage(msgType, timeout, fromId)
local timerId = os.startTimer(timeout)
repeat
local e, side, _id, id, msg, distance = os.pullEvent()
if e == 'modem_message' then
if msg and msg.type and msg.type == msgType then
if not fromId or id == fromId then
return e, id, msg, distance
end
end
end
until e == 'timer' and side == timerId
end
function Message.enableWirelessLogging()
Logger.setWirelessLogging()
end
return Message

76
sys/apis/nft.lua Normal file
View File

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

95
sys/apis/peripheral.lua Normal file
View File

@ -0,0 +1,95 @@
local Peripheral = { }
function Peripheral.addDevice(side)
local name = side
local ptype = peripheral.getType(side)
if not ptype then
return
end
if ptype == 'modem' then
if peripheral.call(name, 'isWireless') then
ptype = 'wireless_modem'
else
ptype = 'wired_modem'
end
end
local sides = {
front = true,
back = true,
top = true,
bottom = true,
left = true,
right = true
}
if sides[name] then
local i = 1
local uniqueName = ptype
while device[uniqueName] do
uniqueName = ptype .. '_' .. i
i = i + 1
end
name = uniqueName
end
device[name] = peripheral.wrap(side)
Util.merge(device[name], {
name = name,
type = ptype,
side = side,
})
return device[name]
end
function Peripheral.getBySide(side)
return Util.find(device, 'side', side)
end
function Peripheral.getByType(typeName)
return Util.find(device, 'type', typeName)
end
function Peripheral.getByMethod(method)
for _,p in pairs(device) do
if p[method] then
return p
end
end
end
-- match any of the passed arguments
function Peripheral.get(args)
if type(args) == 'string' then
args = { type = args }
end
args = args or { type = pType }
if args.type then
local p = Peripheral.getByType(args.type)
if p then
return p
end
end
if args.method then
local p = Peripheral.getByMethod(args.method)
if p then
return p
end
end
if args.side then
local p = Peripheral.getBySide(args.side)
if p then
return p
end
end
end
return Peripheral

147
sys/apis/point.lua Normal file
View File

@ -0,0 +1,147 @@
local Point = { }
function Point.copy(pt)
return { x = pt.x, y = pt.y, z = pt.z }
end
function Point.subtract(a, b)
a.x = a.x - b.x
a.y = a.y - b.y
a.z = a.z - b.z
end
-- real distance
function Point.pythagoreanDistance(a, b)
return math.sqrt(
math.pow(a.x - b.x, 2) +
math.pow(a.y - b.y, 2) +
math.pow(a.z - b.z, 2))
end
-- turtle distance
function Point.turtleDistance(a, b)
if a.y and b.y then
return math.abs(a.x - b.x) +
math.abs(a.y - b.y) +
math.abs(a.z - b.z)
else
return math.abs(a.x - b.x) +
math.abs(a.z - b.z)
end
end
function Point.calculateTurns(ih, oh)
if ih == oh then
return 0
end
if (ih % 2) == (oh % 2) then
return 2
end
return 1
end
-- Calculate distance to location including turns
-- also returns the resulting heading
function Point.calculateMoves(pta, ptb, distance)
local heading = pta.heading
local moves = distance or Point.turtleDistance(pta, ptb)
if (pta.heading % 2) == 0 and pta.z ~= ptb.z then
moves = moves + 1
if ptb.heading and (ptb.heading % 2 == 1) then
heading = ptb.heading
elseif ptb.z > pta.z then
heading = 1
else
heading = 3
end
elseif (pta.heading % 2) == 1 and pta.x ~= ptb.x then
moves = moves + 1
if ptb.heading and (ptb.heading % 2 == 0) then
heading = ptb.heading
elseif ptb.x > pta.x then
heading = 0
else
heading = 2
end
end
if ptb.heading then
if heading ~= ptb.heading then
moves = moves + Point.calculateTurns(heading, ptb.heading)
heading = ptb.heading
end
end
return moves, heading
end
-- given a set of points, find the one taking the least moves
function Point.closest(reference, pts)
local lpt, lm -- lowest
for _,pt in pairs(pts) do
local m = Point.calculateMoves(reference, pt)
if not lm or m < lm then
lpt = pt
lm = m
end
end
return lpt
end
function Point.adjacentPoints(pt)
local pts = { }
for _, hi in pairs(turtle.getHeadings()) do
table.insert(pts, { x = pt.x + hi.xd, y = pt.y + hi.yd, z = pt.z + hi.zd })
end
return pts
end
return Point
--[[
function Point.toBox(pt, width, length, height)
return { ax = pt.x,
ay = pt.y,
az = pt.z,
bx = pt.x + width - 1,
by = pt.y + height - 1,
bz = pt.z + length - 1
}
end
function Point.inBox(pt, box)
return pt.x >= box.ax and
pt.z >= box.az and
pt.x <= box.bx and
pt.z <= box.bz
end
Box = { }
function Box.contain(boundingBox, containedBox)
local shiftX = boundingBox.ax - containedBox.ax
if shiftX > 0 then
containedBox.ax = containedBox.ax + shiftX
containedBox.bx = containedBox.bx + shiftX
end
local shiftZ = boundingBox.az - containedBox.az
if shiftZ > 0 then
containedBox.az = containedBox.az + shiftZ
containedBox.bz = containedBox.bz + shiftZ
end
shiftX = boundingBox.bx - containedBox.bx
if shiftX < 0 then
containedBox.ax = containedBox.ax + shiftX
containedBox.bx = containedBox.bx + shiftX
end
shiftZ = boundingBox.bz - containedBox.bz
if shiftZ < 0 then
containedBox.az = containedBox.az + shiftZ
containedBox.bz = containedBox.bz + shiftZ
end
end
--]]

111
sys/apis/process.lua Normal file
View File

@ -0,0 +1,111 @@
local Process = { }
function Process:init(args)
self.args = { }
self.uid = 0
self.threads = { }
Util.merge(self, args)
self.name = self.name or 'Thread:' .. self.uid
end
function Process:isDead()
return coroutine.status(self.co) == 'dead'
end
function Process:terminate()
print('terminating ' .. self.name)
self:resume('terminate')
end
function Process:threadEvent(...)
for _,key in pairs(Util.keys(self.threads)) do
local thread = self.threads[key]
if thread then
thread:resume(...)
end
end
end
function Process:newThread(name, fn, ...)
self.uid = self.uid + 1
local thread = { }
setmetatable(thread, { __index = Process })
thread:init({
fn = fn,
name = name,
uid = self.uid,
})
local args = { ... }
thread.co = coroutine.create(function()
local s, m = pcall(function() fn(unpack(args)) end)
if not s and m then
if m == 'Terminated' then
printError(thread.name .. ' terminated')
else
printError(m)
end
end
--print('thread died ' .. thread.name)
self.threads[thread.uid] = nil
thread:threadEvent('terminate')
return s, m
end)
self.threads[thread.uid] = thread
thread:resume()
return thread
end
function Process:resume(event, ...)
-- threads get a chance to process the event regardless of the main process filter
self:threadEvent(event, ...)
if not self.filter or self.filter == event or event == "terminate" then
local ok, result = coroutine.resume(self.co, event, ...)
if ok then
self.filter = result
end
return ok, result
end
return true, self.filter
end
function Process:pullEvent(filter)
while true do
local e = { os.pullEventRaw() }
self:threadEvent(unpack(e))
if not filter or e[1] == filter or e[1] == 'terminate' then
return unpack(e)
end
end
end
function Process:pullEvents(filter)
while true do
local e = { os.pullEventRaw(filter) }
self:threadEvent(unpack(e))
if e[1] == 'terminate' then
return unpack(e)
end
end
end
local process = { }
setmetatable(process, { __index = Process })
process:init({ name = 'Main', co = coroutine.running() })
return process

50
sys/apis/profile.lua Normal file
View File

@ -0,0 +1,50 @@
local Logger = require('logger')
local Profile = {
start = function() end,
stop = function() end,
display = function() end,
methods = { },
}
local function Profile_display()
Logger.log('profile', 'Profiling results')
for k,v in pairs(Profile.methods) do
Logger.log('profile', '%s: %f %d %f',
k, Util.round(v.elapsed, 2), v.count, Util.round(v.elapsed/v.count, 2))
end
Profile.methods = { }
end
local function Profile_start(name)
local p = Profile.methods[name]
if not p then
p = { }
p.elapsed = 0
p.count = 0
Profile.methods[name] = p
end
p.clock = os.clock()
return p
end
local function Profile_stop(name)
local p = Profile.methods[name]
p.elapsed = p.elapsed + (os.clock() - p.clock)
p.count = p.count + 1
end
function Profile.enable()
Logger.log('profile', 'Profiling enabled')
Profile.start = Profile_start
Profile.stop = Profile_stop
Profile.display = Profile_display
end
function Profile.disable()
Profile.start = function() end
Profile.stop = function() end
Profile.display = function() end
end
return Profile

58
sys/apis/require.lua Normal file
View File

@ -0,0 +1,58 @@
local function resolveFile(filename, dir, lua_path)
local ch = string.sub(filename, 1, 1)
if ch == "/" then
return filename
end
if dir then
local path = fs.combine(dir, filename)
if fs.exists(path) and not fs.isDir(path) then
return path
end
end
if lua_path then
for dir in string.gmatch(lua_path, "[^:]+") do
local path = fs.combine(dir, filename)
if fs.exists(path) and not fs.isDir(path) then
return path
end
end
end
end
local modules = { }
return function(filename)
local dir = DIR
if not dir and shell and type(shell.dir) == 'function' then
dir = shell.dir()
end
local fname = resolveFile(filename:gsub('%.', '/') .. '.lua',
dir or '', LUA_PATH or '/sys/apis')
if not fname or not fs.exists(fname) then
error('Unable to load: ' .. filename, 2)
end
local rname = fname:gsub('%/', '.'):gsub('%.lua', '')
local module = modules[rname]
if not module then
local f, err = loadfile(fname)
if not f then
error(err)
end
setfenv(f, getfenv(1))
module = f(rname)
modules[rname] = module
end
return module
end

1178
sys/apis/schematic.lua Normal file

File diff suppressed because it is too large Load Diff

297
sys/apis/sha1.lua Normal file
View File

@ -0,0 +1,297 @@
local sha1 = {
_VERSION = "sha.lua 0.5.0",
_URL = "https://github.com/kikito/sha.lua",
_DESCRIPTION = [[
SHA-1 secure hash computation, and HMAC-SHA1 signature computation in Lua (5.1)
Based on code originally by Jeffrey Friedl (http://regex.info/blog/lua/sha1)
And modified by Eike Decker - (http://cube3d.de/uploads/Main/sha1.txt)
]],
_LICENSE = [[
MIT LICENSE
Copyright (c) 2013 Enrique Garcia Cota + Eike Decker + Jeffrey Friedl
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
-----------------------------------------------------------------------------------
-- loading this file (takes a while but grants a boost of factor 13)
local PRELOAD_CACHE = false
local BLOCK_SIZE = 64 -- 512 bits
-- local storing of global functions (minor speedup)
local floor,modf = math.floor,math.modf
local char,format,rep = string.char,string.format,string.rep
-- merge 4 bytes to an 32 bit word
local function bytes_to_w32(a,b,c,d) return a*0x1000000+b*0x10000+c*0x100+d end
-- split a 32 bit word into four 8 bit numbers
local function w32_to_bytes(i)
return floor(i/0x1000000)%0x100,floor(i/0x10000)%0x100,floor(i/0x100)%0x100,i%0x100
end
-- shift the bits of a 32 bit word. Don't use negative values for "bits"
local function w32_rot(bits,a)
local b2 = 2^(32-bits)
local a,b = modf(a/b2)
return a+b*b2*(2^(bits))
end
-- caching function for functions that accept 2 arguments, both of values between
-- 0 and 255. The function to be cached is passed, all values are calculated
-- during loading and a function is returned that returns the cached values (only)
local function cache2arg(fn)
if not PRELOAD_CACHE then return fn end
local lut = {}
for i=0,0xffff do
local a,b = floor(i/0x100),i%0x100
lut[i] = fn(a,b)
end
return function(a,b)
return lut[a*0x100+b]
end
end
-- splits an 8-bit number into 8 bits, returning all 8 bits as booleans
local function byte_to_bits(b)
local b = function(n)
local b = floor(b/n)
return b%2==1
end
return b(1),b(2),b(4),b(8),b(16),b(32),b(64),b(128)
end
-- builds an 8bit number from 8 booleans
local function bits_to_byte(a,b,c,d,e,f,g,h)
local function n(b,x) return b and x or 0 end
return n(a,1)+n(b,2)+n(c,4)+n(d,8)+n(e,16)+n(f,32)+n(g,64)+n(h,128)
end
-- bitwise "and" function for 2 8bit number
local band = cache2arg (function(a,b)
local A,B,C,D,E,F,G,H = byte_to_bits(b)
local a,b,c,d,e,f,g,h = byte_to_bits(a)
return bits_to_byte(
A and a, B and b, C and c, D and d,
E and e, F and f, G and g, H and h)
end)
-- bitwise "or" function for 2 8bit numbers
local bor = cache2arg(function(a,b)
local A,B,C,D,E,F,G,H = byte_to_bits(b)
local a,b,c,d,e,f,g,h = byte_to_bits(a)
return bits_to_byte(
A or a, B or b, C or c, D or d,
E or e, F or f, G or g, H or h)
end)
-- bitwise "xor" function for 2 8bit numbers
local bxor = cache2arg(function(a,b)
local A,B,C,D,E,F,G,H = byte_to_bits(b)
local a,b,c,d,e,f,g,h = byte_to_bits(a)
return bits_to_byte(
A ~= a, B ~= b, C ~= c, D ~= d,
E ~= e, F ~= f, G ~= g, H ~= h)
end)
-- bitwise complement for one 8bit number
local function bnot(x)
return 255-(x % 256)
end
-- creates a function to combine to 32bit numbers using an 8bit combination function
local function w32_comb(fn)
return function(a,b)
local aa,ab,ac,ad = w32_to_bytes(a)
local ba,bb,bc,bd = w32_to_bytes(b)
return bytes_to_w32(fn(aa,ba),fn(ab,bb),fn(ac,bc),fn(ad,bd))
end
end
-- create functions for and, xor and or, all for 2 32bit numbers
local w32_and = w32_comb(band)
local w32_xor = w32_comb(bxor)
local w32_or = w32_comb(bor)
-- xor function that may receive a variable number of arguments
local function w32_xor_n(a,...)
local aa,ab,ac,ad = w32_to_bytes(a)
for i=1,select('#',...) do
local ba,bb,bc,bd = w32_to_bytes(select(i,...))
aa,ab,ac,ad = bxor(aa,ba),bxor(ab,bb),bxor(ac,bc),bxor(ad,bd)
end
return bytes_to_w32(aa,ab,ac,ad)
end
-- combining 3 32bit numbers through binary "or" operation
local function w32_or3(a,b,c)
local aa,ab,ac,ad = w32_to_bytes(a)
local ba,bb,bc,bd = w32_to_bytes(b)
local ca,cb,cc,cd = w32_to_bytes(c)
return bytes_to_w32(
bor(aa,bor(ba,ca)), bor(ab,bor(bb,cb)), bor(ac,bor(bc,cc)), bor(ad,bor(bd,cd))
)
end
-- binary complement for 32bit numbers
local function w32_not(a)
return 4294967295-(a % 4294967296)
end
-- adding 2 32bit numbers, cutting off the remainder on 33th bit
local function w32_add(a,b) return (a+b) % 4294967296 end
-- adding n 32bit numbers, cutting off the remainder (again)
local function w32_add_n(a,...)
for i=1,select('#',...) do
a = (a+select(i,...)) % 4294967296
end
return a
end
-- converting the number to a hexadecimal string
local function w32_to_hexstring(w) return format("%08x",w) end
local function hex_to_binary(hex)
return hex:gsub('..', function(hexval)
return string.char(tonumber(hexval, 16))
end)
end
-- building the lookuptables ahead of time (instead of littering the source code
-- with precalculated values)
local xor_with_0x5c = {}
local xor_with_0x36 = {}
for i=0,0xff do
xor_with_0x5c[char(i)] = char(bxor(i,0x5c))
xor_with_0x36[char(i)] = char(bxor(i,0x36))
end
-----------------------------------------------------------------------------
-- calculating the SHA1 for some text
function sha1.sha1(msg)
local H0,H1,H2,H3,H4 = 0x67452301,0xEFCDAB89,0x98BADCFE,0x10325476,0xC3D2E1F0
local msg_len_in_bits = #msg * 8
local first_append = char(0x80) -- append a '1' bit plus seven '0' bits
local non_zero_message_bytes = #msg +1 +8 -- the +1 is the appended bit 1, the +8 are for the final appended length
local current_mod = non_zero_message_bytes % 64
local second_append = current_mod>0 and rep(char(0), 64 - current_mod) or ""
-- now to append the length as a 64-bit number.
local B1, R1 = modf(msg_len_in_bits / 0x01000000)
local B2, R2 = modf( 0x01000000 * R1 / 0x00010000)
local B3, R3 = modf( 0x00010000 * R2 / 0x00000100)
local B4 = 0x00000100 * R3
local L64 = char( 0) .. char( 0) .. char( 0) .. char( 0) -- high 32 bits
.. char(B1) .. char(B2) .. char(B3) .. char(B4) -- low 32 bits
msg = msg .. first_append .. second_append .. L64
assert(#msg % 64 == 0)
local chunks = #msg / 64
local W = { }
local start, A, B, C, D, E, f, K, TEMP
local chunk = 0
while chunk < chunks do
--
-- break chunk up into W[0] through W[15]
--
start,chunk = chunk * 64 + 1,chunk + 1
for t = 0, 15 do
W[t] = bytes_to_w32(msg:byte(start, start + 3))
start = start + 4
end
--
-- build W[16] through W[79]
--
for t = 16, 79 do
-- For t = 16 to 79 let Wt = S1(Wt-3 XOR Wt-8 XOR Wt-14 XOR Wt-16).
W[t] = w32_rot(1, w32_xor_n(W[t-3], W[t-8], W[t-14], W[t-16]))
end
A,B,C,D,E = H0,H1,H2,H3,H4
for t = 0, 79 do
if t <= 19 then
-- (B AND C) OR ((NOT B) AND D)
f = w32_or(w32_and(B, C), w32_and(w32_not(B), D))
K = 0x5A827999
elseif t <= 39 then
-- B XOR C XOR D
f = w32_xor_n(B, C, D)
K = 0x6ED9EBA1
elseif t <= 59 then
-- (B AND C) OR (B AND D) OR (C AND D
f = w32_or3(w32_and(B, C), w32_and(B, D), w32_and(C, D))
K = 0x8F1BBCDC
else
-- B XOR C XOR D
f = w32_xor_n(B, C, D)
K = 0xCA62C1D6
end
-- TEMP = S5(A) + ft(B,C,D) + E + Wt + Kt;
A,B,C,D,E = w32_add_n(w32_rot(5, A), f, E, W[t], K),
A, w32_rot(30, B), C, D
end
-- Let H0 = H0 + A, H1 = H1 + B, H2 = H2 + C, H3 = H3 + D, H4 = H4 + E.
H0,H1,H2,H3,H4 = w32_add(H0, A),w32_add(H1, B),w32_add(H2, C),w32_add(H3, D),w32_add(H4, E)
end
local f = w32_to_hexstring
return f(H0) .. f(H1) .. f(H2) .. f(H3) .. f(H4)
end
function sha1.binary(msg)
return hex_to_binary(sha1.sha1(msg))
end
function sha1.hmac(key, text)
assert(type(key) == 'string', "key passed to sha1.hmac should be a string")
assert(type(text) == 'string', "text passed to sha1.hmac should be a string")
if #key > BLOCK_SIZE then
key = sha1.binary(key)
end
local key_xord_with_0x36 = key:gsub('.', xor_with_0x36) .. string.rep(string.char(0x36), BLOCK_SIZE - #key)
local key_xord_with_0x5c = key:gsub('.', xor_with_0x5c) .. string.rep(string.char(0x5c), BLOCK_SIZE - #key)
return sha1.sha1(key_xord_with_0x5c .. sha1.binary(key_xord_with_0x36 .. text))
end
function sha1.hmac_binary(key, text)
return hex_to_binary(sha1.hmac(key, text))
end
setmetatable(sha1, {__call = function(_,msg) return sha1.sha1(msg) end })
return sha1

211
sys/apis/socket.lua Normal file
View File

@ -0,0 +1,211 @@
local Logger = require('logger')
local socketClass = { }
function socketClass:read(timeout)
if not self.connected then
Logger.log('socket', 'read: No connection')
return
end
local timerId
local filter
if timeout then
timerId = os.startTimer(timeout)
elseif self.keepAlive then
timerId = os.startTimer(3)
else
filter = 'modem_message'
end
while true do
local e, s, dport, dhost, msg, distance = os.pullEvent(filter)
if e == 'modem_message' and
dport == self.sport and dhost == self.shost and
msg then
if msg.type == 'DISC' then
-- received disconnect from other end
self.connected = false
self:close()
return
elseif msg.type == 'DATA' then
if msg.data then
if timerId then
os.cancelTimer(timerId)
end
return msg.data, distance
end
end
elseif e == 'timer' and s == timerId then
if timeout or not self.connected then
break
end
timerId = os.startTimer(3)
end
end
end
function socketClass:write(data)
if not self.connected then
Logger.log('socket', 'write: No connection')
return false
end
self.transmit(self.dport, self.dhost, {
type = 'DATA',
data = data,
})
return true
end
function socketClass:close()
if self.connected then
Logger.log('socket', 'closing socket ' .. self.sport)
self.transmit(self.dport, self.dhost, {
type = 'DISC',
})
self.connected = false
end
device.wireless_modem.close(self.sport)
end
-- write a ping every second (too much traffic!)
local function pinger(socket)
local process = require('process')
socket.keepAlive = true
Logger.log('socket', 'keepAlive enabled')
process:newThread('socket_ping', function()
local timerId = os.startTimer(1)
local timeStamp = os.clock()
while true do
local e, id, dport, dhost, msg = os.pullEvent()
if e == 'modem_message' then
if dport == socket.sport and
dhost == socket.shost and
msg and
msg.type == 'PING' then
timeStamp = os.clock()
end
elseif e == 'timer' and id == timerId then
if os.clock() - timeStamp > 3 then
Logger.log('socket', 'Connection timed out')
socket:close()
break
end
timerId = os.startTimer(1)
socket.transmit(socket.dport, socket.dhost, {
type = 'PING',
})
end
end
end)
end
local Socket = { }
local function loopback(port, sport, msg)
os.queueEvent('modem_message', 'loopback', port, sport, msg, 0)
end
local function newSocket(isLoopback)
for i = 16384, 32768 do
if not device.wireless_modem.isOpen(i) then
local socket = {
shost = os.getComputerID(),
sport = i,
transmit = device.wireless_modem.transmit,
}
setmetatable(socket, { __index = socketClass })
device.wireless_modem.open(socket.sport)
if isLoopback then
socket.transmit = loopback
end
return socket
end
end
error('No ports available')
end
function Socket.connect(host, port)
local socket = newSocket(host == os.getComputerID())
socket.dhost = host
Logger.log('socket', 'connecting to ' .. port)
socket.transmit(port, socket.sport, {
type = 'OPEN',
shost = socket.shost,
dhost = socket.dhost,
})
local timerId = os.startTimer(3)
repeat
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)
if msg.keepAlive then
pinger(socket)
end
os.cancelTimer(timerId)
return socket
end
until e == 'timer' and id == timerId
socket:close()
end
function Socket.server(port, keepAlive)
device.wireless_modem.open(port)
Logger.log('socket', 'Waiting for connections on port ' .. port)
while true do
local e, _, 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.connected = true
socket.transmit(socket.dport, socket.sport, {
type = 'CONN',
dhost = socket.dhost,
shost = socket.shost,
keepAlive = keepAlive,
})
Logger.log('socket', 'Connection established %d->%d', socket.sport, socket.dport)
if keepAlive then
pinger(socket)
end
return socket
end
end
end
return Socket

24
sys/apis/sync.lua Normal file
View File

@ -0,0 +1,24 @@
local syncLocks = { }
return function(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
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)
end
end

53
sys/apis/tableDB.lua Normal file
View File

@ -0,0 +1,53 @@
local class = require('class')
local TableDB = class()
function TableDB:init(args)
local defaults = {
fileName = '',
dirty = false,
data = { },
tabledef = { },
}
Util.merge(defaults, args) -- refactor
Util.merge(self, defaults)
end
function TableDB:load()
local table = Util.readTable(self.fileName)
if table then
self.data = table.data
self.tabledef = table.tabledef
end
end
function TableDB:add(key, entry)
if type(key) == 'table' then
key = table.concat(key, ':')
end
self.data[key] = entry
self.dirty = true
end
function TableDB:get(key)
if type(key) == 'table' then
key = table.concat(key, ':')
end
return self.data[key]
end
function TableDB:remove(key)
self.data[key] = nil
self.dirty = true
end
function TableDB:flush()
if self.dirty then
Util.writeTable(self.fileName, {
tabledef = self.tabledef,
data = self.data,
})
self.dirty = false
end
end
return TableDB

145
sys/apis/terminal.lua Normal file
View File

@ -0,0 +1,145 @@
local Terminal = { }
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 oldWin = Util.shallowCopy(win)
local scrollPos = 0
local function drawScrollbar(oldPos, newPos)
local x, y = oldWin.getCursorPos()
local pos = math.floor(oldPos / size * (h - 1))
oldWin.setCursorPos(w, oldPos + pos + 1)
oldWin.write(' ')
pos = math.floor(newPos / size * (h - 1))
oldWin.setCursorPos(w, newPos + pos + 1)
oldWin.write('#')
oldWin.setCursorPos(x, y)
end
win.setCursorPos = function(x, y)
oldWin.setCursorPos(x, y)
if y > scrollPos + h then
win.scrollTo(y - h)
elseif y < scrollPos then
win.scrollTo(y - 2)
end
end
win.scrollUp = function()
win.scrollTo(scrollPos - 1)
end
win.scrollDown = function()
win.scrollTo(scrollPos + 1)
end
win.scrollTo = function(p)
p = math.min(math.max(p, 0), size)
if p ~= scrollPos then
drawScrollbar(scrollPos, p)
scrollPos = p
win.reposition(1, -scrollPos + 1)
end
end
win.clear = function()
oldWin.clear()
scrollPos = 0
end
drawScrollbar(0, 0)
return win
end
function Terminal.toGrayscale(ct)
local scolors = {
[ 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 methods = { 'setBackgroundColor', 'setBackgroundColour',
'setTextColor', 'setTextColour' }
for _,v in pairs(methods) do
local fn = ct[v]
ct[v] = function(c)
if scolors[c] then
fn(scolors[c])
end
end
end
local bcolors = {
[ '1' ] = '8',
[ '2' ] = '8',
[ '3' ] = '8',
[ '4' ] = '8',
[ '5' ] = '8',
[ '6' ] = '8',
[ '9' ] = '8',
[ 'a' ] = '7',
[ 'b' ] = '7',
[ 'c' ] = '7',
[ 'd' ] = '8',
[ 'e' ] = '7',
}
local function translate(s)
if s then
for k,v in pairs(bcolors) do
s = s:gsub(k, v)
end
end
return s
end
local fn = ct.blit
ct.blit = function(text, fg, bg)
fn(text, translate(fg), translate(bg))
end
end
function Terminal.copy(ot)
local ct = { }
for k,v in pairs(ot) do
if type(v) == 'function' then
ct[k] = v
end
end
return ct
end
function Terminal.mirror(ct, dt)
for k,f in pairs(ct) do
ct[k] = function(...)
local ret = { f(...) }
if dt[k] then
dt[k](...)
end
return unpack(ret)
end
end
end
return Terminal

2801
sys/apis/ui.lua Normal file

File diff suppressed because it is too large Load Diff

530
sys/apis/util.lua Normal file
View File

@ -0,0 +1,530 @@
local Util = { }
function Util.tryTimed(timeout, f, ...)
local c = os.clock()
repeat
local ret = f(...)
if ret then
return ret
end
until os.clock()-c >= timeout
end
function Util.tryTimes(attempts, f, ...)
local result
for i = 1, attempts do
result = { f(...) }
if result[1] then
return unpack(result)
end
end
return unpack(result)
end
function Util.print(pattern, ...)
local function serialize(tbl, width)
local str = '{\n'
for k, v in pairs(tbl) do
local value
if type(v) == 'table' then
value = string.format('table: %d', Util.size(v))
else
value = tostring(v)
end
str = str .. string.format(' %s: %s\n', k, value)
end
if #str < width then
str = str:gsub('\n', '') .. ' }'
else
str = str .. '}'
end
return str
end
if type(pattern) == 'string' then
print(string.format(pattern, ...))
elseif type(pattern) == 'table' then
print(serialize(pattern, term.current().getSize()))
else
print(tostring(pattern))
end
end
function Util.runFunction(env, fn, ...)
setfenv(fn, env)
setmetatable(env, { __index = _G })
local args = { ... }
return pcall(function()
return fn(table.unpack(args))
end)
end
-- http://lua-users.org/wiki/SimpleRound
function Util.round(num, idp)
local mult = 10^(idp or 0)
return math.floor(num * mult + 0.5) / mult
end
function Util.random(max, min)
min = min or 0
return math.random(0, max-min) + min
end
--[[ Table functions ]] --
function Util.clear(t)
local keys = Util.keys(t)
for _,k in pairs(keys) do
t[k] = nil
end
end
function Util.empty(t)
return not next(t)
end
function Util.key(t, value)
for k,v in pairs(t) do
if v == value then
return k
end
end
end
function Util.keys(t)
local keys = {}
for k in pairs(t) do
keys[#keys+1] = k
end
return keys
end
function Util.merge(obj, args)
if args then
for k,v in pairs(args) do
obj[k] = v
end
end
end
function Util.deepMerge(obj, args)
if args then
for k,v in pairs(args) do
if type(v) == 'table' then
if not obj[k] then
obj[k] = { }
end
Util.deepMerge(obj[k], v)
else
obj[k] = v
end
end
end
end
function Util.transpose(t)
local tt = { }
for k,v in pairs(t) do
tt[v] = k
end
return tt
end
function Util.find(t, name, value)
for k,v in pairs(t) do
if v[name] == value then
return v, k
end
end
end
function Util.findAll(t, name, value)
local rt = { }
for k,v in pairs(t) do
if v[name] == value then
table.insert(rt, v)
end
end
return rt
end
function Util.shallowCopy(t)
local t2 = {}
for k,v in pairs(t) do
t2[k] = v
end
return t2
end
function Util.deepCopy(t)
if type(t) ~= 'table' then
return t
end
--local mt = getmetatable(t)
local res = {}
for k,v in pairs(t) do
if type(v) == 'table' then
v = Util.deepCopy(v)
end
res[k] = v
end
--setmetatable(res,mt)
return res
end
-- http://snippets.luacode.org/?p=snippets/Filter_a_table_in-place_119
function Util.filterInplace(t, predicate)
local j = 1
for i = 1,#t do
local v = t[i]
if predicate(v) then
t[j] = v
j = j + 1
end
end
while t[j] ~= nil do
t[j] = nil
j = j + 1
end
return t
end
function Util.filter(it, f)
local ot = { }
for k,v in pairs(it) do
if f(k, v) then
ot[k] = v
end
end
return ot
end
function Util.size(list)
if type(list) == 'table' then
local length = 0
table.foreach(list, function() length = length + 1 end)
return length
end
return 0
end
function Util.each(list, func)
for index, value in pairs(list) do
func(value, index, list)
end
end
-- http://stackoverflow.com/questions/15706270/sort-a-table-in-lua
function Util.spairs(t, order)
local keys = Util.keys(t)
-- if order function given, sort by it by passing the table and keys a, b,
-- otherwise just sort the keys
if order then
table.sort(keys, function(a,b) return order(t[a], t[b]) end)
else
table.sort(keys)
end
-- return the iterator function
local i = 0
return function()
i = i + 1
if keys[i] then
return keys[i], t[keys[i]]
end
end
end
function Util.first(t, order)
local keys = Util.keys(t)
if order then
table.sort(keys, function(a,b) return order(t[a], t[b]) end)
else
table.sort(keys)
end
return keys[1], t[keys[1]]
end
--[[ File functions ]]--
function Util.readFile(fname)
local f = fs.open(fname, "r")
if f then
local t = f.readAll()
f.close()
return t
end
end
function Util.writeFile(fname, data)
local file = io.open(fname, "w")
if not file then
error('Unable to open ' .. fname, 2)
end
file:write(data)
file:close()
end
function Util.readLines(fname)
local file = fs.open(fname, "r")
if file then
local t = {}
local line = file.readLine()
while line do
table.insert(t, line)
line = file.readLine()
end
file.close()
return t
end
end
function Util.writeLines(fname, lines)
local file = fs.open(fname, 'w')
if file then
for _,line in ipairs(lines) do
line = file.writeLine(line)
end
file.close()
return true
end
end
function Util.readTable(fname)
local t = Util.readFile(fname)
if t then
return textutils.unserialize(t)
end
end
function Util.writeTable(fname, data)
Util.writeFile(fname, textutils.serialize(data))
end
function Util.loadTable(fname)
local fc = Util.readFile(fname)
if not fc then
return false, 'Unable to read file'
end
local s, m = loadstring('return ' .. fc, fname)
if s then
s, m = pcall(s)
if s then
return m
end
end
return s, m
end
--[[ URL functions ]] --
function Util.download(url, filename)
local h = http.get(url)
if not h then
error('Failed to download ' .. url)
end
local contents = h.readAll()
h.close()
if not contents then
error('Failed to download ' .. url)
end
if filename then
Util.writeFile(filename, contents)
end
return contents
end
function Util.loadUrl(url, env) -- loadfile equivalent
local c = Util.download(url)
return load(c, url, nil, env)
end
function Util.runUrl(env, url, ...) -- os.run equivalent
local fn, m = Util.loadUrl(url, env)
if fn then
local args = { ... }
fn, m = pcall(function() fn(unpack(args)) end)
end
if not fn and m and m ~= '' then
printError(m)
end
return fn, m
end
--[[ String functions ]] --
function Util.toBytes(n)
if n >= 1000000 then
return string.format('%sM', Util.round(n/1000000, 1))
elseif n >= 1000 then
return string.format('%sK', Util.round(n/1000, 1))
end
return tostring(n)
end
function Util.split(str, pattern)
pattern = pattern or "(.-)\n"
local t = {}
local function helper(line) table.insert(t, line) return "" end
helper((str:gsub(pattern, helper)))
return t
end
function Util.matches(str, pattern)
pattern = pattern or '%S+'
local t = { }
for s in str:gmatch(pattern) do
table.insert(t, s)
end
return t
end
function Util.widthify(s, len)
s = s or ''
local slen = #s
if slen < len then
s = s .. string.rep(' ', len - #s)
elseif slen > len then
s = s:sub(1, len)
end
return s
end
-- http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76
function Util.trim(s)
return s:find'^%s*$' and '' or s:match'^%s*(.*%S)'
end
-- trim whitespace from left end of string
function Util.triml(s)
return s:match'^%s*(.*)'
end
-- trim whitespace from right end of string
function Util.trimr(s)
return s:find'^%s*$' and '' or s:match'^(.*%S)'
end
-- end http://snippets.luacode.org/?p=snippets/trim_whitespace_from_string_76
-- word wrapping based on:
-- https://www.rosettacode.org/wiki/Word_wrap#Lua and
-- http://lua-users.org/wiki/StringRecipes
local function splittokens(s)
local res = {}
for w in s:gmatch("%S+") do
res[#res+1] = w
end
return res
end
local function paragraphwrap(text, linewidth, res)
linewidth = linewidth or 75
local spaceleft = linewidth
local line = {}
for _, word in ipairs(splittokens(text)) do
if #word + 1 > spaceleft then
table.insert(res, table.concat(line, ' '))
line = { word }
spaceleft = linewidth - #word
else
table.insert(line, word)
spaceleft = spaceleft - (#word + 1)
end
end
table.insert(res, table.concat(line, ' '))
return table.concat(res, '\n')
end
-- end word wrapping
function Util.wordWrap(str, limit)
local longLines = Util.split(str)
local lines = { }
for _,line in ipairs(longLines) do
paragraphwrap(line, limit, lines)
end
return lines
end
-- http://lua-users.org/wiki/AlternativeGetOpt
local function getopt( arg, options )
local tab = {}
for k, v in ipairs(arg) do
if string.sub( v, 1, 2) == "--" then
local x = string.find( v, "=", 1, true )
if x then tab[ string.sub( v, 3, x-1 ) ] = string.sub( v, x+1 )
else tab[ string.sub( v, 3 ) ] = true
end
elseif string.sub( v, 1, 1 ) == "-" then
local y = 2
local l = string.len(v)
local jopt
while ( y <= l ) do
jopt = string.sub( v, y, y )
if string.find( options, jopt, 1, true ) then
if y < l then
tab[ jopt ] = string.sub( v, y+1 )
y = l
else
tab[ jopt ] = arg[ k + 1 ]
end
else
tab[ jopt ] = true
end
y = y + 1
end
end
end
return tab
end
function Util.showOptions(options)
print('Arguments: ')
for k, v in pairs(options) do
print(string.format('-%s %s', v.arg, v.desc))
end
end
function Util.getOptions(options, args, ignoreInvalid)
local argLetters = ''
for _,o in pairs(options) do
if o.type ~= 'flag' then
argLetters = argLetters .. o.arg
end
end
local rawOptions = getopt(args, argLetters)
local argCount = 0
for k,ro in pairs(rawOptions) do
local found = false
for _,o in pairs(options) do
if o.arg == k then
found = true
if o.type == 'number' then
o.value = tonumber(ro)
elseif o.type == 'help' then
Util.showOptions(options)
return false
else
o.value = ro
end
end
end
if not found and not ignoreInvalid then
print('Invalid argument')
Util.showOptions(options)
return false
end
end
return true, Util.size(rawOptions)
end
return Util

3
sys/boot/default.boot Normal file
View File

@ -0,0 +1,3 @@
term.clear()
term.setCursorPos(1, 1)
print(os.version())

33
sys/boot/multishell.boot Normal file
View File

@ -0,0 +1,33 @@
print('\nStarting multishell..')
LUA_PATH = '/sys/apis'
math.randomseed(os.clock())
_G.debug = function() end
_G.Util = dofile('/sys/apis/util.lua')
_G.requireInjector = dofile('/sys/apis/injector.lua')
os.run(Util.shallowCopy(getfenv(1)), '/sys/extensions/device.lua')
-- vfs
local s, m = os.run(Util.shallowCopy(getfenv(1)), '/sys/extensions/vfs.lua')
if not s then
error(m)
end
-- process fstab
local mounts = Util.readFile('config/fstab')
if mounts then
for _,l in ipairs(Util.split(mounts)) do
if l:sub(1, 1) ~= '#' then
fs.mount(unpack(Util.matches(l)))
end
end
end
local env = Util.shallowCopy(getfenv(1))
env.multishell = { }
local _, m = os.run(env, '/apps/shell', '/apps/multishell')
printError(m or 'Multishell aborted')

37
sys/boot/tlco.boot Normal file
View File

@ -0,0 +1,37 @@
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
error('die')
end
os.queueEvent('modem_message')

View File

@ -0,0 +1,8 @@
_G.device = { }
require = requireInjector(getfenv(1))
local Peripheral = require('peripheral')
for _,side in pairs(peripheral.getNames()) do
Peripheral.addDevice(side)
end

134
sys/extensions/os.lua Normal file
View File

@ -0,0 +1,134 @@
require = requireInjector(getfenv(1))
local Config = require('config')
local config = {
enable = false,
pocketId = 10,
distance = 8,
}
Config.load('lock', config)
local lockId
function lockScreen()
require = requireInjector(getfenv(1))
local UI = require('ui')
local Event = require('event')
local SHA1 = require('sha1')
local center = math.floor(UI.term.width / 2)
local page = UI.Page({
backgroundColor = colors.blue,
prompt = UI.Text({
x = center - 9,
y = math.floor(UI.term.height / 2),
value = 'Password',
}),
password = UI.TextEntry({
x = center,
y = math.floor(UI.term.height / 2),
width = 8,
limit = 8
}),
statusBar = UI.StatusBar(),
accelerators = {
q = 'back',
},
})
function page:eventHandler(event)
if event.type == 'key' and event.key == 'enter' then
if SHA1.sha1(self.password.value) == config.password then
os.locked = false
Event.exitPullEvents()
lockId = false
return true
else
self.statusBar:timedStatus('Invalid Password', 3)
end
end
UI.Page.eventHandler(self, event)
end
UI:setPage(page)
Event.pullEvents()
end
os.lock = function()
--os.locked = true
if not lockId then
lockId = multishell.openTab({
title = 'Lock',
env = getfenv(1),
fn = lockScreen,
focused = true,
})
end
end
os.unlock = function()
os.locked = false
if lockId then
multishell.terminate(lockId)
lockId = nil
end
end
function os.isTurtle()
return not not turtle
end
function os.isAdvanced()
return term.native().isColor()
end
function os.isPocket()
return not not pocket
end
function os.registerApp(entry)
local apps = { }
Config.load('apps', apps)
local run = fs.combine(entry.run, '')
for k,app in pairs(apps) do
if app.run == run then
table.remove(apps, k)
break
end
end
table.insert(apps, {
run = run,
title = entry.title,
args = entry.args,
category = entry.category,
icon = entry.icon,
})
Config.update('apps', apps)
os.queueEvent('os_register_app')
end
function os.unregisterApp(run)
local apps = { }
Config.load('apps', apps)
local run = fs.combine(run, '')
for k,app in pairs(apps) do
if app.run == run then
table.remove(apps, k)
Config.update('apps', apps)
os.queueEvent('os_register_app')
break
end
end
end

223
sys/extensions/pathfind.lua Normal file
View File

@ -0,0 +1,223 @@
if not turtle then
return
end
require = requireInjector(getfenv(1))
local Grid = require ("jumper.grid")
local Pathfinder = require ("jumper.pathfinder")
local Point = require('point')
local WALKABLE = 0
local function createMap(dim)
local map = { }
for z = 0, dim.ez do
local row = {}
for x = 0, dim.ex do
local col = { }
for y = 0, dim.ey do
table.insert(col, WALKABLE)
end
table.insert(row, col)
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 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
end
adjust(dest)
for _,b in ipairs(blocks) do
adjust(b)
end
-- expand one block out in all directions
sx = math.max(sx - 1, boundingBox.sx)
sz = math.max(sz - 1, boundingBox.sz)
sy = math.max(sy - 1, boundingBox.sy)
ex = math.min(ex + 1, boundingBox.ex)
ez = math.min(ez + 1, boundingBox.ez)
ey = math.min(ey + 1, boundingBox.ey)
return {
ex = ex - sx + 1,
ez = ez - sz + 1,
ey = ey - sy + 1,
ox = -sx + 1,
oz = -sz + 1,
oy = -sy + 1
}
end
local function nodeToString(n)
return string.format('%d:%d:%d:%d', n._x, n._y, n._z, n.__heading or 9)
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 }
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
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
end
-- turtle sensor returns blocks in relation to the world - not turtle orientation
-- so cannot figure out block location unless we know our orientation in the world
-- really kinda dumb since it returns the coordinates as offsets of our location
-- instead of true coordinates
local function addSensorBlocks(blocks, sblocks)
for _,b in pairs(sblocks) do
if b.type ~= 'AIR' then
local pt = { x = turtle.point.x, y = turtle.point.y + b.y, z = turtle.point.z }
pt.x = pt.x - b.x
pt.z = pt.z - b.z -- this will only work if we were originally facing west
local found = false
for _,ob in pairs(blocks) do
if pt.x == ob.x and pt.y == ob.y and pt.z == ob.z then
found = true
break
end
end
if not found then
table.insert(blocks, pt)
end
end
end
end
local function pathTo(dest, blocks, maxRadius)
blocks = blocks or { }
maxRadius = maxRadius or 1000000
local lastDim = nil
local map = nil
local grid = nil
local boundingBox = {
sx = math.min(turtle.point.x, dest.x) - maxRadius,
sy = math.min(turtle.point.y, dest.y) - maxRadius,
sz = math.min(turtle.point.z, dest.z) - maxRadius,
ex = math.max(turtle.point.x, dest.x) + maxRadius,
ey = math.max(turtle.point.y, dest.y) + maxRadius,
ez = math.max(turtle.point.z, dest.z) + maxRadius,
}
-- Creates a pathfinder object
local myFinder = Pathfinder(grid, 'ASTAR', walkable)
myFinder:setMode('ORTHOGONAL')
myFinder:setHeuristic(heuristic)
while turtle.point.x ~= dest.x or turtle.point.z ~= dest.z or turtle.point.y ~= dest.y do
-- map expands as we encounter obstacles
local dim = mapDimensions(dest, blocks, boundingBox)
-- 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)
lastDim = dim
end
for _,b in ipairs(blocks) do
addBlock(map, dim, b)
end
-- Define start and goal locations coordinates
local startPt = pointToMap(dim, turtle.point)
local endPt = pointToMap(dim, dest)
-- 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)
if not path then
return false, 'failed to recalculate'
end
for node, count in path:nodes() do
local pt = nodeToPoint(dim, node)
if turtle.abort then
return false, 'aborted'
end
-- use single turn method so the turtle doesn't turn around when encountering obstacles
if not turtle.gotoSingleTurn(pt.x, pt.z, pt.y) then
table.insert(blocks, pt)
--if device.turtlesensorenvironment then
-- addSensorBlocks(blocks, device.turtlesensorenvironment.sonicScan())
--end
break
end
end
end
if dest.heading then
turtle.setHeading(dest.heading)
end
return true
end
turtle.pathfind = function(dest, blocks, maxRadius)
if not blocks and turtle.gotoPoint(dest) then
return true
end
return pathTo(dest, blocks, maxRadius)
end

View File

@ -0,0 +1,78 @@
if not turtle then
return
end
local Scheduler = {
uid = 0,
queue = { },
idle = true,
}
function turtle.abortAction()
if turtle.status ~= 'idle' then
turtle.abort = true
os.queueEvent('turtle_abort')
end
Util.clear(Scheduler.queue)
os.queueEvent('turtle_ticket', 0, true)
end
local function getTicket(fn, ...)
Scheduler.uid = Scheduler.uid + 1
if Scheduler.idle then
Scheduler.idle = false
turtle.status = 'busy'
os.queueEvent('turtle_ticket', Scheduler.uid)
else
table.insert(Scheduler.queue, Scheduler.uid)
end
return Scheduler.uid
end
local function releaseTicket(id)
for k,v in ipairs(Scheduler.queue) do
if v == id then
table.remove(Scheduler.queue, k)
return
end
end
local id = table.remove(Scheduler.queue, 1)
if id then
os.queueEvent('turtle_ticket', id)
else
Scheduler.idle = true
turtle.status = 'idle'
end
end
function turtle.run(fn, ...)
local ticketId = getTicket()
if type(fn) == 'string' then
fn = turtle[fn]
end
while true do
local e, id, abort = os.pullEventRaw('turtle_ticket')
if e == 'terminate' then
releaseTicket(ticketId)
error('Terminated')
end
if abort then
-- the function was queued, but the queue was cleared
return false, 'aborted'
end
if id == ticketId then
turtle.abort = false
local args = { ... }
local s, m = pcall(function() fn(unpack(args)) end)
turtle.abort = false
releaseTicket(ticketId)
if not s and m then
printError(m)
end
return s, m
end
end
end

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

@ -0,0 +1,40 @@
if not turtle then
return
end
require = requireInjector(getfenv(1))
local GPS = require('gps')
function turtle.enableGPS()
local pt = GPS.getPointAndHeading()
if pt then
turtle.setPoint(pt)
return true
end
end
function turtle.gotoGPSHome()
local homePt = turtle.loadLocation('gpsHome')
if homePt then
local pt = GPS.getPointAndHeading()
if pt then
turtle.setPoint(pt)
turtle.pathfind(homePt)
end
end
end
function turtle.setGPSHome()
local GPS = require('gps')
local pt = GPS.getPoint()
if pt then
turtle.setPoint(pt)
pt.heading = GPS.getHeading()
if pt.heading then
turtle.point.heading = pt.heading
turtle.storeLocation('gpsHome', pt)
turtle.gotoPoint(pt)
end
end
end

Some files were not shown because too many files have changed in this diff Show More