mirror of
https://github.com/kepler155c/opus
synced 2025-02-02 18:39:09 +00:00
Initial commit
This commit is contained in:
commit
fc243a9c12
386
apps/Appstore.lua
Normal file
386
apps/Appstore.lua
Normal 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
118
apps/Events.lua
Normal 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
375
apps/Files.lua
Normal 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
84
apps/Help.lua
Normal 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
247
apps/Lua.lua
Normal 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
153
apps/Network.lua
Normal 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
445
apps/Overview.lua
Normal 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
213
apps/Peripherals.lua
Normal 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
105
apps/Pim.lua
Normal 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
561
apps/Script.lua
Normal 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
190
apps/System.lua
Normal 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
78
apps/Tabs.lua
Normal 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
2064
apps/builder.lua
Normal file
File diff suppressed because it is too large
Load Diff
24
apps/cat.lua
Normal file
24
apps/cat.lua
Normal 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
1184
apps/edit.lua
Normal file
File diff suppressed because it is too large
Load Diff
102
apps/logMonitor.lua
Normal file
102
apps/logMonitor.lua
Normal 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
84
apps/mirror.lua
Normal 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
45
apps/mirrorHost.lua
Normal 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
600
apps/multishell
Normal 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
343
apps/pickup.lua
Normal 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
229
apps/pickupRemote.lua
Normal 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
529
apps/recorder.lua
Normal 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
1
apps/scripts/abort
Normal file
@ -0,0 +1 @@
|
||||
turtle.abortAction()
|
120
apps/scripts/follow
Normal file
120
apps/scripts/follow
Normal 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
1
apps/scripts/goHome
Normal file
@ -0,0 +1 @@
|
||||
turtle.run(turtle.gotoGPSHome)
|
29
apps/scripts/moveTo
Normal file
29
apps/scripts/moveTo
Normal 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
1
apps/scripts/reboot
Normal file
@ -0,0 +1 @@
|
||||
os.reboot()
|
1
apps/scripts/setHome
Normal file
1
apps/scripts/setHome
Normal file
@ -0,0 +1 @@
|
||||
turtle.run(turtle.setGPSHome)
|
1
apps/scripts/shutdown
Normal file
1
apps/scripts/shutdown
Normal file
@ -0,0 +1 @@
|
||||
os.shutdown()
|
72
apps/scripts/summon
Normal file
72
apps/scripts/summon
Normal 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
1
apps/scripts/update
Normal file
@ -0,0 +1 @@
|
||||
shell.run('/apps/update.lua')
|
623
apps/shell
Normal file
623
apps/shell
Normal 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
570
apps/simpleMiner.lua
Normal 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
194
apps/storageActivity.lua
Normal 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
917
apps/storageManager.lua
Normal 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
392
apps/supplier.lua
Normal 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
98
apps/t.lua
Normal 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
82
apps/telnet.lua
Normal 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
52
apps/trace.lua
Normal 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
6
apps/update.lua
Normal 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
88
apps/vnc.lua
Normal 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
51
autorun/gps.lua
Normal 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
45
startup
Normal 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
893
sys/apis/blocks.lua
Normal 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
103
sys/apis/chestProvider.lua
Normal 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
46
sys/apis/class.lua
Normal 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
24
sys/apis/config.lua
Normal 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
870
sys/apis/deflatelua.lua
Normal 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
177
sys/apis/event.lua
Normal 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
142
sys/apis/fileui.lua
Normal 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
16
sys/apis/fs/gitfs.lua
Normal 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
61
sys/apis/fs/linkfs.lua
Normal 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
161
sys/apis/fs/netfs.lua
Normal 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
153
sys/apis/fs/ramfs.lua
Normal 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
78
sys/apis/fs/urlfs.lua
Normal 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
40
sys/apis/git.lua
Normal 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
196
sys/apis/glasses.lua
Normal 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
152
sys/apis/gps.lua
Normal 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
47
sys/apis/history.lua
Normal 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
69
sys/apis/injector.lua
Normal 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
112
sys/apis/input.lua
Normal 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
213
sys/apis/json.lua
Normal 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
|
105
sys/apis/jumper/core/assert.lua
Normal file
105
sys/apis/jumper/core/assert.lua
Normal 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
|
175
sys/apis/jumper/core/bheap.lua
Normal file
175
sys/apis/jumper/core/bheap.lua
Normal 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
|
98
sys/apis/jumper/core/heuristics.lua
Normal file
98
sys/apis/jumper/core/heuristics.lua
Normal 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
|
32
sys/apis/jumper/core/lookuptable.lua
Normal file
32
sys/apis/jumper/core/lookuptable.lua
Normal 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
|
100
sys/apis/jumper/core/node.lua
Normal file
100
sys/apis/jumper/core/node.lua
Normal 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
|
201
sys/apis/jumper/core/path.lua
Normal file
201
sys/apis/jumper/core/path.lua
Normal 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
|
168
sys/apis/jumper/core/utils.lua
Normal file
168
sys/apis/jumper/core/utils.lua
Normal 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
429
sys/apis/jumper/grid.lua
Normal 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
|
412
sys/apis/jumper/pathfinder.lua
Normal file
412
sys/apis/jumper/pathfinder.lua
Normal 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
|
88
sys/apis/jumper/search/astar.lua
Normal file
88
sys/apis/jumper/search/astar.lua
Normal 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
|
46
sys/apis/jumper/search/bfs.lua
Normal file
46
sys/apis/jumper/search/bfs.lua
Normal 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
133
sys/apis/logger.lua
Normal 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
165
sys/apis/me.lua
Normal 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
137
sys/apis/meProvider.lua
Normal 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
106
sys/apis/message.lua
Normal 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
76
sys/apis/nft.lua
Normal 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
95
sys/apis/peripheral.lua
Normal 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
147
sys/apis/point.lua
Normal 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
111
sys/apis/process.lua
Normal 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
50
sys/apis/profile.lua
Normal 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
58
sys/apis/require.lua
Normal 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
1178
sys/apis/schematic.lua
Normal file
File diff suppressed because it is too large
Load Diff
297
sys/apis/sha1.lua
Normal file
297
sys/apis/sha1.lua
Normal 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
211
sys/apis/socket.lua
Normal 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
24
sys/apis/sync.lua
Normal 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
53
sys/apis/tableDB.lua
Normal 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
145
sys/apis/terminal.lua
Normal 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
2801
sys/apis/ui.lua
Normal file
File diff suppressed because it is too large
Load Diff
530
sys/apis/util.lua
Normal file
530
sys/apis/util.lua
Normal 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
3
sys/boot/default.boot
Normal file
@ -0,0 +1,3 @@
|
||||
term.clear()
|
||||
term.setCursorPos(1, 1)
|
||||
print(os.version())
|
33
sys/boot/multishell.boot
Normal file
33
sys/boot/multishell.boot
Normal 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
37
sys/boot/tlco.boot
Normal 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')
|
8
sys/extensions/device.lua
Normal file
8
sys/extensions/device.lua
Normal 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
134
sys/extensions/os.lua
Normal 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
223
sys/extensions/pathfind.lua
Normal 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
|
78
sys/extensions/scheduler.lua
Normal file
78
sys/extensions/scheduler.lua
Normal 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
40
sys/extensions/tgps.lua
Normal 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
Loading…
Reference in New Issue
Block a user