blob: b98af7faabb70668bc38cd423db91fe4b4265f41 [file] [log] [blame] [raw]
package li.cil.oc.common.item
import java.lang.Iterable
import java.util
import java.util.UUID
import java.util.concurrent.Callable
import java.util.concurrent.TimeUnit
import com.google.common.cache.CacheBuilder
import com.google.common.cache.RemovalListener
import com.google.common.cache.RemovalNotification
import cpw.mods.fml.common.eventhandler.SubscribeEvent
import cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent
import cpw.mods.fml.common.gameevent.TickEvent.ServerTickEvent
import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import li.cil.oc.Constants
import li.cil.oc.Localization
import li.cil.oc.OpenComputers
import li.cil.oc.Settings
import li.cil.oc.api
import li.cil.oc.api.Driver
import li.cil.oc.api.Machine
import li.cil.oc.api.driver.item.Chargeable
import li.cil.oc.api.driver.item.Container
import li.cil.oc.api.internal
import li.cil.oc.api.machine.MachineHost
import li.cil.oc.api.network.Message
import li.cil.oc.api.network.Node
import li.cil.oc.client.KeyBindings
import li.cil.oc.common.GuiType
import li.cil.oc.common.Slot
import li.cil.oc.common.Tier
import li.cil.oc.common.inventory.ComponentInventory
import li.cil.oc.common.item.data.TabletData
import li.cil.oc.integration.opencomputers.DriverScreen
import li.cil.oc.server.component
import li.cil.oc.util.Audio
import li.cil.oc.util.BlockPosition
import li.cil.oc.util.ExtendedNBT._
import li.cil.oc.util.Rarity
import li.cil.oc.util.RotationHelper
import li.cil.oc.util.Tooltip
import net.minecraft.entity.Entity
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NBTTagCompound
import net.minecraft.world.World
import net.minecraftforge.common.util.ForgeDirection
import net.minecraftforge.event.world.WorldEvent
import scala.collection.convert.WrapAsJava._
import scala.collection.convert.WrapAsScala._
class Tablet(val parent: Delegator) extends Delegate with Chargeable {
final val TimeToAnalyze = 10
// Must be assembled to be usable so we hide it in the item list.
showInItemList = false
override def maxStackSize = 1
private var iconOn: Option[Icon] = None
private var iconOff: Option[Icon] = None
@SideOnly(Side.CLIENT)
override def icon(stack: ItemStack, pass: Int) = {
if (stack.hasTagCompound) {
val data = new TabletData(stack)
if (data.isRunning) iconOn else iconOff
} else super.icon(stack, pass)
}
override def registerIcons(iconRegister: IconRegister) = {
super.registerIcons(iconRegister)
iconOn = Option(iconRegister.registerIcon(Settings.resourceDomain + ":TabletOn"))
iconOff = Option(iconRegister.registerIcon(Settings.resourceDomain + ":TabletOff"))
}
// ----------------------------------------------------------------------- //
override protected def tooltipExtended(stack: ItemStack, tooltip: util.List[String]): Unit = {
if (KeyBindings.showExtendedTooltips) {
val info = new TabletData(stack)
// Ignore/hide the screen.
val components = info.items.drop(1)
if (components.length > 1) {
tooltip.addAll(Tooltip.get("Server.Components"))
components.collect {
case Some(component) => tooltip.add("- " + component.getDisplayName)
}
}
}
}
override def rarity(stack: ItemStack) = {
val data = new TabletData(stack)
Rarity.byTier(data.tier)
}
override def isDamageable = true
override def damage(stack: ItemStack) = {
val nbt = stack.getTagCompound
if (nbt != null) {
val data = new TabletData()
data.load(nbt)
(data.maxEnergy - data.energy).toInt
}
else 100
}
override def maxDamage(stack: ItemStack) = {
val nbt = stack.getTagCompound
if (nbt != null) {
val data = new TabletData()
data.load(nbt)
data.maxEnergy.toInt max 1
}
else 100
}
// ----------------------------------------------------------------------- //
def canCharge(stack: ItemStack): Boolean = true
def charge(stack: ItemStack, amount: Double, simulate: Boolean): Double = {
val data = new TabletData(stack)
val charge = math.min(data.maxEnergy - data.energy, amount)
if (!simulate) {
data.energy += charge
data.save(stack)
}
amount - charge
}
// ----------------------------------------------------------------------- //
override def update(stack: ItemStack, world: World, entity: Entity, slot: Int, selected: Boolean) =
entity match {
case player: EntityPlayer =>
// Play an audio cue to let players know when they finished analyzing a block.
if (world.isRemote && player.getItemInUseDuration == TimeToAnalyze && api.Items.get(player.getItemInUse) == api.Items.get(Constants.ItemName.Tablet)) {
Audio.play(player.posX.toFloat, player.posY.toFloat + 2, player.posZ.toFloat, ".")
}
Tablet.get(stack, player).update(world, player, slot, selected)
case _ =>
}
override def onItemUseFirst(stack: ItemStack, player: EntityPlayer, position: BlockPosition, side: Int, hitX: Float, hitY: Float, hitZ: Float): Boolean = {
Tablet.currentlyAnalyzing = Some((position, side, hitX, hitY, hitZ))
super.onItemUseFirst(stack, player, position, side, hitX, hitY, hitZ)
}
override def onItemUse(stack: ItemStack, player: EntityPlayer, position: BlockPosition, side: Int, hitX: Float, hitY: Float, hitZ: Float): Boolean = {
player.setItemInUse(stack, getMaxItemUseDuration(stack))
true
}
override def onItemRightClick(stack: ItemStack, world: World, player: EntityPlayer) = {
player.setItemInUse(stack, getMaxItemUseDuration(stack))
stack
}
override def getMaxItemUseDuration(stack: ItemStack): Int = 72000
override def onPlayerStoppedUsing(stack: ItemStack, player: EntityPlayer, duration: Int): Unit = {
val world = player.getEntityWorld
val didAnalyze = getMaxItemUseDuration(stack) - duration >= TimeToAnalyze
if (didAnalyze) {
if (!world.isRemote) {
Tablet.currentlyAnalyzing match {
case Some((position, side, hitX, hitY, hitZ)) => try {
val computer = Tablet.get(stack, player).machine
if (computer.isRunning) {
val data = new NBTTagCompound()
computer.node.sendToReachable("tablet.use", data, stack, player, position, ForgeDirection.getOrientation(side), float2Float(hitX), float2Float(hitY), float2Float(hitZ))
if (!data.hasNoTags) {
computer.signal("tablet_use", data)
}
}
}
catch {
case t: Throwable => OpenComputers.log.warn("Block analysis on tablet right click failed gloriously!", t)
}
case _ =>
}
}
}
else {
if (player.isSneaking) {
if (!world.isRemote) {
val tablet = Tablet.Server.get(stack, player)
tablet.machine.stop()
if (tablet.data.tier > Tier.One) {
player.openGui(OpenComputers, GuiType.TabletInner.id, world, 0, 0, 0)
}
}
}
else {
if (!world.isRemote) {
val computer = Tablet.get(stack, player).machine
computer.start()
computer.lastError match {
case message if message != null => player.addChatMessage(Localization.Analyzer.LastError(message))
case _ =>
}
}
else {
player.openGui(OpenComputers, GuiType.Tablet.id, world, 0, 0, 0)
}
}
}
}
}
class TabletWrapper(var stack: ItemStack, var player: EntityPlayer) extends ComponentInventory with MachineHost with internal.Tablet {
// Remember our *original* world, so we know which tablets to clear on dimension
// changes of players holding tablets - since the player entity instance may be
// kept the same and components are not required to properly handle world changes.
val world = player.worldObj
lazy val machine = if (world.isRemote) null else Machine.create(this)
val data = new TabletData()
val tablet = if (world.isRemote) null else new component.Tablet(this)
private var isInitialized = !world.isRemote
private var lastRunning = false
def items = data.items
override def facing = RotationHelper.fromYaw(player.rotationYaw)
override def toLocal(value: ForgeDirection) = value // -T-O-D-O- do we care? no we don't
override def toGlobal(value: ForgeDirection) = value // -T-O-D-O- do we care? no we don't
def readFromNBT() {
if (stack.hasTagCompound) {
val data = stack.getTagCompound
load(data)
if (!world.isRemote) {
tablet.load(data.getCompoundTag(Settings.namespace + "component"))
machine.load(data.getCompoundTag(Settings.namespace + "data"))
}
}
}
def writeToNBT() {
if (!stack.hasTagCompound) {
stack.setTagCompound(new NBTTagCompound())
}
val data = stack.getTagCompound
if (!world.isRemote) {
if (!data.hasKey(Settings.namespace + "data")) {
data.setTag(Settings.namespace + "data", new NBTTagCompound())
}
data.setNewCompoundTag(Settings.namespace + "component", tablet.save)
data.setNewCompoundTag(Settings.namespace + "data", machine.save)
// Force tablets into stopped state to avoid errors when trying to
// load deleted machine states.
data.getCompoundTag(Settings.namespace + "data").removeTag("state")
}
save(data)
}
readFromNBT()
if (!world.isRemote) {
api.Network.joinNewNetwork(machine.node)
machine.stop()
val charge = math.max(0, this.data.energy - tablet.node.globalBuffer)
tablet.node.changeBuffer(charge)
writeToNBT()
}
// ----------------------------------------------------------------------- //
override def onConnect(node: Node) {
if (node == this.node) {
connectComponents()
node.connect(tablet.node)
}
else node.host match {
case buffer: api.component.TextBuffer =>
buffer.setMaximumColorDepth(api.component.TextBuffer.ColorDepth.FourBit)
buffer.setMaximumResolution(80, 25)
case _ =>
}
}
override protected def connectItemNode(node: Node) {
super.connectItemNode(node)
if (node != null) node.host match {
case buffer: api.component.TextBuffer => components collect {
case Some(keyboard: api.component.Keyboard) => buffer.node.connect(keyboard.node)
}
case keyboard: api.component.Keyboard => components collect {
case Some(buffer: api.component.TextBuffer) => keyboard.node.connect(buffer.node)
}
case _ =>
}
}
override def onDisconnect(node: Node) {
if (node == this.node) {
disconnectComponents()
tablet.node.remove()
}
}
override def onMessage(message: Message) {}
override def host = this
override def getSizeInventory = items.length
override def isItemValidForSlot(slot: Int, stack: ItemStack) = slot == getSizeInventory - 1 && (Option(Driver.driverFor(stack, getClass)) match {
case Some(driver) =>
// Same special cases, similar as in robot, but allow keyboards,
// because clip-on keyboards kinda seem to make sense, I guess.
driver != DriverScreen &&
driver.slot(stack) == containerSlotType &&
driver.tier(stack) <= containerSlotTier
case _ => false
})
override def isUseableByPlayer(player: EntityPlayer) = machine.canInteract(player.getCommandSenderName)
override def markDirty(): Unit = {
data.save(stack)
player.inventory.markDirty()
}
// ----------------------------------------------------------------------- //
override def xPosition = player.posX
override def yPosition = player.posY + player.getEyeHeight
override def zPosition = player.posZ
override def markChanged() {}
// ----------------------------------------------------------------------- //
def containerSlotType = data.container.fold(Slot.None)(stack =>
Option(Driver.driverFor(stack, getClass)) match {
case Some(driver: Container) => driver.providedSlot(stack)
case _ => Slot.None
})
def containerSlotTier = data.container.fold(Tier.None)(stack =>
Option(Driver.driverFor(stack, getClass)) match {
case Some(driver: Container) => driver.providedTier(stack)
case _ => Tier.None
})
override def internalComponents(): Iterable[ItemStack] = (0 until getSizeInventory).collect {
case slot if getStackInSlot(slot) != null && isComponentSlot(slot, getStackInSlot(slot)) => getStackInSlot(slot)
}
override def componentSlot(address: String) = components.indexWhere(_.exists(env => env.node != null && env.node.address == address))
override def onMachineConnect(node: Node) = onConnect(node)
override def onMachineDisconnect(node: Node) = onDisconnect(node)
// ----------------------------------------------------------------------- //
override def node = Option(machine).fold(null: Node)(_.node)
// ----------------------------------------------------------------------- //
def update(world: World, player: EntityPlayer, slot: Int, selected: Boolean) {
this.player = player
if (!isInitialized) {
isInitialized = true
// This delayed initialization on the client side is required to allow
// the server to set up the tablet wrapper first (since packets generated
// in the component setup would otherwise be queued before the events that
// caused this wrapper's initialization).
connectComponents()
components collect {
case Some(buffer: api.component.TextBuffer) =>
buffer.setMaximumColorDepth(api.component.TextBuffer.ColorDepth.FourBit)
buffer.setMaximumResolution(80, 25)
}
}
if (!world.isRemote) {
machine.update()
updateComponents()
data.isRunning = machine.isRunning
data.energy = tablet.node.globalBuffer()
data.maxEnergy = tablet.node.globalBufferSize()
if (lastRunning != machine.isRunning) {
lastRunning = machine.isRunning
markDirty()
}
}
}
// ----------------------------------------------------------------------- //
override def load(nbt: NBTTagCompound) {
data.load(nbt)
}
override def save(nbt: NBTTagCompound) {
saveComponents()
data.save(nbt)
}
}
object Tablet {
// This is super-hacky, but since it's only used on the client we get away
// with storing context information for analyzing a block in the singleton.
var currentlyAnalyzing: Option[(BlockPosition, Int, Float, Float, Float)] = None
def getId(stack: ItemStack) = {
if (!stack.hasTagCompound) {
stack.setTagCompound(new NBTTagCompound())
}
if (!stack.getTagCompound.hasKey(Settings.namespace + "tablet")) {
stack.getTagCompound.setString(Settings.namespace + "tablet", UUID.randomUUID().toString)
}
stack.getTagCompound.getString(Settings.namespace + "tablet")
}
def get(stack: ItemStack, holder: EntityPlayer) = {
if (holder.worldObj.isRemote) Client.get(stack, holder)
else Server.get(stack, holder)
}
@SubscribeEvent
def onWorldSave(e: WorldEvent.Save) {
Server.saveAll(e.world)
}
@SubscribeEvent
def onWorldUnload(e: WorldEvent.Unload) {
Client.clear(e.world)
Server.clear(e.world)
}
@SubscribeEvent
def onClientTick(e: ClientTickEvent) {
Client.cleanUp()
}
@SubscribeEvent
def onServerTick(e: ServerTickEvent) {
Server.cleanUp()
}
abstract class Cache extends Callable[TabletWrapper] with RemovalListener[String, TabletWrapper] {
val cache = com.google.common.cache.CacheBuilder.newBuilder().
expireAfterAccess(timeout, TimeUnit.SECONDS).
removalListener(this).
asInstanceOf[CacheBuilder[String, TabletWrapper]].
build[String, TabletWrapper]()
protected def timeout = 10
// To allow access in cache entry init.
private var currentStack: ItemStack = _
private var currentHolder: EntityPlayer = _
def get(stack: ItemStack, holder: EntityPlayer) = {
val id = getId(stack)
cache.synchronized {
currentStack = stack
currentHolder = holder
var wrapper = cache.get(id, this)
// Force re-load on world change, in case some components store a
// reference to the world object.
if (holder.worldObj != wrapper.world) {
cache.invalidate(id)
cache.cleanUp()
wrapper = cache.get(id, this)
}
wrapper.stack = stack
wrapper.player = holder
wrapper
}
}
def call = {
new TabletWrapper(currentStack, currentHolder)
}
def onRemoval(e: RemovalNotification[String, TabletWrapper]) {
val tablet = e.getValue
if (tablet.node != null) {
// Server.
tablet.writeToNBT()
tablet.machine.stop()
for (node <- tablet.machine.node.network.nodes) {
node.remove()
}
tablet.writeToNBT()
}
}
def clear(world: World) {
cache.synchronized {
val tabletsInWorld = cache.asMap.filter(_._2.world == world)
cache.invalidateAll(asJavaIterable(tabletsInWorld.keys))
cache.cleanUp()
}
}
def cleanUp() {
cache.synchronized(cache.cleanUp())
}
}
object Client extends Cache {
override protected def timeout = 5
def get(stack: ItemStack) = {
if (stack.hasTagCompound && stack.getTagCompound.hasKey(Settings.namespace + "tablet")) {
val id = stack.getTagCompound.getString(Settings.namespace + "tablet")
cache.synchronized(Option(cache.getIfPresent(id)))
}
else None
}
}
object Server extends Cache {
def saveAll(world: World) {
cache.synchronized {
for (tablet <- cache.asMap.values if tablet.world == world) {
tablet.writeToNBT()
}
}
}
}
}