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 + } +}