diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
index cad22888..a06b26c0 100644
--- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
+++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala
@@ -567,7 +567,7 @@ object GamePacketOpcode extends Enumeration {
// OPCODES 0xd0-df
case 0xd0 => noDecoder(UnknownMessage208)
- case 0xd1 => noDecoder(DisplayedAwardMessage)
+ case 0xd1 => game.DisplayedAwardMessage.decode
case 0xd2 => noDecoder(RespawnAMSInfoMessage)
case 0xd3 => noDecoder(ComponentDamageMessage)
case 0xd4 => noDecoder(GenericObjectActionAtPositionMessage)
diff --git a/common/src/main/scala/net/psforever/packet/game/DisplayedAwardMessage.scala b/common/src/main/scala/net/psforever/packet/game/DisplayedAwardMessage.scala
new file mode 100644
index 00000000..fe0e1215
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/DisplayedAwardMessage.scala
@@ -0,0 +1,59 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.packet.game
+
+import net.psforever.packet.game.objectcreate.RibbonBars
+import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
+import scodec.Codec
+import scodec.codecs._
+
+/**
+ * An `Enumeration` of the slots for award ribbons on a player's `RibbonBars`.
+ */
+object RibbonBarsSlot extends Enumeration {
+ type Type = Value
+
+ val Top,
+ Middle,
+ Bottom,
+ TermOfService //technically,the slot above "Top"
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
+}
+
+/**
+ * Dispatched to configure a player's merit commendation ribbons.
+ *
+ * Normally, this packet is dispatched by the client when managing merit commendations through the "Character Info/Achievements" tab.
+ * On Gemini Live, this packet was also always dispatched once by the server during character login.
+ * It set the term of service ribbon explicitly.
+ * Generally, this was unnecessary, as the encoded character data maintains information about displayed ribbons.
+ * This behavior was probably a routine that ensured that correct yearly progression was tracked if the player earned it while offline.
+ * It never set any of the other ribbon slot positions during login.
+ *
+ * A specific ribbon may only be set once to one slot.
+ * The last set slot is considered the valid position to which that ribbon will be placed/moved.
+ * @param player_guid the player
+ * @param ribbon the award to be displayed;
+ * defaults to `RibbonsBars.noRibbon`;
+ * use `RibbonsBars.noRibbon` when indicating "no ribbon"
+ * @param bar any of the four positions where the award ribbon is to be displayed;
+ * defaults to `TermOfService`
+ * @see `RibbonBars`
+ */
+final case class DisplayedAwardMessage(player_guid : PlanetSideGUID,
+ ribbon : Long = RibbonBars.noRibbon,
+ bar : RibbonBarsSlot.Value = RibbonBarsSlot.TermOfService)
+ extends PlanetSideGamePacket {
+ type Packet = DisplayedAwardMessage
+ def opcode = GamePacketOpcode.DisplayedAwardMessage
+ def encode = DisplayedAwardMessage.encode(this)
+}
+
+object DisplayedAwardMessage extends Marshallable[DisplayedAwardMessage] {
+ implicit val codec : Codec[DisplayedAwardMessage] = (
+ ("player_guid" | PlanetSideGUID.codec) ::
+ ("ribbon" | uint32L) ::
+ ("bar" | RibbonBarsSlot.codec)
+ ).as[DisplayedAwardMessage]
+}
diff --git a/common/src/test/scala/game/DisplayedAwardMessageTest.scala b/common/src/test/scala/game/DisplayedAwardMessageTest.scala
new file mode 100644
index 00000000..9ad7c1d6
--- /dev/null
+++ b/common/src/test/scala/game/DisplayedAwardMessageTest.scala
@@ -0,0 +1,29 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class DisplayedAwardMessageTest extends Specification {
+ val string = hex"D1 9F06 A6010000 3 0"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case DisplayedAwardMessage(player_guid, ribbon, bar) =>
+ player_guid mustEqual PlanetSideGUID(1695)
+ ribbon mustEqual 422L
+ bar mustEqual RibbonBarsSlot.TermOfService
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = DisplayedAwardMessage(PlanetSideGUID(1695), 422L, RibbonBarsSlot.TermOfService)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}