diff --git a/assets/opencomputers/lua/boot.lua b/assets/opencomputers/lua/boot.lua index 2c7c6b2b3..52aeadd91 100644 --- a/assets/opencomputers/lua/boot.lua +++ b/assets/opencomputers/lua/boot.lua @@ -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) \ No newline at end of file diff --git a/assets/opencomputers/lua/drivers/filesystem.lua b/assets/opencomputers/lua/drivers/filesystem.lua new file mode 100644 index 000000000..da6f55beb --- /dev/null +++ b/assets/opencomputers/lua/drivers/filesystem.lua @@ -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 \ No newline at end of file diff --git a/assets/opencomputers/lua/drivers/fs.lua b/assets/opencomputers/lua/drivers/fs.lua deleted file mode 100644 index ca2ba9a71..000000000 --- a/assets/opencomputers/lua/drivers/fs.lua +++ /dev/null @@ -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 diff --git a/assets/opencomputers/lua/drivers/gpu.lua b/assets/opencomputers/lua/drivers/gpu.lua index 8913e568c..5bfcfd7fe 100644 --- a/assets/opencomputers/lua/drivers/gpu.lua +++ b/assets/opencomputers/lua/drivers/gpu.lua @@ -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 \ No newline at end of file + 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 diff --git a/assets/opencomputers/lua/drivers/redstone.lua b/assets/opencomputers/lua/drivers/redstone.lua index b2d5c707d..907a820d7 100644 --- a/assets/opencomputers/lua/drivers/redstone.lua +++ b/assets/opencomputers/lua/drivers/redstone.lua @@ -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 diff --git a/assets/opencomputers/lua/kernel.lua b/assets/opencomputers/lua/kernel.lua index c2238ecca..bd158f51e 100644 --- a/assets/opencomputers/lua/kernel.lua +++ b/assets/opencomputers/lua/kernel.lua @@ -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())) diff --git a/assets/opencomputers/lua/rom/api/event.lua b/assets/opencomputers/lua/rom/api/event.lua index 443c484c9..16d438fb2 100644 --- a/assets/opencomputers/lua/rom/api/event.lua +++ b/assets/opencomputers/lua/rom/api/event.lua @@ -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 diff --git a/assets/opencomputers/lua/rom/api/filesystem.lua b/assets/opencomputers/lua/rom/api/filesystem.lua index 3efd14020..4e657ae8f 100644 --- a/assets/opencomputers/lua/rom/api/filesystem.lua +++ b/assets/opencomputers/lua/rom/api/filesystem.lua @@ -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) diff --git a/assets/opencomputers/lua/rom/api/term.lua b/assets/opencomputers/lua/rom/api/term.lua index 4d6ace6de..51344b5f9 100644 --- a/assets/opencomputers/lua/rom/api/term.lua +++ b/assets/opencomputers/lua/rom/api/term.lua @@ -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) \ No newline at end of file diff --git a/assets/opencomputers/lua/rom/bin/automount.lua b/assets/opencomputers/lua/rom/bin/automount.lua new file mode 100644 index 000000000..f5aef889f --- /dev/null +++ b/assets/opencomputers/lua/rom/bin/automount.lua @@ -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) \ No newline at end of file diff --git a/assets/opencomputers/lua/rom/sh.lua b/assets/opencomputers/lua/rom/bin/sh.lua similarity index 84% rename from assets/opencomputers/lua/rom/sh.lua rename to assets/opencomputers/lua/rom/bin/sh.lua index e89e74b0a..1d2cf9120 100644 --- a/assets/opencomputers/lua/rom/sh.lua +++ b/assets/opencomputers/lua/rom/bin/sh.lua @@ -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) diff --git a/assets/opencomputers/lua/rom/init.lua b/assets/opencomputers/lua/rom/init.lua index 342b925c6..d573fada7 100644 --- a/assets/opencomputers/lua/rom/init.lua +++ b/assets/opencomputers/lua/rom/init.lua @@ -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 diff --git a/li/cil/oc/Blocks.scala b/li/cil/oc/Blocks.scala index d5b56f9ed..a45c0dbd0 100644 --- a/li/cil/oc/Blocks.scala +++ b/li/cil/oc/Blocks.scala @@ -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) } } \ No newline at end of file diff --git a/li/cil/oc/api/FileSystem.scala b/li/cil/oc/api/FileSystem.scala index 26d126436..b9e47be33 100644 --- a/li/cil/oc/api/FileSystem.scala +++ b/li/cil/oc/api/FileSystem.scala @@ -16,8 +16,33 @@ import li.cil.oc.api.network.Node *

* Alternatively to using the factory methods for file systems in `Filesystem` * you are free to implement this interface yourself. + *

+ * 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. + *

+ * 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. + *

+ * 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. *

@@ -75,6 +100,20 @@ trait FileSystem extends Persistable { // ----------------------------------------------------------------------- // + /** + * Create the specified directory, and if necessary any parent directories + * that do not yet exist. + *

+ * 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. *

@@ -153,6 +192,23 @@ trait FileSystem extends Persistable { */ def close() + // ----------------------------------------------------------------------- // + + /** + * Actual directory creation implementation. + *

+ * 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. + *

+ * 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. *

diff --git a/li/cil/oc/server/component/Computer.scala b/li/cil/oc/server/component/Computer.scala index 21787a63f..2f7e6f2ce 100644 --- a/li/cil/oc/server/component/Computer.scala +++ b/li/cil/oc/server/component/Computer.scala @@ -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() } } diff --git a/li/cil/oc/server/component/Filesystem.scala b/li/cil/oc/server/component/Filesystem.scala index 9de63a938..a96ae9b2b 100644 --- a/li/cil/oc/server/component/Filesystem.scala +++ b/li/cil/oc/server/component/Filesystem.scala @@ -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" => diff --git a/li/cil/oc/server/component/GraphicsCard.scala b/li/cil/oc/server/component/GraphicsCard.scala index 90957c414..246a6d82e 100644 --- a/li/cil/oc/server/component/GraphicsCard.scala +++ b/li/cil/oc/server/component/GraphicsCard.scala @@ -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: _*) + }) + } } \ No newline at end of file diff --git a/li/cil/oc/server/driver/FileSystem.scala b/li/cil/oc/server/driver/FileSystem.scala index 95cb10c57..d909e24fd 100644 --- a/li/cil/oc/server/driver/FileSystem.scala +++ b/li/cil/oc/server/driver/FileSystem.scala @@ -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) diff --git a/li/cil/oc/server/fs/Capacity.scala b/li/cil/oc/server/fs/Capacity.scala index 968775211..9eb310b6f 100644 --- a/li/cil/oc/server/fs/Capacity.scala +++ b/li/cil/oc/server/fs/Capacity.scala @@ -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) + diff --git a/li/cil/oc/server/fs/FileInputStreamFileSystem.scala b/li/cil/oc/server/fs/FileInputStreamFileSystem.scala index 147d29765..43608ac0e 100644 --- a/li/cil/oc/server/fs/FileInputStreamFileSystem.scala +++ b/li/cil/oc/server/fs/FileInputStreamFileSystem.scala @@ -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))) } diff --git a/li/cil/oc/server/fs/FileOutputStreamFileSystem.scala b/li/cil/oc/server/fs/FileOutputStreamFileSystem.scala index 0b742966a..1fecac109 100644 --- a/li/cil/oc/server/fs/FileOutputStreamFileSystem.scala +++ b/li/cil/oc/server/fs/FileOutputStreamFileSystem.scala @@ -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)) } diff --git a/li/cil/oc/server/fs/FileSystem.scala b/li/cil/oc/server/fs/FileSystem.scala index e428f629e..add1e7a2d 100644 --- a/li/cil/oc/server/fs/FileSystem.scala +++ b/li/cil/oc/server/fs/FileSystem.scala @@ -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 diff --git a/li/cil/oc/server/fs/InputStreamFileSystem.scala b/li/cil/oc/server/fs/InputStreamFileSystem.scala index 28ce090eb..62434978e 100644 --- a/li/cil/oc/server/fs/InputStreamFileSystem.scala +++ b/li/cil/oc/server/fs/InputStreamFileSystem.scala @@ -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") } } diff --git a/li/cil/oc/server/fs/OutputStreamFileSystem.scala b/li/cil/oc/server/fs/OutputStreamFileSystem.scala index 5d65c7ced..4368e871c 100644 --- a/li/cil/oc/server/fs/OutputStreamFileSystem.scala +++ b/li/cil/oc/server/fs/OutputStreamFileSystem.scala @@ -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) diff --git a/li/cil/oc/server/network/Network.scala b/li/cil/oc/server/network/Network.scala index d836b4c32..7f9309e3b 100644 --- a/li/cil/oc/server/network/Network.scala +++ b/li/cil/oc/server/network/Network.scala @@ -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 { diff --git a/li/cil/oc/util/LuaStateFactory.scala b/li/cil/oc/util/LuaStateFactory.scala index c99e14f02..9ba8a3479 100644 --- a/li/cil/oc/util/LuaStateFactory.scala +++ b/li/cil/oc/util/LuaStateFactory.scala @@ -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 {