blob: 305f70ce5d0ae55648b1658d049215def0d8ab6d [file] [log] [blame] [raw]
package li.cil.oc.common.tileentity
import java.util
import cpw.mods.fml.relauncher.Side
import cpw.mods.fml.relauncher.SideOnly
import li.cil.oc._
import li.cil.oc.api.driver.DeviceInfo.DeviceAttribute
import li.cil.oc.api.driver.DeviceInfo.DeviceClass
import li.cil.oc.api.driver.DeviceInfo
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.network.Analyzable
import li.cil.oc.api.network._
import li.cil.oc.common.SaveHandler
import li.cil.oc.integration.util.Waila
import li.cil.oc.server.{PacketSender => ServerPacketSender}
import net.minecraft.entity.player.EntityPlayer
import net.minecraft.nbt.NBTTagCompound
import net.minecraft.util.AxisAlignedBB
import net.minecraft.util.Vec3
import net.minecraftforge.common.util.ForgeDirection
import scala.collection.convert.WrapAsJava._
import scala.collection.mutable
class Hologram(var tier: Int) extends traits.Environment with SidedEnvironment with Analyzable with traits.Rotatable with DeviceInfo {
def this() = this(0)
val node = api.Network.newNode(this, Visibility.Network).
withComponent("hologram").
withConnector().
create()
final val width = 3 * 16
final val height = 2 * 16 // 32 bit in an int
private final lazy val deviceInfo = Map(
DeviceAttribute.Class -> DeviceClass.Display,
DeviceAttribute.Description -> "Holographic projector",
DeviceAttribute.Vendor -> Constants.DeviceInfo.DefaultVendor,
DeviceAttribute.Product -> ("VirtualViewer H1-" + (tier + 1).toString),
DeviceAttribute.Capacity -> (width * width * height).toString,
DeviceAttribute.Width -> colors.length.toString
)
override def getDeviceInfo: util.Map[String, String] = deviceInfo
// ----------------------------------------------------------------------- //
// Layout is: first half is lower bit, second half is higher bit for the
// voxels in the cube. This is to retain compatibility with pre 1.3 saves.
val volume = new Array[Int](width * width * 2)
// Render scale.
var scale = 1.0
// Projection Y position offset - consider adding X,Z later perhaps
var translation = Vec3.createVectorHelper(0, 0, 0)
// Relative number of lit columns (for energy cost).
var litRatio = -1.0
// Whether we need to recompile our display list.
var needsRendering = false
// Store it here for convenience, this is the number of visible voxel faces
// as determined in the last VBO index update. See HologramRenderer.
var visibleQuads = 0
// What parts of the hologram changed and need an update packet.
var dirty = mutable.Set.empty[Short]
// Interval of dirty columns.
var dirtyFromX = Int.MaxValue
var dirtyUntilX = -1
var dirtyFromZ = Int.MaxValue
var dirtyUntilZ = -1
var hasPower = true
// Rotation base state. Current rotation is based on world time. See HologramRenderer.
var rotationAngle = 0f
var rotationX = 0f
var rotationY = 0f
var rotationZ = 0f
var rotationSpeed = 0f
var rotationSpeedX = 0f
var rotationSpeedY = 0f
var rotationSpeedZ = 0f
final val colorsByTier = Array(Array(0x00FF00), Array(0x0000FF, 0x00FF00, 0xFF0000)) // 0xBBGGRR for rendering convenience
// This is a def and not a val for loading (where the tier comes from the nbt and is always 0 here).
def colors = colorsByTier(tier)
def getColor(x: Int, y: Int, z: Int) = {
val lbit = (volume(x + z * width) >>> y) & 1
val hbit = (volume(x + z * width + width * width) >>> y) & 1
lbit | (hbit << 1)
}
def setColor(x: Int, y: Int, z: Int, value: Int) {
if ((value & 3) != getColor(x, y, z)) {
val lbit = value & 1
val hbit = (value >>> 1) & 1
volume(x + z * width) = (volume(x + z * width) & ~(1 << y)) | (lbit << y)
volume(x + z * width + width * width) = (volume(x + z * width + width * width) & ~(1 << y)) | (hbit << y)
setDirty(x, z)
}
}
private def setDirty(x: Int, z: Int) {
dirty += ((x.toByte << 8) | z.toByte).toShort
dirtyFromX = math.min(dirtyFromX, x)
dirtyUntilX = math.max(dirtyUntilX, x + 1)
dirtyFromZ = math.min(dirtyFromZ, z)
dirtyUntilZ = math.max(dirtyUntilZ, z + 1)
litRatio = -1
}
private def resetDirtyFlag() {
dirty.clear()
dirtyFromX = Int.MaxValue
dirtyUntilX = -1
dirtyFromZ = Int.MaxValue
dirtyUntilZ = -1
}
// ----------------------------------------------------------------------- //
@SideOnly(Side.CLIENT)
override def canConnect(side: ForgeDirection) = toLocal(side) == ForgeDirection.DOWN
override def sidedNode(side: ForgeDirection) = if (toLocal(side) == ForgeDirection.DOWN) node else null
// Override automatic analyzer implementation for sided environments.
override def onAnalyze(player: EntityPlayer, side: Int, hitX: Float, hitY: Float, hitZ: Float) = Array(node)
// ----------------------------------------------------------------------- //
@Callback(doc = """function() -- Clears the hologram.""")
def clear(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
for (i <- volume.indices) volume(i) = 0
ServerPacketSender.sendHologramClear(this)
resetDirtyFlag()
litRatio = 0
null
}
@Callback(direct = true, doc = """function(x:number, y:number, z:number):number -- Returns the value for the specified voxel.""")
def get(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
val (x, y, z) = checkCoordinates(args)
result(getColor(x, y, z))
}
@Callback(direct = true, limit = 256, doc = """function(x:number, y:number, z:number, value:number or boolean) -- Set the value for the specified voxel.""")
def set(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
val (x, y, z) = checkCoordinates(args)
val value = checkColor(args, 3)
setColor(x, y, z, value)
null
}
@Callback(direct = true, limit = 128, doc = """function(x:number, z:number[, minY:number], maxY:number, value:number or boolean) -- Fills an interval of a column with the specified value.""")
def fill(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
val (x, _, z) = checkCoordinates(args, 0, -1, 1)
val (minY, maxY, value) =
if (args.count > 4)
(math.min(32, math.max(1, args.checkInteger(2))), math.min(32, math.max(1, args.checkInteger(3))), checkColor(args, 4))
else
(1, math.min(32, math.max(1, args.checkInteger(2))), checkColor(args, 3))
if (minY > maxY) throw new IllegalArgumentException("interval is empty")
val mask = (0xFFFFFFFF >>> (31 - (maxY - minY))) << (minY - 1)
val lbit = value & 1
val hbit = (value >>> 1) & 1
if (lbit == 0 || height == 0) volume(x + z * width) &= ~mask
else volume(x + z * width) |= mask
if (hbit == 0 || height == 0) volume(x + z * width + width * width) &= ~mask
else volume(x + z * width + width * width) |= mask
setDirty(x, z)
null
}
@Callback(doc = """function(data:string) -- Set the raw buffer to the specified byte array, where each byte represents a voxel color. Nesting is x,z,y.""")
def setRaw(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
val data = args.checkByteArray(0)
for (x <- 0 until width; z <- 0 until width) {
val offset = z * height + x * height * width
if (data.length >= offset + height) {
var lbit = 0
var hbit = 0
for (y <- (height - 1) to 0 by -1) {
val color = data(offset + y)
lbit |= (color & 1) << y
hbit |= ((color & 3) >>> 1) << y
}
val index = x + z * width
if (volume(index) != lbit || volume(index + width * width) != hbit) {
volume(index) = lbit
volume(index + width * width) = hbit
setDirty(x, z)
}
}
}
context.pause(Settings.get.hologramSetRawDelay)
null
}
@Callback(doc = """function(x:number, z:number, sx:number, sz:number, tx:number, tz:number) -- Copies an area of columns by the specified translation.""")
def copy(context: Context, args: Arguments): Array[AnyRef] = this.synchronized {
val (x, _, z) = checkCoordinates(args, 0, -1, 1)
val w = args.checkInteger(2)
val h = args.checkInteger(3)
val tx = args.checkInteger(4)
val tz = args.checkInteger(5)
// Anything to do at all?
if (w <= 0 || h <= 0) return null
if (tx == 0 && tz == 0) return null
// Loop over the target rectangle, starting from the directions away from
// the source rectangle and copy the data. This way we ensure we don't
// overwrite anything we still need to copy.
val (dx0, dx1) = (math.max(0, math.min(width - 1, x + tx + w - 1)), math.max(0, math.min(width, x + tx))) match {
case dx if tx > 0 => dx
case dx => dx.swap
}
val (dz0, dz1) = (math.max(0, math.min(width - 1, z + tz + h - 1)), math.max(0, math.min(width, z + tz))) match {
case dz if tz > 0 => dz
case dz => dz.swap
}
val (sx, sz) = (if (tx > 0) -1 else 1, if (tz > 0) -1 else 1)
// Copy values to destination rectangle if there source is valid.
for (nz <- dz0 to dz1 by sz) {
nz - tz match {
case oz if oz >= 0 && oz < width =>
for (nx <- dx0 to dx1 by sx) {
nx - tx match {
case ox if ox >= 0 && ox < width =>
volume(nz * width + nx) = volume(oz * width + ox)
volume(nz * width + nx + width * width) = volume(oz * width + ox + width * width)
case _ => /* Got no source column. */
}
}
case _ => /* Got no source row. */
}
}
// Mark target rectangle dirty.
setDirty(math.min(dx0, dx1), math.min(dz0, dz1))
setDirty(math.max(dx0, dx1), math.max(dz0, dz1))
// The reasoning here is: it'd take 18 ticks to do the whole are with fills,
// so make this slightly more efficient (15 ticks - 0.75 seconds). Make it
// 'free' if it's less than 0.25 seconds, i.e. for small copies.
val area = (math.max(dx0, dx1) - math.min(dx0, dx1)) * (math.max(dz0, dz1) - math.min(dz0, dz1))
val relativeArea = math.max(0, area / (width * width).toFloat - 0.25)
context.pause(relativeArea)
null
}
@Callback(direct = true, doc = """function():number -- Returns the render scale of the hologram.""")
def getScale(context: Context, args: Arguments): Array[AnyRef] = {
result(scale)
}
@Callback(doc = """function(value:number) -- Set the render scale. A larger scale consumes more energy.""")
def setScale(context: Context, args: Arguments): Array[AnyRef] = {
scale = math.max(0.333333, math.min(Settings.get.hologramMaxScaleByTier(tier), args.checkDouble(0)))
ServerPacketSender.sendHologramScale(this)
null
}
@Callback(direct = true, doc = """function():number, number, number -- Returns the relative render projection offsets of the hologram.""")
def getTranslation(context: Context, args: Arguments): Array[AnyRef] = {
result(translation.xCoord, translation.yCoord, translation.zCoord)
}
@Callback(doc = """function(tx:number, ty:number, tz:number) -- Sets the relative render projection offsets of the hologram.""")
def setTranslation(context: Context, args: Arguments): Array[AnyRef] = {
// Validate all axes before setting the values.
val maxTranslation = Settings.get.hologramMaxTranslationByTier(tier)
val tx = math.max(-maxTranslation, math.min(maxTranslation, args.checkDouble(0)))
val ty = math.max(0, math.min(maxTranslation * 2, args.checkDouble(1)))
val tz = math.max(-maxTranslation, math.min(maxTranslation, args.checkDouble(2)))
translation.xCoord = tx
translation.yCoord = ty
translation.zCoord = tz
ServerPacketSender.sendHologramOffset(this)
null
}
@Callback(direct = true, doc = """function():number -- The color depth supported by the hologram.""")
def maxDepth(context: Context, args: Arguments): Array[AnyRef] = {
result(tier + 1)
}
@Callback(doc = """function(index:number):number -- Get the color defined for the specified value.""")
def getPaletteColor(context: Context, args: Arguments): Array[AnyRef] = {
val index = args.checkInteger(0)
if (index < 1 || index > colors.length) throw new ArrayIndexOutOfBoundsException()
// Colors are stored as 0xAABBGGRR for rendering convenience, so convert them.
result(convertColor(colors(index - 1)))
}
@Callback(doc = """function(index:number, value:number):number -- Set the color defined for the specified value.""")
def setPaletteColor(context: Context, args: Arguments): Array[AnyRef] = {
val index = args.checkInteger(0)
if (index < 1 || index > colors.length) throw new ArrayIndexOutOfBoundsException()
val value = args.checkInteger(1)
val oldValue = colors(index - 1)
// Change byte order here to allow passing stored color to OpenGL "as-is"
// (as whole Int, i.e. 0xAABBGGRR, alpha is unused but present for alignment)
colors(index - 1) = convertColor(value)
ServerPacketSender.sendHologramColor(this, index - 1, colors(index - 1))
result(oldValue)
}
@Callback(doc = """function(angle:number, x:number, y:number, z:number):boolean -- Set the base rotation of the displayed hologram.""")
def setRotation(context: Context, args: Arguments): Array[AnyRef] = {
if (tier > 0) {
val r = args.checkDouble(0) % 360
val x = args.checkDouble(1)
val y = args.checkDouble(2)
val z = args.checkDouble(3)
rotationAngle = r.toFloat
rotationX = x.toFloat
rotationY = y.toFloat
rotationZ = z.toFloat
ServerPacketSender.sendHologramRotation(this)
result(true)
}
else result(Unit, "not supported")
}
@Callback(doc = """function(speed:number, x:number, y:number, z:number):boolean -- Set the rotation speed of the displayed hologram.""")
def setRotationSpeed(context: Context, args: Arguments): Array[AnyRef] = {
if (tier > 0) {
val v = args.checkDouble(0) max -360 * 4 min 360 * 4
val x = args.checkDouble(1)
val y = args.checkDouble(2)
val z = args.checkDouble(3)
rotationSpeed = v.toFloat
rotationSpeedX = x.toFloat
rotationSpeedY = y.toFloat
rotationSpeedZ = z.toFloat
ServerPacketSender.sendHologramRotationSpeed(this)
result(true)
}
else result(Unit, "not supported")
}
private def checkCoordinates(args: Arguments, idxX: Int = 0, idxY: Int = 1, idxZ: Int = 2) = {
val x = if (idxX >= 0) args.checkInteger(idxX) - 1 else 0
if (x < 0 || x >= width) throw new ArrayIndexOutOfBoundsException("x")
val y = if (idxY >= 0) args.checkInteger(idxY) - 1 else 0
if (y < 0 || y >= height) throw new ArrayIndexOutOfBoundsException("y")
val z = if (idxZ >= 0) args.checkInteger(idxZ) - 1 else 0
if (z < 0 || z >= width) throw new ArrayIndexOutOfBoundsException("z")
(x, y, z)
}
private def checkColor(args: Arguments, index: Int) = {
val value =
if (args.isBoolean(index))
if (args.checkBoolean(index)) 1 else 0
else
args.checkInteger(index)
if (value < 0 || value > colors.length) throw new IllegalArgumentException("invalid value")
value
}
private def convertColor(color: Int) = {
((color & 0x0000FF) << 16) | (color & 0x00FF00) | ((color & 0xFF0000) >>> 16)
}
// ----------------------------------------------------------------------- //
override def canUpdate = isServer
override def updateEntity() {
super.updateEntity()
if (isServer) {
if (dirty.nonEmpty) this.synchronized {
val dirtySizeX = dirtyUntilX - dirtyFromX
val dirtySizeZ = dirtyUntilZ - dirtyFromZ
// Sending the dirty area requires
// dirtySizeX * dirtySizeZ * (4 + 4)
// bytes (2 = low + high byte).
// Sending a single changes requires
// changes * (4 + 4 + 2)
// bytes (other 2 byte = coords).
// So at some point it'll be cheaper to just send the area:
// changes * (4 + 4 + 2) = dirtySizeX * dirtySizeZ * (4 + 4)
// changes = dirtySizeX * dirtySizeZ * (4 + 4) / (4 + 4 + 2) = dirtySizeX * dirtySizeZ * 0.8
// So if changes are larger than that, just send the full hologram.
if (dirty.size > dirtySizeX * dirtySizeZ * 0.8)
ServerPacketSender.sendHologramArea(this)
else
ServerPacketSender.sendHologramValues(this)
resetDirtyFlag()
}
if (world.getTotalWorldTime % Settings.get.tickFrequency == 0) {
if (litRatio < 0) this.synchronized {
litRatio = 0
for (i <- volume.indices) {
if (volume(i) != 0) litRatio += 1
}
litRatio /= volume.length
}
val hadPower = hasPower
val neededPower = Settings.get.hologramCost * litRatio * scale * Settings.get.tickFrequency
hasPower = node.tryChangeBuffer(-neededPower)
if (hasPower != hadPower) {
ServerPacketSender.sendHologramPowerChange(this)
}
}
}
}
// ----------------------------------------------------------------------- //
override def shouldRenderInPass(pass: Int) = pass == 1
override def getMaxRenderDistanceSquared = scale / Settings.get.hologramMaxScaleByTier.max * Settings.get.hologramRenderDistance * Settings.get.hologramRenderDistance
def getFadeStartDistanceSquared = scale / Settings.get.hologramMaxScaleByTier.max * Settings.get.hologramFadeStartDistance * Settings.get.hologramFadeStartDistance
private final val Sqrt2 = Math.sqrt(2)
override def getRenderBoundingBox = {
val cx = x + 0.5
val cy = y + 0.5
val cz = z + 0.5
val sh = width / 16 * scale * Sqrt2 // overscale to take into account 45 degree rotation
val sv = height / 16 * scale * Sqrt2
AxisAlignedBB.getBoundingBox(
cx + (-0.5 + translation.xCoord) * sh,
cy + translation.yCoord * sv,
cz + (-0.5 + translation.zCoord) * sh,
cx + (0.5 + translation.xCoord) * sh,
cy + (1 + translation.yCoord) * sv,
cz + (0.5 + translation.xCoord) * sh)
}
// ----------------------------------------------------------------------- //
override def readFromNBTForServer(nbt: NBTTagCompound) {
tier = nbt.getByte(Settings.namespace + "tier") max 0 min 1
super.readFromNBTForServer(nbt)
val tag = SaveHandler.loadNBT(nbt, node.address + "_data")
tag.getIntArray("volume").copyToArray(volume)
tag.getIntArray("colors").map(convertColor).copyToArray(colors)
scale = nbt.getDouble(Settings.namespace + "scale")
translation.xCoord = nbt.getDouble(Settings.namespace + "offsetX")
translation.yCoord = nbt.getDouble(Settings.namespace + "offsetY")
translation.zCoord = nbt.getDouble(Settings.namespace + "offsetZ")
rotationAngle = nbt.getFloat(Settings.namespace + "rotationAngle")
rotationX = nbt.getFloat(Settings.namespace + "rotationX")
rotationY = nbt.getFloat(Settings.namespace + "rotationY")
rotationZ = nbt.getFloat(Settings.namespace + "rotationZ")
rotationSpeed = nbt.getFloat(Settings.namespace + "rotationSpeed")
rotationSpeedX = nbt.getFloat(Settings.namespace + "rotationSpeedX")
rotationSpeedY = nbt.getFloat(Settings.namespace + "rotationSpeedY")
rotationSpeedZ = nbt.getFloat(Settings.namespace + "rotationSpeedZ")
}
override def writeToNBTForServer(nbt: NBTTagCompound) = this.synchronized {
nbt.setByte(Settings.namespace + "tier", tier.toByte)
super.writeToNBTForServer(nbt)
if (!Waila.isSavingForTooltip) {
SaveHandler.scheduleSave(world, x, z, nbt, node.address + "_data", tag => {
tag.setIntArray("volume", volume)
tag.setIntArray("colors", colors.map(convertColor))
})
}
nbt.setDouble(Settings.namespace + "scale", scale)
nbt.setDouble(Settings.namespace + "offsetX", translation.xCoord)
nbt.setDouble(Settings.namespace + "offsetY", translation.yCoord)
nbt.setDouble(Settings.namespace + "offsetZ", translation.zCoord)
nbt.setFloat(Settings.namespace + "rotationAngle", rotationAngle)
nbt.setFloat(Settings.namespace + "rotationX", rotationX)
nbt.setFloat(Settings.namespace + "rotationY", rotationY)
nbt.setFloat(Settings.namespace + "rotationZ", rotationZ)
nbt.setFloat(Settings.namespace + "rotationSpeed", rotationSpeed)
nbt.setFloat(Settings.namespace + "rotationSpeedX", rotationSpeedX)
nbt.setFloat(Settings.namespace + "rotationSpeedY", rotationSpeedY)
nbt.setFloat(Settings.namespace + "rotationSpeedZ", rotationSpeedZ)
}
@SideOnly(Side.CLIENT)
override def readFromNBTForClient(nbt: NBTTagCompound) {
super.readFromNBTForClient(nbt)
nbt.getIntArray("volume").copyToArray(volume)
nbt.getIntArray("colors").copyToArray(colors)
scale = nbt.getDouble("scale")
hasPower = nbt.getBoolean("hasPower")
translation.xCoord = nbt.getDouble("offsetX")
translation.yCoord = nbt.getDouble("offsetY")
translation.zCoord = nbt.getDouble("offsetZ")
rotationAngle = nbt.getFloat("rotationAngle")
rotationX = nbt.getFloat("rotationX")
rotationY = nbt.getFloat("rotationY")
rotationZ = nbt.getFloat("rotationZ")
rotationSpeed = nbt.getFloat("rotationSpeed")
rotationSpeedX = nbt.getFloat("rotationSpeedX")
rotationSpeedY = nbt.getFloat("rotationSpeedY")
rotationSpeedZ = nbt.getFloat("rotationSpeedZ")
}
override def writeToNBTForClient(nbt: NBTTagCompound) {
super.writeToNBTForClient(nbt)
nbt.setIntArray("volume", volume)
nbt.setIntArray("colors", colors)
nbt.setDouble("scale", scale)
nbt.setBoolean("hasPower", hasPower)
nbt.setDouble("offsetX", translation.xCoord)
nbt.setDouble("offsetY", translation.yCoord)
nbt.setDouble("offsetZ", translation.zCoord)
nbt.setFloat("rotationAngle", rotationAngle)
nbt.setFloat("rotationX", rotationX)
nbt.setFloat("rotationY", rotationY)
nbt.setFloat("rotationZ", rotationZ)
nbt.setFloat("rotationSpeed", rotationSpeed)
nbt.setFloat("rotationSpeedX", rotationSpeedX)
nbt.setFloat("rotationSpeedY", rotationSpeedY)
nbt.setFloat("rotationSpeedZ", rotationSpeedZ)
}
}