adjusted workflow of Codec to better account for String padding and to consolidate different data formats; added comments; added working (simple) test

This commit is contained in:
FateJH 2017-04-27 23:16:36 -04:00
parent 73c88fb9de
commit 7d158eba1a
2 changed files with 233 additions and 56 deletions

View file

@ -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.<br>
* <br>
* 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] (
{

View file

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