diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 2fe9a5630..3dab2fbf2 100644 --- a/common/src/main/scala/net/psforever/packet/PSPacket.scala +++ b/common/src/main/scala/net/psforever/packet/PSPacket.scala @@ -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`.
+ *
+ * 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.
+ *
+ * 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`.
+ *
+ * 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`.
+ *
+ * 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`.
+ *
+ * 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`.
+ *
+ * Unchanged from original. + * @return the `String` representation + */ + override def toString = s"list($valueCodec)" } diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala index b397ff552..61d7d5523 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -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 }