diff --git a/assets/opencomputers/lang/en_US.lang b/assets/opencomputers/lang/en_US.lang index 5750508af..273cf59fa 100644 --- a/assets/opencomputers/lang/en_US.lang +++ b/assets/opencomputers/lang/en_US.lang @@ -56,6 +56,7 @@ oc:gui.Analyzer.RobotName=Name oc:gui.Analyzer.RobotOwner=Owner oc:gui.Analyzer.StoredEnergy=Stored energy oc:gui.Analyzer.TotalEnergy=Total stored energy +oc:gui.Analyzer.Users=Users oc:gui.Robot.Power=Power oc:gui.Robot.TurnOff=Turn off oc:gui.Robot.TurnOn=Turn on diff --git a/li/cil/oc/client/PacketHandler.scala b/li/cil/oc/client/PacketHandler.scala index 4f902846c..12bec638e 100644 --- a/li/cil/oc/client/PacketHandler.scala +++ b/li/cil/oc/client/PacketHandler.scala @@ -26,6 +26,7 @@ class PacketHandler extends CommonPacketHandler { case PacketType.Analyze => onAnalyze(p) case PacketType.ChargerState => onChargerState(p) case PacketType.ComputerState => onComputerState(p) + case PacketType.ComputerUserList => onComputerUserList(p) case PacketType.PowerState => onPowerState(p) case PacketType.RedstoneState => onRedstoneState(p) case PacketType.RobotAnimateSwing => onRobotAnimateSwing(p) @@ -73,6 +74,14 @@ class PacketHandler extends CommonPacketHandler { case _ => // Invalid packet. } + def onComputerUserList(p: PacketParser) = + p.readTileEntity[Computer]() match { + case Some(t) => + val count = p.readInt() + t.users = (0 until count).map(_ => p.readUTF()) + case _ => // Invalid packet. + } + def onPowerState(p: PacketParser) = p.readTileEntity[PowerInformation]() match { case Some(t) => diff --git a/li/cil/oc/common/PacketType.scala b/li/cil/oc/common/PacketType.scala index ad475fd25..d69894be9 100644 --- a/li/cil/oc/common/PacketType.scala +++ b/li/cil/oc/common/PacketType.scala @@ -6,6 +6,7 @@ object PacketType extends Enumeration { Analyze, ChargerState, ComputerState, + ComputerUserList, PowerState, RedstoneState, RobotAnimateSwing, diff --git a/li/cil/oc/common/block/Case.scala b/li/cil/oc/common/block/Case.scala index 2bbe5c748..f17c1e18f 100644 --- a/li/cil/oc/common/block/Case.scala +++ b/li/cil/oc/common/block/Case.scala @@ -80,8 +80,7 @@ abstract class Case(val parent: SimpleDelegator) extends Computer with SimpleDel // TODO do we have to manually sync the client since we can only check this on the server side? override def removedByEntity(world: World, x: Int, y: Int, z: Int, player: EntityPlayer) = world.getBlockTileEntity(x, y, z) match { - case c: tileentity.Case if !world.isRemote => - c.computer.canInteract(player.getCommandSenderName) + case c: tileentity.Case => c.canInteract(player.getCommandSenderName) case _ => super.removedByEntity(world, x, y, z, player) } } diff --git a/li/cil/oc/common/block/RobotAfterimage.scala b/li/cil/oc/common/block/RobotAfterimage.scala index 3125f92ed..8402d08cd 100644 --- a/li/cil/oc/common/block/RobotAfterimage.scala +++ b/li/cil/oc/common/block/RobotAfterimage.scala @@ -54,7 +54,7 @@ class RobotAfterimage(val parent: SpecialDelegator) extends SpecialDelegate { override def rightClick(world: World, x: Int, y: Int, z: Int, player: EntityPlayer, side: ForgeDirection, hitX: Float, hitY: Float, hitZ: Float) = { findMovingRobot(world, x, y, z) match { - case Some(robot) => robot.getBlockType.onBlockActivated(world, robot.x, robot.y, robot.z, player, side.ordinal, hitX, hitY, hitZ) + case Some(robot) => Blocks.robotProxy.rightClick(world, robot.x, robot.y, robot.z, player, side, hitX, hitY, hitZ) case _ => world.setBlockToAir(x, y, z) } } diff --git a/li/cil/oc/common/block/RobotProxy.scala b/li/cil/oc/common/block/RobotProxy.scala index 1a8cd2a6f..8e6ed5e91 100644 --- a/li/cil/oc/common/block/RobotProxy.scala +++ b/li/cil/oc/common/block/RobotProxy.scala @@ -2,6 +2,7 @@ package li.cil.oc.common.block import java.util import li.cil.oc.common.{GuiType, tileentity} +import li.cil.oc.server.PacketSender import li.cil.oc.server.component.robot import li.cil.oc.util.Tooltip import li.cil.oc.{Settings, OpenComputers} @@ -84,6 +85,14 @@ class RobotProxy(val parent: SpecialDelegator) extends Computer with SpecialDele side: ForgeDirection, hitX: Float, hitY: Float, hitZ: Float) = { if (!player.isSneaking) { if (!world.isRemote) { + // We only send slot changes to nearby players, so if there was no slot + // change since this player got into range he might have the wrong one, + // so we send him the current one just in case. + world.getBlockTileEntity(x, y, z) match { + case proxy: tileentity.RobotProxy => + PacketSender.sendRobotSelectedSlotChange(proxy.robot) + case _ => + } player.openGui(OpenComputers, GuiType.Robot.id, world, x, y, z) } true diff --git a/li/cil/oc/common/tileentity/Computer.scala b/li/cil/oc/common/tileentity/Computer.scala index 984a0a7ec..af64e6aac 100644 --- a/li/cil/oc/common/tileentity/Computer.scala +++ b/li/cil/oc/common/tileentity/Computer.scala @@ -6,9 +6,10 @@ import li.cil.oc.api.network._ import li.cil.oc.server.{PacketSender => ServerPacketSender, driver, component} import li.cil.oc.util.ExtendedNBT._ import net.minecraft.entity.player.EntityPlayer -import net.minecraft.nbt.NBTTagCompound +import net.minecraft.nbt.{NBTTagString, NBTTagCompound} import net.minecraftforge.common.ForgeDirection import scala.Some +import scala.collection.mutable abstract class Computer(isRemote: Boolean) extends Environment with ComponentInventory with Rotatable with BundledRedstone with Analyzable { protected val _computer = if (isRemote) null else new component.Computer(this) @@ -23,10 +24,14 @@ abstract class Computer(isRemote: Boolean) extends Environment with ComponentInv private var hasChanged = false + @SideOnly(Side.CLIENT) + private val _users = mutable.Set.empty[String] + // ----------------------------------------------------------------------- // def isRunning = _isRunning + @SideOnly(Side.CLIENT) def isRunning_=(value: Boolean) = { _isRunning = value world.markBlockForRenderUpdate(x, y, z) @@ -40,6 +45,21 @@ abstract class Computer(isRemote: Boolean) extends Environment with ComponentInv case _ => false } + def users: Iterable[String] = + if (isServer) computer.users + else _users + + @SideOnly(Side.CLIENT) + def users_=(list: Iterable[String]) { + _users.clear() + _users ++= list + } + + def canInteract(player: String) = + if (isServer) computer.canInteract(player) + else !Settings.get.canComputersBeOwned || + _users.isEmpty || _users.contains(player) + // ----------------------------------------------------------------------- // override def updateEntity() { @@ -95,11 +115,14 @@ abstract class Computer(isRemote: Boolean) extends Environment with ComponentInv override def readFromNBTForClient(nbt: NBTTagCompound) { super.readFromNBTForClient(nbt) isRunning = nbt.getBoolean("isRunning") + _users.clear() + _users ++= nbt.getTagList("users").iterator[NBTTagString].map(_.data) } override def writeToNBTForClient(nbt: NBTTagCompound) { super.writeToNBTForClient(nbt) nbt.setBoolean("isRunning", isRunning) + nbt.setNewTagList("users", computer.users.map(user => new NBTTagString(null, user))) } // ----------------------------------------------------------------------- // @@ -126,9 +149,14 @@ abstract class Computer(isRemote: Boolean) extends Environment with ComponentInv def onAnalyze(stats: NBTTagCompound, player: EntityPlayer, side: Int, hitX: Float, hitY: Float, hitZ: Float): Node = { if (computer != null) computer.lastError match { - case Some(value) => stats.setString(Settings.namespace + "gui.Analyzer.LastError", value) + case Some(value) => + stats.setString(Settings.namespace + "gui.Analyzer.LastError", value) case _ => } + val list = users + if (list.size > 0) { + stats.setString(Settings.namespace + "gui.Analyzer.Users", list.mkString(", ")) + } computer.node } } diff --git a/li/cil/oc/server/PacketSender.scala b/li/cil/oc/server/PacketSender.scala index 36e833c87..3720be180 100644 --- a/li/cil/oc/server/PacketSender.scala +++ b/li/cil/oc/server/PacketSender.scala @@ -44,6 +44,16 @@ object PacketSender { } } + def sendComputerUserList(t: Computer, list: Array[String]) { + val pb = new PacketBuilder(PacketType.ComputerUserList) + + pb.writeTileEntity(t) + pb.writeInt(list.length) + list.foreach(pb.writeUTF) + + pb.sendToNearbyPlayers(t) + } + def sendPowerState(t: PowerInformation, player: Option[Player] = None) { val pb = new PacketBuilder(PacketType.PowerState) @@ -91,7 +101,7 @@ object PacketSender { pb.writeTileEntity(t.proxy) pb.writeInt(t.animationTicksTotal) - pb.sendToNearbyPlayers(t) + pb.sendToNearbyPlayers(t, 64) } def sendRobotAnimateTurn(t: Robot) { @@ -101,7 +111,7 @@ object PacketSender { pb.writeByte(t.turnAxis) pb.writeInt(t.animationTicksTotal) - pb.sendToNearbyPlayers(t) + pb.sendToNearbyPlayers(t, 64) } def sendRobotEquippedItemChange(t: Robot, stack: ItemStack) { @@ -119,7 +129,7 @@ object PacketSender { pb.writeTileEntity(t.proxy) pb.writeInt(t.selectedSlot) - pb.sendToNearbyPlayers(t) + pb.sendToNearbyPlayers(t, 16) } def sendRotatableState(t: Rotatable, player: Option[Player] = None) { @@ -187,7 +197,7 @@ object PacketSender { pb.writeTileEntity(t) pb.writeBoolean(hasPower) - pb.sendToNearbyPlayers(t) + pb.sendToNearbyPlayers(t, 64) } def sendScreenResolutionChange(t: Buffer, w: Int, h: Int) { diff --git a/li/cil/oc/server/component/Computer.scala b/li/cil/oc/server/component/Computer.scala index 8cdef8dd7..8531c20c9 100644 --- a/li/cil/oc/server/component/Computer.scala +++ b/li/cil/oc/server/component/Computer.scala @@ -7,6 +7,7 @@ import li.cil.oc.api import li.cil.oc.api.network._ import li.cil.oc.common.tileentity import li.cil.oc.server +import li.cil.oc.server.PacketSender import li.cil.oc.util.ExtendedLuaState.extendLuaState import li.cil.oc.util.ExtendedNBT._ import li.cil.oc.util.{ThreadPoolFactory, GameTimeFormatter, LuaStateFactory} @@ -45,7 +46,7 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con private val addedComponents = mutable.Set.empty[Component] - private val users = mutable.Set.empty[String] + private val _users = mutable.Set.empty[String] private val signals = new mutable.Queue[Computer.Signal] @@ -67,6 +68,8 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con 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. // ----------------------------------------------------------------------- // @@ -83,6 +86,8 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con def lastError = message + def users = _users.synchronized(_users.toArray) + def isRobot = false // ----------------------------------------------------------------------- // @@ -90,7 +95,7 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con def address = node.address def canInteract(player: String) = !Settings.get.canComputersBeOwned || - users.isEmpty || users.contains(player) || + _users.synchronized(_users.isEmpty || _users.contains(player)) || MinecraftServer.getServer.isSinglePlayer || MinecraftServer.getServer.getConfigurationManager.isPlayerOpped(player) @@ -232,6 +237,15 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con } }) + // Avoid spamming user list across the network. + if (worldTime % 20 == 0 && usersChanged) { + val list = _users.synchronized { + usersChanged = false + users + } + PacketSender.sendComputerUserList(owner, list) + } + // 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 { @@ -419,14 +433,14 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con override def load(nbt: NBTTagCompound) = this.synchronized { assert(state.top == Computer.State.Stopped) - assert(users.isEmpty) + assert(_users.isEmpty) assert(signals.isEmpty) state.clear() super.load(nbt) state.pushAll(nbt.getTagList("state").iterator[NBTTagInt].reverse.map(s => Computer.State(s.data))) - nbt.getTagList("users").foreach[NBTTagString](u => users += u.data) + nbt.getTagList("users").foreach[NBTTagString](u => _users += u.data) if (state.size > 0 && state.top != Computer.State.Stopped && init()) { // Unlimit memory use while unpersisting. @@ -506,7 +520,7 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con processAddedComponents() nbt.setNewTagList("state", state.map(_.id)) - nbt.setNewTagList("users", users) + nbt.setNewTagList("users", _users) if (state.top != Computer.State.Stopped) { // Unlimit memory while persisting. @@ -826,25 +840,28 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con // User management. lua.pushScalaFunction(lua => { - users.foreach(lua.pushString) - users.size + _users.foreach(lua.pushString) + _users.size }) lua.setField(-2, "users") lua.pushScalaFunction(lua => try { - if (users.size >= Settings.get.maxUsers) + if (_users.size >= Settings.get.maxUsers) throw new Exception("too many users") val name = lua.checkString(1) - if (users.contains(name)) + 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 += name + _users.synchronized { + _users += name + usersChanged = true + } lua.pushBoolean(true) 1 } catch { @@ -857,7 +874,13 @@ class Computer(val owner: tileentity.Computer) extends ManagedComponent with Con lua.pushScalaFunction(lua => { val name = lua.checkString(1) - lua.pushBoolean(users.remove(name)) + _users.synchronized { + val success = _users.remove(name) + if (success) { + usersChanged = true + } + lua.pushBoolean(success) + } 1 }) lua.setField(-2, "removeUser") diff --git a/li/cil/oc/server/component/ManagedComponent.scala b/li/cil/oc/server/component/ManagedComponent.scala index 98030d27f..e1657d522 100644 --- a/li/cil/oc/server/component/ManagedComponent.scala +++ b/li/cil/oc/server/component/ManagedComponent.scala @@ -22,15 +22,6 @@ abstract class ManagedComponent extends ManagedEnvironment { if (node != null) nbt.setNewCompoundTag("node", node.save) } - /** - * Handy function for returning a list of results. - *
- * This is primarily meant to be used for returning result arrays from Lua - * callbacks, to avoid having to write `XYZ.box(...)` all the time. - * - * @param args the values to return. - * @return and array of objects. - */ final protected def result(args: Any*): Array[AnyRef] = { def unwrap(arg: Any): AnyRef = arg match { case x: ScalaNumber => x.underlying