| package li.cil.oc.server.machine |
| |
| import java.util.concurrent.TimeUnit |
| |
| import li.cil.oc.OpenComputers |
| import li.cil.oc.Settings |
| import li.cil.oc.api.Driver |
| import li.cil.oc.api.FileSystem |
| import li.cil.oc.api.Network |
| import li.cil.oc.api.detail.MachineAPI |
| import li.cil.oc.api.driver.item.Processor |
| import li.cil.oc.api.machine |
| import li.cil.oc.api.machine.Architecture |
| import li.cil.oc.api.machine.Arguments |
| import li.cil.oc.api.machine.Callback |
| import li.cil.oc.api.machine.Context |
| import li.cil.oc.api.machine.ExecutionResult |
| import li.cil.oc.api.machine.LimitReachedException |
| import li.cil.oc.api.machine.MachineHost |
| import li.cil.oc.api.machine.Value |
| import li.cil.oc.api.network.Component |
| import li.cil.oc.api.network.Message |
| import li.cil.oc.api.network.Node |
| import li.cil.oc.api.network.Visibility |
| import li.cil.oc.api.prefab |
| import li.cil.oc.common.EventHandler |
| import li.cil.oc.common.SaveHandler |
| import li.cil.oc.common.Slot |
| import li.cil.oc.common.Tier |
| import li.cil.oc.common.tileentity |
| import li.cil.oc.server.PacketSender |
| import li.cil.oc.server.driver.Registry |
| import li.cil.oc.util.ExtendedNBT._ |
| import li.cil.oc.util.ResultWrapper.result |
| import li.cil.oc.util.ThreadPoolFactory |
| import net.minecraft.client.Minecraft |
| import net.minecraft.entity.player.EntityPlayer |
| import net.minecraft.item.ItemStack |
| import net.minecraft.nbt._ |
| import net.minecraft.server.MinecraftServer |
| import net.minecraft.server.integrated.IntegratedServer |
| import net.minecraftforge.common.util.Constants.NBT |
| |
| import scala.Array.canBuildFrom |
| import scala.collection.convert.WrapAsJava._ |
| import scala.collection.convert.WrapAsScala._ |
| import scala.collection.mutable |
| |
| class Machine(val host: MachineHost) extends prefab.ManagedEnvironment with machine.Machine with Runnable { |
| override val node = Network.newNode(this, Visibility.Network). |
| withComponent("computer", Visibility.Neighbors). |
| withConnector(Settings.get.bufferComputer). |
| create() |
| |
| val tmp = if (Settings.get.tmpSize > 0) { |
| Option(FileSystem.asManagedEnvironment(FileSystem. |
| fromMemory(Settings.get.tmpSize * 1024), "tmpfs", null, null, 5)) |
| } else None |
| |
| var architecture: Architecture = _ |
| |
| private[machine] val state = mutable.Stack(Machine.State.Stopped) |
| |
| private val _components = mutable.Map.empty[String, String] |
| |
| private val addedComponents = mutable.Set.empty[Component] |
| |
| private val _users = mutable.Set.empty[String] |
| |
| private val signals = mutable.Queue.empty[Machine.Signal] |
| |
| var maxComponents = 0 |
| |
| private var maxCallBudget = 1.0 |
| |
| private var hasMemory = false |
| |
| @volatile private var callBudget = 0.0 |
| |
| // We want to ignore the call limit in synchronized calls to avoid errors. |
| private var inSynchronizedCall = false |
| |
| // ----------------------------------------------------------------------- // |
| |
| var worldTime = 0L // Game-world time for os.time(). |
| |
| private var uptime = 0L // Game-world time [ticks] for os.uptime(). |
| |
| private var cpuTotal = 0L // Pseudo-real-world time [ns] for os.clock(). |
| |
| private var cpuStart = 0L // Pseudo-real-world time [ns] for os.clock(). |
| |
| private var remainIdle = 0 // Ticks left to sleep before resuming. |
| |
| private var remainingPause = 0 // Ticks left to wait before resuming. |
| |
| private var usersChanged = false // Send updated users list to clients? |
| |
| private var message: Option[String] = None // For error messages. |
| |
| private var cost = Settings.get.computerCost * Settings.get.tickFrequency |
| |
| // ----------------------------------------------------------------------- // |
| |
| override def onHostChanged(): Unit = { |
| val components = host.internalComponents |
| maxComponents = components.foldLeft(0)((sum, item) => sum + (Option(item) match { |
| case Some(stack) => Option(Driver.driverFor(stack, host.getClass)) match { |
| case Some(driver: Processor) => driver.supportedComponents(stack) |
| case _ => 0 |
| } |
| case _ => 0 |
| })) |
| maxCallBudget = components.foldLeft(0.0)((sum, item) => sum + (Option(item) match { |
| case Some(stack) => Option(Driver.driverFor(stack, host.getClass)) match { |
| case Some(driver: Processor) if driver.slot(stack) == Slot.CPU => Settings.get.callBudgets(driver.tier(stack) max Tier.One min Tier.Three) |
| case _ => 0 |
| } |
| case _ => 0 |
| })) |
| var newArchitecture: Architecture = null |
| components.find { |
| case stack: ItemStack => Option(Driver.driverFor(stack, host.getClass)) match { |
| case Some(driver: Processor) if driver.slot(stack) == Slot.CPU => |
| Option(driver.architecture(stack)) match { |
| case Some(clazz) => |
| if (architecture == null || architecture.getClass != clazz) try { |
| newArchitecture = clazz.getConstructor(classOf[machine.Machine]).newInstance(this) |
| } |
| catch { |
| case t: Throwable => OpenComputers.log.warn("Failed instantiating a CPU architecture.", t) |
| } |
| else { |
| newArchitecture = architecture |
| } |
| true |
| case _ => false |
| } |
| case _ => false |
| } |
| case _ => false |
| } |
| // This needs to operate synchronized against the worker thread, to avoid the |
| // architecture changing while it is currently being executed. |
| if (newArchitecture != architecture) this.synchronized { |
| architecture = newArchitecture |
| if (architecture != null && node.network != null) architecture.onConnect() |
| } |
| hasMemory = Option(architecture).fold(false)(_.recomputeMemory(components)) |
| } |
| |
| override def components = scala.collection.convert.WrapAsJava.mapAsJavaMap(_components) |
| |
| def componentCount = (_components.foldLeft(0.0)((acc, entry) => entry match { |
| case (_, name) => acc + (if (name != "filesystem") 1.0 else 0.25) |
| }) + addedComponents.foldLeft(0.0)((acc, component) => acc + (if (component.name != "filesystem") 1 else 0.25)) - 1).toInt // -1 = this computer |
| |
| override def tmpAddress = tmp.fold(null: String)(_.node.address) |
| |
| def lastError = message.orNull |
| |
| override def setCostPerTick(value: Double) = cost = value * Settings.get.tickFrequency |
| |
| override def getCostPerTick = cost / Settings.get.tickFrequency |
| |
| override def users = _users.synchronized(_users.toArray) |
| |
| override def upTime() = { |
| // Convert from old saves (set to -timeStarted on load). |
| if (uptime < 0) { |
| uptime = worldTime + uptime |
| } |
| // 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. |
| uptime / 20.0 |
| } |
| |
| override def cpuTime = (cpuTotal + (System.nanoTime() - cpuStart)) * 10e-10 |
| |
| // ----------------------------------------------------------------------- // |
| |
| override def canInteract(player: String) = !Settings.get.canComputersBeOwned || |
| _users.synchronized(_users.isEmpty || _users.contains(player)) || |
| MinecraftServer.getServer.isSinglePlayer || { |
| val config = MinecraftServer.getServer.getConfigurationManager |
| val entity = config.func_152612_a(player) |
| entity != null && config.func_152596_g(entity.getGameProfile) |
| } |
| |
| override def isRunning = state.synchronized(state.top != Machine.State.Stopped && state.top != Machine.State.Stopping) |
| |
| override def isPaused = state.synchronized(state.top == Machine.State.Paused && remainingPause > 0) |
| |
| override def start(): Boolean = state.synchronized(state.top match { |
| case Machine.State.Stopped if node.network != null => |
| onHostChanged() |
| processAddedComponents() |
| verifyComponents() |
| if (!Settings.get.ignorePower && node.globalBuffer < cost) { |
| // No beep! We have no energy after all :P |
| crash("gui.Error.NoEnergy") |
| false |
| } |
| else if (architecture == null || maxComponents == 0) { |
| beep("-") |
| crash("gui.Error.NoCPU") |
| false |
| } |
| else if (componentCount > maxComponents) { |
| beep("-..") |
| crash("gui.Error.ComponentOverflow") |
| false |
| } |
| else if (!hasMemory) { |
| beep("-.") |
| crash("gui.Error.NoRAM") |
| false |
| } |
| else if (!init()) { |
| beep("--") |
| false |
| } |
| else { |
| switchTo(Machine.State.Starting) |
| uptime = 0 |
| node.sendToReachable("computer.started") |
| true |
| } |
| case Machine.State.Paused if remainingPause > 0 => |
| remainingPause = 0 |
| host.markChanged() |
| true |
| case Machine.State.Stopping => |
| switchTo(Machine.State.Restarting) |
| EventHandler.unscheduleClose(this) |
| true |
| case _ => |
| false |
| }) |
| |
| override def pause(seconds: Double): Boolean = { |
| val ticksToPause = math.max((seconds * 20).toInt, 0) |
| def shouldPause(state: Machine.State.Value) = state match { |
| case Machine.State.Stopping | Machine.State.Stopped => false |
| case Machine.State.Paused if ticksToPause <= remainingPause => false |
| case _ => true |
| } |
| if (shouldPause(state.synchronized(state.top))) { |
| // Check again when we get the lock, might have changed since. |
| Machine.this.synchronized(state.synchronized(if (shouldPause(state.top)) { |
| if (state.top != Machine.State.Paused) { |
| assert(!state.contains(Machine.State.Paused)) |
| state.push(Machine.State.Paused) |
| } |
| remainingPause = ticksToPause |
| host.markChanged() |
| return true |
| })) |
| } |
| false |
| } |
| |
| override def stop() = state.synchronized(state.headOption match { |
| case Machine.State.Stopped | Machine.State.Stopping => |
| false |
| case _ => |
| state.push(Machine.State.Stopping) |
| EventHandler.scheduleClose(this) |
| true |
| }) |
| |
| override def beep(frequency: Short, duration: Short): Unit = { |
| PacketSender.sendSound(host.world, host.xPosition, host.yPosition, host.zPosition, frequency, duration) |
| } |
| |
| override def beep(pattern: String) { |
| PacketSender.sendSound(host.world, host.xPosition, host.yPosition, host.zPosition, pattern) |
| } |
| |
| override def crash(message: String) = { |
| this.message = Option(message) |
| state.synchronized { |
| val result = stop() |
| if (state.top == Machine.State.Stopping) { |
| // When crashing, make sure there's no "Running" left in the stack. |
| state.clear() |
| state.push(Machine.State.Stopping) |
| } |
| result |
| } |
| } |
| |
| override def signal(name: String, args: AnyRef*) = state.synchronized(state.top match { |
| case Machine.State.Stopped | Machine.State.Stopping => false |
| case _ => signals.synchronized { |
| if (signals.size >= 256) false |
| else if (args == null) { |
| signals.enqueue(new Machine.Signal(name, Array.empty)) |
| true |
| } |
| else { |
| signals.enqueue(new Machine.Signal(name, args.map { |
| case null | Unit | None => null |
| case arg: java.lang.Boolean => arg |
| case arg: java.lang.Character => Double.box(arg.toDouble) |
| case arg: java.lang.Long => arg |
| case arg: java.lang.Number => Double.box(arg.doubleValue) |
| case arg: java.lang.String => arg |
| case arg: Array[Byte] => arg |
| case arg: Map[_, _] if arg.isEmpty || arg.head._1.isInstanceOf[String] && arg.head._2.isInstanceOf[String] => arg |
| case arg: NBTTagCompound => arg |
| case arg => |
| OpenComputers.log.warn("Trying to push signal with an unsupported argument of type " + arg.getClass.getName) |
| null |
| }.toArray[AnyRef])) |
| true |
| } |
| } |
| }) |
| |
| override def popSignal(): Machine.Signal = signals.synchronized(if (signals.isEmpty) null else signals.dequeue().convert()) |
| |
| override def methods(value: scala.AnyRef) = Callbacks(value).map(entry => { |
| val (name, callback) = entry |
| name -> callback.annotation |
| }) |
| |
| override def invoke(address: String, method: String, args: Array[AnyRef]) = |
| if (node != null && node.network != null) Option(node.network.node(address)) match { |
| case Some(component: Component) if component.canBeSeenFrom(node) || component == node => |
| val direct = component.annotation(method).direct |
| if (direct && architecture.isInitialized) { |
| checkLimit(component.annotation(method).limit) |
| } |
| component.invoke(method, this, args: _*) |
| case _ => throw new IllegalArgumentException("no such component") |
| } |
| else { |
| // Not really, but makes the VM stop, which is what we want in this case, |
| // because it means we've been disconnected / disposed already. |
| throw new LimitReachedException() |
| } |
| |
| override def invoke(value: Value, method: String, args: Array[AnyRef]): Array[AnyRef] = Callbacks(value).get(method) match { |
| case Some(callback) => |
| val direct = callback.annotation.direct |
| if (direct && architecture.isInitialized) { |
| checkLimit(callback.annotation.limit) |
| } |
| Registry.convert(callback(value, this, new ArgumentsImpl(Seq(args: _*)))) |
| case _ => throw new NoSuchMethodException() |
| } |
| |
| private def checkLimit(limit: Int): Unit = if (!inSynchronizedCall) { |
| val callCost = math.max(1.0 / limit, 0.001) |
| if (callCost >= callBudget) { |
| throw new LimitReachedException() |
| } |
| callBudget -= callCost |
| } |
| |
| override def addUser(name: String) { |
| if (_users.size >= Settings.get.maxUsers) |
| throw new Exception("too many users") |
| |
| if (_users.contains(name)) |
| throw new Exception("user exists") |
| if (name.length > Settings.get.maxUsernameLength) |
| throw new Exception("username too long") |
| if (!MinecraftServer.getServer.getConfigurationManager.getAllUsernames.contains(name)) |
| throw new Exception("player must be online") |
| |
| _users.synchronized { |
| _users += name |
| usersChanged = true |
| } |
| } |
| |
| override def removeUser(name: String) = _users.synchronized { |
| val success = _users.remove(name) |
| if (success) { |
| usersChanged = true |
| } |
| success |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| @Callback(doc = """function():boolean -- Starts the computer. Returns true if the state changed.""") |
| def start(context: Context, args: Arguments): Array[AnyRef] = |
| result(!isPaused && start()) |
| |
| @Callback(doc = """function():boolean -- Stops the computer. Returns true if the state changed.""") |
| def stop(context: Context, args: Arguments): Array[AnyRef] = |
| result(stop()) |
| |
| @Callback(direct = true, doc = """function():boolean -- Returns whether the computer is running.""") |
| def isRunning(context: Context, args: Arguments): Array[AnyRef] = |
| result(isRunning) |
| |
| @Callback(doc = """function([frequency:number[, duration:number]]) -- Plays a tone, useful to alert users via audible feedback.""") |
| def beep(context: Context, args: Arguments): Array[AnyRef] = { |
| val frequency = args.optInteger(0, 440) |
| if (frequency < 20 || frequency > 2000) { |
| throw new IllegalArgumentException("invalid frequency, must be in [20, 2000]") |
| } |
| val duration = args.optDouble(1, 0.1) |
| val durationInMilliseconds = math.max(50, math.min(5000, (duration * 1000).toInt)) |
| context.pause(durationInMilliseconds / 1000.0) |
| PacketSender.sendSound(host.world, host.xPosition, host.yPosition, host.zPosition, frequency, durationInMilliseconds) |
| null |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def isExecuting = state.synchronized(state.contains(Machine.State.Running)) |
| |
| override val canUpdate = true |
| |
| override def update() = if (state.synchronized(state.top != Machine.State.Stopped)) { |
| // Add components that were added since the last update to the actual list |
| // of components if we can see them. We use this delayed approach to avoid |
| // issues with components that have a visibility lower than their |
| // reachability, because in that case if they get connected in the wrong |
| // order we wouldn't add them (since they'd be invisible in their connect |
| // message, and only become visible with a later node-to-node connection, |
| // but that wouldn't trigger a connect message anymore due to the higher |
| // reachability). |
| processAddedComponents() |
| |
| // Component overflow check, crash if too many components are connected, to |
| // avoid confusion on the user's side due to components not showing up. |
| if (componentCount > maxComponents) { |
| crash("gui.Error.ComponentOverflow") |
| } |
| |
| // Update world time for time() and uptime(). |
| worldTime = host.world.getWorldTime |
| uptime += 1 |
| |
| if (remainIdle > 0) { |
| remainIdle -= 1 |
| } |
| |
| // Reset direct call budget. |
| callBudget = maxCallBudget |
| |
| // Make sure we have enough power. |
| if (host.world.getTotalWorldTime % Settings.get.tickFrequency == 0) { |
| state.synchronized(state.top match { |
| case Machine.State.Paused | |
| Machine.State.Restarting | |
| Machine.State.Stopping | |
| Machine.State.Stopped => // No power consumption. |
| case Machine.State.Sleeping if remainIdle > 0 && signals.isEmpty => |
| if (!node.tryChangeBuffer(-cost * Settings.get.sleepCostFactor)) { |
| crash("gui.Error.NoEnergy") |
| } |
| case _ => |
| if (!node.tryChangeBuffer(-cost)) { |
| crash("gui.Error.NoEnergy") |
| } |
| }) |
| } |
| |
| // Avoid spamming user list across the network. |
| if (host.world.getTotalWorldTime % 20 == 0 && usersChanged) { |
| val list = _users.synchronized { |
| usersChanged = false |
| users |
| } |
| host match { |
| case computer: tileentity.traits.Computer => PacketSender.sendComputerUserList(computer, list) |
| case _ => |
| } |
| } |
| |
| // Check if we should switch states. These are all the states in which we're |
| // guaranteed that the executor thread isn't running anymore. |
| state.synchronized(state.top) match { |
| // Booting up. |
| case Machine.State.Starting => |
| verifyComponents() |
| switchTo(Machine.State.Yielded) |
| // Computer is rebooting. |
| case Machine.State.Restarting => |
| close() |
| if (Settings.get.eraseTmpOnReboot) { |
| tmp.foreach(_.node.remove()) // To force deleting contents. |
| tmp.foreach(tmp => node.connect(tmp.node)) |
| } |
| node.sendToReachable("computer.stopped") |
| start() |
| // Resume from pauses based on sleep or signal underflow. |
| case Machine.State.Sleeping if remainIdle <= 0 || signals.nonEmpty => |
| switchTo(Machine.State.Yielded) |
| // Resume in case we paused because the game was paused. |
| case Machine.State.Paused => |
| if (remainingPause > 0) { |
| remainingPause -= 1 |
| } |
| else { |
| verifyComponents() // In case we're resuming after loading. |
| state.pop() |
| switchTo(state.top) // Trigger execution if necessary. |
| } |
| // Perform a synchronized call (message sending). |
| case Machine.State.SynchronizedCall => |
| // We switch into running state, since we'll behave as though the call |
| // were performed from our executor thread. |
| switchTo(Machine.State.Running) |
| try { |
| inSynchronizedCall = true |
| architecture.runSynchronized() |
| inSynchronizedCall = false |
| // Check if the callback called pause() or stop(). |
| state.top match { |
| case Machine.State.Running => |
| switchTo(Machine.State.SynchronizedReturn) |
| case Machine.State.Paused => |
| state.pop() // Paused |
| state.pop() // Running, no switchTo to avoid new future. |
| state.push(Machine.State.SynchronizedReturn) |
| state.push(Machine.State.Paused) |
| case Machine.State.Stopping => |
| state.clear() |
| state.push(Machine.State.Stopping) |
| case _ => throw new AssertionError() |
| } |
| } |
| catch { |
| case e: java.lang.Error if e.getMessage == "not enough memory" => |
| crash("gui.Error.OutOfMemory") |
| case e: Throwable => |
| OpenComputers.log.warn("Faulty architecture implementation for synchronized calls.", e) |
| crash("gui.Error.InternalError") |
| } |
| finally { |
| inSynchronizedCall = false |
| } |
| case _ => // Nothing special to do, just avoid match errors. |
| } |
| |
| // Finally check if we should stop the computer. We cannot lock the state |
| // because we may have to wait for the executor thread to finish, which |
| // might turn into a deadlock depending on where it currently is. |
| state.synchronized(state.top) match { |
| // Computer is shutting down. |
| case Machine.State.Stopping => Machine.this.synchronized(state.synchronized(tryClose())) |
| case _ => |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| override def onMessage(message: Message) { |
| message.data match { |
| case Array(name: String, args@_*) if message.name == "computer.signal" => |
| signal(name, Seq(message.source.address) ++ args: _*) |
| case Array(player: EntityPlayer, name: String, args@_*) if message.name == "computer.checked_signal" => |
| if (canInteract(player.getCommandSenderName)) |
| signal(name, Seq(message.source.address) ++ args: _*) |
| case _ => |
| if (message.name == "computer.start" && !isPaused) start() |
| else if (message.name == "computer.stop") stop() |
| } |
| } |
| |
| override def onConnect(node: Node) { |
| if (node == this.node) { |
| _components += this.node.address -> this.node.name |
| tmp.foreach(fs => node.connect(fs.node)) |
| Option(architecture).foreach(_.onConnect()) |
| } |
| else { |
| node match { |
| case component: Component => addComponent(component) |
| case _ => |
| } |
| } |
| // For computers, to generate the components in their inventory. |
| host.onMachineConnect(node) |
| } |
| |
| override def onDisconnect(node: Node) { |
| if (node == this.node) { |
| close() |
| tmp.foreach(_.node.remove()) |
| } |
| else { |
| node match { |
| case component: Component => removeComponent(component) |
| case _ => |
| } |
| } |
| // For computers, to save the components in their inventory. |
| host.onMachineDisconnect(node) |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def addComponent(component: Component) { |
| if (!_components.contains(component.address)) { |
| addedComponents += component |
| } |
| } |
| |
| def removeComponent(component: Component) { |
| if (_components.contains(component.address)) { |
| _components.synchronized(_components -= component.address) |
| signal("component_removed", component.address, component.name) |
| } |
| addedComponents -= component |
| } |
| |
| private def processAddedComponents() { |
| if (addedComponents.nonEmpty) { |
| for (component <- addedComponents) { |
| if (component.canBeSeenFrom(node)) { |
| _components.synchronized(_components += component.address -> component.name) |
| // Skip the signal if we're not initialized yet, since we'd generate a |
| // duplicate in the startup script otherwise. |
| if (architecture != null && architecture.isInitialized) { |
| signal("component_added", component.address, component.name) |
| } |
| } |
| } |
| addedComponents.clear() |
| } |
| } |
| |
| private def verifyComponents() { |
| val invalid = mutable.Set.empty[String] |
| for ((address, name) <- _components) { |
| node.network.node(address) match { |
| case component: Component if component.name == name => // All is well. |
| case _ => |
| if (name == "filesystem") { |
| OpenComputers.log.trace(s"A component of type '$name' disappeared ($address)! This usually means that it didn't save its node.") |
| OpenComputers.log.trace("If this was a file system provided by a ComputerCraft peripheral, this is normal.") |
| } |
| else OpenComputers.log.warn(s"A component of type '$name' disappeared ($address)! This usually means that it didn't save its node.") |
| signal("component_removed", address, name) |
| invalid += address |
| } |
| } |
| for (address <- invalid) { |
| _components -= address |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| override def load(nbt: NBTTagCompound) = Machine.this.synchronized(state.synchronized { |
| assert(state.top == Machine.State.Stopped) |
| assert(_users.isEmpty) |
| assert(signals.isEmpty) |
| state.clear() |
| |
| super.load(nbt) |
| |
| state.pushAll(nbt.getIntArray("state").reverseMap(Machine.State(_))) |
| nbt.getTagList("users", NBT.TAG_STRING).foreach((tag: NBTTagString) => _users += tag.func_150285_a_()) |
| if (nbt.hasKey("message")) { |
| message = Some(nbt.getString("message")) |
| } |
| |
| _components ++= nbt.getTagList("components", NBT.TAG_COMPOUND).map((tag: NBTTagCompound) => |
| tag.getString("address") -> tag.getString("name")) |
| |
| tmp.foreach(fs => { |
| if (nbt.hasKey("tmp")) fs.load(nbt.getCompoundTag("tmp")) |
| else fs.load(SaveHandler.loadNBT(nbt, node.address + "_tmp")) |
| }) |
| |
| if (state.nonEmpty && isRunning && init()) try { |
| architecture.load(nbt) |
| |
| signals ++= nbt.getTagList("signals", NBT.TAG_COMPOUND).map((signalNbt: NBTTagCompound) => { |
| val argsNbt = signalNbt.getCompoundTag("args") |
| val argsLength = argsNbt.getInteger("length") |
| new Machine.Signal(signalNbt.getString("name"), |
| (0 until argsLength).map("arg" + _).map(argsNbt.getTag).map { |
| case tag: NBTTagByte if tag.func_150290_f == -1 => null |
| case tag: NBTTagByte => Boolean.box(tag.func_150290_f == 1) |
| case tag: NBTTagLong => Long.box(tag.func_150291_c) |
| case tag: NBTTagDouble => Double.box(tag.func_150286_g) |
| case tag: NBTTagString => tag.func_150285_a_ |
| case tag: NBTTagByteArray => tag.func_150292_c |
| case tag: NBTTagList => |
| val data = mutable.Map.empty[String, String] |
| for (i <- 0 until tag.tagCount by 2) { |
| data += tag.getStringTagAt(i) -> tag.getStringTagAt(i + 1) |
| } |
| data |
| case tag: NBTTagCompound => tag |
| case _ => null |
| }.toArray[AnyRef]) |
| }) |
| |
| uptime = nbt.getLong("uptime") |
| cpuTotal = nbt.getLong("cpuTime") |
| remainingPause = nbt.getInteger("remainingPause") |
| |
| // Delay execution for a second to allow the world around us to settle. |
| if (state.top != Machine.State.Restarting) { |
| pause(Settings.get.startupDelay) |
| } |
| } |
| catch { |
| case t: Throwable => |
| OpenComputers.log.error( s"""Unexpected error loading a state of computer at (${host.xPosition}, ${host.yPosition}, ${host.zPosition}). """ + |
| s"""State: ${state.headOption.fold("no state")(_.toString)}. Unless you're upgrading/downgrading across a major version, please report this! Thank you.""", t) |
| close() |
| } |
| else { |
| // Clean up in case we got a weird state stack. |
| close() |
| } |
| }) |
| |
| override def save(nbt: NBTTagCompound): Unit = Machine.this.synchronized(state.synchronized { |
| assert(!isExecuting) // Lock on 'this' should guarantee this. |
| |
| // Make sure we don't continue running until everything has saved. |
| pause(0.05) |
| |
| super.save(nbt) |
| |
| // Make sure the component list is up-to-date. |
| processAddedComponents() |
| |
| nbt.setIntArray("state", state.map(_.id).toArray) |
| nbt.setNewTagList("users", _users) |
| message.foreach(nbt.setString("message", _)) |
| |
| val componentsNbt = new NBTTagList() |
| for ((address, name) <- _components) { |
| val componentNbt = new NBTTagCompound() |
| componentNbt.setString("address", address) |
| componentNbt.setString("name", name) |
| componentsNbt.appendTag(componentNbt) |
| } |
| nbt.setTag("components", componentsNbt) |
| |
| tmp.foreach(fs => SaveHandler.scheduleSave(host, nbt, node.address + "_tmp", fs.save _)) |
| |
| if (state.top != Machine.State.Stopped) try { |
| architecture.save(nbt) |
| |
| val signalsNbt = new NBTTagList() |
| for (s <- signals.iterator) { |
| val signalNbt = new NBTTagCompound() |
| signalNbt.setString("name", s.name) |
| signalNbt.setNewCompoundTag("args", args => { |
| args.setInteger("length", s.args.length) |
| s.args.zipWithIndex.foreach { |
| case (null, i) => args.setByte("arg" + i, -1) |
| case (arg: java.lang.Boolean, i) => args.setByte("arg" + i, if (arg) 1 else 0) |
| case (arg: java.lang.Long, i) => args.setLong("arg" + i, arg) |
| case (arg: java.lang.Double, i) => args.setDouble("arg" + i, arg) |
| case (arg: String, i) => args.setString("arg" + i, arg) |
| case (arg: Array[Byte], i) => args.setByteArray("arg" + i, arg) |
| case (arg: Map[_, _], i) => |
| val list = new NBTTagList() |
| for ((key, value) <- arg) { |
| list.append(key.toString) |
| list.append(value.toString) |
| } |
| args.setTag("arg" + i, list) |
| case (arg: NBTTagCompound, i) => args.setTag("arg" + i, arg) |
| case (_, i) => args.setByte("arg" + i, -1) |
| } |
| }) |
| signalsNbt.appendTag(signalNbt) |
| } |
| nbt.setTag("signals", signalsNbt) |
| |
| nbt.setLong("uptime", uptime) |
| nbt.setLong("cpuTime", cpuTotal) |
| nbt.setInteger("remainingPause", remainingPause) |
| } |
| catch { |
| case t: Throwable => |
| OpenComputers.log.error( s"""Unexpected error saving a state of computer at (${host.xPosition}, ${host.yPosition}, ${host.zPosition}). """ + |
| s"""State: ${state.headOption.fold("no state")(_.toString)}. Unless you're upgrading/downgrading across a major version, please report this! Thank you.""", t) |
| } |
| }) |
| |
| // ----------------------------------------------------------------------- // |
| |
| private def init(): Boolean = { |
| onHostChanged() |
| if (architecture == null) return false |
| |
| // Reset error state. |
| message = None |
| |
| // Clear any left-over signals from a previous run. |
| signals.clear() |
| |
| // Connect the `/tmp` node to our owner. We're not in a network in |
| // case we're loading, which is why we have to check it here. |
| if (node.network != null) { |
| tmp.foreach(fs => node.connect(fs.node)) |
| } |
| |
| try { |
| return architecture.initialize() |
| } |
| catch { |
| case ex: Throwable => |
| OpenComputers.log.warn("Failed initializing computer.", ex) |
| close() |
| } |
| false |
| } |
| |
| def tryClose(): Boolean = |
| if (isExecuting) false |
| else { |
| close() |
| tmp.foreach(_.node.remove()) // To force deleting contents. |
| if (node.network != null) { |
| tmp.foreach(tmp => node.connect(tmp.node)) |
| } |
| node.sendToReachable("computer.stopped") |
| true |
| } |
| |
| private def close() = |
| if (state.synchronized(state.isEmpty || state.top != Machine.State.Stopped)) { |
| // Give up the state lock, then get the more generic lock on this instance first |
| // before locking on state again. Always must be in that order to avoid deadlocks. |
| this.synchronized(state.synchronized { |
| state.clear() |
| state.push(Machine.State.Stopped) |
| Option(architecture).foreach(_.close()) |
| signals.clear() |
| uptime = 0 |
| cpuTotal = 0 |
| cpuStart = 0 |
| remainIdle = 0 |
| }) |
| |
| // Mark state change in owner, to send it to clients. |
| host.markChanged() |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| private def switchTo(value: Machine.State.Value) = state.synchronized { |
| val result = state.pop() |
| if (value == Machine.State.Stopping || value == Machine.State.Restarting) { |
| state.clear() |
| } |
| state.push(value) |
| if (value == Machine.State.Yielded || value == Machine.State.SynchronizedReturn) { |
| remainIdle = 0 |
| Machine.threadPool.schedule(this, Settings.get.executionDelay, TimeUnit.MILLISECONDS) |
| } |
| |
| // Mark state change in owner, to send it to clients. |
| host.markChanged() |
| |
| result |
| } |
| |
| private def isGamePaused = !MinecraftServer.getServer.isDedicatedServer && (MinecraftServer.getServer match { |
| case integrated: IntegratedServer => Minecraft.getMinecraft.isGamePaused |
| case _ => false |
| }) |
| |
| // This is a really high level lock that we only use for saving and loading. |
| override def run(): Unit = Machine.this.synchronized { |
| val isSynchronizedReturn = state.synchronized { |
| if (state.top != Machine.State.Yielded && |
| state.top != Machine.State.SynchronizedReturn) { |
| return |
| } |
| // See if the game appears to be paused, in which case we also pause. |
| if (isGamePaused) { |
| state.push(Machine.State.Paused) |
| return |
| } |
| switchTo(Machine.State.Running) == Machine.State.SynchronizedReturn |
| } |
| |
| cpuStart = System.nanoTime() |
| |
| try { |
| val result = architecture.runThreaded(isSynchronizedReturn) |
| |
| // Check if someone called pause() or stop() in the meantime. |
| state.synchronized { |
| state.top match { |
| case Machine.State.Running => |
| result match { |
| case result: ExecutionResult.Sleep => |
| signals.synchronized { |
| // Immediately check for signals to allow processing more than one |
| // signal per game tick. |
| if (signals.isEmpty && result.ticks > 0) { |
| switchTo(Machine.State.Sleeping) |
| remainIdle = result.ticks |
| } else { |
| switchTo(Machine.State.Yielded) |
| } |
| } |
| case result: ExecutionResult.SynchronizedCall => |
| switchTo(Machine.State.SynchronizedCall) |
| case result: ExecutionResult.Shutdown => |
| if (result.reboot) { |
| switchTo(Machine.State.Restarting) |
| } |
| else { |
| switchTo(Machine.State.Stopping) |
| } |
| case result: ExecutionResult.Error => |
| beep("--") |
| crash(Option(result.message).getOrElse("unknown error")) |
| } |
| case Machine.State.Paused => |
| state.pop() // Paused |
| state.pop() // Running, no switchTo to avoid new future. |
| result match { |
| case result: ExecutionResult.Sleep => |
| remainIdle = result.ticks |
| state.push(Machine.State.Sleeping) |
| case result: ExecutionResult.SynchronizedCall => |
| state.push(Machine.State.SynchronizedCall) |
| case result: ExecutionResult.Shutdown => |
| if (result.reboot) { |
| state.push(Machine.State.Restarting) |
| } |
| else { |
| state.push(Machine.State.Stopping) |
| } |
| case result: ExecutionResult.Error => |
| crash(Option(result.message).getOrElse("unknown error")) |
| } |
| state.push(Machine.State.Paused) |
| case Machine.State.Stopping => |
| state.clear() |
| state.push(Machine.State.Stopping) |
| case Machine.State.Restarting => |
| // Nothing to do! |
| case _ => throw new AssertionError("Invalid state in executor post-processing.") |
| } |
| assert(!isExecuting) |
| } |
| } |
| catch { |
| case e: Throwable => |
| OpenComputers.log.warn("Architecture's runThreaded threw an error. This should never happen!", e) |
| crash("gui.Error.InternalError") |
| } |
| |
| // Keep track of time spent executing the computer. |
| cpuTotal += System.nanoTime() - cpuStart |
| } |
| } |
| |
| object Machine extends MachineAPI { |
| // Keep registration order, to allow deterministic iteration of the architectures. |
| val checked = mutable.LinkedHashSet.empty[Class[_ <: Architecture]] |
| |
| override def add(architecture: Class[_ <: Architecture]) { |
| if (!checked.contains(architecture)) { |
| try { |
| architecture.getConstructor(classOf[machine.Machine]) |
| } |
| catch { |
| case t: Throwable => throw new IllegalArgumentException("Architecture does not have required constructor.") |
| } |
| checked += architecture |
| } |
| } |
| |
| override def architectures = checked.toSeq |
| |
| // TODO Expose in Machine API in 1.6 |
| def getArchitectureName(architecture: Class[_ <: Architecture]) = |
| architecture.getAnnotation(classOf[Architecture.Name]) match { |
| case annotation: Architecture.Name => annotation.value |
| case _ => architecture.getSimpleName |
| } |
| |
| override def create(host: MachineHost) = new Machine(host) |
| |
| /** Possible states of the computer, and in particular its executor. */ |
| private[machine] object State extends Enumeration { |
| /** The computer is not running right now and there is no Lua state. */ |
| val Stopped = Value("Stopped") |
| |
| /** Booting up, doing the first run to initialize the kernel and libs. */ |
| val Starting = Value("Starting") |
| |
| /** Computer is currently rebooting. */ |
| val Restarting = Value("Restarting") |
| |
| /** The computer is currently shutting down. */ |
| val Stopping = Value("Stopping") |
| |
| /** The computer is paused and waiting for the game to resume. */ |
| val Paused = Value("Paused") |
| |
| /** The computer executor is waiting for a synchronized call to be made. */ |
| val SynchronizedCall = Value("SynchronizedCall") |
| |
| /** The computer should resume with the result of a synchronized call. */ |
| val SynchronizedReturn = Value("SynchronizedReturn") |
| |
| /** The computer will resume as soon as possible. */ |
| val Yielded = Value("Yielded") |
| |
| /** The computer is yielding for a longer amount of time. */ |
| val Sleeping = Value("Sleeping") |
| |
| /** The computer is up and running, executing Lua code. */ |
| val Running = Value("Running") |
| } |
| |
| /** Signals are messages sent to the Lua state from Java asynchronously. */ |
| private[machine] class Signal(val name: String, val args: Array[AnyRef]) extends machine.Signal { |
| def convert() = new Signal(name, Registry.convert(args)) |
| } |
| |
| private val threadPool = ThreadPoolFactory.create("Computer", Settings.get.threads) |
| } |