| package li.cil.oc.common.tileentity |
| |
| import cpw.mods.fml.relauncher.{SideOnly, Side} |
| import li.cil.oc.api.driver.Slot |
| import li.cil.oc.api.network._ |
| import li.cil.oc.common.block.Delegator |
| import li.cil.oc.server.component.GraphicsCard |
| import li.cil.oc.server.component.robot.Player |
| import li.cil.oc.server.driver.Registry |
| import li.cil.oc.server.{PacketSender => ServerPacketSender, driver, component} |
| import li.cil.oc.util.ExtendedNBT._ |
| import li.cil.oc.{Blocks, Settings, api, common} |
| import net.minecraft.client.Minecraft |
| import net.minecraft.entity.player.EntityPlayer |
| import net.minecraft.inventory.ISidedInventory |
| import net.minecraft.item.ItemStack |
| import net.minecraft.nbt.NBTTagCompound |
| import net.minecraftforge.common.ForgeDirection |
| import scala.Some |
| |
| // Implementation note: this tile entity is never directly added to the world. |
| // It is always wrapped by a `RobotProxy` tile entity, which forwards any |
| // necessary calls to this class. This is done to make moves efficient: when a |
| // robot moves we only create a new proxy tile entity, hook the instance of this |
| // class that was held by the old proxy to it and can then safely forget the |
| // old proxy, which will be cleaned up by Minecraft like any other tile entity. |
| class Robot(isRemote: Boolean) extends Computer(isRemote) with ISidedInventory with Buffer with PowerInformation with RobotContext { |
| def this() = this(false) |
| |
| var proxy: RobotProxy = _ |
| |
| // ----------------------------------------------------------------------- // |
| |
| // Note: we implement IRobotContext in the TE to allow external components |
| //to cast their owner to it (to allow interacting with their owning robot). |
| |
| var selectedSlot = actualSlot(0) |
| |
| def player() = player(facing, facing) |
| |
| def saveUpgrade() = this.synchronized { |
| components(3) match { |
| case Some(environment) => |
| val stack = getStackInSlot(3) |
| // We're guaranteed to have a driver for entries. |
| environment.save(dataTag(Registry.driverFor(stack).get, stack)) |
| ServerPacketSender.sendRobotEquippedUpgradeChange(this, stack) |
| case _ => |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| override def node = if (isClient) null else computer.node |
| |
| override val _buffer = new common.component.Buffer(this) { |
| override def maxResolution = (48, 14) |
| } |
| override val _computer = if (isRemote) null else new component.Robot(this) |
| val (gpu, keyboard) = if (isServer) { |
| val gpu = new GraphicsCard.Tier1 { |
| override val maxResolution = (48, 14) |
| } |
| val keyboard = new component.Keyboard(this) { |
| override def isUseableByPlayer(p: EntityPlayer) = |
| world.getBlockTileEntity(x, y, z) == proxy && |
| p.getDistanceSq(x + 0.5, y + 0.5, z + 0.5) <= 64 |
| } |
| (gpu, keyboard) |
| } |
| else (null, null) |
| |
| var owner = "OpenComputers" |
| |
| var xp = 0.0 |
| |
| def xpForNextLevel = Settings.get.baseXpToLevel + Math.pow((level + 1) * Settings.get.constantXpGrowth, Settings.get.exponentialXpGrowth) |
| |
| var level = 0 |
| |
| var xpChanged = false |
| |
| var globalBuffer, globalBufferSize = 0.0 |
| |
| var equippedItem: Option[ItemStack] = None |
| |
| var equippedUpgrade: Option[ItemStack] = None |
| |
| var animationTicksLeft = 0 |
| |
| var animationTicksTotal = 0 |
| |
| var moveFromX, moveFromY, moveFromZ = Int.MaxValue |
| |
| var swingingTool = false |
| |
| var turnAxis = 0 |
| |
| private lazy val player_ = new Player(this) |
| |
| def addXp(value: Double) { |
| if (level < 30 && isServer) { |
| xp = xp + value |
| xpChanged = true |
| if (xp >= xpForNextLevel) { |
| updateXpInfo() |
| } |
| } |
| } |
| |
| def updateXpInfo() { |
| // xp(level) = base + (level * const) ^ exp |
| // pow(xp(level) - base, 1/exp) / const = level |
| level = math.min((Math.pow(xp - Settings.get.baseXpToLevel, 1 / Settings.get.exponentialXpGrowth) / Settings.get.constantXpGrowth).toInt, 30) |
| if (isServer) { |
| computer.node.setLocalBufferSize(Settings.get.bufferRobot + Settings.get.bufferPerLevel * level) |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| override def onAnalyze(stats: NBTTagCompound, player: EntityPlayer, side: Int, hitX: Float, hitY: Float, hitZ: Float) = { |
| stats.setString(Settings.namespace + "gui.Analyzer.RobotOwner", owner) |
| stats.setString(Settings.namespace + "gui.Analyzer.RobotName", player_.getCommandSenderName) |
| stats.setString(Settings.namespace + "gui.Analyzer.RobotXp", xp.formatted("%.2f")) |
| super.onAnalyze(stats, player, side, hitX, hitY, hitZ) |
| } |
| |
| def player(facing: ForgeDirection = facing, side: ForgeDirection = facing) = { |
| player_.updatePositionAndRotation(facing, side) |
| player_ |
| } |
| |
| def actualSlot(n: Int) = n + 4 |
| |
| def move(direction: ForgeDirection) = { |
| val (ox, oy, oz) = (x, y, z) |
| val (nx, ny, nz) = (x + direction.offsetX, y + direction.offsetY, z + direction.offsetZ) |
| val blockId = world.getBlockId(nx, ny, nz) |
| val metadata = world.getBlockMetadata(nx, ny, nz) |
| try { |
| // Setting this will make the tile entity created via the following call |
| // to setBlock to re-use our "real" instance as the inner object, instead |
| // of creating a new one. |
| Blocks.robotProxy.moving.set(Some(this)) |
| // Do *not* immediately send the change to clients to allow checking if it |
| // worked before the client is notified so that we can use the same trick on |
| // the client by sending a corresponding packet. This also saves us from |
| // having to send the complete state again (e.g. screen buffer) each move. |
| val created = Blocks.robotProxy.setBlock(world, nx, ny, nz, 1) |
| if (created) { |
| assert(world.getBlockTileEntity(nx, ny, nz) == proxy) |
| assert(x == nx && y == ny && z == nz) |
| Blocks.robotAfterimage.setBlock(world, ox, oy, oz, 1) |
| assert(Delegator.subBlock(world, ox, oy, oz).exists(_ == Blocks.robotAfterimage)) |
| // Here instead of Lua callback so that it gets called on client, too. |
| val moveTicks = math.max((Settings.get.moveDelay * 20).toInt, 1) |
| setAnimateMove(ox, oy, oz, moveTicks) |
| if (isServer) { |
| ServerPacketSender.sendRobotMove(this, ox, oy, oz, direction) |
| checkRedstoneInputChanged() |
| } |
| else { |
| // If we broke some replaceable block (like grass) play its break sound. |
| if (blockId > 0) { |
| world.playAuxSFX(2001, nx, ny, nz, blockId + (metadata << 12)) |
| } |
| world.markBlockForRenderUpdate(ox, oy, oz) |
| world.markBlockForRenderUpdate(nx, ny, nz) |
| } |
| assert(!isInvalid) |
| } |
| created |
| } |
| finally { |
| Blocks.robotProxy.moving.set(None) |
| } |
| } |
| |
| def createItemStack() = { |
| val stack = Blocks.robotProxy.createItemStack() |
| if (globalBuffer > 1 || xp > 0) { |
| stack.setTagCompound(new NBTTagCompound("tag")) |
| } |
| if (xp > 0) { |
| stack.getTagCompound.setDouble(Settings.namespace + "xp", xp) |
| } |
| if (globalBuffer > 1) { |
| stack.getTagCompound.setInteger(Settings.namespace + "storedEnergy", globalBuffer.toInt) |
| } |
| stack |
| } |
| |
| def parseItemStack(stack: ItemStack) { |
| if (stack.hasTagCompound) { |
| xp = stack.getTagCompound.getDouble(Settings.namespace + "xp") |
| updateXpInfo() |
| computer.node.changeBuffer(stack.getTagCompound.getInteger(Settings.namespace + "storedEnergy")) |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def isAnimatingMove = animationTicksLeft > 0 && (moveFromX != Int.MaxValue || moveFromY != Int.MaxValue || moveFromZ != Int.MaxValue) |
| |
| def isAnimatingSwing = animationTicksLeft > 0 && swingingTool |
| |
| def isAnimatingTurn = animationTicksLeft > 0 && turnAxis != 0 |
| |
| def animateSwing(duration: Double) = { |
| setAnimateSwing((duration * 20).toInt) |
| ServerPacketSender.sendRobotAnimateSwing(this) |
| } |
| |
| def animateTurn(clockwise: Boolean, duration: Double) = { |
| setAnimateTurn(if (clockwise) 1 else -1, (duration * 20).toInt) |
| ServerPacketSender.sendRobotAnimateTurn(this) |
| } |
| |
| def setAnimateMove(fromX: Int, fromY: Int, fromZ: Int, ticks: Int) { |
| animationTicksTotal = ticks |
| prepareForAnimation() |
| moveFromX = fromX |
| moveFromY = fromY |
| moveFromZ = fromZ |
| } |
| |
| def setAnimateSwing(ticks: Int) { |
| animationTicksTotal = ticks |
| prepareForAnimation() |
| swingingTool = true |
| } |
| |
| def setAnimateTurn(axis: Int, ticks: Int) { |
| animationTicksTotal = ticks |
| prepareForAnimation() |
| turnAxis = axis |
| } |
| |
| private def prepareForAnimation() { |
| animationTicksLeft = animationTicksTotal |
| moveFromX = Int.MaxValue |
| moveFromY = Int.MaxValue |
| moveFromZ = Int.MaxValue |
| swingingTool = false |
| turnAxis = 0 |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| override def shouldRenderInPass(pass: Int) = true |
| |
| override def getRenderBoundingBox = |
| getBlockType.getCollisionBoundingBoxFromPool(world, x, y, z).expand(0.5, 0.5, 0.5) |
| |
| // ----------------------------------------------------------------------- // |
| |
| override def updateEntity() { |
| if (!addedToNetwork) { |
| addedToNetwork = true |
| api.Network.joinNewNetwork(node) |
| } |
| if (animationTicksLeft > 0) { |
| animationTicksLeft -= 1 |
| if (animationTicksLeft == 0) { |
| moveFromX = Int.MaxValue |
| moveFromY = Int.MaxValue |
| moveFromZ = Int.MaxValue |
| swingingTool = false |
| turnAxis = 0 |
| } |
| } |
| super.updateEntity() |
| if (isServer) { |
| gpu.update() |
| globalBuffer = computer.node.globalBuffer |
| globalBufferSize = computer.node.globalBufferSize |
| updatePowerInformation() |
| if (xpChanged && world.getWorldInfo.getWorldTotalTime % 200 == 0) { |
| xpChanged = false |
| ServerPacketSender.sendRobotXp(this) |
| } |
| } |
| } |
| |
| override def validate() { |
| super.validate() |
| if (isServer) { |
| items(0) match { |
| case Some(item) => player_.getAttributeMap.applyAttributeModifiers(item.getAttributeModifiers) |
| case _ => |
| } |
| updateXpInfo() |
| } |
| } |
| |
| override def invalidate() { |
| super.invalidate() |
| if (currentGui.isDefined) { |
| Minecraft.getMinecraft.displayGuiScreen(null) |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| override def readFromNBT(nbt: NBTTagCompound) { |
| if (isServer) { |
| buffer.load(nbt.getCompoundTag(Settings.namespace + "buffer")) |
| gpu.load(nbt.getCompoundTag(Settings.namespace + "gpu")) |
| keyboard.load(nbt.getCompoundTag(Settings.namespace + "keyboard")) |
| } |
| if (nbt.hasKey(Settings.namespace + "owner")) { |
| owner = nbt.getString(Settings.namespace + "owner") |
| } |
| xp = nbt.getDouble(Settings.namespace + "xp") max 0 |
| updateXpInfo() |
| selectedSlot = nbt.getInteger(Settings.namespace + "selectedSlot") max actualSlot(0) min (getSizeInventory - 1) |
| animationTicksTotal = nbt.getInteger(Settings.namespace + "animationTicksTotal") |
| animationTicksLeft = nbt.getInteger(Settings.namespace + "animationTicksLeft") |
| if (animationTicksLeft > 0) { |
| moveFromX = nbt.getInteger(Settings.namespace + "moveFromX") |
| moveFromY = nbt.getInteger(Settings.namespace + "moveFromY") |
| moveFromZ = nbt.getInteger(Settings.namespace + "moveFromZ") |
| swingingTool = nbt.getBoolean(Settings.namespace + "swingingTool") |
| turnAxis = nbt.getByte(Settings.namespace + "turnAxis") |
| } |
| } |
| |
| override def writeToNBT(nbt: NBTTagCompound) = this.synchronized { |
| // Note: computer is saved when proxy is saved (in proxy's super writeToNBT) |
| // which is a bit ugly, and may be refactored some day, but it works. |
| nbt.setNewCompoundTag(Settings.namespace + "buffer", buffer.save) |
| nbt.setNewCompoundTag(Settings.namespace + "gpu", gpu.save) |
| nbt.setNewCompoundTag(Settings.namespace + "keyboard", keyboard.save) |
| nbt.setString(Settings.namespace + "owner", owner) |
| nbt.setDouble(Settings.namespace + "xp", xp) |
| nbt.setInteger(Settings.namespace + "selectedSlot", selectedSlot) |
| if (isAnimatingMove || isAnimatingSwing || isAnimatingTurn) { |
| nbt.setInteger(Settings.namespace + "animationTicksTotal", animationTicksTotal) |
| nbt.setInteger(Settings.namespace + "animationTicksLeft", animationTicksLeft) |
| nbt.setInteger(Settings.namespace + "moveFromX", moveFromX) |
| nbt.setInteger(Settings.namespace + "moveFromY", moveFromY) |
| nbt.setInteger(Settings.namespace + "moveFromZ", moveFromZ) |
| nbt.setBoolean(Settings.namespace + "swingingTool", swingingTool) |
| nbt.setByte(Settings.namespace + "turnAxis", turnAxis.toByte) |
| } |
| } |
| |
| @SideOnly(Side.CLIENT) |
| override def readFromNBTForClient(nbt: NBTTagCompound) { |
| super.readFromNBTForClient(nbt) |
| selectedSlot = nbt.getInteger("selectedSlot") |
| if (nbt.hasKey("equipped")) { |
| equippedItem = Option(ItemStack.loadItemStackFromNBT(nbt.getCompoundTag("equipped"))) |
| } |
| if (nbt.hasKey("upgrade")) { |
| equippedUpgrade = Option(ItemStack.loadItemStackFromNBT(nbt.getCompoundTag("upgrade"))) |
| } |
| xp = nbt.getDouble(Settings.namespace + "xp") |
| updateXpInfo() |
| animationTicksTotal = nbt.getInteger("animationTicksTotal") |
| animationTicksLeft = nbt.getInteger("animationTicksLeft") |
| moveFromX = nbt.getInteger("moveFromX") |
| moveFromY = nbt.getInteger("moveFromY") |
| moveFromZ = nbt.getInteger("moveFromZ") |
| if (animationTicksLeft > 0) { |
| swingingTool = nbt.getBoolean("swingingTool") |
| turnAxis = nbt.getByte("turnAxis") |
| } |
| } |
| |
| override def writeToNBTForClient(nbt: NBTTagCompound) = this.synchronized { |
| super.writeToNBTForClient(nbt) |
| nbt.setInteger("selectedSlot", selectedSlot) |
| if (getStackInSlot(0) != null) { |
| nbt.setNewCompoundTag("equipped", getStackInSlot(0).writeToNBT) |
| } |
| if (getStackInSlot(3) != null) { |
| // Force saving to item's NBT if necessary before sending, to make sure |
| // we transfer the component's current state (e.g. running or not for |
| // generator upgrades). |
| components(3) match { |
| case Some(environment) => |
| val stack = getStackInSlot(3) |
| // We're guaranteed to have a driver for entries. |
| environment.save(dataTag(Registry.driverFor(stack).get, stack)) |
| case _ => // See onConnect() |
| } |
| nbt.setNewCompoundTag("upgrade", getStackInSlot(3).writeToNBT) |
| } |
| nbt.setDouble(Settings.namespace + "xp", xp) |
| if (isAnimatingMove || isAnimatingSwing || isAnimatingTurn) { |
| nbt.setInteger("animationTicksTotal", animationTicksTotal) |
| nbt.setInteger("animationTicksLeft", animationTicksLeft) |
| nbt.setInteger("moveFromX", moveFromX) |
| nbt.setInteger("moveFromY", moveFromY) |
| nbt.setInteger("moveFromZ", moveFromZ) |
| nbt.setBoolean("swingingTool", swingingTool) |
| nbt.setByte("turnAxis", turnAxis.toByte) |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| override def onConnect(node: Node) { |
| super.onConnect(node) |
| if (node == this.node) { |
| computer.node.connect(buffer.node) |
| computer.node.connect(gpu.node) |
| buffer.node.connect(keyboard.node) |
| // There's a chance the server sends a robot tile entity to its clients |
| // before the tile entity's first update was called, in which case the |
| // component list isn't initialized (e.g. when a client triggers a chunk |
| // load, most noticeable in single player). In that case the current |
| // equipment will be initialized incorrectly. So we have to send it |
| // again when the first update is run. One of the two (this and the info |
| // sent in writeToNBTForClient) may be superfluous, but the packet is |
| // quite small compared to what else is sent on a chunk load, so we don't |
| // really worry about it and just send it. |
| saveUpgrade() |
| } |
| } |
| |
| override def onDisconnect(node: Node) { |
| super.onDisconnect(node) |
| if (node == this.node) { |
| buffer.node.remove() |
| computer.node.remove() |
| gpu.node.remove() |
| keyboard.node.remove() |
| } |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| override protected def onItemRemoved(slot: Int, stack: ItemStack) { |
| super.onItemRemoved(slot, stack) |
| if (isServer) { |
| if (isToolSlot(slot)) { |
| player_.getAttributeMap.removeAttributeModifiers(stack.getAttributeModifiers) |
| ServerPacketSender.sendRobotEquippedItemChange(this, null) |
| } |
| if (isUpgradeSlot(slot)) { |
| ServerPacketSender.sendRobotEquippedUpgradeChange(this, null) |
| } |
| if (isInventorySlot(slot)) { |
| computer.signal("inventory_changed", Int.box(slot - actualSlot(0) + 1)) |
| } |
| } |
| } |
| |
| override protected def onItemAdded(slot: Int, stack: ItemStack) { |
| if (isServer) { |
| if (isToolSlot(slot)) { |
| player_.getAttributeMap.applyAttributeModifiers(stack.getAttributeModifiers) |
| ServerPacketSender.sendRobotEquippedItemChange(this, getStackInSlot(slot)) |
| } |
| if (isUpgradeSlot(slot)) { |
| ServerPacketSender.sendRobotEquippedUpgradeChange(this, getStackInSlot(slot)) |
| } |
| if (isComponentSlot(slot)) { |
| super.onItemAdded(slot, stack) |
| } |
| if (isInventorySlot(slot)) { |
| computer.signal("inventory_changed", Int.box(slot - actualSlot(0) + 1)) |
| } |
| } |
| } |
| |
| override protected def isComponentSlot(slot: Int) = slot > 0 && slot < actualSlot(0) |
| |
| private def isInventorySlot(slot: Int) = slot >= actualSlot(0) |
| |
| private def isToolSlot(slot: Int) = slot == 0 |
| |
| private def isUpgradeSlot(slot: Int) = slot == 3 |
| |
| // ----------------------------------------------------------------------- // |
| |
| override def installedMemory = 64 * 1024 |
| |
| def tier = 0 |
| |
| override def hasRedstoneCard = items(1).fold(false)(driver.item.RedstoneCard.worksWith) |
| |
| @SideOnly(Side.CLIENT) |
| override protected def markForRenderUpdate() { |
| super.markForRenderUpdate() |
| currentGui.foreach(_.recompileDisplayLists()) |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def getInvName = Settings.namespace + "container.Robot" |
| |
| def getSizeInventory = 20 |
| |
| override def getInventoryStackLimit = 64 |
| |
| override def setInventorySlotContents(slot: Int, stack: ItemStack) = { |
| if ((1 to 2 contains slot) && stack != null && stack.stackSize > 1) { |
| super.setInventorySlotContents(slot, stack.splitStack(1)) |
| if (stack.stackSize > 0 && isServer) { |
| val p = player() |
| p.inventory.addItemStackToInventory(stack) |
| p.dropPlayerItemWithRandomChoice(stack, inPlace = false) |
| } |
| } |
| else super.setInventorySlotContents(slot, stack) |
| } |
| |
| override def isUseableByPlayer(player: EntityPlayer) = |
| world.getBlockTileEntity(x, y, z) match { |
| case t: RobotProxy if t == proxy && computer.canInteract(player.getCommandSenderName) => |
| player.getDistanceSq(x + 0.5, y + 0.5, z + 0.5) <= 64 |
| case _ => false |
| } |
| |
| def isItemValidForSlot(slot: Int, stack: ItemStack) = (slot, Registry.driverFor(stack)) match { |
| case (0, _) => true // Allow anything in the tool slot. |
| case (1, Some(driver)) => driver.slot(stack) == Slot.Card |
| case (2, Some(driver)) => driver.slot(stack) == Slot.Disk |
| case (3, Some(driver)) => driver.slot(stack) == Slot.Upgrade |
| case (i, _) if actualSlot(0) until getSizeInventory contains i => true // Normal inventory. |
| case _ => false // Invalid slot. |
| } |
| |
| // ----------------------------------------------------------------------- // |
| |
| def canExtractItem(slot: Int, stack: ItemStack, side: Int) = |
| getAccessibleSlotsFromSide(side).contains(slot) |
| |
| def canInsertItem(slot: Int, stack: ItemStack, side: Int) = |
| getAccessibleSlotsFromSide(side).contains(slot) && |
| isItemValidForSlot(slot, stack) |
| |
| def getAccessibleSlotsFromSide(side: Int) = |
| toLocal(ForgeDirection.getOrientation(side)) match { |
| case ForgeDirection.WEST => Array(0) |
| case ForgeDirection.EAST => Array(1) |
| case ForgeDirection.NORTH => Array(2) |
| case _ => (actualSlot(3) until getSizeInventory).toArray |
| } |
| } |