diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index 6e9984e5..01cc9437 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -532,7 +532,7 @@ object GamePacketOpcode extends Enumeration {
case 0xb1 => noDecoder(VoiceHostKill)
case 0xb2 => noDecoder(VoiceHostInfo)
case 0xb3 => noDecoder(BattleplanMessage)
- case 0xb4 => noDecoder(BattleExperienceMessage)
+ case 0xb4 => game.BattleExperienceMessage.decode
case 0xb5 => noDecoder(TargetingImplantRequest)
case 0xb6 => game.ZonePopulationUpdateMessage.decode
case 0xb7 => noDecoder(DisconnectMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/BattleExperienceMessage.scala b/common/src/main/scala/net/psforever/packet/game/BattleExperienceMessage.scala
new file mode 100644
index 00000000..542f76da
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/BattleExperienceMessage.scala
@@ -0,0 +1,39 @@
+// Copyright (c) 2016 PSForever.net to present
+package net.psforever.packet.game
+
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * Inform the client how many battle experience points (BEP) the player currently has earned.
+ *
+ * The amount of `experience` earned is an accumulating value.
+ * Whenever the server sends this packet, the value of this field is equal to the player's current total BEP.
+ * Each packet updates to a higher BEP score and the client occasionally reports of the difference as an event message.
+ * "You have been awarded `x` battle experience points."
+ * Milestone notifications that occur due to BEP gain, e.g., rank progression, will trigger naturally as the client is updated.
+ *
+ * It is possible to award more battle experience than is necessary to progress one's character to the highest battle rank.
+ * (This must be accomplished in a single event packet.)
+ * Only the most significant notification will be displayed.
+ * @param player_guid the player
+ * @param experience the current total experience
+ * @param unk na; always zero?
+ */
+final case class BattleExperienceMessage(player_guid : PlanetSideGUID,
+ experience : Long,
+ unk : Int)
+ extends PlanetSideGamePacket {
+ type Packet = BattleExperienceMessage
+ def opcode = GamePacketOpcode.BattleExperienceMessage
+ def encode = BattleExperienceMessage.encode(this)
+}
+
+object BattleExperienceMessage extends Marshallable[BattleExperienceMessage] {
+ implicit val codec : Codec[BattleExperienceMessage] = (
+ ("player_guid" | PlanetSideGUID.codec) ::
+ ("experience" | ulongL(32)) ::
+ ("unk" | uint8L)
+ ).as[BattleExperienceMessage]
+}
diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala
index 2f7e8a11..a1b6a9c7 100644
--- a/common/src/test/scala/GamePacketTest.scala
+++ b/common/src/test/scala/GamePacketTest.scala
@@ -703,6 +703,28 @@ class GamePacketTest extends Specification {
}
}
+ "BattleExperienceMessage" should {
+ val string = hex"B4 8A0A E7030000 00"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case BattleExperienceMessage(player_guid, experience, unk) =>
+ player_guid mustEqual PlanetSideGUID(2698)
+ experience mustEqual 999
+ unk mustEqual 0
+ case default =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = BattleExperienceMessage(PlanetSideGUID(2698), 999, 0)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+ }
+
"ZonePopulationUpdateMessage" should {
val string = hex"B6 0400 9E010000 8A000000 25000000 8A000000 25000000 8A000000 25000000 8A000000 25000000"