From 0fdbd386436e3f079892afcd07f1a69f814338f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sun, 1 Feb 2015 23:02:11 +0100 Subject: [PATCH 1/8] Allow pushing NBTTagCompounds as signals and added converter for NBTTagCompound. --- .../java/li/cil/oc/api/machine/Context.java | 1 + .../oc/integration/vanilla/ConverterNBT.scala | 35 +++++++++++++++++++ .../oc/integration/vanilla/ModVanilla.scala | 1 + .../li/cil/oc/server/machine/Machine.scala | 9 +++-- 4 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/main/scala/li/cil/oc/integration/vanilla/ConverterNBT.scala diff --git a/src/main/java/li/cil/oc/api/machine/Context.java b/src/main/java/li/cil/oc/api/machine/Context.java index 622696e2a..fe82dacb6 100644 --- a/src/main/java/li/cil/oc/api/machine/Context.java +++ b/src/main/java/li/cil/oc/api/machine/Context.java @@ -153,6 +153,7 @@ public interface Context { *
  • Strings.
  • *
  • Byte arrays (which appear as strings on the Lua side, e.g.).
  • *
  • Maps if and only if both keys and values are strings.
  • + *
  • NBTTagCompounds.
  • * * If an unsupported type is specified the method will enqueue nothing * instead, resulting in a nil on the Lua side, e.g., and log a diff --git a/src/main/scala/li/cil/oc/integration/vanilla/ConverterNBT.scala b/src/main/scala/li/cil/oc/integration/vanilla/ConverterNBT.scala new file mode 100644 index 000000000..c5a815c8c --- /dev/null +++ b/src/main/scala/li/cil/oc/integration/vanilla/ConverterNBT.scala @@ -0,0 +1,35 @@ +package li.cil.oc.integration.vanilla + +import java.util + +import li.cil.oc.api +import net.minecraft.nbt._ + +import scala.collection.convert.WrapAsScala._ + +object ConverterNBT extends api.driver.Converter { + override def convert(value: AnyRef, output: util.Map[AnyRef, AnyRef]) = + value match { + case nbt: NBTTagCompound => output += "oc:flatten" -> convert(nbt) + case _ => + } + + private def convert(nbt: NBTBase): AnyRef = nbt match { + case tag: NBTTagByte => byte2Byte(tag.func_150290_f()) + case tag: NBTTagShort => short2Short(tag.func_150289_e()) + case tag: NBTTagInt => int2Integer(tag.func_150287_d()) + case tag: NBTTagLong => long2Long(tag.func_150291_c()) + case tag: NBTTagFloat => float2Float(tag.func_150288_h()) + case tag: NBTTagDouble => double2Double(tag.func_150286_g()) + case tag: NBTTagByteArray => tag.func_150292_c() + case tag: NBTTagString => tag.func_150285_a_() + case tag: NBTTagList => + val copy = tag.copy().asInstanceOf[NBTTagList] + (0 until copy.tagCount).map(_ => convert(copy.removeTag(0))).toArray + case tag: NBTTagCompound => + tag.func_150296_c().collect { + case key: String => key -> convert(tag.getTag(key)) + }.toMap + case tag: NBTTagIntArray => tag.func_150302_c() + } +} diff --git a/src/main/scala/li/cil/oc/integration/vanilla/ModVanilla.scala b/src/main/scala/li/cil/oc/integration/vanilla/ModVanilla.scala index cf98113bf..f525cc064 100644 --- a/src/main/scala/li/cil/oc/integration/vanilla/ModVanilla.scala +++ b/src/main/scala/li/cil/oc/integration/vanilla/ModVanilla.scala @@ -31,5 +31,6 @@ object ModVanilla extends ModProxy { Driver.add(ConverterFluidStack) Driver.add(ConverterFluidTankInfo) Driver.add(ConverterItemStack) + Driver.add(ConverterNBT) } } diff --git a/src/main/scala/li/cil/oc/server/machine/Machine.scala b/src/main/scala/li/cil/oc/server/machine/Machine.scala index c895aeaee..39b481079 100644 --- a/src/main/scala/li/cil/oc/server/machine/Machine.scala +++ b/src/main/scala/li/cil/oc/server/machine/Machine.scala @@ -238,6 +238,7 @@ class Machine(val host: MachineHost) extends prefab.ManagedEnvironment with mach 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 @@ -247,7 +248,7 @@ class Machine(val host: MachineHost) extends prefab.ManagedEnvironment with mach } }) - override def popSignal(): Machine.Signal = signals.synchronized(if (signals.isEmpty) null else signals.dequeue()) + 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 @@ -628,6 +629,7 @@ class Machine(val host: MachineHost) extends prefab.ManagedEnvironment with mach data += tag.getStringTagAt(i) -> tag.getStringTagAt(i + 1) } data + case tag: NBTTagCompound => tag case _ => null }.toArray[AnyRef]) }) @@ -705,6 +707,7 @@ class Machine(val host: MachineHost) extends prefab.ManagedEnvironment with mach 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) } }) @@ -936,7 +939,9 @@ object Machine extends MachineAPI { } /** 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 + 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) } \ No newline at end of file From 5a4dedf6d1028f3d4fa803f9efdec1a90b2885d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Sun, 1 Feb 2015 23:38:43 +0100 Subject: [PATCH 2/8] Right-clicking blocks with tablet in hand will now allow components to populate a data table. If any is given, a `tablet_use` signal is sent to the tablet, with the data passed as a table as the only parameter for the signal. Closes #858. --- .../scala/li/cil/oc/common/item/Tablet.scala | 20 +++++++- .../li/cil/oc/server/component/Geolyzer.scala | 49 +++++++++++++++++-- .../server/component/UpgradeNavigation.scala | 21 ++++++++ 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/main/scala/li/cil/oc/common/item/Tablet.scala b/src/main/scala/li/cil/oc/common/item/Tablet.scala index 3f0ce0016..6b95a776b 100644 --- a/src/main/scala/li/cil/oc/common/item/Tablet.scala +++ b/src/main/scala/li/cil/oc/common/item/Tablet.scala @@ -32,9 +32,9 @@ 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.DriverKeyboard import li.cil.oc.integration.opencomputers.DriverScreen import li.cil.oc.server.component +import li.cil.oc.util.BlockPosition import li.cil.oc.util.ExtendedNBT._ import li.cil.oc.util.Rarity import li.cil.oc.util.RotationHelper @@ -125,6 +125,24 @@ class Tablet(val parent: Delegator) extends Delegate { case _ => } + override def onItemUse(stack: ItemStack, player: EntityPlayer, position: BlockPosition, side: Int, hitX: Float, hitY: Float, hitZ: Float): Boolean = { + val world = player.getEntityWorld + if (!world.isRemote) 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) + } + true + } + override def onItemRightClick(stack: ItemStack, world: World, player: EntityPlayer) = { if (!player.isSneaking) { if (world.isRemote) { diff --git a/src/main/scala/li/cil/oc/server/component/Geolyzer.scala b/src/main/scala/li/cil/oc/server/component/Geolyzer.scala index 5dc708c2f..b86c0edd8 100644 --- a/src/main/scala/li/cil/oc/server/component/Geolyzer.scala +++ b/src/main/scala/li/cil/oc/server/component/Geolyzer.scala @@ -1,26 +1,32 @@ package li.cil.oc.server.component +import com.google.common.base.Strings import li.cil.oc.Settings import li.cil.oc.api import li.cil.oc.api.driver.EnvironmentHost import li.cil.oc.api.event.GeolyzerEvent import li.cil.oc.api.event.GeolyzerEvent.Analyze -import li.cil.oc.api.internal.Rotatable +import li.cil.oc.api.internal 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.Message import li.cil.oc.api.network.Visibility import li.cil.oc.api.prefab import li.cil.oc.util.BlockPosition import li.cil.oc.util.DatabaseAccess import li.cil.oc.util.ExtendedArguments._ import li.cil.oc.util.ExtendedWorld._ +import net.minecraft.block.Block +import net.minecraft.entity.player.EntityPlayer import net.minecraft.item.Item import net.minecraft.item.ItemStack +import net.minecraft.nbt.NBTTagCompound import net.minecraftforge.common.MinecraftForge import net.minecraftforge.common.util.ForgeDirection import scala.collection.convert.WrapAsJava._ +import scala.collection.convert.WrapAsScala._ class Geolyzer(val host: EnvironmentHost) extends prefab.ManagedEnvironment { override val node = api.Network.newNode(this, Visibility.Network). @@ -51,7 +57,7 @@ class Geolyzer(val host: EnvironmentHost) extends prefab.ManagedEnvironment { def analyze(computer: Context, args: Arguments): Array[AnyRef] = if (Settings.get.allowItemStackInspection) { val side = args.checkSide(0, ForgeDirection.VALID_DIRECTIONS: _*) val globalSide = host match { - case rotatable: Rotatable => rotatable.toGlobal(side) + case rotatable: internal.Rotatable => rotatable.toGlobal(side) case _ => side } val options = args.optTable(1, Map.empty[AnyRef, AnyRef]) @@ -70,7 +76,7 @@ class Geolyzer(val host: EnvironmentHost) extends prefab.ManagedEnvironment { def store(computer: Context, args: Arguments): Array[AnyRef] = { val side = args.checkSide(0, ForgeDirection.VALID_DIRECTIONS: _*) val globalSide = host match { - case rotatable: Rotatable => rotatable.toGlobal(side) + case rotatable: internal.Rotatable => rotatable.toGlobal(side) case _ => side } @@ -93,4 +99,41 @@ class Geolyzer(val host: EnvironmentHost) extends prefab.ManagedEnvironment { }) } } + + override def onMessage(message: Message): Unit = { + super.onMessage(message) + if (message.name == "tablet.use") message.source.host match { + case machine: api.machine.Machine => (machine.host, message.data) match { + case (tablet: internal.Tablet, Array(nbt: NBTTagCompound, stack: ItemStack, player: EntityPlayer, blockPos: BlockPosition, side: ForgeDirection, hitX: java.lang.Float, hitY: java.lang.Float, hitZ: java.lang.Float)) => + if (node.tryChangeBuffer(-Settings.get.geolyzerScanCost)) { + // TODO 1.5 replace with event (change event to allow arbitrary coordinates) + val world = player.getEntityWorld + val block = world.getBlock(blockPos) + + if (!Strings.isNullOrEmpty(Block.blockRegistry.getNameForObject(block))) { + nbt.setString("name", Block.blockRegistry.getNameForObject(block)) + } + nbt.setInteger("metadata", world.getBlockMetadata(blockPos)) + nbt.setFloat("hardness", world.getBlockHardness(blockPos)) + nbt.setInteger("harvestLevel", world.getBlockHarvestLevel(blockPos)) + if (!Strings.isNullOrEmpty(world.getBlockHarvestTool(blockPos))) { + nbt.setString("harvestTool", world.getBlockHarvestTool(blockPos)) + } + nbt.setInteger("color", world.getBlockMapColor(blockPos).colorValue) + +// val event = new Analyze(host, Map.empty[AnyRef, AnyRef], side) +// MinecraftForge.EVENT_BUS.post(event) +// if (!event.isCanceled) { +// for ((key, value) <- event.data) value match { +// case number: java.lang.Number => nbt.setDouble(key, number.doubleValue()) +// case string: String if !string.isEmpty => nbt.setString(key, string) +// case _ => // Unsupported, ignore. +// } +// } + } + case _ => // Ignore. + } + case _ => // Ignore. + } + } } diff --git a/src/main/scala/li/cil/oc/server/component/UpgradeNavigation.scala b/src/main/scala/li/cil/oc/server/component/UpgradeNavigation.scala index aefdd926a..08523b0a6 100644 --- a/src/main/scala/li/cil/oc/server/component/UpgradeNavigation.scala +++ b/src/main/scala/li/cil/oc/server/component/UpgradeNavigation.scala @@ -1,7 +1,9 @@ package li.cil.oc.server.component +import li.cil.oc.api import li.cil.oc.api.Network import li.cil.oc.api.driver.EnvironmentHost +import li.cil.oc.api.internal import li.cil.oc.api.internal.Rotatable import li.cil.oc.api.machine.Arguments import li.cil.oc.api.machine.Callback @@ -9,7 +11,11 @@ import li.cil.oc.api.machine.Context import li.cil.oc.api.network._ import li.cil.oc.api.prefab import li.cil.oc.common.item.data.NavigationUpgradeData +import li.cil.oc.util.BlockPosition +import net.minecraft.entity.player.EntityPlayer +import net.minecraft.item.ItemStack import net.minecraft.nbt.NBTTagCompound +import net.minecraftforge.common.util.ForgeDirection class UpgradeNavigation(val host: EnvironmentHost with Rotatable) extends prefab.ManagedEnvironment { override val node = Network.newNode(this, Visibility.Network). @@ -43,6 +49,21 @@ class UpgradeNavigation(val host: EnvironmentHost with Rotatable) extends prefab result(size / 2) } + override def onMessage(message: Message): Unit = { + super.onMessage(message) + if (message.name == "tablet.use") message.source.host match { + case machine: api.machine.Machine => (machine.host, message.data) match { + case (tablet: internal.Tablet, Array(nbt: NBTTagCompound, stack: ItemStack, player: EntityPlayer, blockPos: BlockPosition, side: ForgeDirection, hitX: java.lang.Float, hitY: java.lang.Float, hitZ: java.lang.Float)) => + val info = data.mapData(host.world) + nbt.setInteger("posX", blockPos.x - info.xCenter) + nbt.setInteger("posY", blockPos.y) + nbt.setInteger("posZ", blockPos.z - info.zCenter) + case _ => // Ignore. + } + case _ => // Ignore. + } + } + // ----------------------------------------------------------------------- // override def load(nbt: NBTTagCompound) { From b136e2ab7fea2ca332e8c57a90db22427a631f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Mon, 2 Feb 2015 18:05:50 +0100 Subject: [PATCH 3/8] Added `inventory_controller.compareToDatabase`, closes #861. --- .../component/traits/InventoryAnalytics.scala | 19 +++++++++++++++---- .../component/traits/InventoryAware.scala | 3 ++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/main/scala/li/cil/oc/server/component/traits/InventoryAnalytics.scala b/src/main/scala/li/cil/oc/server/component/traits/InventoryAnalytics.scala index 2bd5e1c25..ced66d1cc 100644 --- a/src/main/scala/li/cil/oc/server/component/traits/InventoryAnalytics.scala +++ b/src/main/scala/li/cil/oc/server/component/traits/InventoryAnalytics.scala @@ -7,7 +7,6 @@ import li.cil.oc.api.machine.Context import li.cil.oc.server.component.result import li.cil.oc.util.DatabaseAccess import li.cil.oc.util.ExtendedArguments._ -import net.minecraft.item.ItemStack trait InventoryAnalytics extends InventoryAware with NetworkAware { @Callback(doc = """function([slot:number]):table -- Get a description of the stack in the specified slot or the selected slot.""") @@ -21,12 +20,24 @@ trait InventoryAnalytics extends InventoryAware with NetworkAware { def storeInternal(context: Context, args: Arguments): Array[AnyRef] = { val localSlot = args.checkSlot(inventory, 0) val dbAddress = args.checkString(1) - def store(stack: ItemStack) = DatabaseAccess.withDatabase(node, dbAddress, database => { + val localStack = inventory.getStackInSlot(localSlot) + DatabaseAccess.withDatabase(node, dbAddress, database => { val dbSlot = args.checkSlot(database.data, 2) val nonEmpty = database.data.getStackInSlot(dbSlot) != null - database.data.setInventorySlotContents(dbSlot, stack.copy()) + database.data.setInventorySlotContents(dbSlot, localStack.copy()) result(nonEmpty) }) - store(inventory.getStackInSlot(localSlot)) + } + + @Callback(doc = """function(slot:number, dbAddress:string, dbSlot:number):boolean -- Compare an item in the specified slot with one in the database with the specified address.""") + def compareToDatabase(context: Context, args: Arguments): Array[AnyRef] = { + val localSlot = args.checkSlot(inventory, 0) + val dbAddress = args.checkString(1) + val localStack = inventory.getStackInSlot(localSlot) + DatabaseAccess.withDatabase(node, dbAddress, database => { + val dbSlot = args.checkSlot(database.data, 2) + val dbStack = database.data.getStackInSlot(dbSlot) + result(haveSameItemType(localStack, dbStack)) + }) } } diff --git a/src/main/scala/li/cil/oc/server/component/traits/InventoryAware.scala b/src/main/scala/li/cil/oc/server/component/traits/InventoryAware.scala index 2342fb1d7..5bc635ea2 100644 --- a/src/main/scala/li/cil/oc/server/component/traits/InventoryAware.scala +++ b/src/main/scala/li/cil/oc/server/component/traits/InventoryAware.scala @@ -23,6 +23,7 @@ trait InventoryAware { protected def stackInSlot(slot: Int) = Option(inventory.getStackInSlot(slot)) protected def haveSameItemType(stackA: ItemStack, stackB: ItemStack) = - stackA.getItem == stackB.getItem && + stackA != null && stackB != null && + stackA.getItem == stackB.getItem && (!stackA.getHasSubtypes || stackA.getItemDamage == stackB.getItemDamage) } From e5d8f3ab5dbdc7e332b9196d4cf14f277752f1dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Mon, 2 Feb 2015 18:48:45 +0100 Subject: [PATCH 4/8] Disallow breaking creative tier robots by non-creative, non-authorized players. Closes #868. Also fixed visual desync when "breaking" a creative case. --- .../scala/li/cil/oc/common/EventHandler.scala | 17 +++++++++++++++++ .../scala/li/cil/oc/common/block/Case.scala | 6 ++++-- .../li/cil/oc/common/block/RobotProxy.scala | 5 +++++ .../li/cil/oc/common/tileentity/Case.scala | 6 +++--- .../li/cil/oc/common/tileentity/Robot.scala | 5 +++++ 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/main/scala/li/cil/oc/common/EventHandler.scala b/src/main/scala/li/cil/oc/common/EventHandler.scala index f807c3ac3..b66e73471 100644 --- a/src/main/scala/li/cil/oc/common/EventHandler.scala +++ b/src/main/scala/li/cil/oc/common/EventHandler.scala @@ -27,6 +27,7 @@ import net.minecraft.tileentity.TileEntity import net.minecraftforge.common.MinecraftForge import net.minecraftforge.common.util.FakePlayer import net.minecraftforge.common.util.ForgeDirection +import net.minecraftforge.event.world.BlockEvent import net.minecraftforge.event.world.WorldEvent import scala.collection.mutable @@ -145,6 +146,22 @@ object EventHandler { } } + @SubscribeEvent + def onBlockBreak(e: BlockEvent.BreakEvent): Unit = { + e.world.getTileEntity(e.x, e.y, e.z) match { + case c: tileentity.Case => + if (c.isCreative && (!e.getPlayer.capabilities.isCreativeMode || !c.canInteract(e.getPlayer.getCommandSenderName))) { + e.setCanceled(true) + } + case r: tileentity.RobotProxy => + val robot = r.robot + if (robot.isCreative && (!e.getPlayer.capabilities.isCreativeMode || !robot.canInteract(e.getPlayer.getCommandSenderName))) { + e.setCanceled(true) + } + case _ => + } + } + lazy val drone = api.Items.get("drone") lazy val eeprom = api.Items.get("eeprom") lazy val mcu = api.Items.get("microcontroller") diff --git a/src/main/scala/li/cil/oc/common/block/Case.scala b/src/main/scala/li/cil/oc/common/block/Case.scala index e3d1dc6a1..d0e5276bf 100644 --- a/src/main/scala/li/cil/oc/common/block/Case.scala +++ b/src/main/scala/li/cil/oc/common/block/Case.scala @@ -88,9 +88,11 @@ class Case(val tier: Int) extends RedstoneAware with traits.PowerAcceptor with t else super.onBlockActivated(world, x, y, z, player, side, hitX, hitY, hitZ) } - override def removedByPlayer(world: World, player: EntityPlayer, x: Int, y: Int, z: Int, willHarvest: Boolean) = + override def removedByPlayer(world: World, player: EntityPlayer, x: Int, y: Int, z: Int, willHarvest: Boolean): Boolean = world.getTileEntity(x, y, z) match { - case c: tileentity.Case => c.canInteract(player.getCommandSenderName) && super.removedByPlayer(world, player, x, y, z, willHarvest) + case c: tileentity.Case => + if (c.isCreative && (!player.capabilities.isCreativeMode || !c.canInteract(player.getCommandSenderName))) false + else c.canInteract(player.getCommandSenderName) && super.removedByPlayer(world, player, x, y, z, willHarvest) case _ => super.removedByPlayer(world, player, x, y, z, willHarvest) } } diff --git a/src/main/scala/li/cil/oc/common/block/RobotProxy.scala b/src/main/scala/li/cil/oc/common/block/RobotProxy.scala index b825a14d7..9656669b7 100644 --- a/src/main/scala/li/cil/oc/common/block/RobotProxy.scala +++ b/src/main/scala/li/cil/oc/common/block/RobotProxy.scala @@ -244,6 +244,11 @@ class RobotProxy extends RedstoneAware with traits.SpecialBlock with traits.Stat world.getTileEntity(x, y, z) match { case proxy: tileentity.RobotProxy => val robot = proxy.robot + // Only allow breaking creative tier robots by allowed users. + // Unlike normal robots, griefing isn't really a valid concern + // here, because to get a creative robot you need creative + // mode in the first place. + if (robot.isCreative && (!player.capabilities.isCreativeMode || !robot.canInteract(player.getCommandSenderName))) return false if (!world.isRemote) { if (robot.player == player) return false robot.node.remove() diff --git a/src/main/scala/li/cil/oc/common/tileentity/Case.scala b/src/main/scala/li/cil/oc/common/tileentity/Case.scala index 0b386a467..2a8e2d11b 100644 --- a/src/main/scala/li/cil/oc/common/tileentity/Case.scala +++ b/src/main/scala/li/cil/oc/common/tileentity/Case.scala @@ -38,7 +38,7 @@ class Case(var tier: Int) extends traits.PowerAcceptor with traits.Computer with var maxComponents = 0 - private def isCreativeCase = tier == Tier.Four + def isCreative = tier == Tier.Four // ----------------------------------------------------------------------- // @@ -77,7 +77,7 @@ class Case(var tier: Int) extends traits.PowerAcceptor with traits.Computer with override def canUpdate = isServer override def updateEntity() { - if (isServer && isCreativeCase && world.getTotalWorldTime % Settings.get.tickFrequency == 0) { + if (isServer && isCreative && world.getTotalWorldTime % Settings.get.tickFrequency == 0) { // Creative case, make it generate power. node.asInstanceOf[Connector].changeBuffer(Double.PositiveInfinity) } @@ -139,7 +139,7 @@ class Case(var tier: Int) extends traits.PowerAcceptor with traits.Computer with override def getSizeInventory = if (tier < 0 || tier >= InventorySlots.computer.length) 0 else InventorySlots.computer(tier).length override def isUseableByPlayer(player: EntityPlayer) = - super.isUseableByPlayer(player) && (!isCreativeCase || player.capabilities.isCreativeMode) + super.isUseableByPlayer(player) && (!isCreative || player.capabilities.isCreativeMode) override def isItemValidForSlot(slot: Int, stack: ItemStack) = Option(Driver.driverFor(stack, getClass)).fold(false)(driver => { diff --git a/src/main/scala/li/cil/oc/common/tileentity/Robot.scala b/src/main/scala/li/cil/oc/common/tileentity/Robot.scala index c0a5cd84b..1384351a6 100644 --- a/src/main/scala/li/cil/oc/common/tileentity/Robot.scala +++ b/src/main/scala/li/cil/oc/common/tileentity/Robot.scala @@ -68,6 +68,8 @@ class Robot extends traits.Computer with traits.PowerInformation with IFluidHand override def tier = info.tier + def isCreative = tier == Tier.Four + // Wrapper for the part of the inventory that is mutable. val dynamicInventory = new IInventory { override def getSizeInventory = Robot.this.inventorySize @@ -715,6 +717,9 @@ class Robot extends traits.Computer with traits.PowerInformation with IFluidHand else if (stack != null && stack.stackSize > 0) spawnStackInWorld(stack, Option(ForgeDirection.UP)) } + override def isUseableByPlayer(player: EntityPlayer) = + super.isUseableByPlayer(player) && (!isCreative || player.capabilities.isCreativeMode) + override def isItemValidForSlot(slot: Int, stack: ItemStack) = (slot, Option(Driver.driverFor(stack, getClass))) match { case (0, _) => true // Allow anything in the tool slot. case (i, Some(driver)) if isContainerSlot(i) => From cea9c1cdbc61f766ef493b0034e0cdbf0c175d1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Mon, 2 Feb 2015 19:12:32 +0100 Subject: [PATCH 5/8] Added debug.insertFluid and debug.removeFluid, closes #882. --- .../cil/oc/server/component/DebugCard.scala | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/main/scala/li/cil/oc/server/component/DebugCard.scala b/src/main/scala/li/cil/oc/server/component/DebugCard.scala index cb35afbf0..d40158dd8 100644 --- a/src/main/scala/li/cil/oc/server/component/DebugCard.scala +++ b/src/main/scala/li/cil/oc/server/component/DebugCard.scala @@ -15,6 +15,7 @@ import li.cil.oc.api.prefab import li.cil.oc.server.component.DebugCard.CommandSender import li.cil.oc.util.BlockPosition import li.cil.oc.util.ExtendedArguments._ +import li.cil.oc.util.ExtendedWorld._ import li.cil.oc.util.InventoryUtils import net.minecraft.block.Block import net.minecraft.command.ICommandSender @@ -32,6 +33,9 @@ import net.minecraft.world.WorldSettings.GameType import net.minecraftforge.common.DimensionManager import net.minecraftforge.common.util.FakePlayerFactory import net.minecraftforge.common.util.ForgeDirection +import net.minecraftforge.fluids.FluidRegistry +import net.minecraftforge.fluids.FluidStack +import net.minecraftforge.fluids.IFluidHandler class DebugCard(host: EnvironmentHost) extends prefab.ManagedEnvironment { override val node = Network.newNode(this, Visibility.Neighbors). @@ -423,6 +427,7 @@ object DebugCard { @Callback(doc = """function(x:number, y:number, z:number, slot:number[, count:number]):number - Reduce the size of an item stack in the inventory at the specified location.""") def removeItem(context: Context, args: Arguments): Array[AnyRef] = { + checkEnabled() val position = BlockPosition(args.checkDouble(0), args.checkDouble(1), args.checkDouble(2), world) InventoryUtils.inventoryAt(position) match { case Some(inventory) => @@ -435,6 +440,34 @@ object DebugCard { } } + @Callback(doc = """function(id:string, amount:number, x:number, y:number, z:number, side:number):boolean - Insert some fluid into the tank at the specified location.""") + def insertFluid(context: Context, args: Arguments): Array[AnyRef] = { + checkEnabled() + val fluid = FluidRegistry.getFluid(args.checkString(0)) + if (fluid == null) { + throw new IllegalArgumentException("invalid fluid id") + } + val amount = args.checkInteger(1) + val position = BlockPosition(args.checkDouble(2), args.checkDouble(3), args.checkDouble(4), world) + val side = args.checkSide(5, ForgeDirection.VALID_DIRECTIONS: _*) + world.getTileEntity(position) match { + case handler: IFluidHandler => result(handler.fill(side, new FluidStack(fluid, amount), true)) + case _ => result(null, "no tank") + } + } + + @Callback(doc = """function(amount:number, x:number, y:number, z:number, side:number):boolean - Remove some fluid from a tank at the specified location.""") + def removeFluid(context: Context, args: Arguments): Array[AnyRef] = { + checkEnabled() + val amount = args.checkInteger(0) + val position = BlockPosition(args.checkDouble(1), args.checkDouble(2), args.checkDouble(3), world) + val side = args.checkSide(4, ForgeDirection.VALID_DIRECTIONS: _*) + world.getTileEntity(position) match { + case handler: IFluidHandler => result(handler.drain(side, amount, true)) + case _ => result(null, "no tank") + } + } + // ----------------------------------------------------------------------- // override def load(nbt: NBTTagCompound) { From 16170e31ee692c5274eb586fbc2ec90b3f5f4202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Mon, 2 Feb 2015 20:57:21 +0100 Subject: [PATCH 6/8] Bumped remote terminal resolution and color depth to tier three, to keep them competitive with tablets with container slots. Closes #822. Will need breaking and re-placing server racks to work in existing worlds (because max res is saved now). --- src/main/scala/li/cil/oc/common/component/Terminal.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/scala/li/cil/oc/common/component/Terminal.scala b/src/main/scala/li/cil/oc/common/component/Terminal.scala index 6712c545d..d24c6ed75 100644 --- a/src/main/scala/li/cil/oc/common/component/Terminal.scala +++ b/src/main/scala/li/cil/oc/common/component/Terminal.scala @@ -8,6 +8,7 @@ import li.cil.oc.api.component.Keyboard.UsabilityChecker import li.cil.oc.api.network.Component import li.cil.oc.api.network.Node import li.cil.oc.api.network.Visibility +import li.cil.oc.common.Tier import li.cil.oc.common.init.Items import li.cil.oc.common.item import li.cil.oc.common.tileentity @@ -23,9 +24,9 @@ class Terminal(val rack: tileentity.ServerRack, val number: Int) { val buffer = { val screenItem = api.Items.get("screen1").createItemStack(1) val buffer = api.Driver.driverFor(screenItem, rack.getClass).createEnvironment(screenItem, rack).asInstanceOf[api.component.TextBuffer] - val (maxWidth, maxHeight) = Settings.screenResolutionsByTier(1) + val (maxWidth, maxHeight) = Settings.screenResolutionsByTier(Tier.Three) buffer.setMaximumResolution(maxWidth, maxHeight) - buffer.setMaximumColorDepth(Settings.screenDepthsByTier(1)) + buffer.setMaximumColorDepth(Settings.screenDepthsByTier(Tier.Three)) buffer } From f69250b565e4398ed64a0ff04a88eb72b8f28929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Mon, 2 Feb 2015 23:05:21 +0100 Subject: [PATCH 7/8] Added ability to copy lines from screens using the analyzer (shift-rightclicking the line with the analyzer), then paste it on a screen by simply rightclicking it with the analyzer. Closes #644. --- .../scala/li/cil/oc/client/PacketSender.scala | 9 ++++ .../scala/li/cil/oc/common/PacketType.scala | 1 + .../scala/li/cil/oc/common/block/Screen.scala | 8 +++- .../cil/oc/common/component/TextBuffer.scala | 31 +++++++++++++ .../li/cil/oc/common/item/Analyzer.scala | 29 ++++++++++++- .../li/cil/oc/common/tileentity/Screen.scala | 43 +++++++++++++------ .../li/cil/oc/server/PacketHandler.scala | 8 ++++ 7 files changed, 114 insertions(+), 15 deletions(-) diff --git a/src/main/scala/li/cil/oc/client/PacketSender.scala b/src/main/scala/li/cil/oc/client/PacketSender.scala index 640aedd53..3ee46208f 100644 --- a/src/main/scala/li/cil/oc/client/PacketSender.scala +++ b/src/main/scala/li/cil/oc/client/PacketSender.scala @@ -110,6 +110,15 @@ object PacketSender { pb.sendToServer() } + def sendCopyToAnalyzer(address: String, line: Int): Unit = { + val pb = new SimplePacketBuilder(PacketType.CopyToAnalyzer) + + pb.writeUTF(address) + pb.writeInt(line) + + pb.sendToServer() + } + def sendMultiPlace() { val pb = new SimplePacketBuilder(PacketType.MultiPartPlace) pb.sendToServer() diff --git a/src/main/scala/li/cil/oc/common/PacketType.scala b/src/main/scala/li/cil/oc/common/PacketType.scala index c925def9d..6a0a0f201 100644 --- a/src/main/scala/li/cil/oc/common/PacketType.scala +++ b/src/main/scala/li/cil/oc/common/PacketType.scala @@ -52,6 +52,7 @@ object PacketType extends Enumeration { // Client -> Server ComputerPower, + CopyToAnalyzer, DronePower, KeyDown, KeyUp, diff --git a/src/main/scala/li/cil/oc/common/block/Screen.scala b/src/main/scala/li/cil/oc/common/block/Screen.scala index fce0e3c5a..7944f269c 100644 --- a/src/main/scala/li/cil/oc/common/block/Screen.scala +++ b/src/main/scala/li/cil/oc/common/block/Screen.scala @@ -6,10 +6,12 @@ import cpw.mods.fml.relauncher.Side import cpw.mods.fml.relauncher.SideOnly import li.cil.oc.OpenComputers import li.cil.oc.Settings +import li.cil.oc.api import li.cil.oc.common.GuiType import li.cil.oc.common.tileentity import li.cil.oc.integration.util.Wrench import li.cil.oc.util._ +import net.minecraft.client.Minecraft import net.minecraft.client.renderer.texture.IIconRegister import net.minecraft.entity.Entity import net.minecraft.entity.EntityLivingBase @@ -332,6 +334,7 @@ class Screen(val tier: Int) extends RedstoneAware { def rightClick(world: World, x: Int, y: Int, z: Int, player: EntityPlayer, side: ForgeDirection, hitX: Float, hitY: Float, hitZ: Float, force: Boolean) = { if (Wrench.holdsApplicableWrench(player, BlockPosition(x, y, z)) && getValidRotations(world, x, y, z).contains(side) && !force) false + else if (api.Items.get(player.getHeldItem) == api.Items.get("analyzer")) false else world.getTileEntity(x, y, z) match { case screen: tileentity.Screen if screen.hasKeyboard && (force || player.isSneaking == screen.invertTouchMode) => // Yep, this GUI is actually purely client side. We could skip this @@ -342,7 +345,10 @@ class Screen(val tier: Int) extends RedstoneAware { } true case screen: tileentity.Screen if screen.tier > 0 && side == screen.facing => - screen.click(player, hitX, hitY, hitZ) + if (world.isRemote && player == Minecraft.getMinecraft.thePlayer) { + screen.click(hitX, hitY, hitZ) + } + else true case _ => false } } diff --git a/src/main/scala/li/cil/oc/common/component/TextBuffer.scala b/src/main/scala/li/cil/oc/common/component/TextBuffer.scala index be1654486..4c9a1d243 100644 --- a/src/main/scala/li/cil/oc/common/component/TextBuffer.scala +++ b/src/main/scala/li/cil/oc/common/component/TextBuffer.scala @@ -429,6 +429,10 @@ class TextBuffer(val host: EnvironmentHost) extends prefab.ManagedEnvironment wi override def mouseScroll(x: Int, y: Int, delta: Int, player: EntityPlayer) = mouseScroll(x, y, delta, player) + def copyToAnalyzer(line: Int, player: EntityPlayer): Unit = { + proxy.copyToAnalyzer(line, player) + } + // ----------------------------------------------------------------------- // override def onConnect(node: Node) { @@ -602,6 +606,8 @@ object TextBuffer { def mouseUp(x: Double, y: Double, button: Int, player: EntityPlayer): Unit def mouseScroll(x: Double, y: Double, delta: Int, player: EntityPlayer): Unit + + def copyToAnalyzer(line: Int, player: EntityPlayer): Unit } class ClientProxy(val owner: TextBuffer) extends Proxy { @@ -686,6 +692,10 @@ object TextBuffer { ClientPacketSender.sendMouseScroll(nodeAddress, x, y, delta) } + override def copyToAnalyzer(line: Int, player: EntityPlayer): Unit = { + ClientPacketSender.sendCopyToAnalyzer(nodeAddress, line) + } + private lazy val Debugger = api.Items.get("debugger") private def debug(message: String) { @@ -789,6 +799,27 @@ object TextBuffer { sendMouseEvent(player, "scroll", x, y, delta) } + override def copyToAnalyzer(line: Int, player: EntityPlayer): Unit = { + val stack = player.getHeldItem + if (stack != null) { + if (!stack.hasTagCompound) { + stack.setTagCompound(new NBTTagCompound()) + } + stack.getTagCompound.removeTag(Settings.namespace + "clipboard") + + if (line >= 0 && line < owner.data.height) { + val text = new String(owner.data.buffer(line)).trim + if (!Strings.isNullOrEmpty(text)) { + stack.getTagCompound.setString(Settings.namespace + "clipboard", text) + } + } + + if (stack.getTagCompound.hasNoTags) { + stack.setTagCompound(null) + } + } + } + private def sendMouseEvent(player: EntityPlayer, name: String, x: Double, y: Double, data: Int) = { val args = mutable.ArrayBuffer.empty[AnyRef] diff --git a/src/main/scala/li/cil/oc/common/item/Analyzer.scala b/src/main/scala/li/cil/oc/common/item/Analyzer.scala index 89b4e5df4..b12b871d5 100644 --- a/src/main/scala/li/cil/oc/common/item/Analyzer.scala +++ b/src/main/scala/li/cil/oc/common/item/Analyzer.scala @@ -2,15 +2,18 @@ package li.cil.oc.common.item import cpw.mods.fml.common.eventhandler.SubscribeEvent import li.cil.oc.Localization +import li.cil.oc.Settings import li.cil.oc.api import li.cil.oc.api.network.Analyzable import li.cil.oc.api.network._ +import li.cil.oc.common.tileentity import li.cil.oc.server.PacketSender import li.cil.oc.util.BlockPosition import li.cil.oc.util.ExtendedWorld._ import net.minecraft.entity.player.EntityPlayer import net.minecraft.entity.player.EntityPlayerMP import net.minecraft.item.ItemStack +import net.minecraft.world.World import net.minecraftforge.common.util.ForgeDirection import net.minecraftforge.event.entity.player.EntityInteractEvent @@ -79,7 +82,31 @@ object Analyzer { } class Analyzer(val parent: Delegator) extends Delegate { + override def onItemRightClick(stack: ItemStack, world: World, player: EntityPlayer): ItemStack = { + if (player.isSneaking && stack.hasTagCompound) { + stack.getTagCompound.removeTag(Settings.namespace + "clipboard") + if (stack.getTagCompound.hasNoTags) { + stack.setTagCompound(null) + } + } + super.onItemRightClick(stack, world, player) + } + override def onItemUse(stack: ItemStack, player: EntityPlayer, position: BlockPosition, side: Int, hitX: Float, hitY: Float, hitZ: Float) = { - Analyzer.analyze(position.world.get.getTileEntity(position), player, side, hitX, hitY, hitZ) + val world = player.getEntityWorld + world.getTileEntity(position) match { + case screen: tileentity.Screen if ForgeDirection.getOrientation(side) == screen.facing => + if (player.isSneaking) { + screen.copyToAnalyzer(hitX, hitY, hitZ) + } + else if (stack.hasTagCompound && stack.getTagCompound.hasKey(Settings.namespace + "clipboard")) { + if (!world.isRemote) { + screen.origin.buffer.clipboard(stack.getTagCompound.getString(Settings.namespace + "clipboard"), player) + } + true + } + else false + case _ => Analyzer.analyze(position.world.get.getTileEntity(position), player, side, hitX, hitY, hitZ) + } } } diff --git a/src/main/scala/li/cil/oc/common/tileentity/Screen.scala b/src/main/scala/li/cil/oc/common/tileentity/Screen.scala index ffac38a2e..55dbd3ca7 100644 --- a/src/main/scala/li/cil/oc/common/tileentity/Screen.scala +++ b/src/main/scala/li/cil/oc/common/tileentity/Screen.scala @@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly import li.cil.oc.Settings import li.cil.oc.api.network.Analyzable import li.cil.oc.api.network._ +import li.cil.oc.common.component.TextBuffer import li.cil.oc.util.Color import net.minecraft.client.Minecraft import net.minecraft.entity.Entity @@ -93,7 +94,7 @@ class Screen(var tier: Int) extends traits.TextBuffer with SidedEnvironment with invertTouchMode = false } - def click(player: EntityPlayer, hitX: Double, hitY: Double, hitZ: Double): Boolean = { + def toScreenCoordinates(hitX: Double, hitY: Double, hitZ: Double): (Boolean, Option[(Double, Double)]) = { // Compute absolute position of the click on the face, measured in blocks. def dot(f: ForgeDirection) = f.offsetX * hitX + f.offsetY * hitY + f.offsetZ * hitZ val (hx, hy) = (dot(toGlobal(ForgeDirection.EAST)), dot(toGlobal(ForgeDirection.UP))) @@ -105,9 +106,9 @@ class Screen(var tier: Int) extends traits.TextBuffer with SidedEnvironment with // Get the relative position in the *display area* of the face. val border = 2.25 / 16.0 if (ax <= border || ay <= border || ax >= width - border || ay >= height - border) { - return false + return (false, None) } - if (!world.isRemote) return true + if (!world.isRemote) return (true, None) val (iw, ih) = (width - border * 2, height - border * 2) val (rx, ry) = ((ax - border) / iw, (ay - border) / ih) @@ -119,27 +120,43 @@ class Screen(var tier: Int) extends traits.TextBuffer with SidedEnvironment with val (brx, bry) = if (bpw > bph) { val rh = bph.toDouble / bpw.toDouble val bry = (ry - (1 - rh) * 0.5) / rh - if (bry <= 0 || bry >= 1) { - return true - } (rx, bry) } else if (bph > bpw) { val rw = bpw.toDouble / bph.toDouble val brx = (rx - (1 - rw) * 0.5) / rw - if (brx <= 0 || brx >= 1) { - return true - } (brx, ry) } else { (rx, ry) } - // Convert to absolute coordinates and send the packet to the server. - origin.buffer.mouseDown(brx * bw, bry * bh, 0, null) + val inBounds = bry >= 0 && bry <= 1 && brx >= 0 || brx <= 1 + (inBounds, Some((brx * bw, bry * bh))) + } - true + def copyToAnalyzer(hitX: Double, hitY: Double, hitZ: Double): Boolean = { + val (inBounds, coordinates) = toScreenCoordinates(hitX, hitY, hitZ) + coordinates match { + case Some((x, y)) => origin.buffer match { + case buffer: TextBuffer => + buffer.copyToAnalyzer(y.toInt, null) + true + case _ => false + } + case _ => inBounds + } + } + + def click(hitX: Double, hitY: Double, hitZ: Double): Boolean = { + val (inBounds, coordinates) = toScreenCoordinates(hitX, hitY, hitZ) + coordinates match { + case Some((x, y)) => + // Send the packet to the server (manually, for accuracy). + origin.buffer.mouseDown(x, y, 0, null) + true + case _ => inBounds + } } def walk(entity: Entity) { @@ -236,7 +253,7 @@ class Screen(var tier: Int) extends traits.TextBuffer with SidedEnvironment with hitXInner && !hitYInner && hitZInner || !hitXInner && hitYInner && hitZInner) { arrow.shootingEntity match { - case player: EntityPlayer if player == Minecraft.getMinecraft.thePlayer => click(player, hitX, hitY, hitZ) + case player: EntityPlayer if player == Minecraft.getMinecraft.thePlayer => click(hitX, hitY, hitZ) case _ => } } diff --git a/src/main/scala/li/cil/oc/server/PacketHandler.scala b/src/main/scala/li/cil/oc/server/PacketHandler.scala index 1b6e92cd0..ba005a7fb 100644 --- a/src/main/scala/li/cil/oc/server/PacketHandler.scala +++ b/src/main/scala/li/cil/oc/server/PacketHandler.scala @@ -32,6 +32,7 @@ object PacketHandler extends CommonPacketHandler { override def dispatch(p: PacketParser) { p.packetType match { case PacketType.ComputerPower => onComputerPower(p) + case PacketType.CopyToAnalyzer => onCopyToAnalyzer(p) case PacketType.DronePower => onDronePower(p) case PacketType.KeyDown => onKeyDown(p) case PacketType.KeyUp => onKeyUp(p) @@ -67,6 +68,13 @@ object PacketHandler extends CommonPacketHandler { case _ => // Invalid packet. } + def onCopyToAnalyzer(p: PacketParser) { + ComponentTracker.get(p.player.worldObj, p.readUTF()) match { + case Some(buffer: TextBuffer) => buffer.copyToAnalyzer(p.readInt(), p.player.asInstanceOf[EntityPlayer]) + case _ => // Invalid Packet + } + } + def onDronePower(p: PacketParser) = p.readEntity[Drone]() match { case Some(drone) => p.player match { From 596ba6f2c1b6ac7ead1da754cd743c6e2e790dc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20N=C3=BCcke?= Date: Mon, 2 Feb 2015 23:19:01 +0100 Subject: [PATCH 8/8] All the vals! Also removed minor oddity. --- .../oc/client/renderer/tileentity/RobotRenderer.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/scala/li/cil/oc/client/renderer/tileentity/RobotRenderer.scala b/src/main/scala/li/cil/oc/client/renderer/tileentity/RobotRenderer.scala index 8c3c75e02..2fedbbd45 100644 --- a/src/main/scala/li/cil/oc/client/renderer/tileentity/RobotRenderer.scala +++ b/src/main/scala/li/cil/oc/client/renderer/tileentity/RobotRenderer.scala @@ -217,10 +217,10 @@ object RobotRenderer extends TileEntitySpecialRenderer { // Additive blending for the light. GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE) // Light color. - var lightColor = if (robot != null && robot.info != null) robot.info.lightColor else 0xF23030 - var r = ((lightColor >>> 16) & 0xFF).toByte - var g = ((lightColor >>> 8) & 0xFF).toByte - var b = ((lightColor >>> 0) & 0xFF).toByte + val lightColor = if (robot != null && robot.info != null) robot.info.lightColor else 0xF23030 + val r = ((lightColor >>> 16) & 0xFF).toByte + val g = ((lightColor >>> 8) & 0xFF).toByte + val b = ((lightColor >>> 0) & 0xFF).toByte GL11.glColor3ub(r, g, b) val t = Tessellator.instance @@ -280,7 +280,7 @@ object RobotRenderer extends TileEntitySpecialRenderer { val timeJitter = robot.hashCode ^ 0xFF val hover = - if (robot.isRunning) (Math.sin(timeJitter + (worldTime + f) / 20.0) * 0.03).toFloat + if (robot.isRunning) (Math.sin(timeJitter + worldTime / 20.0) * 0.03).toFloat else -0.03f GL11.glTranslatef(0, hover, 0)