diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 8855bb51..cd45fa0e 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -519,7 +519,7 @@ object GamePacketOpcode extends Enumeration {
case 0xa7 => noDecoder(GenericActionMessage)
// 0xa8
case 0xa8 => game.ContinentalLockUpdateMessage.decode
- case 0xa9 => noDecoder(AvatarGrenadeStateMessage)
+ case 0xa9 => game.AvatarGrenadeStateMessage.decode
case 0xaa => noDecoder(UnknownMessage170)
case 0xab => noDecoder(UnknownMessage171)
case 0xac => noDecoder(ReleaseAvatarRequestMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/AvatarGrenadeStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/AvatarGrenadeStateMessage.scala
new file mode 100644
index 00000000..08e9e27d
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/AvatarGrenadeStateMessage.scala
@@ -0,0 +1,57 @@
+// Copyright (c) 2016 PSForever.net to present
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * An `Enumeration` of the kinds of states applicable to the grenade animation.
+ */
+object GrenadeState extends Enumeration {
+ type Type = Value
+ val UNK0,
+ PRIMED, //avatars and other depicted player characters
+ THROWN //avatars only
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L)
+}
+
+/**
+ * Report the state of the grenade throw animation for this player.
+ * The default state is "held at side," though the client's avatar never has to announce this.
+ *
+ * The throwing animation has a minor timing glitch.
+ * Causing another player to raise his arm will always result in that arm being lowered a few seconds later.
+ * This is as opposed to the client's avatar, who can seem to hold a grenade in the "prepare to throw" state indefinitely.
+ * If the avatar looks away from a player whose grenade arm is up ("prepare to throw"), however, when they look back at the player
+ * his grenade arm will occasionally have been lowered ("held at side") again before it would normally be lowered.
+ *
+ * A client will dispatch state '1' and state '2' for the avatar's actions.
+ * A client will only react temporarily for another character other than the avatar when the given a state '1'.
+ * If that internal state is not changed, however, that other character will not respond to any subsequent '1' state.
+ * (This may also be a glitch.)
+ *
+ * States:
+ * `
+ * 1 - prepare to throw (grenade held back over shoulder)
+ * 2 - throwing (grenade released overhand and then reset) (avatar only)
+ * `
+ * @param player_guid the player
+ * @param state the animation state
+ */
+final case class AvatarGrenadeStateMessage(player_guid : PlanetSideGUID,
+ state : GrenadeState.Value)
+ extends PlanetSideGamePacket {
+ type Packet = AvatarGrenadeStateMessage
+ def opcode = GamePacketOpcode.AvatarGrenadeStateMessage
+ def encode = AvatarGrenadeStateMessage.encode(this)
+}
+
+object AvatarGrenadeStateMessage extends Marshallable[AvatarGrenadeStateMessage] {
+ implicit val codec : Codec[AvatarGrenadeStateMessage] = (
+ ("player_guid" | PlanetSideGUID.codec) ::
+ ("state" | GrenadeState.codec)
+ ).as[AvatarGrenadeStateMessage]
+}
diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala
index c5d847da..ad5c5017 100644
--- a/common/src/test/scala/GamePacketTest.scala
+++ b/common/src/test/scala/GamePacketTest.scala
@@ -1398,6 +1398,27 @@ class GamePacketTest extends Specification {
}
}
+ "AvatarGrenadeStateMessage" should {
+ val string = hex"A9 DA11 01"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case AvatarGrenadeStateMessage(player_guid, state) =>
+ player_guid mustEqual PlanetSideGUID(4570)
+ state mustEqual GrenadeState.PRIMED
+ case default =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = AvatarGrenadeStateMessage(PlanetSideGUID(4570), GrenadeState.PRIMED)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+ }
+
"BroadcastWarpgateUpdateMessage" should {
val string = hex"D9 0D 00 01 00 20"
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index fbce7b3e..23f5f6d9 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -329,6 +329,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ MountVehicleMsg(player_guid, vehicle_guid, unk) =>
log.info("MounVehicleMsg: "+msg)
+ case msg @ AvatarGrenadeStateMessage(player_guid, state) =>
+ log.info("AvatarGrenadeStateMessage: " + msg)
+
case default => log.debug(s"Unhandled GamePacket ${pkt}")
}