| package li.cil.oc.server.component.machine |
| |
| import java.io.{IOException, FileNotFoundException} |
| import java.util.logging.Level |
| import li.cil.oc.server.component.Machine |
| import li.cil.oc.server.component.Machine.State |
| import li.cil.oc.util.ScalaClosure._ |
| import li.cil.oc.util.{ScalaClosure, GameTimeFormatter} |
| import li.cil.oc.{OpenComputers, server, Settings} |
| import net.minecraft.nbt.NBTTagCompound |
| import org.luaj.vm2._ |
| import org.luaj.vm2.lib.jse.JsePlatform |
| import scala.Some |
| import scala.collection.convert.WrapAsScala._ |
| |
| class LuaJLuaArchitecture(machine: Machine) extends Architecture { |
| private var lua: Globals = _ |
| |
| private var thread: LuaThread = _ |
| |
| private var synchronizedCall: LuaFunction = _ |
| |
| private var synchronizedResult: LuaValue = _ |
| |
| private var doneWithInitRun = false |
| |
| private var memory = 0 |
| |
| // ----------------------------------------------------------------------- // |
| |
| private def node = machine.node |
| |
| private def components = machine.components |
| |
| // ----------------------------------------------------------------------- // |
| |
| def isInitialized = doneWithInitRun |
| |
| def recomputeMemory() = memory = machine.owner.installedMemory |
| |
| // ----------------------------------------------------------------------- // |
| |
| def runSynchronized() { |
| synchronizedResult = synchronizedCall.call() |
| synchronizedCall = null |
| } |
| |
| def runThreaded(enterState: State.Value) = { |
| try { |
| // Resume the Lua state and remember the number of results we get. |
| val results = enterState match { |
| case Machine.State.SynchronizedReturn => |
| // If we were doing a synchronized call, continue where we left off. |
| val result = thread.resume(synchronizedResult) |
| synchronizedResult = null |
| result |
| case Machine.State.Yielded => |
| if (!doneWithInitRun) { |
| doneWithInitRun = true |
| // We're doing the initialization run. |
| val result = thread.resume(LuaValue.NONE) |
| // We expect to get nothing here, if we do we had an error. |
| if (result.narg == 1) { |
| // Fake zero sleep to avoid stopping if there are no signals. |
| LuaValue.varargsOf(LuaValue.TRUE, LuaValue.valueOf(0)) |
| } |
| else { |
| LuaValue.NONE |
| } |
| } |
| else machine.popSignal() match { |
| case Some(signal) => |
| thread.resume(LuaValue.varargsOf(Array(LuaValue.valueOf(signal.name)) ++ signal.args.map(ScalaClosure.toLuaValue))) |
| case _ => |
| thread.resume(LuaValue.NONE) |
| } |
| case s => throw new AssertionError("Running computer from invalid state " + s.toString) |
| } |
| |
| // Check if the kernel is still alive. |
| if (thread.state.status == LuaThread.STATUS_SUSPENDED) { |
| // If we get one function it must be a wrapper for a synchronized |
| // call. The protocol is that a closure is pushed that is then called |
| // from the main server thread, and returns a table, which is in turn |
| // passed to the originating coroutine.yield(). |
| if (results.narg == 2 && results.isfunction(2)) { |
| synchronizedCall = results.checkfunction(2) |
| new ExecutionResult.SynchronizedCall() |
| } |
| // Check if we are shutting down, and if so if we're rebooting. This |
| // is signalled by boolean values, where `false` means shut down, |
| // `true` means reboot (i.e shutdown then start again). |
| else if (results.narg == 2 && results.`type`(2) == LuaValue.TBOOLEAN) { |
| new ExecutionResult.Shutdown(results.toboolean(2)) |
| } |
| else { |
| // If we have a single number, that's how long we may wait before |
| // resuming the state again. Note that the sleep may be interrupted |
| // early if a signal arrives in the meantime. If we have something |
| // else we just process the next signal or wait for one. |
| val ticks = if (results.narg == 2 && results.isnumber(2)) (results.todouble(2) * 20).toInt else Int.MaxValue |
| new ExecutionResult.Sleep(ticks) |
| } |
| } |
| // The kernel thread returned. If it threw we'd be in the catch below. |
| else { |
| // We're expecting the result of a pcall, if anything, so boolean + (result | string). |
| if (results.`type`(2) != LuaValue.TBOOLEAN || !(results.isstring(3) || results.isnil(3))) { |
| OpenComputers.log.warning("Kernel returned unexpected results.") |
| } |
| // The pcall *should* never return normally... but check for it nonetheless. |
| if (results.toboolean(1)) { |
| OpenComputers.log.warning("Kernel stopped unexpectedly.") |
| new ExecutionResult.Shutdown(false) |
| } |
| else { |
| val error = results.tojstring(3) |
| if (error != null) new ExecutionResult.Error(error) |
| else new ExecutionResult.Error("unknown error") |
| } |
| } |
| } |
| catch { |
| case e: LuaError => |
| OpenComputers.log.log(Level.WARNING, "Kernel crashed. This is a bug!" + e) |
| new ExecutionResult.Error("kernel panic: this is a bug, check your log file and report it") |
| case e: Throwable => |
| OpenComputers.log.log(Level.WARNING, "Unexpected error in kernel. This is a bug!", e) |
| new ExecutionResult.Error("kernel panic: this is a bug, check your log file and report it") |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def init() = { |
| lua = JsePlatform.debugGlobals() |
| lua.set("package", LuaValue.NIL) |
| lua.set("io", LuaValue.NIL) |
| lua.set("luajava", LuaValue.NIL) |
| |
| // Prepare table for os stuff. |
| val os = LuaValue.tableOf() |
| lua.set("os", os) |
| |
| // Remove some other functions we don't need and are dangerous. |
| lua.set("dofile", LuaValue.NIL) |
| lua.set("loadfile", LuaValue.NIL) |
| |
| // Provide some better Unicode support. |
| val unicode = LuaValue.tableOf() |
| |
| unicode.set("lower", (args: Varargs) => LuaValue.valueOf(args.checkjstring(1).toLowerCase)) |
| |
| unicode.set("upper", (args: Varargs) => LuaValue.valueOf(args.checkjstring(1).toUpperCase)) |
| |
| unicode.set("char", (args: Varargs) => LuaValue.valueOf(String.valueOf((1 to args.narg).map(args.checkint).map(_.toChar).toArray))) |
| |
| unicode.set("len", (args: Varargs) => LuaValue.valueOf(args.checkjstring(1).length)) |
| |
| unicode.set("reverse", (args: Varargs) => LuaValue.valueOf(args.checkjstring(1).reverse)) |
| |
| unicode.set("sub", (args: Varargs) => { |
| val string = args.checkjstring(1) |
| val start = math.max(0, args.checkint(2) match { |
| case i if i < 0 => string.length + i |
| case i => i - 1 |
| }) |
| val end = |
| if (args.narg > 2) math.min(string.length, args.checkint(3) match { |
| case i if i < 0 => string.length + i + 1 |
| case i => i |
| }) |
| else string.length |
| if (end <= start) LuaValue.valueOf("") |
| else LuaValue.valueOf(string.substring(start, end)) |
| }) |
| |
| lua.set("unicode", unicode) |
| |
| os.set("clock", (_: Varargs) => LuaValue.valueOf((machine.cpuTime + (System.nanoTime() - machine.cpuStart)) * 10e-10)) |
| |
| // Date formatting function. |
| os.set("date", (args: Varargs) => { |
| val format = |
| if (args.narg > 0 && args.isstring(1)) args.tojstring(1) |
| else "%d/%m/%y %H:%M:%S" |
| val time = |
| if (args.narg > 1 && args.isnumber(2)) args.todouble(2) * 1000 / 60 / 60 |
| else machine.worldTime + 6000 |
| |
| val dt = GameTimeFormatter.parse(time) |
| def fmt(format: String) = { |
| if (format == "*t") { |
| val table = LuaValue.tableOf(0, 8) |
| table.set("year", LuaValue.valueOf(dt.year)) |
| table.set("month", LuaValue.valueOf(dt.month)) |
| table.set("day", LuaValue.valueOf(dt.day)) |
| table.set("hour", LuaValue.valueOf(dt.hour)) |
| table.set("min", LuaValue.valueOf(dt.minute)) |
| table.set("sec", LuaValue.valueOf(dt.second)) |
| table.set("wday", LuaValue.valueOf(dt.weekDay)) |
| table.set("yday", LuaValue.valueOf(dt.yearDay)) |
| table |
| } |
| else { |
| LuaValue.valueOf(GameTimeFormatter.format(format, dt)) |
| } |
| } |
| |
| // Just ignore the allowed leading '!', Minecraft has no time zones... |
| if (format.startsWith("!")) |
| fmt(format.substring(1)) |
| else |
| fmt(format) |
| }) |
| |
| // Return ingame time for os.time(). |
| os.set("time", (_: Varargs) => { |
| // Game time is in ticks, so that each day has 24000 ticks, meaning |
| // one hour is game time divided by one thousand. Also, Minecraft |
| // starts days at 6 o'clock, so we add those six hours. Thus: |
| // timestamp = (time + 6000) * 60[kh] * 60[km] / 1000[s] |
| LuaValue.valueOf((machine.worldTime + 6000) * 60 * 60 / 1000) |
| }) |
| |
| // Computer API, stuff that kinda belongs to os, but we don't want to |
| // clutter it. |
| val computer = LuaValue.tableOf() |
| |
| // Allow getting the real world time for timeouts. |
| computer.set("realTime", (_: Varargs) => LuaValue.valueOf(System.currentTimeMillis() / 1000.0)) |
| |
| // The time the computer has been running, as opposed to the CPU time. |
| // World time is in ticks, and each second has 20 ticks. Since we |
| // want uptime() to return real seconds, though, we'll divide it |
| // accordingly. |
| computer.set("uptime", (_: Varargs) => LuaValue.valueOf((machine.worldTime - machine.timeStarted) / 20.0)) |
| |
| // Allow the computer to figure out its own id in the component network. |
| computer.set("address", (_: Varargs) => Option(node.address) match { |
| case Some(address) => LuaValue.valueOf(address) |
| case _ => LuaValue.NIL |
| }) |
| |
| // Are we a robot? (No this is not a CAPTCHA.) |
| computer.set("isRobot", (_: Varargs) => LuaValue.valueOf(machine.isRobot)) |
| |
| computer.set("freeMemory", (_: Varargs) => LuaValue.valueOf(memory / 2)) |
| |
| computer.set("totalMemory", (_: Varargs) => LuaValue.valueOf(memory)) |
| |
| computer.set("pushSignal", (args: Varargs) => LuaValue.valueOf(machine.signal(args.checkjstring(1), toSimpleJavaObjects(args, 2): _*))) |
| |
| // And its ROM address. |
| computer.set("romAddress", (_: Varargs) => machine.rom.fold(LuaValue.NIL)(rom => Option(rom.node.address) match { |
| case Some(address) => LuaValue.valueOf(address) |
| case _ => LuaValue.NIL |
| })) |
| |
| // And it's /tmp address... |
| computer.set("tmpAddress", (_: Varargs) => machine.tmp.fold(LuaValue.NIL)(tmp => Option(tmp.node.address) match { |
| case Some(address) => LuaValue.valueOf(address) |
| case _ => LuaValue.NIL |
| })) |
| |
| // User management. |
| computer.set("users", (_: Varargs) => LuaValue.varargsOf(machine.users.map(LuaValue.valueOf))) |
| |
| computer.set("addUser", (args: Varargs) => { |
| machine.addUser(args.checkjstring(1)) |
| LuaValue.TRUE |
| }) |
| |
| computer.set("removeUser", (args: Varargs) => LuaValue.valueOf(machine.removeUser(args.checkjstring(1)))) |
| |
| computer.set("energy", (_: Varargs) => LuaValue.valueOf(node.globalBuffer)) |
| |
| computer.set("maxEnergy", (_: Varargs) => LuaValue.valueOf(node.globalBufferSize)) |
| |
| // Set the computer table. |
| lua.set("computer", computer) |
| |
| // Whether bytecode may be loaded directly. |
| lua.set("allowBytecode", (_: Varargs) => LuaValue.valueOf(Settings.get.allowBytecode)) |
| |
| // How long programs may run without yielding before we stop them. |
| lua.set("timeout", LuaValue.valueOf(Settings.get.timeout)) |
| |
| // Component interaction stuff. |
| val component = LuaValue.tableOf() |
| |
| component.set("list", (args: Varargs) => components.synchronized { |
| val filter = if (args.isstring(1)) Option(args.tojstring(1)) else None |
| val table = LuaValue.tableOf(0, components.size) |
| for ((address, name) <- components) { |
| if (filter.isEmpty || name.contains(filter.get)) { |
| table.set(address, name) |
| } |
| } |
| table |
| }) |
| |
| component.set("type", (args: Varargs) => components.synchronized { |
| components.get(args.checkjstring(1)) match { |
| case Some(name: String) => |
| LuaValue.valueOf(name) |
| case _ => |
| LuaValue.varargsOf(LuaValue.NIL, LuaValue.valueOf("no such component")) |
| } |
| }) |
| |
| component.set("methods", (args: Varargs) => { |
| Option(node.network.node(args.checkjstring(1))) match { |
| case Some(component: server.network.Component) if component.canBeSeenFrom(node) || component == node => |
| val table = LuaValue.tableOf() |
| for (method <- component.methods()) { |
| table.set(method, LuaValue.valueOf(component.isDirect(method))) |
| } |
| table |
| case _ => |
| LuaValue.varargsOf(LuaValue.NIL, LuaValue.valueOf("no such component")) |
| } |
| }) |
| |
| component.set("invoke", (args: Varargs) => { |
| val address = args.checkjstring(1) |
| val method = args.checkjstring(2) |
| val params = toSimpleJavaObjects(args, 3) |
| try { |
| machine.invoke(address, method, params) match { |
| case results: Array[_] => |
| LuaValue.varargsOf(Array(LuaValue.TRUE) ++ results.map(toLuaValue)) |
| case _ => |
| LuaValue.TRUE |
| } |
| } |
| catch { |
| case e: Throwable => |
| if (Settings.get.logLuaCallbackErrors && !e.isInstanceOf[Machine.LimitReachedException]) { |
| OpenComputers.log.log(Level.WARNING, "Exception in Lua callback.", e) |
| } |
| e match { |
| case _: Machine.LimitReachedException => |
| LuaValue.NONE |
| case e: IllegalArgumentException if e.getMessage != null => |
| LuaValue.varargsOf(LuaValue.FALSE, LuaValue.valueOf(e.getMessage)) |
| case e: Throwable if e.getMessage != null => |
| LuaValue.varargsOf(LuaValue.TRUE, LuaValue.NIL, LuaValue.valueOf(e.getMessage)) |
| case _: IndexOutOfBoundsException => |
| LuaValue.varargsOf(LuaValue.FALSE, LuaValue.valueOf("index out of bounds")) |
| case _: IllegalArgumentException => |
| LuaValue.varargsOf(LuaValue.FALSE, LuaValue.valueOf("bad argument")) |
| case _: NoSuchMethodException => |
| LuaValue.varargsOf(LuaValue.FALSE, LuaValue.valueOf("no such method")) |
| case _: FileNotFoundException => |
| LuaValue.varargsOf(LuaValue.TRUE, LuaValue.NIL, LuaValue.valueOf("file not found")) |
| case _: SecurityException => |
| LuaValue.varargsOf(LuaValue.TRUE, LuaValue.NIL, LuaValue.valueOf("access denied")) |
| case _: IOException => |
| LuaValue.varargsOf(LuaValue.TRUE, LuaValue.NIL, LuaValue.valueOf("i/o error")) |
| case e: Throwable => |
| OpenComputers.log.log(Level.WARNING, "Unexpected error in Lua callback.", e) |
| LuaValue.varargsOf(LuaValue.TRUE, LuaValue.NIL, LuaValue.valueOf("unknown error")) |
| } |
| } |
| }) |
| |
| lua.set("component", component) |
| |
| recomputeMemory() |
| |
| val kernel = lua.load(classOf[Machine].getResourceAsStream(Settings.scriptPath + "kernel.lua"), "=kernel", "t", lua) |
| thread = new LuaThread(lua, kernel) // Left as the first value on the stack. |
| |
| true |
| } |
| |
| def close() = { |
| lua = null |
| thread = null |
| synchronizedCall = null |
| synchronizedResult = null |
| doneWithInitRun = false |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def load(nbt: NBTTagCompound) { |
| if (machine.isRunning) { |
| machine.stop() |
| machine.start() |
| } |
| } |
| |
| def save(nbt: NBTTagCompound) {} |
| } |