diff --git a/common/src/main/scala/net/psforever/packet/game/BattleplanMessage.scala b/common/src/main/scala/net/psforever/packet/game/BattleplanMessage.scala
index b5005ad2..277eb103 100644
--- a/common/src/main/scala/net/psforever/packet/game/BattleplanMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/BattleplanMessage.scala
@@ -7,35 +7,78 @@ import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
+/**
+ * A common ancestor of all the different "sheets" used to keep track of the data.
+ */
+sealed trait DiagramSheet
+
+/**
+ * na
+ * @param unk1 na
+ * @param unk2 na
+ */
final case class SheetOne(unk1 : Float,
- unk2 : Int)
+ unk2 : Int) extends DiagramSheet
+/**
+ * na
+ * @param unk1 na
+ * @param unk2 na
+ */
final case class SheetTwo(unk1 : Float,
- unk2 : Float)
+ unk2 : Float) extends DiagramSheet
+/**
+ * na
+ * @param unk1 na
+ * @param unk2 na
+ * @param unk3 na
+ * @param unk4 na
+ */
final case class SheetFive(unk1 : Float,
unk2 : Float,
unk3 : Float,
- unk4 : Int)
+ unk4 : Int) extends DiagramSheet
+/**
+ * na
+ * @param unk1 na
+ * @param unk2 na
+ * @param unk3 na
+ * @param unk4 na
+ * @param unk5 na
+ */
final case class SheetSix(unk1 : Float,
unk2 : Float,
unk3 : Int,
unk4 : Int,
- unk5 : String)
+ unk5 : String) extends DiagramSheet
-final case class SheetSeven(unk : Int)
+/**
+ * na
+ * @param unk na
+ */
+final case class SheetSeven(unk : Int) extends DiagramSheet
-final case class BattleDiagram(sheet1 : Option[SheetOne] = None,
- sheet2 : Option[SheetTwo] = None,
- sheet3 : Option[SheetFive] = None,
- sheet4 : Option[SheetSix] = None,
- sheet5 : Option[SheetSeven] = None)
+/**
+ * na
+ * @param pageNum a hint to kind of data stored
+ * @param sheet the data
+ */
+final case class BattleDiagram(pageNum : Int,
+ sheet : Option[DiagramSheet] = None)
+/**
+ * na
+ * @param unk1 na
+ * @param mastermind the player who contributed this battle plan
+ * @param unk2 na
+ * @param diagrams a list of the individual `BattleDiagram`s that compose this plan
+ */
final case class BattleplanMessage(unk1 : Long,
- unk2 : String,
- unk3 : Int,
- unk4 : List[BattleDiagram])
+ mastermind : String,
+ unk2 : Int,
+ diagrams : List[BattleDiagram])
extends PlanetSideGamePacket {
type Packet = BattleplanMessage
def opcode = GamePacketOpcode.BattleplanMessage
@@ -43,36 +86,90 @@ final case class BattleplanMessage(unk1 : Long,
}
object BattelplanDiagram {
+ /**
+ * Create a `BattleDiagram` object containing `SheetOne` data.
+ * @param unk1 na
+ * @param unk2 na
+ * @return a `BattleDiagram` object
+ */
def sheet1(unk1 : Float, unk2 : Int) : BattleDiagram =
- BattleDiagram(Some(SheetOne(unk1, unk2)))
+ BattleDiagram(1, Some(SheetOne(unk1, unk2)))
+ /**
+ * Create a `BattleDiagram` object containing `SheetTwo` data.
+ * @param unk1 na
+ * @param unk2 na
+ * @return a `BattleDiagram` object
+ */
def sheet2(unk1 : Float, unk2 : Float) : BattleDiagram =
- BattleDiagram(None, Some(SheetTwo(unk1, unk2)))
+ BattleDiagram(2, Some(SheetTwo(unk1, unk2)))
- def sheet3(unk1 : Float, unk2 : Float, unk3 : Float, unk4 : Int) : BattleDiagram =
- BattleDiagram(None, None, Some(SheetFive(unk1, unk2, unk3, unk4)))
+ /**
+ * Create a `BattleDiagram` object containing `SheetFive` data.
+ * @param unk1 na
+ * @param unk2 na
+ * @param unk3 na
+ * @param unk4 na
+ * @return a `BattleDiagram` object
+ */
+ def sheet5(unk1 : Float, unk2 : Float, unk3 : Float, unk4 : Int) : BattleDiagram =
+ BattleDiagram(5, Some(SheetFive(unk1, unk2, unk3, unk4)))
- def sheet4(unk1 : Float, unk2 : Float, unk3 : Int, unk4 : Int, unk5 : String) : BattleDiagram =
- BattleDiagram(None, None, None, Some(SheetSix(unk1, unk2, unk3, unk4, unk5)))
+ /**
+ * Create a `BattleDiagram` object containing `SheetSix` data.
+ * @param unk1 na
+ * @param unk2 na
+ * @param unk3 na
+ * @param unk4 na
+ * @param unk5 na
+ * @return a `BattleDiagram` object
+ */
+ def sheet6(unk1 : Float, unk2 : Float, unk3 : Int, unk4 : Int, unk5 : String) : BattleDiagram =
+ BattleDiagram(6, Some(SheetSix(unk1, unk2, unk3, unk4, unk5)))
- def sheet5(unk1 : Int) : BattleDiagram =
- BattleDiagram(None, None, None, None, Some(SheetSeven(unk1)))
+ /**
+ * Create a `BattleDiagram` object containing `SheetSeven` data.
+ * @param unk na
+ * @return a `BattleDiagram` object
+ */
+ def sheet7(unk : Int) : BattleDiagram =
+ BattleDiagram(7, Some(SheetSeven(unk)))
}
object BattleplanMessage extends Marshallable[BattleplanMessage] {
+
+ /**
+ * An intermediary object intended to temporarily store `BattleDiagram` objects.
+ *
+ * This hidden object is arranged like a linked list;
+ * but, later, it is converted into an accessible formal `List` of `BattleDiagram` objects during decoding;
+ * likewise, during the encoding process, the `List` is transformed back into a linked list structure.
+ * `Scala`'s own linked list `Collection` is deprecated, without substitution, so this custom one must be used.
+ * @param diagram the contained `BattleDiagram` with the sheet that maintains the data
+ * @param next the next `BattleDiagramLayer`, if any, arranging into a linked list
+ */
private final case class BattleDiagramLayer(diagram : BattleDiagram,
next : Option[BattleDiagramLayer])
+ /**
+ * Parse data into a `SheetOne` object.
+ */
private val plan1_codec : Codec[SheetOne] = ( //size: 8; pad: +0
("unk1" | newcodecs.q_float(0.0, 16.0, 5)) ::
("unk2" | uintL(3))
).as[SheetOne]
+ /**
+ * Parse data into a `SheetTwo` object.
+ */
private val plan2_codec : Codec[SheetTwo] = ( //size: 22; pad: +2
("unk1" | newcodecs.q_float(-4096.0, 12288.0, 11)) ::
("unk2" | newcodecs.q_float(-4096.0, 12288.0, 11))
).as[SheetTwo]
+ /**
+ * Parse data into a `SheetFive` object.
+ */
private val plan5_codec : Codec[SheetFive] = ( //size: 44; pad: +4
("unk1" | newcodecs.q_float(-4096.0, 12288.0, 11)) ::
("unk2" | newcodecs.q_float(-4096.0, 12288.0, 11)) ::
@@ -80,6 +177,10 @@ object BattleplanMessage extends Marshallable[BattleplanMessage] {
("unk4" | uintL(11))
).as[SheetFive]
+ /**
+ * Parse data into a `SheetSix` object.
+ * @param pad the current padding for the `String` entry
+ */
private def plan6_codec(pad : Int) : Codec[SheetSix] = ( //size: 31 + string.length.field + string.length * 16 + padding; pad: value resets
("unk1" | newcodecs.q_float(-4096.0, 12288.0, 11)) ::
("unk2" | newcodecs.q_float(-4096.0, 12288.0, 11)) ::
@@ -88,8 +189,18 @@ object BattleplanMessage extends Marshallable[BattleplanMessage] {
("unk5" | PacketHelpers.encodedWideStringAligned( (pad + 1) % 8 ))
).as[SheetSix]
+ /**
+ * Parse data into a `SheetSeven` object.
+ */
private val plan7_codec : Codec[SheetSeven] = ("unk" | uintL(6)).as[SheetSeven] // size: 6; pad: +2
+ /**
+ * Switch between different patterns to create a `BattleDiagram` for the following data.
+ * @param plan a hint to help parse the following data
+ * @param pad the current padding for any `String` entry stored within the parsed elements;
+ * when `plan == 6`, `plan6_codec` utilizes this value
+ * @return a `BattleDiagram` object
+ */
private def diagram_codec(plan : Int, pad : Int) : Codec[BattleDiagram] = (
conditional(plan == 1, plan1_codec) ::
conditional(plan == 2, plan2_codec) ::
@@ -98,33 +209,59 @@ object BattleplanMessage extends Marshallable[BattleplanMessage] {
conditional(plan == 7, plan7_codec)
).exmap[BattleDiagram] (
{
- case None :: None :: None :: None :: None :: HNil =>
- Attempt.failure(Err(s"unknown sheet number $plan"))
+ case Some(sheet) :: None :: None :: None :: None :: HNil =>
+ Attempt.successful(BattleDiagram(plan, Some(sheet)))
- case a :: b :: c :: d :: e :: HNil =>
- Attempt.successful(BattleDiagram(a, b, c, d, e))
+ case None :: Some(sheet) :: None :: None :: None :: HNil =>
+ Attempt.successful(BattleDiagram(plan, Some(sheet)))
+
+ case None :: None :: Some(sheet) :: None :: None :: HNil =>
+ Attempt.successful(BattleDiagram(plan, Some(sheet)))
+
+ case None :: None :: None :: Some(sheet) :: None :: HNil =>
+ Attempt.successful(BattleDiagram(plan, Some(sheet)))
+
+ case None :: None :: None :: None :: Some(sheet) :: HNil =>
+ Attempt.successful(BattleDiagram(plan, Some(sheet)))
+
+ case None :: None :: None :: None :: None :: HNil =>
+ Attempt.successful(BattleDiagram(plan, None))
+
+ case _:: _ :: _ :: _ :: _ :: HNil =>
+ Attempt.failure(Err(s"too many sheets at once for $plan"))
},
{
- case BattleDiagram(Some(sheet), _, _, _, _) =>
- Attempt.successful(Some(sheet) :: None :: None :: None :: None :: HNil)
+ case BattleDiagram(1, Some(sheet)) =>
+ Attempt.successful(Some(sheet.asInstanceOf[SheetOne]) :: None :: None :: None :: None :: HNil)
- case BattleDiagram(None, Some(sheet), _, _, _) =>
- Attempt.successful(None :: Some(sheet) :: None :: None :: None :: HNil)
+ case BattleDiagram(2, Some(sheet)) =>
+ Attempt.successful(None :: Some(sheet.asInstanceOf[SheetTwo]) :: None :: None :: None :: HNil)
- case BattleDiagram(None, None, Some(sheet), _, _) =>
- Attempt.successful(None :: None :: Some(sheet) :: None :: None :: HNil)
+ case BattleDiagram(5, Some(sheet)) =>
+ Attempt.successful(None :: None :: Some(sheet.asInstanceOf[SheetFive]) :: None :: None :: HNil)
- case BattleDiagram(None, None, None, Some(sheet), _) =>
- Attempt.successful(None :: None :: None :: Some(sheet) :: None :: HNil)
+ case BattleDiagram(6, Some(sheet)) =>
+ Attempt.successful(None :: None :: None :: Some(sheet.asInstanceOf[SheetSix]) :: None :: HNil)
- case BattleDiagram(None, None, None, None, Some(sheet)) =>
- Attempt.successful(None :: None :: None :: None :: Some(sheet) :: HNil)
+ case BattleDiagram(7, Some(sheet)) =>
+ Attempt.successful(None :: None :: None :: None :: Some(sheet.asInstanceOf[SheetSeven]) :: HNil)
- case BattleDiagram(None, None, None, None, None) =>
- Attempt.failure(Err("can not deal with blank sheet"))
+ case BattleDiagram(_, None) =>
+ Attempt.successful(None :: None :: None :: None :: None :: HNil)
+
+ case BattleDiagram(n, _) =>
+ Attempt.failure(Err(s"unhandled sheet number $n"))
}
)
+ /**
+ * Parse what was originally an encoded `List` of elements as a linked list of elements.
+ * Maintain a `String` padding value that applies an appropriate offset value.
+ * @param remaining the number of elements remaining to parse
+ * @param pad the current padding for any `String` entry stored within the parsed elements;
+ * different elements add different padding offset to this field on subsequent passes
+ * @return a `Codec` for `BattleDiagramLayer` objects
+ */
private def parse_diagrams_codec(remaining : Int, pad : Int = 0) : Codec[BattleDiagramLayer] = (
uint4L >>:~ { plan =>
("diagram" | diagram_codec(plan, pad)) ::
@@ -140,43 +277,44 @@ object BattleplanMessage extends Marshallable[BattleplanMessage] {
Attempt.successful(BattleDiagramLayer(diagram, next))
},
{
- case BattleDiagramLayer(BattleDiagram(Some(sheet), _, _, _, _), next) =>
- Attempt.successful(1 :: BattleDiagram(Some(sheet)) :: next :: HNil)
-
- case BattleDiagramLayer(BattleDiagram(None, Some(sheet), _, _, _), next) =>
- Attempt.successful(2 :: BattleDiagram(None, Some(sheet)) :: next :: HNil)
-
- case BattleDiagramLayer(BattleDiagram(None, None, Some(sheet), _, _), next) =>
- Attempt.successful(5 :: BattleDiagram(None, None, Some(sheet)) :: next :: HNil)
-
- case BattleDiagramLayer(BattleDiagram(None, None, None, Some(sheet), _), next) =>
- Attempt.successful(6 :: BattleDiagram(None, None, None, Some(sheet)) :: next :: HNil)
-
- case BattleDiagramLayer(BattleDiagram(None, None, None, None, Some(sheet)), next) =>
- Attempt.successful(7 :: BattleDiagram(None, None, None, None, Some(sheet)) :: next :: HNil)
+ case BattleDiagramLayer(BattleDiagram(num, sheet), next) =>
+ Attempt.successful(num :: BattleDiagram(num, sheet) :: next :: HNil)
}
)
import scala.collection.mutable.ListBuffer
+ /**
+ * Transform a linked list of `BattleDiagramLayer` into a `List` of `BattleDiagram` objects.
+ * @param element the current link in a chain of `BattleDiagramLayer` objects
+ * @param list a `List` of extracted `BattleDiagrams`;
+ * technically, the output
+ */
private def rollDiagramLayers(element : BattleDiagramLayer, list : ListBuffer[BattleDiagram]) : Unit = {
list += element.diagram
if(element.next.isDefined)
- rollDiagramLayers(element.next.get, list)
+ rollDiagramLayers(element.next.get, list) //tail call optimization
}
+ /**
+ * Transform a `List` of `BattleDiagram` objects into a linked list of `BattleDiagramLayer` objects.
+ * @param revIter a reverse `List` `Iterator` for a `List` of `BattleDiagrams`
+ * @param layers the current head of a chain of `BattleDiagramLayer` objects;
+ * technically, the output
+ * @return a linked list of `BattleDiagramLayer` objects
+ */
private def unrollDiagramLayers(revIter : Iterator[BattleDiagram], layers : Option[BattleDiagramLayer] = None) : Option[BattleDiagramLayer] = {
if(!revIter.hasNext)
return layers
val elem : BattleDiagram = revIter.next
- unrollDiagramLayers(revIter, Some(BattleDiagramLayer(elem, layers)))
+ unrollDiagramLayers(revIter, Some(BattleDiagramLayer(elem, layers))) //tail call optimization
}
implicit val codec : Codec[BattleplanMessage] = (
("unk1" | uint32L) ::
- ("unk2" | PacketHelpers.encodedWideString) ::
- ("unk3" | uint16L) ::
+ ("mastermind" | PacketHelpers.encodedWideString) ::
+ ("unk2" | uint16L) ::
(uint8L >>:~ { count =>
- conditional(count > 0, parse_diagrams_codec(count)).hlist
+ conditional(count > 0, "diagrams" | parse_diagrams_codec(count)).hlist
})
).exmap[BattleplanMessage] (
{
diff --git a/common/src/test/scala/game/BattleplanMessageTest.scala b/common/src/test/scala/game/BattleplanMessageTest.scala
new file mode 100644
index 00000000..2eb171bd
--- /dev/null
+++ b/common/src/test/scala/game/BattleplanMessageTest.scala
@@ -0,0 +1,39 @@
+// Copyright (c) 2017 PSForever
+package game
+
+import org.specs2.mutable._
+import net.psforever.packet._
+import net.psforever.packet.game._
+import scodec.bits._
+
+class BattleplanMessageTest extends Specification {
+ val string = hex"b3 3a197902 94 59006500740041006e006f0074006800650072004600610069006c0075007200650041006c007400 0000 01 e0"
+
+ "decode" in {
+ PacketCoding.DecodePacket(string).require match {
+ case BattleplanMessage(unk1, mastermind, unk2, diagrams) =>
+ unk1 mustEqual 41490746
+ mastermind mustEqual "YetAnotherFailureAlt"
+ unk2 mustEqual 0
+ diagrams.size mustEqual 1
+ //h0
+ diagrams.head.pageNum mustEqual 14
+ diagrams.head.sheet.isDefined mustEqual false
+ case _ =>
+ ko
+ }
+ }
+
+ "encode" in {
+ val msg = BattleplanMessage(
+ 41490746,
+ "YetAnotherFailureAlt",
+ 0,
+ BattleDiagram(14) ::
+ Nil
+ )
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string
+ }
+}