prototype for weapon data codec

This commit is contained in:
FateJH 2016-11-29 18:31:22 -05:00
parent 1f629cf117
commit 72b400e1d0
2 changed files with 316 additions and 28 deletions

View file

@ -74,38 +74,35 @@ object PlanetSidePacketFlags extends Marshallable[PlanetSidePacketFlags] {
object PacketHelpers {
/** Used in certain instances where Codec defintions are stubbed out */
def emptyCodec[T](instance : T) = {
def to(pkt: T) = HNil
def from(a: HNil) = instance
def to(pkt : T) = HNil
def from(a : HNil) = instance
Codec[HNil].xmap[T](from, to)
}
/** Create a Codec for an enumeration type that can correctly represent its value
*
* @param enum the enumeration type to create a codec for
* @param enum the enumeration type to create a codec for
* @param storageCodec the Codec used for actually representing the value
* @tparam E The inferred type
* @return Generated codec
*/
def createEnumerationCodec[E <: Enumeration](enum : E, storageCodec : Codec[Int]) : Codec[E#Value] = {
type Struct = Int :: HNil
val struct: Codec[Struct] = storageCodec.hlist
val struct : Codec[Struct] = storageCodec.hlist
val primitiveLimit = Math.pow(2, storageCodec.sizeBound.exact.get)
// Assure that the enum will always be able to fit in a N-bit int
assert(enum.maxId <= primitiveLimit,
enum.getClass.getCanonicalName + s": maxId exceeds primitive type (limit of $primitiveLimit, maxId ${enum.maxId})")
def to(pkt: E#Value): Struct = {
def to(pkt : E#Value) : Struct = {
pkt.id :: HNil
}
def from(struct: Struct): Attempt[E#Value] = struct match {
def from(struct : Struct) : Attempt[E#Value] = struct match {
case enumVal :: HNil =>
// verify that this int can match the enum
val first = enum.values.firstKey.id
val last = enum.maxId-1
val last = enum.maxId - 1
if(enumVal >= first && enumVal <= last)
Attempt.successful(enum(enumVal))
@ -144,13 +141,11 @@ object PacketHelpers {
/** Codec for how PlanetSide represents strings on the wire */
def encodedString : Codec[String] = variableSizeBytes(encodedStringSize, ascii)
/** Same as [[encodedString]] but with a bit adjustment
*
* This comes in handy when a PlanetSide string is decoded on a non-byte boundary. The PlanetSide client
* will byte align after decoding the string lenght, but BEFORE the string itself. Scodec doesn't like this
* variability and there doesn't appear to be a way to fix this issue.
*
* @param adjustment The adjustment amount in bits
* @return Generated string decoding codec with adjustment
*/
@ -165,15 +160,15 @@ object PacketHelpers {
* input string. We use xmap to transform the [[encodedString]] codec as this change is just a division and multiply
*/
def encodedWideString : Codec[String] = variableSizeBytes(encodedStringSize.xmap(
insize => insize*2, // number of symbols -> number of bytes (decode)
outSize => outSize/2 // number of bytes -> number of symbols (encode)
insize => insize * 2, // number of symbols -> number of bytes (decode)
outSize => outSize / 2 // number of bytes -> number of symbols (encode)
), utf16)
/** Same as [[encodedWideString]] but with a bit alignment after the decoded size
*/
def encodedWideStringAligned(adjustment : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithPad(adjustment).xmap(
insize => insize*2,
outSize => outSize/2
insize => insize * 2,
outSize => outSize / 2
), utf16)
// TODO: make the function below work as there are places it should be used
@ -203,4 +198,105 @@ object PacketHelpers {
def encodedStringWithLimit(limit : Int) : Codec[String] = variableSizeBytes(encodedStringSizeWithLimit(limit), ascii)
*/
/**
* Encode and decode a byte-aligned `List`.<br>
* <br>
* This function is copied almost verbatim from its source, with exception of swapping the normal `ListCodec` for a new `AlignedListCodec`.
* @param countCodec the codec that represents the prefixed size of the `List`
* @param alignment the number of bits padded between the `List` size and the `List` contents
* @param valueCodec a codec that describes each of the contents of the `List`
* @tparam A the type of the `List` contents
* @see codec\package.scala, listOfN
* @return a codec that works on a List of A
*/
def listOfNAligned[A](countCodec : Codec[Long], alignment : Int, valueCodec : Codec[A]) : Codec[List[A]] = {
countCodec.
flatZip { count => new AlignedListCodec(countCodec, valueCodec, alignment, Some(count)) }.
narrow[List[A]]({ case (cnt, xs) =>
if(xs.size == cnt) Attempt.successful(xs)
else Attempt.failure(Err(s"Insufficient number of elements: decoded ${xs.size} but should have decoded $cnt"))
}, xs => (xs.size, xs)).
withToString(s"listOfN($countCodec, $valueCodec)")
}
/**
* Codec that encodes/decodes a list of `n` elements, where `n` is known at compile time.<br>
* <br>
* This function is copied almost verbatim from its source, with exception of swapping the parameter that is normally a `Nat` `literal`.
* The modified function takes a normal unsigned `Integer` and assures that the parameter is non-negative before further processing.
* @param size the known size of the `List`
* @param codec a codec that describes each of the contents of the `List`
* @tparam A the type of the `List` contents
* @see codec\package.scala, sizedList
* @see codec\package.scala, listOfN
* @see codec\package.scala, provides
* @return a codec that works on a List of A but excludes the size from the encoding
*/
def listOfNSized[A](size : Long, codec : Codec[A]) : Codec[List[A]] = PacketHelpers.listOfNAligned(provide(if(size < 0) 0 else size), 0, codec)
}
/**
* The codec that encodes and decodes a byte-aligned `List`.<br>
* <br>
* This class is copied almost verbatim from its source, with only heavy modifications to its `encode` process.
* @param countCodec the codec that represents the prefixed size of the `List`
* @param valueCodec a codec that describes each of the contents of the `List`
* @param alignment the number of bits padded between the `List` size and the `List` contents (on successful)
* @param limit the number of elements in the `List`
* @tparam A the type of the `List` contents
* @see ListCodec.scala
*/
private class AlignedListCodec[A](countCodec : Codec[Long], valueCodec: Codec[A], alignment : Int, limit: Option[Long] = None) extends Codec[List[A]] {
/**
* Convert a `List` of elements into a byte-aligned `BitVector`.<br>
* <br>
* Bit padding after the encoded size of the `List` is only added if the `alignment` value is greater than zero and the initial encoding process was successful.
* The padding is rather heavy-handed and a completely different `BitVector` is returned if successful.
* Performance hits for this complexity are not expected to be significant.
* @param list the `List` to be encoded
* @return the `BitVector` encoding, if successful
*/
override def encode(list : List[A]) : Attempt[BitVector] = {
val solve : Attempt[BitVector] = Encoder.encodeSeq(valueCodec)(list)
if(alignment > 0) {
solve match {
case Attempt.Successful(vector) =>
val countCodecSize : Long = countCodec.sizeBound.lowerBound
return Attempt.successful(vector.take(countCodecSize) ++ BitVector.fill(alignment)(false) ++ vector.drop(countCodecSize))
case _ =>
return Attempt.failure(Err("failed to create a list"))
}
}
solve
}
/**
* Convert a byte-aligned `BitVector` into a `List` of elements.
* @param buffer the encoded bits in the `List`, preceded by the alignment bits
* @return the decoded `List`
*/
def decode(buffer: BitVector) = {
val lim = Option( if(limit.isDefined) limit.get.asInstanceOf[Int] else 0 ) //TODO potentially unsafe size conversion
Decoder.decodeCollect[List, A](valueCodec, lim)(buffer.drop(alignment))
}
/**
* The size of the encoded `List`.<br>
* <br>
* Unchanged from original.
* @return the size as calculated by the size of each element for each element
*/
def sizeBound = limit match {
case None => SizeBound.unknown
case Some(lim) => valueCodec.sizeBound * lim
}
/**
* Get a `String` representation of this `List`.<br>
* <br>
* Unchanged from original.
* @return the `String` representation
*/
override def toString = s"list($valueCodec)"
}

View file

@ -1,13 +1,13 @@
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
//import net.psforever.types.Vector3
import scodec.bits._
import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless._
import scala.annotation.switch
import scala.util.Try
abstract class ConstructorData
@ -31,6 +31,190 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] {
).as[AmmoBoxData]
}
case class WeaponData(ammo : InternalMold) extends ConstructorData
object WeaponData extends Marshallable[WeaponData] {
implicit val codec : Codec[WeaponData] = (
("code" | uint16L) ::
ignore(12) ::
uint4L ::
ignore(4) ::
uintL(12) ::
("data" | InternalMold.codec)
).exmap[WeaponData] (
{
case 0x88 :: _ :: 2 :: _ :: 0x2C0 :: ammo :: HNil =>
Attempt.successful(WeaponData(ammo))
case x :: _ :: y :: _ :: z :: _ :: HNil =>
Attempt.failure(Err("code wrong - looking for 136-2-704, found %d-%d-%d".format(x,y,z)))
},
{
case WeaponData(ammo) =>
Attempt.successful(0x88 :: () :: 2 :: () :: 0x2C0 :: ammo :: HNil)
}
).as[WeaponData]
}
//case class CharacterData(pos : Vector3,
// obj_yaw : Int,
// faction : Int,
// bops : Boolean,
// name : String,
// exosuit : Int,
// sex : Int,
// face1 : Int,
// face2 : Int,
// voice : Int,
// unk1 : Int, //0x8080
// unk2 : Int, //0xFFFF or 0x0
// unk3 : Int, //2
// anchor : Boolean,
// viewPitch : Int,
// viewYaw : Int,
// upperMerit : Int, //0xFFFF means no merit (for all ...)
// middleMerit : Int,
// lowerMerit : Int,
// termOfServiceMerit : Int,
// healthMax : Int,
// health : Int,
// armor : Int,
// unk4 : Int, //1
// unk5 : Int, //7
// unk6 : Int, //7
// staminaMax : Int,
// stamina : Int,
// unk7 : Int, // 192
// unk8 : Int, //66
// unk9 : Int, //197
// unk10 : Int, //70
// unk11 : Int, //134
// unk12 : Int, //199
// firstTimeEvent_length : Long,
// firstEntry : String,
// firstTimEvent_list : List[String],
// tutorial_list : List[String],
// inventory : BitVector
// ) extends ConstructorData
//
//object CharacterData extends Marshallable[CharacterData] {
// //all ignore()s are intentional; please do not mess with them
// implicit val codec : Codec[CharacterData] = (
// ("pos" | Vector3.codec_pos) ::
// ignore(16) ::
// ("obj_yaw" | uint8L) ::
// ignore(1) ::
// ("faction" | uintL(2)) ::
// ("bops" | bool) ::
// ignore(4) ::
// ignore(16) ::
// ("name" | PacketHelpers.encodedWideStringAligned(4)) ::
// ("exosuit" | uintL(3)) ::
// ignore(1) ::
// ignore(1) ::
// ("sex" | uintL(2)) ::
// ("face1" | uint4L) ::
// ("face2" | uint4L) ::
// ("voice" | uintL(3)) ::
// ignore(2) ::
// ignore(4) ::
// ignore(16) ::
// ("unk1" | uint16L) ::
// ignore(40) ::
// ignore(2) ::
// ("unk2" | uint16L) ::
// ignore(2) ::
// ignore(28) ::
// ("unk3" | uintL(4)) ::
// ignore(4) ::
// ignore(16) ::
// ignore(2) ::
// ("anchor" | bool) ::
// ignore(1) ::
// ("viewPitch" | uint8L) ::
// ("viewYaw" | uint8L) ::
// ignore(4) ::
// ignore(4) ::
// ignore(2) ::
// ("upperMerit" | uint16L) ::
// ("middleMerit" | uint16L) ::
// ("lowerMerit" | uint16L) ::
// ("termOfServiceMerit" | uint16L) ::
// ignore(2) ::
// ignore(156) ::
// ignore(2) ::
// ("healthMax" | uint16L) ::
// ("health" | uint16L) ::
// ignore(1) ::
// ("armor" | uint16L) ::
// ignore(1) ::
// ignore(8) ::
// ("unk4" | uint8L) ::
// ignore(8) ::
// ("unk5" | uint4L) ::
// ("unk6" | uintL(3)) ::
// ("staminaMax" | uint16L) ::
// ("stamina" | uint16L) ::
// ignore(148) ::
// ignore(4) ::
// ("unk7" | uint16L) ::
// ("unk8" | uint8L) ::
// ("unk9" | uint8L) ::
// ("unk10" | uint8L) ::
// ("unk11" | uint8L) ::
// ("unk12" | uintL(12)) ::
// ignore(3) ::
// (("firstTimeEvent_length" | uint32L) >>:~ { len =>
// ("firstEntry" | PacketHelpers.encodedStringAligned(5)) ::
// ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString))
// }) ::
// ("tutorial_list" | PacketHelpers.listOfNAligned(uint32L, 0, PacketHelpers.encodedString)) ::
// ignore(204) ::
// ignore(3) ::
// ("inventory" | bits)
// ).as[CharacterData]
//}
case class InternalMold(objectClass : Int,
guid : PlanetSideGUID,
parentSlot : Int,
obj : Option[ConstructorData])
object InternalMold extends Marshallable[InternalMold] {
type objPattern = Int :: PlanetSideGUID :: Int :: Option[ConstructorData] :: HNil
implicit val codec : Codec[InternalMold] = (
ignore(1) :: //TODO determine what this bit does
("objectClass" | uintL(11)) ::
("guid" | PlanetSideGUID.codec) ::
("parentSlot" | PacketHelpers.encodedStringSize) ::
("data" | bits)
).exmap[objPattern] (
{
case _ :: cls :: guid :: slot :: data :: HNil =>
Attempt.successful(cls :: guid :: slot :: Mold.selectMold(cls, data) :: HNil)
},
{
case cls :: guid :: slot :: None :: HNil =>
Attempt.failure(Err("no constuctor data could be found"))
case cls :: guid :: slot :: mold :: HNil =>
Attempt.successful(() :: cls :: guid :: slot :: Mold.serialize(cls, mold.get) :: HNil)
}
).exmap[objPattern] (
{
case cls :: guid :: slot :: None :: HNil =>
Attempt.failure(Err("no decoded constructor data"))
case cls :: guid :: slot :: mold :: HNil =>
Attempt.successful(cls :: guid :: slot :: mold :: HNil)
},
{
case cls :: guid :: slot :: BitVector.empty :: HNil =>
Attempt.failure(Err("no encoded constructor data"))
case cls :: guid :: slot :: data :: HNil =>
Attempt.successful(cls :: guid :: slot :: data :: HNil)
}
).as[InternalMold]
}
case class Mold(objectClass : Int,
data : BitVector) {
private var obj : Option[ConstructorData] = Mold.selectMold(objectClass, data)
@ -53,37 +237,45 @@ object Mold {
def apply(objectClass : Int, obj : ConstructorData) : Mold =
new Mold( objectClass, Mold.serialize(objectClass, obj) )
private def selectMold(objClass : Int, data : BitVector) : Option[ConstructorData] = {
def selectMold(objClass : Int, data : BitVector) : Option[ConstructorData] = {
var out : Option[ConstructorData] = None
if(!data.isEmpty) {
(objClass : @switch) match {
case 0x1C =>
case 0x1C => //9mm
val opt = AmmoBoxData.codec.decode(data).toOption
if(opt.isDefined) {
if(opt.isDefined)
out = Some(opt.get.value)
case 0x46 => //beamer
val opt = WeaponData.codec.decode(data).toOption
if(opt.isDefined)
out = Some(opt.get.value)
}
case _ =>
out = None
}
}
out
}
private def serialize(objClass : Int, obj : ConstructorData) : BitVector = {
def serialize(objClass : Int, obj : ConstructorData) : BitVector = {
var out = BitVector.empty
try {
(objClass : @switch) match {
case 0x1C =>
case 0x1C => //9mm
val opt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption
if(opt.isDefined) {
if(opt.isDefined)
out = opt.get
}
case 0x46 => //beamer
val opt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption
if(opt.isDefined)
out = opt.get
case _ =>
throw new ClassCastException("cannot find object code - "+objClass)
}
}
catch {
case ex : ClassCastException => {
case ex : ClassCastException =>
//TODO generate and log wrong class error message
}
case ex : Exception =>
//TODO generic error
}
out
}