Merge branch 'master' of cil.li:oc

Conflicts:
	li/cil/oc/Blocks.scala
This commit is contained in:
Johannes Lohrer 2013-10-06 22:51:56 +02:00
commit 4489781e34
26 changed files with 1362 additions and 694 deletions

View File

@ -64,6 +64,7 @@ flattenAndStore("_ENV", _ENV)
--]]
-- OK, I admit this is a little crazy... here goes:
local function wrap(f)
assert(f)
-- This is the function that replaces the original API function. It is
-- called from userland when it wants something from a driver.
return function(...)
@ -94,5 +95,5 @@ local function wrap(f)
end
end
sendToNode = wrap(sendToNode)
sendToAddress = wrap(sendToAddress)
nodeName = wrap(nodeName)

View File

@ -0,0 +1,752 @@
local mtab = {children={}}
local function segments(path)
path = path:gsub("\\", "/")
repeat local n; path, n = path:gsub("//", "/") until n == 0
local parts = {}
for part in path:gmatch("[^/]+") do
table.insert(parts, part)
end
local i = 1
while i <= #parts do
if parts[i] == "." then
table.remove(parts, i)
elseif parts[i] == ".." then
table.remove(parts, i)
i = i - 1
if i > 0 then
table.remove(parts, i)
else
i = 1
end
else
i = i + i
end
end
return parts
end
local function findNode(path, create)
checkArg(1, path, "string")
local parts = segments(path)
local node = mtab
for i = 1, #parts do
if not node.children[parts[i]] then
if create then
node.children[parts[i]] = {children={}, parent=node}
else
return node, table.concat(parts, "/", i)
end
end
node = node.children[parts[i]]
end
return node
end
local function removeEmptyNodes(node)
while node and node.parent and not node.fs and not next(node.children) do
for k, c in pairs(node.parent.children) do
if c == node then
node.parent.children[k] = nil
break
end
end
node = node.parent
end
end
-------------------------------------------------------------------------------
driver.fs = {}
function driver.fs.mount(fs, path)
if fs and path then
checkArg(1, fs, "string")
local node = findNode(path, true)
if node.fs then
return nil, "another filesystem is already mounted here"
end
node.fs = fs
else
local function path(node)
local result = "/"
while node and node.parent do
for name, child in pairs(node.parent.children) do
if child == node then
result = "/" .. name .. result
break
end
end
node = node.parent
end
return result
end
local queue = {mtab}
return function()
if #queue == 0 then
return nil
else
while true do
local node = table.remove(queue)
for _, child in pairs(node.children) do
table.insert(queue, child)
end
if node.fs then
return node.fs, path(node)
end
end
end
end
end
end
function driver.fs.umount(fsOrPath)
local node, rest = findNode(fsOrPath)
if not rest and node.fs then
node.fs = nil
removeEmptyNodes(node)
return true
else
local queue = {mtab}
for fs, path in driver.fs.mount() do
if fs == fsOrPath then
local node = findNode(path)
node.fs = nil
removeEmptyNodes(node)
return true
end
end
end
end
-------------------------------------------------------------------------------
function driver.fs.spaceTotal(path)
local node, rest = findNode(path)
if node.fs then
return send(node.fs, "fs.spaceTotal")
else
return nil, "no such device"
end
end
function driver.fs.spaceUsed(path)
local node, rest = findNode(path)
if node.fs then
return send(node.fs, "fs.spaceUsed")
else
return nil, "no such device"
end
end
-------------------------------------------------------------------------------
function driver.fs.exists(path)
local node, rest = findNode(path)
if not rest then -- virtual directory
return true
end
if node.fs then
return send(node.fs, "fs.exists", rest)
end
end
function driver.fs.size(path)
local node, rest = findNode(path)
if node.fs and rest then
return send(node.fs, "fs.size", rest)
end
return 0 -- no such file or directory or it's a virtual directory
end
function driver.fs.dir(path)
local node, rest = findNode(path)
if not node.fs and rest then
return nil, "no such file or directory"
end
local result
if node.fs then
result = table.pack(send(node.fs, "fs.list", rest or ""))
if not result[1] then
return nil, result[2]
end
else
result = {}
end
if not rest then
for k, _ in pairs(node.children) do
table.insert(result, k .. "/")
end
end
table.sort(result)
return table.unpack(result)
end
-------------------------------------------------------------------------------
function driver.fs.remove(path)
local node, rest = findNode(path)
if node.fs and rest then
return send(node.fs, "fs.remove", rest)
end
end
function driver.fs.rename(oldPath, newPath)
local oldNode, oldRest = findNode(oldPath)
local newNode, newRest = findNode(newPath)
if oldNode.fs and oldRest and newNode.fs and newRest then
if oldNode.fs == newNode.fs then
return send(oldNode.fs, "fs.rename", oldRest, newRest)
else
local result, reason = driver.fs.copy(oldPath, newPath)
if result then
return driver.fs.remove(oldPath)
else
return nil, reason
end
end
end
end
function driver.fs.copy(fromPath, toPath)
--[[ TODO ]]
return nil, "not implemented"
end
-------------------------------------------------------------------------------
local file = {}
function file:close()
if self.handle then
self:flush()
return self.stream:close()
end
end
function file:flush()
if not self.handle then
return nil, "file is closed"
end
if #self.buffer > 0 then
local result, reason = self.stream:write(self.buffer)
if result then
self.buffer = ""
else
if reason then
return nil, reason
else
return nil, "bad file descriptor"
end
end
end
return self
end
function file:lines(...)
local args = table.pack(...)
return function()
local result = table.pack(self:read(table.unpack(args, 1, args.n)))
if not result[1] and result[2] then
error(result[2])
end
return table.unpack(result, 1, result.n)
end
end
function file:read(...)
if not self.handle then
return nil, "file is closed"
end
local function readChunk()
local result, reason = self.stream:read(self.bufferSize)
if result then
self.buffer = self.buffer .. result
return self
else -- error or eof
return nil, reason
end
end
local function readBytesOrChars(n)
local len, sub
if self.mode == "r" then
len = string.len
sub = string.sub
else
assert(self.mode == "rb")
len = rawlen
sub = string.bsub
end
local result = ""
repeat
if len(self.buffer) == 0 then
local result, reason = readChunk()
if not result then
if reason then
return nil, reason
else -- eof
return nil
end
end
end
local left = n - len(result)
result = result .. sub(self.buffer, 1, left)
self.buffer = sub(self.buffer, left + 1)
until len(result) == n
return result
end
local function readLine(chop)
local start = 1
while true do
local l = self.buffer:find("\n", start, true)
if l then
local result = self.buffer:bsub(1, l + (chop and -1 or 0))
self.buffer = self.buffer:bsub(l + 1)
return result
else
start = #self.buffer
local result, reason = readChunk()
if not result then
if reason then
return nil, reason
else -- eof
local result = #self.buffer > 0 and self.buffer or nil
self.buffer = ""
return result
end
end
end
end
end
local function readAll()
repeat
local result, reason = readChunk()
if not result and reason then
return nil, reason
end
until not result -- eof
local result = self.buffer
self.buffer = ""
return result
end
local function read(n, format)
if type(format) == "number" then
return readBytesOrChars(format)
else
if not type(format) == "string" or format:sub(1, 1) ~= "*" then
error("bad argument #" .. n .. " (invalid option)")
end
format = format:sub(2, 2)
if format == "n" then
--[[ TODO ]]
error("not implemented")
elseif format == "l" then
return readLine(true)
elseif format == "L" then
return readLine(false)
elseif format == "a" then
return readAll()
else
error("bad argument #" .. n .. " (invalid format)")
end
end
end
local result = {}
local formats = table.pack(...)
if formats.n == 0 then
return readLine(true)
end
for i = 1, formats.n do
table.insert(result, read(i, formats[i]))
end
return table.unpack(result)
end
function file:seek(whence, offset)
if not self.handle then
return nil, "file is closed"
end
whence = tostring(whence or "cur")
assert(whence == "set" or whence == "cur" or whence == "end",
"bad argument #1 (set, cur or end expected, got " .. whence .. ")")
offset = offset or 0
checkArg(2, offset, "number")
assert(math.floor(offset) == offset, "bad argument #2 (not an integer)")
if whence == "cur" and offset ~= 0 then
offset = offset - #(self.buffer or "")
end
local result, reason = self.stream:seek(whence, offset)
if result then
if offset ~= 0 then
self.buffer = ""
elseif whence == "cur" then
result = result - #self.buffer
end
end
return result, reason
end
function file:setvbuf(mode, size)
if not self.handle then
return nil, "file is closed"
end
mode = mode or self.bufferMode
size = size or self.bufferSize
assert(mode == "no" or mode == "full" or mode == "line",
"bad argument #1 (no, full or line expected, got " .. tostring(mode) .. ")")
assert(mode == "no" or type(size) == "number",
"bad argument #2 (number expected, got " .. type(size) .. ")")
self:flush()
self.bufferMode = mode
self.bufferSize = mode == "no" and 0 or size
return self.bufferMode, self.bufferSize
end
function file:write(...)
if not self.handle then
return nil, "file is closed"
end
local args = table.pack(...)
for i = 1, args.n do
if type(args[i]) == "number" then
args[i] = tostring(args[i])
end
checkArg(i, args[i], "string")
end
for i = 1, args.n do
local arg = args[i]
local result, reason
if (self.bufferMode == "full" or self.bufferMode == "line") and
self.bufferSize - #self.buffer < #arg
then
result, reason = self:flush()
if not result then
return nil, reason
end
end
if self.bufferMode == "full" then
if #arg > self.bufferSize then
result, reason = self.stream:write(arg)
else
self.buffer = self.buffer .. arg
result = self
end
elseif self.bufferMode == "line" then
local l
repeat
local idx = self.buffer:find("\n", l or 1, true)
if idx then
l = idx
end
until not idx
if l then
result, reason = self:flush()
if not result then
return nil, reason
end
result, reason = self.stream:write(arg:bsub(1, l))
if not result then
return nil, reason
end
arg = arg:bsub(l + 1)
end
if #arg > self.bufferSize then
result, reason = self.stream:write(arg)
else
self.buffer = arg
result = self
end
else -- no
result, reason = self.stream:write(arg)
end
if not result then
return nil, reason
end
end
return self
end
-------------------------------------------------------------------------------
function file.new(fs, handle, mode, stream, nogc)
local result = {
fs = fs,
handle = handle,
mode = mode,
buffer = "",
bufferSize = math.min(8 * 1024, os.totalMemory() / 16),
bufferMode = "full"
}
result.stream = setmetatable(stream, {__index = {file = result}})
local metatable = {
__index = file,
__metatable = "file"
}
if not nogc then
metatable.__gc = function(self)
-- file.close does a syscall, which yields, and that's not possible in
-- the __gc metamethod. So we start a timer to do the yield/cleanup.
event.timer(0, function()
self:close()
end)
end
end
return setmetatable(result, metatable)
end
-------------------------------------------------------------------------------
local fileStream = {}
function fileStream:close()
send(self.file.fs, "fs.close", self.file.handle)
self.file.handle = nil
end
function fileStream:read(n)
return send(self.file.fs, "fs.read", self.file.handle, n)
end
function fileStream:seek(whence, offset)
return send(self.file.fs, "fs.seek", self.file.handle, whence, offset)
end
function fileStream:write(str)
return send(self.file.fs, "fs.write", self.file.handle, str)
end
-------------------------------------------------------------------------------
function driver.fs.open(path, mode)
mode = tostring(mode or "r")
checkArg(2, mode, "string")
assert(({r=true, rb=true, w=true, wb=true, a=true, ab=true})[mode],
"bad argument #2 (r[b], w[b] or a[b] expected, got " .. mode .. ")")
local node, rest = findNode(path)
if not node.fs or not rest then
return nil, "file not found"
end
local handle, reason = send(node.fs, "fs.open", rest, mode)
if not handle then
return nil, reason
end
return file.new(node.fs, handle, mode, fileStream)
end
function driver.fs.type(object)
if type(object) == "table" then
if getmetatable(object) == "file" then
if object.handle then
return "file"
else
return "closed file"
end
end
end
return nil
end
-------------------------------------------------------------------------------
function loadfile(filename, env)
local file, reason = driver.fs.open(filename)
if not file then
return nil, reason
end
local source, reason = file:read("*a")
file:close()
if not source then
return nil, reason
end
return load(source, "=" .. filename, env)
end
function dofile(filename)
local program, reason = loadfile(filename)
if not program then
return error(reason)
end
return program()
end
-------------------------------------------------------------------------------
io = {}
-------------------------------------------------------------------------------
local stdinStream = {}
function stdinStream:close()
return nil, "cannot close standard file"
end
function stdinStream:read(n)
end
function stdinStream:seek(whence, offset)
return nil, "bad file descriptor"
end
function stdinStream:write(str)
return nil, "bad file descriptor"
end
local stdoutStream = {}
function stdoutStream:close()
return nil, "cannot close standard file"
end
function stdoutStream:read(n)
return nil, "bad file descriptor"
end
function stdoutStream:seek(whence, offset)
return nil, "bad file descriptor"
end
function stdoutStream:write(str)
term.write(str)
return self
end
io.stdin = file.new(nil, "stdin", "r", stdinStream, true)
io.stdout = file.new(nil, "stdout", "w", stdoutStream, true)
io.stderr = io.stdout
io.stdout:setvbuf("no")
-------------------------------------------------------------------------------
local input, output = io.stdin, io.stdout
-------------------------------------------------------------------------------
function io.close(file)
return (file or io.output()):close()
end
function io.flush()
return io.output():flush()
end
function io.input(file)
if file then
if type(file) == "string" then
local result, reason = io.open(file)
if not result then
error(reason)
end
input = result
elseif io.type(file) then
input = file
else
error("bad argument #1 (string or file expected, got " .. type(file) .. ")")
end
end
return input
end
function io.lines(filename, ...)
if filename then
local result, reason = io.open(filename)
if not result then
error(reason)
end
local args = table.pack(...)
return function()
local result = table.pack(file:read(table.unpack(args, 1, args.n)))
if not result[1] then
if result[2] then
error(result[2])
else -- eof
file:close()
return nil
end
end
return table.unpack(result, 1, result.n)
end
else
return io.input():lines()
end
end
io.open = driver.fs.open
function io.output(file)
if file then
if type(file) == "string" then
local result, reason = io.open(file, "w")
if not result then
error(reason)
end
output = result
elseif io.type(file) then
output = file
else
error("bad argument #1 (string or file expected, got " .. type(file) .. ")")
end
end
return output
end
-- TODO io.popen = function(prog, mode) end
function io.read(...)
return io.input():read(...)
end
-- TODO io.tmpfile = function() end
io.type = driver.fs.type
function io.write(...)
return io.output():write(...)
end
function print(...)
local args = table.pack(...)
for i = 1, args.n do
local arg = tostring(args[i])
if i > 1 then
arg = "\t" .. arg
end
io.stdout:write(arg)
end
io.stdout:write("\n")
end
-------------------------------------------------------------------------------
os.remove = driver.fs.remove
os.rename = driver.fs.rename
-- TODO os.tmpname = function() end

View File

@ -1,446 +0,0 @@
local mtab = {children={}}
local function segments(path)
path = path:gsub("\\", "/")
repeat local n; path, n = path:gsub("//", "/") until n == 0
local parts = {}
for part in path:gmatch("[^/]+") do
table.insert(parts, part)
end
local i = 1
while i <= #parts do
if parts[i] == "." then
table.remove(parts, i)
elseif parts[i] == ".." then
table.remove(parts, i)
i = i - 1
if i > 0 then
table.remove(parts, i)
else
i = 1
end
else
i = i + i
end
end
return parts
end
local function findNode(path, create)
checkArg(1, path, "string")
local parts = segments(path)
local node = mtab
for i = 1, #parts do
if not node.children[parts[i]] then
if create then
node.children[parts[i]] = {children={}, parent=node}
else
return node, table.concat(parts, "/", i)
end
end
node = node.children[parts[i]]
end
return node
end
local function removeEmptyNodes(node)
while node and node.parent and not node.fs and not next(node.children) do
for k, c in pairs(node.parent.children) do
if c == node then
node.parent.children[k] = nil
break
end
end
node = node.parent
end
end
-------------------------------------------------------------------------------
driver.fs = {}
function driver.fs.mount(fs, path)
checkArg(1, fs, "string")
local node = findNode(path, true)
if node.fs then
return nil, "another filesystem is already mounted here"
end
node.fs = fs
end
function driver.fs.umount(fsOrPath)
local node, rest = findNode(fsOrPath)
if not rest and node.fs then
node.fs = nil
removeEmptyNodes(node)
return true
else
local queue = {mtab}
repeat
local node = table.remove(queue)
if node.fs == fsOrPath then
node.fs = nil
removeEmptyNodes(node)
return true
end
for _, child in ipairs(node.children) do
table.insert(queue, child)
end
until #queue == 0
end
end
-------------------------------------------------------------------------------
function driver.fs.exists(path)
local node, rest = findNode(path)
if not rest then -- virtual directory
return true
end
if node.fs then
return sendToNode(node.fs, "fs.exists", rest)
end
end
function driver.fs.size(path)
local node, rest = findNode(path)
if node.fs and rest then
return sendToNode(node.fs, "fs.size", rest)
end
return 0 -- no such file or directory or virtual directory
end
function driver.fs.listdir(path)
local node, rest = findNode(path)
if not node.fs and rest then
return nil, "no such file or directory"
end
local result
if node.fs then
result = {sendToNode(node.fs, "fs.list", rest or "")}
if not result[1] then
return nil, result[2]
end
else
result = {}
end
if not rest then
for k, _ in pairs(node.children) do
table.insert(result, k .. "/")
end
end
table.sort(result)
return table.unpack(result)
end
-------------------------------------------------------------------------------
function driver.fs.remove(path)
local node, rest = findNode(path)
if node.fs and rest then
return sendToNode(node.fs, "fs.remove", rest)
end
end
function driver.fs.rename(oldPath, newPath)
--[[ TODO moving between file systems will require actual data copying...
local node, rest = findNode(path)
local newNode, newRest = findNode(newPath)
if node.fs and rest and newNode and newRest then
return sendToNode(node.fs, "fs.rename", rest)
end
]]
end
-------------------------------------------------------------------------------
local file = {}
function file.close(f)
if f.handle then
f:flush()
sendToNode(f.fs, "fs.close", f.handle)
f.handle = nil
end
end
function file.flush(f)
if not f.handle then
return nil, "file is closed"
end
if #(f.buffer or "") > 0 then
local result, reason = sendToNode(f.fs, "fs.write", f.handle, f.buffer)
if result then
f.buffer = nil
else
if reason then
return nil, reason
else
return nil, "invalid file"
end
end
end
return f
end
function file.read(f, ...)
if not f.handle then
return nil, "file is closed"
end
local function readChunk()
local read, reason = sendToNode(f.fs, "fs.read", f.handle, f.bsize)
if read then
f.buffer = (f.buffer or "") .. read
return true
else
return nil, reason
end
end
local function readBytes(n)
while #(f.buffer or "") < n do
local result, reason = readChunk()
if not result then
if reason then
return nil, reason
end
break
end
end
local result
if f.buffer then
if #f.buffer > format then
result = f.buffer:bsub(1, format)
f.buffer = f.buffer:bsub(format + 1)
else
result = f.buffer
f.buffer = nil
end
end
return result
end
local function readLine(chop)
while true do
local l = (f.buffer or ""):find("\n", 1, true)
if l then
local rl = l + (chop and -1 or 0)
local line = f.buffer:bsub(1, rl)
f.buffer = f.buffer:bsub(l + 1)
return line
else
local result, reason = readChunk()
if not result then
if reason then
return nil, reason
else
local line = f.buffer
f.buffer = nil
return line
end
end
end
end
end
local function readAll()
repeat
local result, reason = readChunk()
if not result and reason then
return nil, reason
end
until not result
local result = f.buffer or ""
f.buffer = nil
return result
end
local function read(n, format)
if type(format) == "number" then
return readBytes(format)
else
if not type(format) == "string" or format:sub(1, 1) ~= "*" then
error("bad argument #" .. n .. " (invalid option)")
end
format = format:sub(2, 2)
if format == "n" then
error("not implemented")
elseif format == "l" then
return readLine(true)
elseif format == "L" then
return readLine(false)
elseif format == "a" then
return readAll()
else
error("bad argument #" .. n .. " (invalid format)")
end
end
end
local results = {}
local formats = {...}
if #formats == 0 then
return readLine(true)
end
for n, format in ipairs(formats) do
table.insert(results, read(n, format))
end
return table.unpack(results)
end
function file.seek(f, whence, offset)
if not f.handle then
return nil, "file is closed"
end
whence = whence or "cur"
assert(whence == "set" or whence == "cur" or whence == "end",
"bad argument #1 (set, cur or end expected, got " .. tostring(whence) .. ")")
offset = offset or 0
checkArg(2, offset, "number")
assert(math.floor(offset) == offset, "bad argument #2 (not an integer)")
if whence == "cur" and offset ~= 0 then
offset = offset - #(f.buffer or "")
end
local result, reason = sendToNode(f.fs, "fs.seek", f.handle, whence, offset)
if result then
if offset ~= 0 then
f.buffer = nil
elseif whence == "cur" then
result = result - #(f.buffer or "")
end
end
return result, reason
end
function file.setvbuf(f, mode, size)
if not f.handle then
return nil, "file is closed"
end
assert(mode == "no" or mode == "full" or mode == "line",
"bad argument #1 (no, full or line expected, got " .. tostring(mode) .. ")")
assert(mode == "no" or type(size) == "number",
"bad argument #2 (number expected, got " .. type(size) .. ")")
f:flush()
f.bmode = mode
f.bsize = size
end
function file.write(f, ...)
if not f.handle then
return nil, "file is closed"
end
local args = {...}
for n, arg in ipairs(args) do
if type(arg) == "number" then
args[n] = tostring(arg)
end
checkArg(n, arg, "string")
end
for _, arg in ipairs(args) do
local result, reason
if f.bmode == "full" or #(f.buffer or "") + #arg > f.bsize then
if #(f.buffer or "") + #arg > f.bsize then
result, reason = f:flush()
if result then
if #arg > f.bsize then
result, reason = sendToNode(f.fs, "fs.write", f.handle, arg)
else
f.buffer = arg
end
end
else
f.buffer = (f.buffer or "") .. arg
result = f
end
elseif f.bmode == "line" then
repeat
local l = (f.buffer or ""):find("\n", 1, true)
if l then
result, reason = f:flush()
if result then
result, reason = sendToNode(f.fs, "fs.write", f.handle, arg:bsub(1, l))
if result then
f.buffer = arg:bsub(l + 1)
end
end
else
f.buffer = f.buffer .. arg
end
until not l or not result
else
if #(f.buffer or "") > 0 then
result, reason = f:flush()
if not result then
return nil, reason
end
end
result, reason = sendToNode(f.fs, "fs.write", f.handle, arg)
end
if not result then
return nil, reason
end
end
return f
end
-------------------------------------------------------------------------------
function driver.fs.open(path, mode)
mode = mode or "r"
checkArg(2, mode, "string")
assert(({r=true, rb=true, w=true, wb=true, a=true, ab=true})[mode],
"bad argument #2 (r[b], w[b] or a[b] expected, got " .. tostring(mode) .. ")")
local node, rest = findNode(path)
if not node.fs or not rest then -- files can only be in file systems
return nil, "file not found"
end
local handle, reason = sendToNode(node.fs, "fs.open", rest or "", mode)
if not handle then
return nil, reason
end
return setmetatable({
fs = node.fs,
handle = handle,
bsize = math.min(8 * 1024, os.totalMemory() / 16),
bmode = "full"
}, {
__index = file,
__gc = function(f)
-- file.close does a syscall, which yields, and that's not possible in
-- the __gc metamethod. So we start a timer to do the yield/cleanup.
event.timer(0, function()
file.close(f)
end)
end
})
end
function driver.fs.type(f)
local info = getFileInfo(f, true)
if not info then
return nil
elseif not info.handle then
return "closed file"
else
return "file"
end
end
-------------------------------------------------------------------------------
function loadfile(file, env)
local f, reason = driver.fs.open(file)
if not f then
return nil, reason
end
local source, reason = f:read("*a")
f:close()
if not source then
return nil, reason
end
return load(source, "=" .. file, env)
end
function dofile(file)
local f, reason = loadfile(file)
if not f then
return nil, reason
end
return f()
end

View File

@ -1,48 +1,52 @@
driver.gpu = {}
function driver.gpu.setResolution(gpu, screen, w, h)
sendToNode(gpu, "gpu.resolution=", screen, w, h)
end
function driver.gpu.getResolution(gpu, screen)
return sendToNode(gpu, "gpu.resolution", screen)
end
function driver.gpu.getResolutions(gpu, screen)
return sendToNode(gpu, "gpu.resolutions", screen)
end
function driver.gpu.set(gpu, screen, col, row, value)
sendToNode(gpu, "gpu.set", screen, col, row, value)
end
function driver.gpu.fill(gpu, screen, col, row, w, h, value)
sendToNode(gpu, "gpu.fill", screen, col, row, w, h, value:sub(1, 1))
end
function driver.gpu.copy(gpu, screen, col, row, w, h, tx, ty)
sendToNode(gpu, "gpu.copy", screen, col, row, w, h, tx, ty)
end
function driver.gpu.bind(gpu, screen)
return {
setResolution = function(w, h)
driver.gpu.setResolution(gpu, screen, w, h)
end,
getResolution = function()
return driver.gpu.getResolution(gpu, screen)
end,
getResolutions = function()
return driver.gpu.getResolutions(gpu, screen)
end,
set = function(col, row, value)
driver.gpu.set(gpu, screen, col, row, value)
end,
fill = function(col, ro, w, h, value)
driver.gpu.fill(gpu, screen, col, ro, w, h, value)
end,
copy = function(col, row, w, h, tx, ty)
driver.gpu.copy(gpu, screen, col, row, w, h, tx, ty)
end
}
end
checkArg(1, gpu, "string")
checkArg(2, screen, "string")
return send(gpu, "gpu.bind", screen)
end
function driver.gpu.resolution(gpu, w, h)
checkArg(1, gpu, "string")
if w and h then
checkArg(2, w, "number")
checkArg(3, h, "number")
return send(gpu, "gpu.resolution=", w, h)
else
return send(gpu, "gpu.resolution")
end
end
function driver.gpu.resolutions(gpu)
checkArg(1, gpu, "string")
return send(gpu, "gpu.resolutions")
end
function driver.gpu.set(gpu, col, row, value)
checkArg(1, gpu, "string")
checkArg(2, col, "number")
checkArg(3, row, "number")
checkArg(4, value, "string")
return send(gpu, "gpu.set", col, row, value)
end
function driver.gpu.fill(gpu, col, row, w, h, value)
checkArg(1, gpu, "string")
checkArg(2, col, "number")
checkArg(3, row, "number")
checkArg(4, w, "number")
checkArg(5, h, "number")
checkArg(6, value, "string")
return send(gpu, "gpu.fill", col, row, w, h, value:sub(1, 1))
end
function driver.gpu.copy(gpu, col, row, w, h, tx, ty)
checkArg(1, gpu, "string")
checkArg(2, col, "number")
checkArg(3, row, "number")
checkArg(4, w, "number")
checkArg(5, h, "number")
checkArg(6, tx, "number")
checkArg(7, ty, "number")
return send(gpu, "gpu.copy", col, row, w, h, tx, ty)
end

View File

@ -14,14 +14,14 @@ driver.redstone.sides.down = driver.redstone.sides.bottom
local owner = os.address()
function driver.redstone.analogInput(card, side)
sendToNode(card, owner, "redstone.input", side)
send(card, owner, "redstone.input", side)
end
function driver.redstone.analogOutput(card, side, value)
if value then
sendToNode(card, owner, "redstone.output=", side, tonumber(value))
send(card, owner, "redstone.output=", side, tonumber(value))
else
return sendToNode(card, owner, "redstone.output", side)
return send(card, owner, "redstone.output", side)
end
end

View File

@ -91,6 +91,7 @@ local sandbox = {
date = os.date,
difftime = os.difftime,
time = os.time,
uptime = os.uptime,
freeMemory = os.freeMemory,
totalMemory = os.totalMemory,
address = os.address,
@ -98,6 +99,8 @@ local sandbox = {
},
string = {
breverse = string.breverse,
bsub = string.bsub,
byte = string.byte,
char = string.char,
dump = string.dump,
@ -125,33 +128,27 @@ local sandbox = {
}
sandbox._G = sandbox
-- Note: 'write' will be replaced by init script/term API.
function sandbox.write(...) end
function sandbox.print(...)
sandbox.write(...)
sandbox.write("\n")
end
function sandbox.load(code, source, env)
return load(code, source, "t", env or sandbox)
end
function sandbox.checkArg(n, have, ...)
have = type(have)
for _, want in pairs({...}) do
if have == want then
local want = table.pack(...)
for i = 1, want.n do
if have == want[i] then
return
end
end
local msg = "bad argument #" .. n .. " (" .. table.concat({...}, " or ") .. " expected, got " .. have .. ")"
error(debug.traceback(msg, 3), 2)
error(debug.traceback(msg, 2), 2)
end
-------------------------------------------------------------------------------
--[[ Install wrappers for coroutine management that reserves the first value
returned by yields for internal stuff. Used for sleeping and message
calls (sendToNode and its ilk) that happen synchronized (Server thread).
calls (sendToAddress) that happen synchronized (Server thread).
--]]
local deadline = 0
@ -166,7 +163,7 @@ local function main(args)
sandbox.driver.fs.mount(os.romAddress(), "/boot")
local result, reason = sandbox.loadfile("/boot/init.lua")
if not result then
error(reason)
error(reason, 0)
end
return coroutine.create(result)
end
@ -176,32 +173,32 @@ local function main(args)
if not debug.gethook(co) then
debug.sethook(co, checkDeadline, "", 10000)
end
local result = {coroutine.resume(co, table.unpack(args))}
local result = table.pack(coroutine.resume(co, table.unpack(args, 1, args.n)))
if result[1] then
args = {coroutine.yield(result[2])} -- system yielded value
args = table.pack(coroutine.yield(result[2])) -- system yielded value
else
error(result[2])
error(result[2], 0)
end
end
end
function sandbox.coroutine.resume(co, ...)
local args = {...}
local args = table.pack(...)
while true do
if not debug.gethook(co) then -- don't reset counter
debug.sethook(co, checkDeadline, "", 10000)
end
local result = {coroutine.resume(co, table.unpack(args))}
local result = table.pack(coroutine.resume(co, table.unpack(args, 1, args.n)))
checkDeadline()
if result[1] then
local isSystemYield = coroutine.status(co) ~= "dead" and result[2] ~= nil
if isSystemYield then
args = coroutine.yield(result[2])
args = table.pack(coroutine.yield(result[2]))
else
return true, table.unpack(result, 3)
return true, table.unpack(result, 3, result.n)
end
else -- error: result = (bool, string)
return table.unpack(result)
return table.unpack(result, 1, result.n)
end
end
end
@ -210,6 +207,18 @@ function sandbox.coroutine.yield(...)
return coroutine.yield(nil, ...)
end
function sandbox.pcall(...)
local result = table.pack(pcall(...))
checkDeadline()
return table.unpack(result, 1, result.n)
end
function sandbox.xpcall(...)
local result = table.pack(xpcall(...))
checkDeadline()
return table.unpack(result, 1, result.n)
end
-------------------------------------------------------------------------------
function sandbox.os.shutdown()
@ -221,13 +230,13 @@ function sandbox.os.reboot()
end
function sandbox.os.signal(name, timeout)
local waitUntil = os.clock() + (type(timeout) == "number" and timeout or math.huge)
while os.clock() < waitUntil do
local signal = {coroutine.yield(waitUntil - os.clock())}
if signal and (name == signal[1] or name == nil) then
return table.unpack(signal)
local waitUntil = os.uptime() + (type(timeout) == "number" and timeout or math.huge)
repeat
local signal = table.pack(coroutine.yield(waitUntil - os.uptime()))
if signal.n > 0 and (name == signal[1] or name == nil) then
return table.unpack(signal, 1, signal.n)
end
end
until os.uptime() >= waitUntil
end
-------------------------------------------------------------------------------
@ -239,9 +248,8 @@ function sandbox.driver.componentType(id)
end
do
local env = setmetatable({
sendToNode = sendToNode,
}, { __index = sandbox, __newindex = sandbox })
local env = setmetatable({ send = sendToAddress },
{ __index = sandbox, __newindex = sandbox })
for name, code in pairs(drivers()) do
local driver, reason = load(code, "=" .. name, "t", env)
if not driver then
@ -260,4 +268,4 @@ end
-- JNLua converts the coroutine to a string immediately, so we can't get the
-- traceback later. Because of that we have to do the error handling here.
-- Also, yield once to allow initializing up to here to get a memory baseline.
return pcall(main, {coroutine.yield()})
return pcall(main, table.pack(coroutine.yield()))

View File

@ -55,7 +55,7 @@ function event.fire(name, ...)
end
local elapsed = {}
for id, info in pairs(timers) do
if info.after < os.clock() then
if info.after < os.uptime() then
table.insert(elapsed, info.callback)
timers[id] = nil
end
@ -70,7 +70,7 @@ end
function event.timer(timeout, callback)
local id = #timers + 1
timers[id] = {after = os.clock() + timeout, callback = callback}
timers[id] = {after = os.uptime() + timeout, callback = callback}
return id
end
@ -96,8 +96,9 @@ function event.error(message)
end
function coroutine.sleep(seconds)
seconds = seconds or math.huge
checkArg(1, seconds, "number")
local target = os.clock() + seconds
local target = os.uptime() + seconds
repeat
local closest = target
for _, info in pairs(timers) do
@ -105,6 +106,6 @@ function coroutine.sleep(seconds)
closest = info.after
end
end
event.fire(os.signal(nil, closest - os.clock()))
until os.clock() >= target
event.fire(os.signal(nil, closest - os.uptime()))
until os.uptime() >= target
end

View File

@ -3,17 +3,144 @@ os.remove = driver.fs.remove
os.rename = driver.fs.rename
-- TODO os.tmpname = function() end
local function unavailable()
return nil, "bad file descriptor"
end
io = {}
-- TODO io.flush = function() end
-- TODO io.lines = function(filename) end
io.open = driver.fs.open
io.stdin = {handle="stdin"}
function io.stdin:close()
return nil, "cannot close standard file"
end
io.stdin.flush = unavailable
function io.stdin:lines(...)
return function()
local result = {self:read(...)}
if not result[1] and result[2] then
error(result[2])
end
return table.unpack(result)
end
end
function io.stdin:read(...)
end
io.stdin.seek = unavailable
io.stdin.setvbuf = unavailable
io.stdin.write = unavailable
io.stdout = {handle="stdout"}
io.stdout.close = io.stdin.close
function io.stdout:flush()
return self -- no-op
end
io.stdout.lines = unavailable
io.stdout.read = unavailable
io.stdout.seek = unavailable
io.stdout.setvbuf = unavailable
function io.stdout:write()
return nil, "bad file descriptor"
end
io.stderr = io.stdout
io.lines = function(filename)
local f = io.open(filename)
return function()
if f then
local result, reason = f:read("*l")
if result then
return result
else
f:close()
return nil, reason
end
end
end
-- TODO io.popen = function(prog, mode) end
io.read = driver.fs.read
-- TODO io.tmpfile = function() end
io.type = driver.fs.type
-------------------------------------------------------------------------------
local input, output = io.stdin, io.stdout
function io.close(file)
(file or output):close()
end
function io.flush()
output:flush()
end
function io.input(file)
if file then
if type(file) == "string" then
local result, reason = io.open(file)
if not result then
error(reason)
end
input = result
elseif io.type(file) then
input = file
else
error("bad argument #1 (string or file expected, got " .. type(file) .. ")")
end
end
return input
end
function io.open(...)
local result, reason = driver.fs.open(...)
if result then
current = result
end
return result, reason
end
function io.output(file)
if file then
if type(file) == "string" then
local result, reason = io.open(file, "w")
if not result then
error(reason)
end
output = result
elseif io.type(file) then
output = file
else
error("bad argument #1 (string or file expected, got " .. type(file) .. ")")
end
end
return output
end
function io.read(...)
if current then
return current:read(...)
end
end
function io.write(...)
if current then
return current:write(...)
end
end
-------------------------------------------------------------------------------
event.listen("component_added", function(_, address)
if component.type(address) == "filesystem" and address ~= os.romAddress() then
local name = address:sub(1, 3)

View File

@ -1,19 +1,20 @@
local gpu = nil
local gpuAddress, screenAddress, keyboardAddress = false, false, false
local screenWidth, screenHeight = 0, 0
local gpuAddress, screenAddress, keyboardAddress = nil, nil, nil
local width, height = 0, 0
local cursorX, cursorY = 1, 1
local cursorBlink = nil
local function bindIfPossible()
if gpuAddress and screenAddress then
if not gpu then
gpu = driver.gpu.bind(gpuAddress, screenAddress)
screenWidth, screenHeight = gpu.getResolution()
event.fire("term_available")
end
elseif gpu then
gpu = nil
screenWidth, screenHeight = 0, 0
local function rebind(gpu, screen)
if gpu == gpuAddress and screen == screenAddress then
return
end
local oldGpu, oldScreen = gpuAddress, screenAddress
gpuAddress, screenAddress = gpu, screen
if gpu and screen then
driver.gpu.bind(gpuAddress, screenAddress)
width, height = driver.gpu.resolution(gpuAddress)
event.fire("term_available")
elseif gpuAddress and screenAddress then
width, height = 0, 0
event.fire("term_unavailable")
end
end
@ -22,35 +23,26 @@ end
term = {}
function term.gpu(address)
if address ~= nil and ({boolean=true, string=true})[type(address)] then
gpuAddress = address
bindIfPossible()
end
return gpuAddress, gpu
function term.available()
return gpuAddress and screenAddress
end
function term.screen(address)
if address ~= nil and ({boolean=true, string=true})[type(address)] then
screenAddress = address
bindIfPossible()
function term.clear()
if term.available() then
driver.gpu.fill(term.gpu(), 1, 1, width, height, " ")
end
return screenAddress
cursorX, cursorY = 1, 1
end
function term.keyboard(address)
if address ~= nil and ({boolean=true, string=true})[type(address)] then
keyboardAddress = address
function term.clearLine()
if term.available() then
driver.gpu.fill(term.gpu(), 1, cursorY, width, 1, " ")
end
return keyboardAddress
end
function term.screenSize()
return screenWidth, screenHeight
cursorX = 1
end
function term.cursor(col, row)
if row and col then
if col and row then
cursorX = math.max(col, 1)
cursorY = math.max(row, 1)
end
@ -61,12 +53,10 @@ function term.cursorBlink(enabled)
if type(enabled) == "boolean" and enabled ~= (cursorBlink ~= nil) then
local function toggleBlink()
cursorBlink.state = not cursorBlink.state
if gpu then
if cursorBlink.state then
gpu.set(cursorX, cursorY, string.char(0x2588)) -- Solid block.
else
gpu.set(cursorX, cursorY, " ")
end
if term.available() then
-- 0x2588 is a solid block.
local char = cursorBlink.state and string.char(0x2588) or " "
driver.gpu.set(term.gpu(), cursorX, cursorY, char)
end
end
if enabled then
@ -83,20 +73,52 @@ function term.cursorBlink(enabled)
return cursorBlink ~= nil
end
function term.gpu(...)
local args = table.pack(...)
if args.n > 0 then
checkArg(1, args[1], "string", "nil")
rebind(args[1], term.screen())
end
return gpuAddress
end
function term.keyboard(...)
local args = table.pack(...)
if args.n > 0 then
checkArg(1, args[1], "string", "nil")
keyboardAddress = args[1]
end
return keyboardAddress
end
function term.screen(...)
local args = table.pack(...)
if args.n > 0 then
checkArg(1, args[1], "string", "nil")
rebind(term.gpu(), args[1])
end
return screenAddress
end
function term.size()
return width, height
end
function term.write(value, wrap)
value = tostring(value)
local w, h = screenWidth, screenHeight
if value:len() == 0 or not gpu or w < 1 or h < 1 then
local w, h = width, height
if value:len() == 0 or not term.available() or w < 1 or h < 1 then
return
end
value = value:gsub("\t", " ")
local function checkCursor()
if cursorX > w then
cursorX = 1
cursorY = cursorY + 1
end
if cursorY > h then
gpu.copy(1, 1, w, h, 0, -1)
gpu.fill(1, h, w, 1, " ")
driver.gpu.copy(term.gpu(), 1, 1, w, h, 0, -1)
driver.gpu.fill(term.gpu(), 1, h, w, 1, " ")
cursorY = h
end
end
@ -104,12 +126,12 @@ function term.write(value, wrap)
while wrap and line:len() > w - cursorX + 1 do
local partial = line:sub(1, w - cursorX + 1)
line = line:sub(partial:len() + 1)
gpu.set(cursorX, cursorY, partial)
driver.gpu.set(term.gpu(), cursorX, cursorY, partial)
cursorX = cursorX + partial:len()
checkCursor()
end
if line:len() > 0 then
gpu.set(cursorX, cursorY, line)
driver.gpu.set(term.gpu(), cursorX, cursorY, line)
cursorX = cursorX + line:len()
end
if nl:len() == 1 then
@ -120,62 +142,38 @@ function term.write(value, wrap)
end
end
function term.clear()
if not gpu then return end
gpu.fill(1, 1, screenWidth, screenHeight, " ")
cursorX, cursorY = 1, 1
end
function term.clearLine()
if not gpu then return end
gpu.fill(1, cursorY, screenWidth, 1, " ")
cursorX = 1
end
write = function(...)
local args = {...}
local first = true
for i = 1, #args do
if not first then
term.write(", ")
end
first = false
term.write(args[i], true)
end
end
-------------------------------------------------------------------------------
event.listen("component_added", function(_, address)
local type = component.type(address)
if type == "gpu" and not gpuAddress then
if type == "gpu" and not term.gpu() then
term.gpu(address)
elseif type == "screen" and not screenAddress then
elseif type == "screen" and not term.screen() then
term.screen(address)
elseif type == "keyboard" and not keyboardAddress then
elseif type == "keyboard" and not term.keyboard() then
term.keyboard(address)
end
end)
event.listen("component_removed", function(_, address)
if gpuAddress == address then
term.gpu(false)
if term.gpu() == address then
term.gpu(nil)
for address in component.list() do
if component.type(address) == "gpu" then
term.gpu(address)
return
end
end
elseif screenAddress == address then
term.screen(false)
elseif term.screen() == address then
term.screen(nil)
for address in component.list() do
if component.type(address) == "screen" then
term.screen(address)
return
end
end
elseif keyboardAddress == address then
term.keyboard(false)
elseif term.keyboard() == address then
term.keyboard(nil)
for address in component.list() do
if component.type(address) == "keyboard" then
term.keyboard(address)
@ -186,8 +184,8 @@ event.listen("component_removed", function(_, address)
end)
event.listen("screen_resized", function(_, address, w, h)
if address == screenAddress then
screenWidth = w
screenHeight = h
if term.screen() == address then
width = w
height = h
end
end)

View File

@ -0,0 +1,21 @@
event.listen("component_added", function(_, address)
if component.type(address) == "filesystem" and address ~= os.romAddress() then
local name = address:sub(1, 3)
repeat
name = address:sub(1, name:len() + 1)
until not driver.fs.exists("/mnt/" .. name)
driver.fs.mount(address, "/mnt/" .. name)
local autorun = "/mnt/" .. name .. "/autorun"
if driver.fs.exists(autorun .. ".lua") then
dofile(autorun .. ".lua")
elseif driver.fs.exists(autorun) then
dofile(autorun)
end
end
end)
event.listen("component_removed", function(_, address)
if component.type(address) == "filesystem" then
driver.fs.umount(address)
end
end)

View File

@ -6,15 +6,15 @@ local isRunning = false
local function onKeyDown(_, address, char, code)
if isRunning then return end -- ignore events while running a command
if address ~= term.keyboard() then return end
local _, gpu = term.gpu()
if not gpu then return end
if not term.available() then return end
local x, y = term.cursor()
local w, h = term.size()
local keys = driver.keyboard.keys
if code == keys.back then
if command:len() == 0 then return end
command = command:sub(1, -2)
term.cursor(command:len() + 3, y) -- from leading "> "
gpu.set(x - 1, y, " ") -- overwrite cursor blink
driver.gpu.set(term.gpu(), x - 1, y, " ") -- overwrite cursor blink
elseif code == keys.enter then
if command:len() == 0 then return end
term.cursorBlink(false)
@ -25,21 +25,21 @@ local function onKeyDown(_, address, char, code)
end
if code then
isRunning = true
local result = {pcall(code)}
local result = table.pack(pcall(code))
isRunning = false
if not result[1] or #result > 1 then
print(table.unpack(result, 2))
if not result[1] or result.n > 1 then
print(table.unpack(result, 2, result.n))
end
else
print(result)
end
lastCommand = command
command = ""
write("> ")
term.write("> ")
term.cursorBlink(true)
elseif code == keys.up then
command = lastCommand
gpu.fill(3, y, screenWidth, 1, " ")
driver.gpu.fill(term.gpu(), 3, y, w, 1, " ")
term.cursor(3, y)
term.write(command)
term.cursor(command:len() + 3, y)
@ -64,7 +64,7 @@ event.listen("term_available", function()
term.clear()
command = ""
print("OpenOS v1.0 (" .. math.floor(os.totalMemory() / 1024) .. "k RAM)")
write("> ")
term.write("> ")
event.listen("key_down", onKeyDown)
event.listen("clipboard", onClipboard)
end)

View File

@ -1,12 +1,12 @@
dofile("/boot/api/event.lua")
dofile("/boot/api/component.lua")
dofile("/boot/api/filesystem.lua")
dofile("/boot/api/term.lua")
dofile("/boot/sh.lua")
dofile("/boot/bin/automount.lua")
dofile("/boot/bin/sh.lua")
driver.fs.umount("/boot")
event.fire(...)
while true do
event.fire(os.signal())
coroutine.sleep()
end

View File

@ -9,7 +9,7 @@ object Blocks {
var screen: Screen = null
var keyboard: Keyboard = null
var powersupply: PowerSupply = null
var powerdistributer: PowerDistributor = null
var powerdistributor: PowerDistributor = null
def init() {
// IMPORTANT: the multi block must come first, since the sub blocks will
// try to register with it. Also, the order the sub blocks are created in
@ -21,6 +21,6 @@ object Blocks {
screen = new Screen(blockSimple)
keyboard = new Keyboard(blockSpecial)
powersupply = new PowerSupply(blockSimple)
powerdistributer = new PowerDistributor(blockSimple)
powerdistributor = new PowerDistributor(blockSimple)
}
}

View File

@ -16,8 +16,33 @@ import li.cil.oc.api.network.Node
* <p/>
* Alternatively to using the factory methods for file systems in `Filesystem`
* you are free to implement this interface yourself.
* <p/>
* Note that all paths passed here are assumed to be absolute in the underlying
* file system implementation, meaning they do not contain any "." or "..", and
* are relative to the root of the file system.
*/
trait FileSystem extends Persistable {
/**
* The total storage capacity of the file system, in bytes.
* <p/>
* For read-only systems this should return zero, for writable file systems
* that do not enforce a storage limit this should be a negative value.
*
* @return the total storage space of this file system.
*/
def spaceTotal = 0L
/**
* The used storage capacity of the file system, in bytes.
* <p/>
* For read-only systems this should return zero.
*
* @return the used storage space of this file system.
*/
def spaceUsed = 0L
// ----------------------------------------------------------------------- //
/**
* Tests if a file or directory exists at the specified path.
* <p/>
@ -75,6 +100,20 @@ trait FileSystem extends Persistable {
// ----------------------------------------------------------------------- //
/**
* Create the specified directory, and if necessary any parent directories
* that do not yet exist.
* <p/>
* This is only available for writable file systems. For read-only systems
* it should just always return false.
*
* @param path the path to the directory to create.
* @return true if the directory was created; false otherwise.
*/
def makeDirectories(path: String): Boolean =
!exists(path) && (makeDirectory(path) ||
(makeDirectories(path.split("/").dropRight(1).mkString("/")) && makeDirectory(path)))
/**
* Deletes a file or folder.
* <p/>
@ -153,6 +192,23 @@ trait FileSystem extends Persistable {
*/
def close()
// ----------------------------------------------------------------------- //
/**
* Actual directory creation implementation.
* <p/>
* This is called from the possibly recursive `makeDirectories` function, and
* guarantees that the parent directory of the directory to be created
* already exists. So this should always only create a single directory.
* <p/>
* This is only available for writable file systems. For read-only systems
* it should just always return false.
*
* @param path the path to the directory to create.
* @return true if the directory was created; false otherwise.
*/
protected def makeDirectory(path: String) = false
/**
* Actual deletion implementation.
* <p/>

View File

@ -70,16 +70,22 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
// ----------------------------------------------------------------------- //
private var timeStarted = 0L // Game-world time for os.clock().
private var timeStarted = 0L // Game-world time [ms] for os.uptime().
private var worldTime = 0L // Game-world time for os.time().
private var lastUpdate = 0L // Real-world time for pause detection.
private var lastUpdate = 0L // Real-world time [ms] for pause detection.
private var sleepUntil = Long.MaxValue // Real-world time.
private var cpuTime = 0L // Pseudo-real-world time [ns] for os.clock().
private var cpuStart = 0L // Pseudo-real-world time [ns] for os.clock().
private var sleepUntil = Double.PositiveInfinity // Real-world time [ms].
private var wasRunning = false // To signal stops synchronously.
private var message: Option[String] = None // For error messages.
// ----------------------------------------------------------------------- //
def recomputeMemory() = if (lua != null)
@ -154,10 +160,25 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
worldTime = owner.world.getWorldInfo.getWorldTotalTime
// Signal stops to the network. This is used to close file handles, for example.
if (wasRunning && !isRunning)
if (wasRunning && !isRunning) {
owner.network.foreach(_.sendToVisible(owner, "computer.stopped"))
// Clear any screens we use while we're at it.
owner.network.foreach(_.sendToNeighbors(owner, "gpu.fill",
1.0, 1.0, Double.PositiveInfinity, Double.PositiveInfinity, " ".getBytes("UTF-8")))
}
wasRunning = isRunning
// If there was an error message (i.e. the computer crashed) display it on
// any screens we used (stored in GPUs).
if (message.isDefined) {
owner.network.foreach(network => {
for ((line, row) <- message.get.lines.zipWithIndex) {
network.sendToNeighbors(owner, "gpu.set", 1.0, 1.0 + row, line.getBytes("UTF-8"))
}
})
message = None
}
// Check if we should switch states.
stateMonitor.synchronized(state match {
// Computer is rebooting.
@ -204,16 +225,14 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
// This can happen if we run out of memory while converting a Java
// exception to a string (which we have to do to avoid keeping
// userdata on the stack, which cannot be persisted).
OpenComputers.log.warning("Out of memory!") // TODO remove this when we have a component that can display crash messages
//owner.network.foreach(_.sendToVisible(owner, "computer.crashed", "not enough memory"))
message = Some("not enough memory")
close()
case e: java.lang.Error if e.getMessage == "not enough memory" =>
OpenComputers.log.warning("Out of memory!") // TODO remove this when we have a component that can display crash messages
// TODO get this to the world as a computer.crashed message. problem: synchronizing it.
//owner.network.foreach(_.sendToVisible(owner, "computer.crashed", "not enough memory"))
message = Some("not enough memory")
close()
case e: Throwable => {
OpenComputers.log.log(Level.WARNING, "Faulty Lua implementation for synchronized calls.", e)
message = Some("protocol error")
close()
}
}
@ -273,6 +292,9 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
rom.foreach(_.load(nbt.getCompoundTag("rom")))
kernelMemory = nbt.getInteger("kernelMemory")
timeStarted = nbt.getLong("timeStarted")
cpuTime = nbt.getLong("cpuTime")
if (nbt.hasKey("message"))
message = Some(nbt.getString("message"))
// Clean up some after we're done and limit memory again.
lua.gc(LuaState.GcAction.COLLECT, 0)
@ -353,6 +375,9 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
nbt.setCompoundTag("rom", romNbt)
nbt.setInteger("kernelMemory", kernelMemory)
nbt.setLong("timeStarted", timeStarted)
nbt.setLong("cpuTime", cpuTime)
if (message.isDefined)
nbt.setString("message", message.get)
}
catch {
case e: Throwable => {
@ -417,12 +442,9 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
lua.getGlobal("os")
// Custom os.clock() implementation returning the time the computer has
// been running, instead of the native library...
// been actively running, instead of the native library...
lua.pushScalaFunction(lua => {
// World time is in ticks, and each second has 20 ticks. Since we
// want os.clock() to return real seconds, though, we'll divide it
// accordingly.
lua.pushNumber((worldTime - timeStarted) / 20.0)
lua.pushNumber((cpuTime + (System.nanoTime() - cpuStart)) * 10e-10)
1
})
lua.setField(-2, "clock")
@ -438,6 +460,16 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
})
lua.setField(-2, "time")
// The time the computer has been running, as opposed to the CPU time.
lua.pushScalaFunction(lua => {
// World time is in ticks, and each second has 20 ticks. Since we
// want os.uptime() to return real seconds, though, we'll divide it
// accordingly.
lua.pushNumber((worldTime - timeStarted) / 20.0)
1
})
lua.setField(-2, "uptime")
// Allow the system to read how much memory it uses and has available.
lua.pushScalaFunction(lua => {
lua.pushInteger(lua.getTotalMemory - kernelMemory)
@ -541,7 +573,7 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
results.length
case _ => 0
})
lua.setGlobal("sendToNode")
lua.setGlobal("sendToAddress")
lua.pushScalaFunction(lua => {
owner.network.fold(None: Option[Node])(_.node(lua.checkString(1))) match {
@ -592,7 +624,7 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
// underlying system (which may change across releases). Add some buffer
// to avoid the init script eating up all the rest immediately.
lua.gc(LuaState.GcAction.COLLECT, 0)
kernelMemory = (lua.getTotalMemory - lua.getFreeMemory) + 2048
kernelMemory = (lua.getTotalMemory - lua.getFreeMemory) + 8 * 1024
recomputeMemory()
// Clear any left-over signals from a previous run.
@ -621,6 +653,8 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
kernelMemory = 0
signals.clear()
timeStarted = 0
cpuTime = 0
cpuStart = 0
future = None
sleepUntil = Long.MaxValue
@ -628,6 +662,8 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
owner.markAsChanged()
})
// ----------------------------------------------------------------------- //
private def execute(value: Computer.State.Value) {
assert(future.isEmpty)
sleepUntil = Long.MaxValue
@ -666,6 +702,7 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
try {
// Resume the Lua state and remember the number of results we get.
cpuStart = System.nanoTime()
val results = if (callReturn) {
// If we were doing a synchronized call, continue where we left off.
assert(lua.getTop == 2)
@ -685,6 +722,7 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
lua.resume(1, 1 + signal.args.length)
}
}
cpuTime += System.nanoTime() - cpuStart
// Check if the kernel is still alive.
stateMonitor.synchronized(if (lua.status(1) == LuaState.YIELD) {
@ -696,7 +734,7 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
// If we have a single number that's how long we may wait before
// resuming the state again.
else if (results == 1 && lua.isNumber(2)) {
val sleep = (lua.toNumber(2) * 1000).toLong
val sleep = lua.toNumber(2) * 1000
lua.pop(results)
// But only sleep if we don't have more signals to process.
if (signals.isEmpty) {
@ -746,9 +784,7 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
else {
// This can trigger another out of memory error if the original
// error was an out of memory error.
OpenComputers.log.warning("Computer crashed.\n" + lua.toString(3)) // TODO remove this when we have a component that can display crash messages
// TODO get this to the world as a computer.crashed message. problem: synchronizing it.
//owner.network.sendToAll(owner, "computer.crashed", lua.toString(3))
message = Some(lua.toString(3))
}
close()
})
@ -756,16 +792,13 @@ class Computer(val owner: Computer.Environment) extends Persistable with Runnabl
catch {
case e: LuaRuntimeException =>
OpenComputers.log.warning("Kernel crashed. This is a bug!\n" + e.toString + "\tat " + e.getLuaStackTrace.mkString("\n\tat "))
message = Some("kernel panic")
close()
case e: LuaMemoryAllocationException =>
OpenComputers.log.warning("Out of memory!") // TODO remove this when we have a component that can display crash messages
// TODO get this to the world as a computer.crashed message. problem: synchronizing it.
//owner.network.sendToAll(owner, "computer.crashed", "not enough memory")
message = Some("not enough memory")
close()
case e: java.lang.Error if e.getMessage == "not enough memory" =>
OpenComputers.log.warning("Out of memory!") // TODO remove this when we have a component that can display crash messages
// TODO get this to the world as a computer.crashed message. problem: synchronizing it.
//owner.network.sendToAll(owner, "computer.crashed", "not enough memory")
message = Some("not enough memory")
close()
}
}

View File

@ -37,6 +37,14 @@ class FileSystem(val fileSystem: api.FileSystem) extends Node {
set.clear()
}
None
case Array() if message.name == "fs.spaceTotal" =>
val space = fileSystem.spaceTotal
if (space < 0)
Some(Array("unlimited"))
else
Some(Array(space.asInstanceOf[Any]))
case Array() if message.name == "fs.spaceUsed" =>
Some(Array(fileSystem.spaceUsed.asInstanceOf[Any]))
case Array(path: Array[Byte]) if message.name == "fs.exists" =>
Some(Array(fileSystem.exists(clean(path)).asInstanceOf[Any]))
case Array(path: Array[Byte]) if message.name == "fs.size" =>

View File

@ -1,10 +1,16 @@
package li.cil.oc.server.component
import li.cil.oc.api.network.{Node, Visibility, Message}
import li.cil.oc.common.component.ScreenEnvironment
import net.minecraft.nbt.NBTTagCompound
class GraphicsCard extends Node {
val supportedResolutions = List(List(40, 24), List(80, 24))
private var screen: Option[String] = None
// ----------------------------------------------------------------------- //
override def name = "gpu"
override def visibility = Visibility.Neighbors
@ -12,32 +18,66 @@ class GraphicsCard extends Node {
override def receive(message: Message) = {
super.receive(message)
message.data match {
case Array(screen: Array[Byte], w: Double, h: Double) if message.name == "gpu.resolution=" =>
case Array(address: Array[Byte]) if message.name == "gpu.bind" =>
network.fold(None: Option[Array[Any]])(network => {
network.node(new String(address, "UTF-8")) match {
case None => Some(Array(Unit, "invalid address"))
case Some(node: ScreenEnvironment) =>
screen = node.address
Some(Array(true.asInstanceOf[Any]))
case _ => Some(Array(Unit, "not a screen"))
}
})
case Array() if message.name == "network.disconnect" && message.source.address == screen => screen = None; None
case Array(w: Double, h: Double) if message.name == "gpu.resolution=" =>
if (supportedResolutions.contains((w.toInt, h.toInt)))
trySend(new String(screen, "UTF-8"), "screen.resolution=", w.toInt, h.toInt)
trySend("screen.resolution=", w.toInt, h.toInt)
else
Some(Array(Unit, "unsupported resolution"))
case Array(screen: Array[Byte]) if message.name == "gpu.resolution" =>
trySend(new String(screen, "UTF-8"), "screen.resolution")
case Array(screen: Array[Byte]) if message.name == "gpu.resolutions" =>
trySend(new String(screen, "UTF-8"), "screen.resolutions") match {
case Some(Array(resolutions@_*)) =>
Some(Array(supportedResolutions.intersect(resolutions): _*))
case _ => None
}
case Array(screen: Array[Byte], x: Double, y: Double, value: Array[Byte]) if message.name == "gpu.set" =>
trySend(new String(screen, "UTF-8"), "screen.set", x.toInt - 1, y.toInt - 1, new String(value, "UTF-8"))
case Array(screen: Array[Byte], x: Double, y: Double, w: Double, h: Double, value: Array[Byte]) if message.name == "gpu.fill" =>
case Array() if message.name == "gpu.resolution" => trySend("screen.resolution")
case Array() if message.name == "gpu.resolutions" => trySend("screen.resolutions") match {
case Some(Array(resolutions@_*)) =>
Some(Array(supportedResolutions.intersect(resolutions): _*))
case _ => None
}
case Array(x: Double, y: Double, value: Array[Byte]) if message.name == "gpu.set" =>
trySend("screen.set", x.toInt - 1, y.toInt - 1, new String(value, "UTF-8"))
case Array(x: Double, y: Double, w: Double, h: Double, value: Array[Byte]) if message.name == "gpu.fill" =>
val s = new String(value, "UTF-8")
if (s.length == 1)
trySend(new String(screen, "UTF-8"), "screen.fill", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, s.charAt(0))
trySend("screen.fill", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, s.charAt(0))
else
Some(Array(Unit, "invalid fill value"))
case Array(screen: Array[Byte], x: Double, y: Double, w: Double, h: Double, tx: Double, ty: Double) if message.name == "gpu.copy" =>
trySend(new String(screen, "UTF-8"), "screen.copy", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, tx.toInt, ty.toInt)
case Array(x: Double, y: Double, w: Double, h: Double, tx: Double, ty: Double) if message.name == "gpu.copy" =>
trySend("screen.copy", x.toInt - 1, y.toInt - 1, w.toInt, h.toInt, tx.toInt, ty.toInt)
case _ => None
}
}
private def trySend(target: String, name: String, data: Any*) = network.fold(None: Option[Array[Any]])(net => net.sendToAddress(this, target, name, data: _*))
override protected def onDisconnect() = {
super.onDisconnect()
screen = None
}
override def load(nbt: NBTTagCompound) = {
super.load(nbt)
if (nbt.hasKey("screen"))
screen = Some(nbt.getString("screen"))
}
override def save(nbt: NBTTagCompound) = {
super.save(nbt)
if (screen.isDefined)
nbt.setString("screen", screen.get)
}
// ----------------------------------------------------------------------- //
private def trySend(name: String, data: Any*): Option[Array[Any]] =
screen match {
case None => Some(Array(Unit, "no screen"))
case Some(screenAddress) => network.fold(None: Option[Array[Any]])(net => {
net.sendToAddress(this, screenAddress, name, data: _*)
})
}
}

View File

@ -7,7 +7,7 @@ import li.cil.oc.{Config, Items}
import net.minecraft.item.ItemStack
object FileSystem extends Item {
override def api = Option(getClass.getResourceAsStream(Config.driverPath + "fs.lua"))
override def api = Option(getClass.getResourceAsStream(Config.driverPath + "filesystem.lua"))
override def worksWith(item: ItemStack) = WorksWith(Items.hdd2, Items.hdd4, Items.hdd8)(item)

View File

@ -7,7 +7,19 @@ import li.cil.oc.api.fs.Mode
trait Capacity extends OutputStreamFileSystem {
private var used = computeSize("/")
def capacity: Int
protected def capacity: Int
// ----------------------------------------------------------------------- //
override def spaceTotal = capacity
override def spaceUsed = used
// ----------------------------------------------------------------------- //
override def makeDirectories(path: String) = {
super.makeDirectories(path)
}
override protected def delete(path: String) = {
val freed = Config.fileCost + size(path)
@ -18,6 +30,8 @@ trait Capacity extends OutputStreamFileSystem {
else false
}
// ----------------------------------------------------------------------- //
override protected abstract def openOutputStream(path: String, mode: Mode.Value): Option[io.OutputStream] = {
val delta =
if (exists(path))
@ -35,6 +49,8 @@ trait Capacity extends OutputStreamFileSystem {
}
}
// ----------------------------------------------------------------------- //
private def computeSize(path: String): Long =
Config.fileCost +
size(path) +

View File

@ -7,6 +7,8 @@ import li.cil.oc.api
trait FileInputStreamFileSystem extends api.FileSystem with InputStreamFileSystem {
protected val root: io.File
// ----------------------------------------------------------------------- //
override def exists(path: String) = new io.File(root, path).exists()
override def size(path: String) = new io.File(root, path) match {
@ -23,6 +25,8 @@ trait FileInputStreamFileSystem extends api.FileSystem with InputStreamFileSyste
case _ => throw new FileNotFoundException("no such file or directory")
}
// ----------------------------------------------------------------------- //
override protected def openInputStream(path: String) =
Some(new FileInputStream(new io.File(root, path)))
}

View File

@ -5,10 +5,22 @@ import java.io.FileOutputStream
import li.cil.oc.api.fs.Mode
trait FileOutputStreamFileSystem extends FileInputStreamFileSystem with OutputStreamFileSystem {
override protected def openOutputStream(path: String, mode: Mode.Value): Option[io.OutputStream] =
Some(new FileOutputStream(new io.File(root, path), mode == Mode.Append))
override def spaceTotal = -1
override def spaceUsed = root.getFreeSpace
// ----------------------------------------------------------------------- //
override def rename(from: String, to: String) = new io.File(root, from).renameTo(new io.File(root, to))
// ----------------------------------------------------------------------- //
override protected def makeDirectory(path: String) = new io.File(root, path).mkdir()
override protected def delete(path: String) = new io.File(root, path).delete()
// ----------------------------------------------------------------------- //
override protected def openOutputStream(path: String, mode: Mode.Value): Option[io.OutputStream] =
Some(new FileOutputStream(new io.File(root, path), mode == Mode.Append))
}

View File

@ -52,7 +52,7 @@ object FileSystem extends api.detail.FileSystemAPI {
extends InputStreamFileSystem
with FileInputStreamFileSystem
private class ReadWriteFileSystem(protected val root: io.File, val capacity: Int)
private class ReadWriteFileSystem(protected val root: io.File, protected val capacity: Int)
extends OutputStreamFileSystem
with FileOutputStreamFileSystem
with Capacity

View File

@ -9,6 +9,8 @@ import scala.collection.mutable
trait InputStreamFileSystem extends api.FileSystem {
private val handles = mutable.Map.empty[Int, Handle]
// ----------------------------------------------------------------------- //
override def open(path: String, mode: Mode.Value) = if (mode == Mode.Read && exists(path) && !isDirectory(path)) {
val handle = Iterator.continually((Math.random() * Int.MaxValue).toInt + 1).filterNot(handles.contains).next()
openInputStream(path) match {
@ -27,6 +29,8 @@ trait InputStreamFileSystem extends api.FileSystem {
handles.clear()
}
// ----------------------------------------------------------------------- //
override def load(nbt: NBTTagCompound) {
val handlesNbt = nbt.getTagList("input")
(0 until handlesNbt.tagCount).map(handlesNbt.tagAt).map(_.asInstanceOf[NBTTagCompound]).foreach(handleNbt => {
@ -56,8 +60,12 @@ trait InputStreamFileSystem extends api.FileSystem {
nbt.setTag("input", handlesNbt)
}
// ----------------------------------------------------------------------- //
protected def openInputStream(path: String): Option[InputStream]
// ----------------------------------------------------------------------- //
private class Handle(val owner: InputStreamFileSystem, val handle: Int, val path: String, val stream: InputStream) extends api.fs.Handle {
var isClosed = false
var position = 0L
@ -82,7 +90,7 @@ trait InputStreamFileSystem extends api.FileSystem {
stream.skip(to)
}
def write(value: Array[Byte]) = throw new IOException("handle is read-only")
def write(value: Array[Byte]) = throw new IOException("bad file descriptor")
}
}

View File

@ -9,6 +9,8 @@ import scala.collection.mutable
trait OutputStreamFileSystem extends InputStreamFileSystem {
private val handles = mutable.Map.empty[Int, Handle]
// ----------------------------------------------------------------------- //
override def open(path: String, mode: Mode.Value) = mode match {
case Mode.Read => super.open(path, mode)
case _ => if (!isDirectory(path)) {
@ -31,6 +33,8 @@ trait OutputStreamFileSystem extends InputStreamFileSystem {
handles.clear()
}
// ----------------------------------------------------------------------- //
override def load(nbt: NBTTagCompound) {
super.load(nbt)
val handlesNbt = nbt.getTagList("output")
@ -59,8 +63,12 @@ trait OutputStreamFileSystem extends InputStreamFileSystem {
nbt.setTag("output", handlesNbt)
}
// ----------------------------------------------------------------------- //
protected def openOutputStream(path: String, mode: Mode.Value): Option[OutputStream]
// ----------------------------------------------------------------------- //
private class Handle(val owner: OutputStreamFileSystem, val handle: Int, val path: String, val stream: OutputStream) extends api.fs.Handle {
var isClosed = false
val position = 0L
@ -72,9 +80,9 @@ trait OutputStreamFileSystem extends InputStreamFileSystem {
stream.close()
}
def read(into: Array[Byte]) = throw new IOException("handle is write-only")
def read(into: Array[Byte]) = throw new IOException("bad file descriptor")
def seek(to: Long) = throw new IOException("handle is write-only")
def seek(to: Long) = throw new IOException("bad file descriptor")
def write(value: Array[Byte]) {
stream.write(value)

View File

@ -26,6 +26,8 @@ class Network private(private val addressedNodes: mutable.Map[String, Network.No
addressedNodes.values.foreach(_.data.network = Some(this))
unaddressedNodes.foreach(_.data.network = Some(this))
// ----------------------------------------------------------------------- //
override def connect(nodeA: api.network.Node, nodeB: api.network.Node) = {
val containsA = contains(nodeA)
val containsB = contains(nodeB)
@ -104,6 +106,8 @@ class Network private(private val addressedNodes: mutable.Map[String, Network.No
}
}
// ----------------------------------------------------------------------- //
override def node(address: String) = addressedNodes.get(address) match {
case Some(node) => Some(node.data)
case _ => None
@ -130,6 +134,8 @@ class Network private(private val addressedNodes: mutable.Map[String, Network.No
}
}
// ----------------------------------------------------------------------- //
override def sendToAddress(source: api.network.Node, target: String, name: String, data: Any*) = {
if (source.network.isEmpty || source.network.get != this)
throw new IllegalArgumentException("Source node must be in this network.")
@ -157,6 +163,8 @@ class Network private(private val addressedNodes: mutable.Map[String, Network.No
else None
}
// ----------------------------------------------------------------------- //
private def contains(node: api.network.Node) = (node.address match {
case None => unaddressedNodes.find(_.data == node)
case Some(address) => addressedNodes.get(address)
@ -306,6 +314,18 @@ object Network extends api.detail.NetworkAPI {
}
}
private def getNetworkNode(world: IBlockAccess, x: Int, y: Int, z: Int): Option[TileEntity with api.network.Node] =
Option(Block.blocksList(world.getBlockId(x, y, z))) match {
case Some(block) if block.hasTileEntity(world.getBlockMetadata(x, y, z)) =>
world.getBlockTileEntity(x, y, z) match {
case tileEntity: TileEntity with api.network.Node => Some(tileEntity)
case _ => None
}
case _ => None
}
// ----------------------------------------------------------------------- //
@ForgeSubscribe
def onChunkUnload(e: ChunkEvent.Unload) =
onUnload(e.world, e.getChunk.chunkTileEntityMap.values.asScala.map(_.asInstanceOf[TileEntity]))
@ -326,15 +346,7 @@ object Network extends api.detail.NetworkAPI {
tileEntities.foreach(t => joinOrCreateNetwork(w, t.xCoord, t.yCoord, t.zCoord))
}
private def getNetworkNode(world: IBlockAccess, x: Int, y: Int, z: Int): Option[TileEntity with api.network.Node] =
Option(Block.blocksList(world.getBlockId(x, y, z))) match {
case Some(block) if block.hasTileEntity(world.getBlockMetadata(x, y, z)) =>
world.getBlockTileEntity(x, y, z) match {
case tileEntity: TileEntity with api.network.Node => Some(tileEntity)
case _ => None
}
case _ => None
}
// ----------------------------------------------------------------------- //
private class Node(val data: api.network.Node) {
val edges = ArrayBuffer.empty[Edge]
@ -382,6 +394,8 @@ object Network extends api.detail.NetworkAPI {
}) filter (_.nonEmpty) map (_.get)
}
// ----------------------------------------------------------------------- //
private class Message(@BeanProperty val source: api.network.Node,
@BeanProperty val name: String,
@BeanProperty val data: Array[Any] = Array()) extends api.network.Message {

View File

@ -197,6 +197,14 @@ object LuaStateFactory {
// Provide some better Unicode support.
state.getGlobal("string")
// Rename stuff for binary functionality, to allow byte-wise operations
// operations on the string.
state.getField(-1, "sub")
state.setField(-2, "bsub")
state.getField(-1, "reverse")
state.setField(-2, "breverse")
state.pushScalaFunction(lua => {
lua.pushString(String.valueOf((1 to lua.getTop).map(lua.checkInteger).map(_.toChar).toArray))
1
@ -231,11 +239,6 @@ object LuaStateFactory {
})
state.setField(-2, "reverse")
// Rename string.sub to string.bsub (for binary sub, to allow byte-wise
// operations on the string).
state.getField(-1, "sub")
state.setField(-2, "bsub")
state.pushScalaFunction(lua => {
val string = lua.checkString(1)
val start = (lua.checkInteger(2) match {