From 87d2f256f1b568a2e53f1ea3b60fc1bfc1acac6a Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 25 Nov 2016 20:02:28 -0500 Subject: [PATCH 01/15] initial commit to changes towards a working ObjectCreateMessage pattern --- .../packet/game/ObjectCreateMessage.scala | 179 ++++++++++++++---- common/src/test/scala/GamePacketTest.scala | 23 ++- 2 files changed, 157 insertions(+), 45 deletions(-) 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 2fcb5c23..0de046ca 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -6,52 +6,161 @@ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless._ -case class ObjectCreateMessageParent(guid : Int, slot : Int) +/** + * The parent information of a created object.
+ *
+ * Rather than a created-parent with a created-child relationship, the whole of the packet still only creates the child. + * The parent is a pre-existing object into which the (created) child is attached.
+ *
+ * The slot is encoded as a string length integer commonly used by PlanetSide. + * It is either a 0-127 eight bit number (0 = 0x80), or a 128-32767 sixteen bit number (128 = 0x0080). + * @param guid the GUID of the parent object + * @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent + */ +case class ObjectCreateMessageParent(guid : PlanetSideGUID, + slot : Int) -case class ObjectCreateMessage(streamLength : Long, // in bits +/** + * Communicate with the client that a certain object with certain properties is to be created. + * The object may also have primitive assignment (attachment) properties.
+ *
+ * In normal packet data order, the parent object is specified before the actual object is specified. + * This is most likely a method of early correction. + * "Does this parent object exist?" + * "Is this new object something that can be attached to this parent?" + * "Does the parent have the appropriate attachment slot?" + * There is no fail-safe method for any of these circumstances being false, however, and the object will simply not be created. + * In instance where the parent data does not exist, the object-specific data is immediately encountered.
+ *
+ * The object's GUID is assigned by the server. + * The clients are required to adhere to this new GUID referring to the object. + * There is no fail-safe for a conflict between what the server thinks is a new GUID and what any client thinks is an already-assigned GUID. + * Likewise, there is no fail-safe between a client failing or refusing to create an object and the server thinking an object has been created. + * (The GM-level command `/sync` tests for objects that "do not match" between the server and the client. + * It's implementation and scope are undefined.)
+ *
+ * Knowing the object's class is essential for parsing the specific information passed by the `data` parameter.
+ *
+ * Exploration:
+ * Can we build a `case class` "foo" that can accept the `objectClass` and the `data` and construct any valid object automatically? + * @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding + * @param objectClass the code for the type of object being constructed + * @param guid the GUID this object will be assigned + * @param parentInfo if defined, the relationship between this object and another object (its parent) + * @param data the data used to construct this type of object; + * requires further object-specific processing + */ +case class ObjectCreateMessage(streamLength : Long, objectClass : Int, - guid : Int, + guid : PlanetSideGUID, parentInfo : Option[ObjectCreateMessageParent], - stream : BitVector - ) + data : BitVector) extends PlanetSideGamePacket { - def opcode = GamePacketOpcode.ObjectCreateMessage def encode = ObjectCreateMessage.encode(this) } +object ObjectCreateMessageParent extends Marshallable[ObjectCreateMessageParent] { + implicit val codec : Codec[ObjectCreateMessageParent] = ( + ("guid" | PlanetSideGUID.codec) :: + ("slot" | PacketHelpers.encodedStringSize) + ).as[ObjectCreateMessageParent] +} + + object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { + type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil + /** + * Codec for formatting around the lack of parent data in the stream. + */ + val noParent : Codec[Pattern] = ( + ("objectClass" | uintL(0xb)) :: //11u + ("guid" | PlanetSideGUID.codec) //16u + ).xmap[Pattern] ( + { + case cls :: guid :: HNil => + cls :: guid :: None :: HNil + }, + { + case cls :: guid :: None :: HNil => + cls :: guid :: HNil + } + ) - type Pattern = Int :: Int :: Option[ObjectCreateMessageParent] :: HNil - type ChoicePattern = Either[Pattern, Pattern] + /** + * Codec for reading and formatting parent data from the stream. + */ + val parent : Codec[Pattern] = ( + ("parentGuid" | PlanetSideGUID.codec) :: //16u + ("objectClass" | uintL(0xb)) :: //11u + ("guid" | PlanetSideGUID.codec) :: //16u + ("parentSlotIndex" | PacketHelpers.encodedStringSize) //8u or 16u + ).xmap[Pattern] ( + { + case pguid :: cls :: guid :: slot :: HNil => + cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil + }, + { + case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil => + pguid :: cls :: guid :: slot :: HNil + } + ) - val noParent : Codec[Pattern] = (("object_class" | uintL(0xb)) :: - ("guid" | uint16L)).xmap[Pattern]( { - case cls :: guid :: HNil => cls :: guid :: None :: HNil - }, { - case cls :: guid :: None :: HNil => cls :: guid :: HNil - }) - val parent : Codec[Pattern] = (("parent_guid" | uint16L) :: - ("object_class" | uintL(0xb)) :: - ("guid" | uint16L) :: - ("parent_slot_index" | PacketHelpers.encodedStringSize)).xmap[Pattern]( { - case pguid :: cls :: guid :: slot :: HNil => - cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil - }, { - case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil => - pguid :: cls :: guid :: slot :: HNil - }) + /** + * Calculate the stream length in number of bits by factoring in the two variable fields.
+ *
+ * Constant fields have already been factored into the results. + * That includes: + * the length of the stream length field (32u), + * the object's class (11u), + * the object's GUID (16u), + * and the bit to determine if there will be parent data. + * In total, these fields form a known fixed length of 60u. + * @param parentInfo if defined, the parentInfo adds either 24u or 32u + * @param data the data length is indeterminate until it is read + * @return the total length of the stream in bits + */ + private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : BitVector) : Long = { + (if(parentInfo.isDefined) { + if(parentInfo.get.slot > 127) 92 else 84 //60u + 16u + (8u or 16u) + } + else { + 60 + } + + data.size) + } implicit val codec : Codec[ObjectCreateMessage] = ( - ("stream_length" | uint32L) :: (either(bool, parent, noParent).exmap[Pattern]( { - case Left(a :: b :: Some(c) :: HNil) => Attempt.successful(a :: b :: Some(c) :: HNil) - case Right(a :: b :: None :: HNil) => Attempt.successful(a :: b :: None :: HNil) - // failure cases - case Left(a :: b :: None :: HNil) => Attempt.failure(Err("expected parent structure")) - case Right(a :: b :: Some(c) :: HNil) => Attempt.failure(Err("got unexpected parent structure")) - }, { - case a :: b :: Some(c) :: HNil => Attempt.successful(Left(a :: b :: Some(c) :: HNil)) - case a :: b :: None :: HNil => Attempt.successful(Right(a :: b :: None :: HNil)) - }) :+ ("rest" | bits) ) - ).as[ObjectCreateMessage] + ("streamLength" | uint32L) :: + (either(bool, parent, noParent).exmap[Pattern] ( + { + case Left(a :: b :: Some(c) :: HNil) => + Attempt.successful(a :: b :: Some(c) :: HNil) //true, _, _, Some(c) + case Right(a :: b :: None :: HNil) => + Attempt.successful(a :: b :: None :: HNil) //false, _, _, None + // failure cases + case Left(a :: b :: None :: HNil) => + Attempt.failure(Err("missing parent structure")) //true, _, _, None + case Right(a :: b :: Some(c) :: HNil) => + Attempt.failure(Err("unexpected parent structure")) //false, _, _, Some(c) + }, + { + case a :: b :: Some(c) :: HNil => + Attempt.successful(Left(a :: b :: Some(c) :: HNil)) + case a :: b :: None :: HNil => + Attempt.successful(Right(a :: b :: None :: HNil)) + } + ) :+ + ("data" | bits) ) + ).xmap[ObjectCreateMessage] ( + { + case len :: cls :: guid :: info :: data :: HNil => + ObjectCreateMessage(len, cls, guid, info, data) + }, + { + //the user should not have to manually supply a proper stream length, that's a restrictive requirement + case ObjectCreateMessage(_, cls, guid, info, data) => + streamLen(info, data) :: cls :: guid :: info :: data :: HNil + } + ).as[ObjectCreateMessage] } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 2f7e8a11..2d25cb26 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -146,24 +146,27 @@ class GamePacketTest extends Specification { "ObjectCreateMessage" should { val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 " - val packet2 = hex"18 17 74 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" + val packet2 = hex"18 F8 00 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" //faked data? + val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16) - "decode" in { + "decode (2)" in { PacketCoding.DecodePacket(packet2).require match { case obj @ ObjectCreateMessage(len, cls, guid, parent, rest) => - val manualRest = packet2.bits.drop(32 + 1 + 0xb + 16) - len === 29719 - cls === 121 - guid === 2497 - rest === manualRest - parent === None + len mustEqual 248 + cls mustEqual 121 + guid mustEqual PlanetSideGUID(2497) + parent mustEqual None + rest mustEqual packet2Rest case default => ko } } - "encode" in { - ok + "encode (2)" in { + val msg = ObjectCreateMessage(0, 121, PlanetSideGUID(2497), None, packet2Rest) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual packet2 } } From 4bbf6277708554722f514f3439f92e2d36a07664 Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 25 Nov 2016 22:14:48 -0500 Subject: [PATCH 02/15] prototype of weapon mold proof-of-concept in progress --- .../packet/game/ObjectCreateMessage.scala | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) 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 0de046ca..727c4da6 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -6,6 +6,46 @@ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless._ +import scala.annotation.switch + +case class Weapon(unk1 : Int, + magazine : Int, + unk2 : Int) + +object Weapon extends Marshallable[Weapon] { + implicit val codec : Codec[Weapon] = ( + ("unk1" | uintL(23)) :: + ("magazine" | uint8L) :: + ("unk2" | uintL(13)) + ).as[Weapon] +} + +case class Mold(objectClass : Int, + dataPortion : BitVector) { + + private var obj : Option[Any] = Mold.selectMold(objectClass, dataPortion) +} + +object Mold extends Marshallable[Mold] { + def apply(objectClass : Int, + obj : T forSome { type T }) : Mold = + new Mold(objectClass, bin"") + + def selectMold(objClass : Int, data : BitVector) : Option[Any] = { + (objClass : @switch) match { + case 0x4D3 => + Weapon.codec.decode(data).toOption + case _ => + None + } + } + + implicit val codec : Codec[Mold] = ( + ("objectClass" | uintL(11)) :: + ("dataPortion" | bits) + ).as[Mold] +} + /** * The parent information of a created object.
*
From 811bf858f865967ac355693e7021d3a6becd42fe Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 26 Nov 2016 00:55:21 -0500 Subject: [PATCH 03/15] working example of object mold (see test for decoding 9mm) but needs work for casting to object type --- .../packet/game/ObjectCreateMessage.scala | 56 ++++++++++--------- common/src/test/scala/GamePacketTest.scala | 23 +++++++- .../src/main/scala/WorldSessionActor.scala | 2 +- 3 files changed, 52 insertions(+), 29 deletions(-) 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 727c4da6..3730482c 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -8,42 +8,48 @@ import shapeless._ import scala.annotation.switch -case class Weapon(unk1 : Int, - magazine : Int, - unk2 : Int) +case class AmmoBox(magazine : Int) -object Weapon extends Marshallable[Weapon] { - implicit val codec : Codec[Weapon] = ( - ("unk1" | uintL(23)) :: - ("magazine" | uint8L) :: - ("unk2" | uintL(13)) - ).as[Weapon] +object AmmoBox extends Marshallable[AmmoBox] { + implicit val codec : Codec[AmmoBox] = ( + ("code" | uintL(23)) :: + ("magazine" | uint16L) + ).exmap[AmmoBox] ( + { + case 0xC8 :: mag :: HNil => + Attempt.successful(AmmoBox(mag)) + case x :: _ :: HNil => + Attempt.failure(Err("code wrong - looking for 200, found "+x)) + }, + { + case AmmoBox(mag) => + Attempt.successful(0xC8 :: mag :: HNil) + } + ).as[AmmoBox] } case class Mold(objectClass : Int, - dataPortion : BitVector) { + data : BitVector) { + private val obj : Option[Any] = Mold.selectMold(objectClass, data) - private var obj : Option[Any] = Mold.selectMold(objectClass, dataPortion) + def isDefined : Boolean = this.obj.isDefined + + def get : T forSome { type T } = this.obj.get } -object Mold extends Marshallable[Mold] { +object Mold { def apply(objectClass : Int, obj : T forSome { type T }) : Mold = new Mold(objectClass, bin"") - def selectMold(objClass : Int, data : BitVector) : Option[Any] = { + def selectMold(objClass : Int, data : BitVector) : Option[_] = { (objClass : @switch) match { - case 0x4D3 => - Weapon.codec.decode(data).toOption + case 0x1C => + Some(AmmoBox.codec.decode(data)) case _ => None } } - - implicit val codec : Codec[Mold] = ( - ("objectClass" | uintL(11)) :: - ("dataPortion" | bits) - ).as[Mold] } /** @@ -87,14 +93,14 @@ case class ObjectCreateMessageParent(guid : PlanetSideGUID, * @param objectClass the code for the type of object being constructed * @param guid the GUID this object will be assigned * @param parentInfo if defined, the relationship between this object and another object (its parent) - * @param data the data used to construct this type of object; + * @param mold the data used to construct this type of object; * requires further object-specific processing */ case class ObjectCreateMessage(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : Option[ObjectCreateMessageParent], - data : BitVector) + mold : Mold) extends PlanetSideGamePacket { def opcode = GamePacketOpcode.ObjectCreateMessage def encode = ObjectCreateMessage.encode(this) @@ -195,12 +201,12 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { ).xmap[ObjectCreateMessage] ( { case len :: cls :: guid :: info :: data :: HNil => - ObjectCreateMessage(len, cls, guid, info, data) + ObjectCreateMessage(len, cls, guid, info, Mold(cls, data)) }, { //the user should not have to manually supply a proper stream length, that's a restrictive requirement - case ObjectCreateMessage(_, cls, guid, info, data) => - streamLen(info, data) :: cls :: guid :: info :: data :: HNil + case ObjectCreateMessage(_, cls, guid, info, mold) => + streamLen(info, mold.data) :: cls :: guid :: info :: mold.data :: HNil } ).as[ObjectCreateMessage] } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 2d25cb26..6366f785 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -148,22 +148,39 @@ class GamePacketTest extends Specification { val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 " val packet2 = hex"18 F8 00 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" //faked data? val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16) + val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" "decode (2)" in { PacketCoding.DecodePacket(packet2).require match { - case obj @ ObjectCreateMessage(len, cls, guid, parent, rest) => + case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => len mustEqual 248 cls mustEqual 121 guid mustEqual PlanetSideGUID(2497) parent mustEqual None - rest mustEqual packet2Rest + mold.data mustEqual packet2Rest + mold.isDefined mustEqual false + case default => + ko + } + } + + "decode (9mm)" in { + PacketCoding.DecodePacket(string_9mm).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => + len mustEqual 124 + cls mustEqual 28 + guid mustEqual PlanetSideGUID(1280) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(75) + parent.get.slot mustEqual 33 + mold.isDefined mustEqual true case default => ko } } "encode (2)" in { - val msg = ObjectCreateMessage(0, 121, PlanetSideGUID(2497), None, packet2Rest) + val msg = ObjectCreateMessage(0, 121, PlanetSideGUID(2497), None, Mold(121, packet2Rest)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual packet2 diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index ea99a03c..27c607b2 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -170,7 +170,7 @@ class WorldSessionActor extends Actor with MDCContextAware { true, //Boosted spawn room pain field true))) //Boosted generator room pain field - sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(PlanetSideGUID(guid),0,0))) + sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(guid,0,0))) import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global From 1f629cf117dd74784076c3ba59ac02148c4eef09 Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 26 Nov 2016 22:40:19 -0500 Subject: [PATCH 04/15] working encode and decode tests --- .../packet/game/ObjectCreateMessage.scala | 96 +++++++++++++------ common/src/test/scala/GamePacketTest.scala | 13 ++- 2 files changed, 78 insertions(+), 31 deletions(-) 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 3730482c..b397ff55 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -7,48 +7,85 @@ import scodec.codecs._ import shapeless._ import scala.annotation.switch +import scala.util.Try -case class AmmoBox(magazine : Int) +abstract class ConstructorData -object AmmoBox extends Marshallable[AmmoBox] { - implicit val codec : Codec[AmmoBox] = ( +case class AmmoBoxData(magazine : Int) extends ConstructorData + +object AmmoBoxData extends Marshallable[AmmoBoxData] { + implicit val codec : Codec[AmmoBoxData] = ( ("code" | uintL(23)) :: ("magazine" | uint16L) - ).exmap[AmmoBox] ( + ).exmap[AmmoBoxData] ( { case 0xC8 :: mag :: HNil => - Attempt.successful(AmmoBox(mag)) + Attempt.successful(AmmoBoxData(mag)) case x :: _ :: HNil => Attempt.failure(Err("code wrong - looking for 200, found "+x)) }, { - case AmmoBox(mag) => + case AmmoBoxData(mag) => Attempt.successful(0xC8 :: mag :: HNil) } - ).as[AmmoBox] + ).as[AmmoBoxData] } case class Mold(objectClass : Int, data : BitVector) { - private val obj : Option[Any] = Mold.selectMold(objectClass, data) + private var obj : Option[ConstructorData] = Mold.selectMold(objectClass, data) def isDefined : Boolean = this.obj.isDefined - def get : T forSome { type T } = this.obj.get + def get : ConstructorData = this.obj.get + + def set(data : ConstructorData) : Boolean = { + var ret = false + if(Some(data).isDefined) { + obj = Some(data) + ret = true + } + ret + } } object Mold { - def apply(objectClass : Int, - obj : T forSome { type T }) : Mold = - new Mold(objectClass, bin"") + def apply(objectClass : Int, obj : ConstructorData) : Mold = + new Mold( objectClass, Mold.serialize(objectClass, obj) ) - def selectMold(objClass : Int, data : BitVector) : Option[_] = { - (objClass : @switch) match { - case 0x1C => - Some(AmmoBox.codec.decode(data)) - case _ => - None + private def selectMold(objClass : Int, data : BitVector) : Option[ConstructorData] = { + var out : Option[ConstructorData] = None + if(!data.isEmpty) { + (objClass : @switch) match { + case 0x1C => + val opt = AmmoBoxData.codec.decode(data).toOption + if(opt.isDefined) { + out = Some(opt.get.value) + } + case _ => + out = None + } } + out + } + + private def serialize(objClass : Int, obj : ConstructorData) : BitVector = { + var out = BitVector.empty + try { + (objClass : @switch) match { + case 0x1C => + val opt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption + if(opt.isDefined) { + out = opt.get + } + } + } + catch { + case ex : ClassCastException => { + //TODO generate and log wrong class error message + } + } + out } } @@ -106,14 +143,6 @@ case class ObjectCreateMessage(streamLength : Long, def encode = ObjectCreateMessage.encode(this) } -object ObjectCreateMessageParent extends Marshallable[ObjectCreateMessageParent] { - implicit val codec : Codec[ObjectCreateMessageParent] = ( - ("guid" | PlanetSideGUID.codec) :: - ("slot" | PacketHelpers.encodedStringSize) - ).as[ObjectCreateMessageParent] -} - - object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil /** @@ -167,13 +196,20 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { * @return the total length of the stream in bits */ private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : BitVector) : Long = { - (if(parentInfo.isDefined) { - if(parentInfo.get.slot > 127) 92 else 84 //60u + 16u + (8u or 16u) + //known length + val first : Long = if(parentInfo.isDefined) { + if(parentInfo.get.slot > 127) 92L else 84L //60u + 16u + (8u or 16u) } else { - 60 + 60L } - + data.size) + //variant length + var second : Long = data.size + val secondMod4 : Long = second % 4L + if(secondMod4 > 0L) { //pad to include last whole nibble + second += 4L - secondMod4 + } + first + second } implicit val codec : Codec[ObjectCreateMessage] = ( diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 6366f785..54be73b6 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -153,7 +153,7 @@ class GamePacketTest extends Specification { "decode (2)" in { PacketCoding.DecodePacket(packet2).require match { case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => - len mustEqual 248 + len mustEqual 248 //60 + 188 cls mustEqual 121 guid mustEqual PlanetSideGUID(2497) parent mustEqual None @@ -174,6 +174,9 @@ class GamePacketTest extends Specification { parent.get.guid mustEqual PlanetSideGUID(75) parent.get.slot mustEqual 33 mold.isDefined mustEqual true + + val obj = mold.get.asInstanceOf[AmmoBoxData] + obj.magazine mustEqual 50 case default => ko } @@ -185,6 +188,14 @@ class GamePacketTest extends Specification { pkt mustEqual packet2 } + + "encode (9mm)" in { + val obj = Mold(28, AmmoBoxData(50)) + val msg = ObjectCreateMessage(0, 28, PlanetSideGUID(1280), Some(ObjectCreateMessageParent(PlanetSideGUID(75), 33)), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_9mm + } } "ChatMsg" should { From 72b400e1d0f443f78bfa0708d4ee905779a67a3f Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 29 Nov 2016 18:31:22 -0500 Subject: [PATCH 05/15] prototype for weapon data codec --- .../scala/net/psforever/packet/PSPacket.scala | 128 +++++++++-- .../packet/game/ObjectCreateMessage.scala | 216 +++++++++++++++++- 2 files changed, 316 insertions(+), 28 deletions(-) diff --git a/common/src/main/scala/net/psforever/packet/PSPacket.scala b/common/src/main/scala/net/psforever/packet/PSPacket.scala index 2fe9a563..3dab2fbf 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 b397ff55..61d7d552 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 } From b38bfe8cae04bfef374a7f4b953166f0668c4d20 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 29 Nov 2016 22:55:38 -0500 Subject: [PATCH 06/15] during debugging decoding of weapons --- .../packet/game/ObjectCreateMessage.scala | 17 ++++++++++--- common/src/test/scala/GamePacketTest.scala | 25 ++++++++++++++++++- 2 files changed, 38 insertions(+), 4 deletions(-) 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 61d7d552..c7ecaa1e 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -34,15 +34,18 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] { case class WeaponData(ammo : InternalMold) extends ConstructorData object WeaponData extends Marshallable[WeaponData] { + type rawPattern = Int :: Unit :: Int :: Unit :: Int :: InternalMold :: HNil implicit val codec : Codec[WeaponData] = ( ("code" | uint16L) :: ignore(12) :: uint4L :: - ignore(4) :: - uintL(12) :: + ignore(16) :: + ("tail" | uintL(11)) :: ("data" | InternalMold.codec) ).exmap[WeaponData] ( { + case 0x48 :: _ :: 2 :: _ :: 0x2C0 :: ammo :: HNil => //TODO: this will work for decoding, but not for encoding + Attempt.successful(WeaponData(ammo)) case 0x88 :: _ :: 2 :: _ :: 0x2C0 :: ammo :: HNil => Attempt.successful(WeaponData(ammo)) case x :: _ :: y :: _ :: z :: _ :: HNil => @@ -50,7 +53,7 @@ object WeaponData extends Marshallable[WeaponData] { }, { case WeaponData(ammo) => - Attempt.successful(0x88 :: () :: 2 :: () :: 0x2C0 :: ammo :: HNil) + Attempt.successful(0x88 :: () :: 2 :: () :: 0x2C0 :: ammo :: HNil) //TODO: this will not work for encoding (see above) } ).as[WeaponData] } @@ -249,6 +252,10 @@ object Mold { val opt = WeaponData.codec.decode(data).toOption if(opt.isDefined) out = Some(opt.get.value) + case 0x159 => //gauss + val opt = WeaponData.codec.decode(data).toOption + if(opt.isDefined) + out = Some(opt.get.value) case _ => } } @@ -267,6 +274,10 @@ object Mold { val opt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption if(opt.isDefined) out = opt.get + case 0x159 => //gauss + val opt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption + if(opt.isDefined) + out = opt.get case _ => throw new ClassCastException("cannot find object code - "+objClass) } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 54be73b6..292367c9 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -149,6 +149,7 @@ class GamePacketTest extends Specification { val packet2 = hex"18 F8 00 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" //faked data? val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16) val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" + val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" "decode (2)" in { PacketCoding.DecodePacket(packet2).require match { @@ -174,7 +175,6 @@ class GamePacketTest extends Specification { parent.get.guid mustEqual PlanetSideGUID(75) parent.get.slot mustEqual 33 mold.isDefined mustEqual true - val obj = mold.get.asInstanceOf[AmmoBoxData] obj.magazine mustEqual 50 case default => @@ -182,6 +182,29 @@ class GamePacketTest extends Specification { } } + "decode (gauss)" in { + PacketCoding.DecodePacket(string_gauss).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => + len mustEqual 220 + cls mustEqual 345 + guid mustEqual PlanetSideGUID(1465) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(75) + parent.get.slot mustEqual 2 + mold.isDefined mustEqual true + val obj_wep = mold.get.asInstanceOf[WeaponData] + val obj_ammo = obj_wep.ammo//.asInstanceOf[InternalMold] + obj_ammo.objectClass mustEqual 28 + obj_ammo.guid mustEqual PlanetSideGUID(1286) + obj_ammo.parentSlot mustEqual 0 + obj_ammo.obj.isDefined mustEqual true + val ammo = obj_ammo.obj.get.asInstanceOf[AmmoBoxData] + ammo.magazine mustEqual 30 + case default => + ko + } + } + "encode (2)" in { val msg = ObjectCreateMessage(0, 121, PlanetSideGUID(2497), None, Mold(121, packet2Rest)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector From ee87e1c6ec908e2ea005cf97ee15b740cf4e2da1 Mon Sep 17 00:00:00 2001 From: FateJH Date: Wed, 30 Nov 2016 08:38:20 -0500 Subject: [PATCH 07/15] variable field at head of weapon data accounted for; encoding and decoding of weapon data should work now --- .../packet/game/ObjectCreateMessage.scala | 46 +++++++++++-------- common/src/test/scala/GamePacketTest.scala | 13 +++++- 2 files changed, 39 insertions(+), 20 deletions(-) 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 c7ecaa1e..19119ff0 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -15,45 +15,48 @@ case class AmmoBoxData(magazine : Int) extends ConstructorData object AmmoBoxData extends Marshallable[AmmoBoxData] { implicit val codec : Codec[AmmoBoxData] = ( - ("code" | uintL(23)) :: + uintL(8) :: + ignore(15) :: ("magazine" | uint16L) ).exmap[AmmoBoxData] ( { - case 0xC8 :: mag :: HNil => + case 0xC8 :: _ :: mag :: HNil => Attempt.successful(AmmoBoxData(mag)) - case x :: _ :: HNil => + case x :: _ :: _ :: HNil => Attempt.failure(Err("code wrong - looking for 200, found "+x)) }, { case AmmoBoxData(mag) => - Attempt.successful(0xC8 :: mag :: HNil) + Attempt.successful(0xC8 :: () :: mag :: HNil) } ).as[AmmoBoxData] } -case class WeaponData(ammo : InternalMold) extends ConstructorData +case class WeaponData(unk : Int, + ammo : InternalMold) extends ConstructorData object WeaponData extends Marshallable[WeaponData] { - type rawPattern = Int :: Unit :: Int :: Unit :: Int :: InternalMold :: HNil + def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData = + new WeaponData(unk, InternalMold(cls, guid, parentSlot, Some(ammo))) + implicit val codec : Codec[WeaponData] = ( - ("code" | uint16L) :: - ignore(12) :: + ("unk" | uint4L) :: + uint4L :: + ignore(20) :: uint4L :: ignore(16) :: - ("tail" | uintL(11)) :: - ("data" | InternalMold.codec) + uintL(11) :: + InternalMold.codec ).exmap[WeaponData] ( { - case 0x48 :: _ :: 2 :: _ :: 0x2C0 :: ammo :: HNil => //TODO: this will work for decoding, but not for encoding - Attempt.successful(WeaponData(ammo)) - 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 code :: 8 :: _ :: 2 :: _ :: 0x2C0 :: ammo :: HNil => + Attempt.successful(WeaponData(code, ammo)) + case _ :: x :: _ :: y :: _ :: z :: _ :: HNil => + Attempt.failure(Err("looking for 8-2-704 pattern, found %d-%d-%d".format(x,y,z))) }, { - case WeaponData(ammo) => - Attempt.successful(0x88 :: () :: 2 :: () :: 0x2C0 :: ammo :: HNil) //TODO: this will not work for encoding (see above) + case WeaponData(code, ammo) => + Attempt.successful(code :: 8 :: () :: 2 :: () :: 0x2C0 :: ammo :: HNil) } ).as[WeaponData] } @@ -177,6 +180,13 @@ object WeaponData extends Marshallable[WeaponData] { // ).as[CharacterData] //} +/** + * na + * @param objectClass na + * @param guid na + * @param parentSlot na + * @param obj na + */ case class InternalMold(objectClass : Int, guid : PlanetSideGUID, parentSlot : Int, diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 292367c9..90e9e8b4 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -145,8 +145,8 @@ class GamePacketTest extends Specification { } "ObjectCreateMessage" should { - val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 " - val packet2 = hex"18 F8 00 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" //faked data? + val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 " //fake data? + val packet2 = hex"18 F8 00 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" //fake data val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16) val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" @@ -193,6 +193,7 @@ class GamePacketTest extends Specification { parent.get.slot mustEqual 2 mold.isDefined mustEqual true val obj_wep = mold.get.asInstanceOf[WeaponData] + obj_wep.unk mustEqual 4 val obj_ammo = obj_wep.ammo//.asInstanceOf[InternalMold] obj_ammo.objectClass mustEqual 28 obj_ammo.guid mustEqual PlanetSideGUID(1286) @@ -219,6 +220,14 @@ class GamePacketTest extends Specification { pkt mustEqual string_9mm } + + "encode (gauss)" in { + val obj = Mold(345, WeaponData(4, 28, PlanetSideGUID(1286), 0, AmmoBoxData(30))) + val msg = ObjectCreateMessage(0, 345, PlanetSideGUID(1465), Some(ObjectCreateMessageParent(PlanetSideGUID(75), 2)), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_gauss + } } "ChatMsg" should { From b0b5a7005bf4e10a25aa3f68562e5c68f34b2f9d Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 2 Dec 2016 15:16:56 -0500 Subject: [PATCH 08/15] in the middle of character decode test implementation --- .../packet/game/ObjectCreateMessage.scala | 285 +++++++++--------- common/src/test/scala/GamePacketTest.scala | 39 ++- 2 files changed, 184 insertions(+), 140 deletions(-) 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 19119ff0..dd211a76 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -1,7 +1,8 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -//import net.psforever.types.Vector3 +import scodec.DecodeResult +import net.psforever.types.Vector3 import scodec.bits._ import scodec.{Attempt, Codec, Err} import scodec.codecs._ @@ -19,25 +20,25 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] { ignore(15) :: ("magazine" | uint16L) ).exmap[AmmoBoxData] ( - { - case 0xC8 :: _ :: mag :: HNil => - Attempt.successful(AmmoBoxData(mag)) - case x :: _ :: _ :: HNil => - Attempt.failure(Err("code wrong - looking for 200, found "+x)) - }, - { - case AmmoBoxData(mag) => - Attempt.successful(0xC8 :: () :: mag :: HNil) - } - ).as[AmmoBoxData] + { + case 0xC8 :: _ :: mag :: HNil => + Attempt.successful(AmmoBoxData(mag)) + case x :: _ :: _ :: HNil => + Attempt.failure(Err("looking for 200, found "+x)) + }, + { + case AmmoBoxData(mag) => + Attempt.successful(0xC8 :: () :: mag :: HNil) + } + ).as[AmmoBoxData] } case class WeaponData(unk : Int, - ammo : InternalMold) extends ConstructorData + ammo : InternalSlot) extends ConstructorData object WeaponData extends Marshallable[WeaponData] { def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData = - new WeaponData(unk, InternalMold(cls, guid, parentSlot, Some(ammo))) + new WeaponData(unk, InternalSlot(cls, guid, parentSlot, Some(ammo))) implicit val codec : Codec[WeaponData] = ( ("unk" | uint4L) :: @@ -46,13 +47,13 @@ object WeaponData extends Marshallable[WeaponData] { uint4L :: ignore(16) :: uintL(11) :: - InternalMold.codec + ("ammo" | InternalSlot.codec) ).exmap[WeaponData] ( { case code :: 8 :: _ :: 2 :: _ :: 0x2C0 :: ammo :: HNil => Attempt.successful(WeaponData(code, ammo)) case _ :: x :: _ :: y :: _ :: z :: _ :: HNil => - Attempt.failure(Err("looking for 8-2-704 pattern, found %d-%d-%d".format(x,y,z))) + Attempt.failure(Err("looking for 8-2-704 pattern, found %d-%d-%d".format(x,y,z))) //TODO I actually don't know what of this is actually important }, { case WeaponData(code, ammo) => @@ -61,26 +62,39 @@ object WeaponData extends Marshallable[WeaponData] { ).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, +case class RibbonBars(upper : Int, //0xFFFF means no merit (for all ...) + middle : Int, + lower : Int, + tos : Int) + +object RibbonBars extends Marshallable[RibbonBars] { + implicit val codec : Codec[RibbonBars] = ( + ("upper" | uint16L) :: + ("middle" | uint16L) :: + ("lower" | uint16L) :: + ("tos" | uint16L) + ).as[RibbonBars] +} + +case class CharacterData(pos : Vector3, + objYaw : 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 + viewPitch : Int, + viewYaw : Int, + upperMerit : Int, //0xFFFF means no merit (for all ...) + middleMerit : Int, + lowerMerit : Int, + termOfServiceMerit : Int, // healthMax : Int, // health : Int, // armor : Int, @@ -96,72 +110,62 @@ object WeaponData extends Marshallable[WeaponData] { // unk11 : Int, //134 // unk12 : Int, //199 // firstTimeEvent_length : Long, -// firstEntry : String, -// firstTimEvent_list : List[String], +// firstEntry : Option[String], +// firstTimeEvent_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) :: + inventory : BitVector + ) extends ConstructorData + +object CharacterData extends Marshallable[CharacterData] { + val ribbonBars : Codec[RibbonBars] = ( + ("upper" | uint16L) :: + ("middle" | uint16L) :: + ("lower" | uint16L) :: + ("tos" | uint16L) + ).as[RibbonBars] + + implicit val codec : Codec[CharacterData] = ( + ("pos" | Vector3.codec_pos) :: + ignore(16) :: + ("objYaw" | uint8L) :: + ignore(1) :: + ("faction" | uintL(2)) :: + ("bops" | bool) :: + ignore(20) :: + ("name" | PacketHelpers.encodedWideStringAligned(4)) :: + ("exosuit" | uintL(3)) :: + ignore(2) :: + ("sex" | uintL(2)) :: + ("face1" | uint4L) :: + ("face2" | uint4L) :: + ("voice" | uintL(3)) :: + ignore(22) :: + ("unk1" | uint16L) :: + ignore(42) :: + ("unk2" | uint16L) :: + ignore(30) :: + ("unk3" | uintL(4)) :: + ignore(24) :: + ("viewPitch" | uint8L) :: + ("viewYaw" | uint8L) :: + ignore(10) :: + ("upperMerit" | uint16L) :: + ("middleMerit" | uint16L) :: + ("lowerMerit" | uint16L) :: + ("termOfServiceMerit" | uint16L) :: +// ignore(160) :: // ("healthMax" | uint16L) :: // ("health" | uint16L) :: // ignore(1) :: // ("armor" | uint16L) :: -// ignore(1) :: -// ignore(8) :: +// ignore(9) :: // ("unk4" | uint8L) :: // ignore(8) :: // ("unk5" | uint4L) :: // ("unk6" | uintL(3)) :: // ("staminaMax" | uint16L) :: // ("stamina" | uint16L) :: -// ignore(148) :: -// ignore(4) :: +// ignore(152) :: // ("unk7" | uint16L) :: // ("unk8" | uint8L) :: // ("unk9" | uint8L) :: @@ -170,49 +174,49 @@ object WeaponData extends Marshallable[WeaponData] { // ("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] -//} +// conditional(len > 0, "firstEntry" | PacketHelpers.encodedStringAligned(5)) :: +// ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: +// ("tutorial_list" | PacketHelpers.listOfNAligned(uint32L, 0, PacketHelpers.encodedString)) :: +// ignore(207) :: + ("inventory" | bits) +// }) + ).as[CharacterData] +} /** - * na + * The same kind of data as required for a formal ObjectCreateMessage but with a required and implicit parent relationship. + * Data preceding this entry will define the existence of the parent. * @param objectClass na * @param guid na * @param parentSlot na * @param obj na */ -case class InternalMold(objectClass : Int, +case class InternalSlot(objectClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : Option[ConstructorData]) -object InternalMold extends Marshallable[InternalMold] { +object InternalSlot extends Marshallable[InternalSlot] { type objPattern = Int :: PlanetSideGUID :: Int :: Option[ConstructorData] :: HNil - implicit val codec : Codec[InternalMold] = ( + implicit val codec : Codec[InternalSlot] = ( ignore(1) :: //TODO determine what this bit does ("objectClass" | uintL(11)) :: ("guid" | PlanetSideGUID.codec) :: ("parentSlot" | PacketHelpers.encodedStringSize) :: - ("data" | bits) + bits ).exmap[objPattern] ( { case _ :: cls :: guid :: slot :: data :: HNil => - Attempt.successful(cls :: guid :: slot :: Mold.selectMold(cls, data) :: HNil) + Attempt.successful(cls :: guid :: slot :: Mold.decode(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) + Attempt.successful(() :: cls :: guid :: slot :: Mold.encode(cls, mold.get) :: HNil) } - ).exmap[objPattern] ( + ).exmap[objPattern] ( { case cls :: guid :: slot :: None :: HNil => Attempt.failure(Err("no decoded constructor data")) @@ -225,12 +229,12 @@ object InternalMold extends Marshallable[InternalMold] { case cls :: guid :: slot :: data :: HNil => Attempt.successful(cls :: guid :: slot :: data :: HNil) } - ).as[InternalMold] + ).as[InternalSlot] } case class Mold(objectClass : Int, data : BitVector) { - private var obj : Option[ConstructorData] = Mold.selectMold(objectClass, data) + private var obj : Option[ConstructorData] = Mold.decode(objectClass, data) def isDefined : Boolean = this.obj.isDefined @@ -248,55 +252,59 @@ case class Mold(objectClass : Int, object Mold { def apply(objectClass : Int, obj : ConstructorData) : Mold = - new Mold( objectClass, Mold.serialize(objectClass, obj) ) + new Mold( objectClass, Mold.encode(objectClass, obj) ) - def selectMold(objClass : Int, data : BitVector) : Option[ConstructorData] = { + def decode(objClass : Int, data : BitVector) : Option[ConstructorData] = { var out : Option[ConstructorData] = None if(!data.isEmpty) { - (objClass : @switch) match { - case 0x1C => //9mm - val opt = AmmoBoxData.codec.decode(data).toOption - 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 0x159 => //gauss - val opt = WeaponData.codec.decode(data).toOption - if(opt.isDefined) - out = Some(opt.get.value) - case _ => + var outOpt : Option[DecodeResult[_]] = None + try { + (objClass : @switch) match { + case 0x79 => //avatars + outOpt = CharacterData.codec.decode(data).toOption + case 0x1C => //9mm + outOpt = AmmoBoxData.codec.decode(data).toOption + case 0x46 => //beamer + outOpt = WeaponData.codec.decode(data).toOption + case 0x159 => //gauss + outOpt = WeaponData.codec.decode(data).toOption + case _ => + } + if(outOpt.isDefined) + out = Some(outOpt.get.value.asInstanceOf[ConstructorData]) + } + catch { + case ex : ClassCastException => + //TODO generate and log wrong class error message + case ex : Exception => + //TODO generic error } } out } - def serialize(objClass : Int, obj : ConstructorData) : BitVector = { + def encode(objClass : Int, obj : ConstructorData) : BitVector = { var out = BitVector.empty try { + var outOpt : Option[BitVector] = None (objClass : @switch) match { case 0x1C => //9mm - val opt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption - if(opt.isDefined) - out = opt.get + outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption case 0x46 => //beamer - val opt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption - if(opt.isDefined) - out = opt.get + outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption case 0x159 => //gauss - val opt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption - if(opt.isDefined) - out = opt.get + outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption case _ => throw new ClassCastException("cannot find object code - "+objClass) } + if(outOpt.isDefined) + out = outOpt.get } catch { case ex : ClassCastException => - //TODO generate and log wrong class error message + //TODO generate and log wrong class error message case ex : Exception => - //TODO generic error + //TODO generic error } out } @@ -453,7 +461,6 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { ObjectCreateMessage(len, cls, guid, info, Mold(cls, data)) }, { - //the user should not have to manually supply a proper stream length, that's a restrictive requirement case ObjectCreateMessage(_, cls, guid, info, mold) => streamLen(info, mold.data) :: cls :: guid :: info :: mold.data :: HNil } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 90e9e8b4..0dce9916 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -150,6 +150,7 @@ class GamePacketTest extends Specification { val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16) val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" + val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" "decode (2)" in { PacketCoding.DecodePacket(packet2).require match { @@ -165,6 +166,42 @@ class GamePacketTest extends Specification { } } + "decode (char)" in { + PacketCoding.DecodePacket(string_testchar).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => + len mustEqual 3159 + cls mustEqual 0x79 + guid mustEqual PlanetSideGUID(75) + parent.isDefined mustEqual false + mold.isDefined mustEqual true + + val char = mold.get.asInstanceOf[CharacterData] + char.pos.x mustEqual 3674.8438f + char.pos.y mustEqual 2726.789f + char.pos.z mustEqual 91.15625f + char.objYaw mustEqual 19 + char.faction mustEqual 2 //vs + char.bops mustEqual false + char.name mustEqual "IlllIIIlllIlIllIlllIllI" + char.exosuit mustEqual 4 //standard + char.sex mustEqual 2 //female + char.face1 mustEqual 2 + char.face2 mustEqual 9 + char.voice mustEqual 1 //female 1 + char.unk1 mustEqual 0x8080 + char.unk2 mustEqual 0xFFFF + char.unk3 mustEqual 2 + char.viewPitch mustEqual 0xFF + char.viewYaw mustEqual 0x6A + char.upperMerit mustEqual 0xFFFF //none + char.middleMerit mustEqual 0xFFFF //none + char.lowerMerit mustEqual 0xFFFF //none + char.termOfServiceMerit mustEqual 0xFFFF //none + case default => + ko + } + } + "decode (9mm)" in { PacketCoding.DecodePacket(string_9mm).require match { case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => @@ -194,7 +231,7 @@ class GamePacketTest extends Specification { mold.isDefined mustEqual true val obj_wep = mold.get.asInstanceOf[WeaponData] obj_wep.unk mustEqual 4 - val obj_ammo = obj_wep.ammo//.asInstanceOf[InternalMold] + val obj_ammo = obj_wep.ammo//.asInstanceOf[InternalSlot] obj_ammo.objectClass mustEqual 28 obj_ammo.guid mustEqual PlanetSideGUID(1286) obj_ammo.parentSlot mustEqual 0 From 7cd9b8f5f14bb279c3c40829f800b491213eb9b9 Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 2 Dec 2016 18:44:40 -0500 Subject: [PATCH 09/15] ignoring the inventory, character stream decoding is functional --- .../packet/game/ObjectCreateMessage.scala | 104 +++++++++--------- common/src/test/scala/GamePacketTest.scala | 29 ++++- 2 files changed, 77 insertions(+), 56 deletions(-) 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 dd211a76..05e15926 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -91,28 +91,28 @@ case class CharacterData(pos : Vector3, unk3 : Int, //2 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 : Option[String], -// firstTimeEvent_list : List[String], -// tutorial_list : List[String], + upperMerit : Long, //0xFFFFFFFF means no merit (for all ...) + middleMerit : Long, + lowerMerit : Long, + termOfServiceMerit : Long, + healthMax : Int, + health : Int, + armor : Int, + unk4 : Int, //1 + unk5 : Int, //7 + unk6 : Int, //7 + staminaMax : Int, + stamina : Int, + unk7 : Int, // 28 + unk8 : Int, //4 + unk9 : Int, //44 + unk10 : Int, //84 + unk11 : Int, //104 + unk12 : Int, //1900 + firstTimeEvent_length : Long, + firstEntry : Option[String], + firstTimeEvent_list : List[String], + tutorial_list : List[String], inventory : BitVector ) extends ConstructorData @@ -149,37 +149,37 @@ object CharacterData extends Marshallable[CharacterData] { ("viewPitch" | uint8L) :: ("viewYaw" | uint8L) :: ignore(10) :: - ("upperMerit" | uint16L) :: - ("middleMerit" | uint16L) :: - ("lowerMerit" | uint16L) :: - ("termOfServiceMerit" | uint16L) :: -// ignore(160) :: -// ("healthMax" | uint16L) :: -// ("health" | uint16L) :: -// ignore(1) :: -// ("armor" | uint16L) :: -// ignore(9) :: -// ("unk4" | uint8L) :: -// ignore(8) :: -// ("unk5" | uint4L) :: -// ("unk6" | uintL(3)) :: -// ("staminaMax" | uint16L) :: -// ("stamina" | uint16L) :: -// ignore(152) :: -// ("unk7" | uint16L) :: -// ("unk8" | uint8L) :: -// ("unk9" | uint8L) :: -// ("unk10" | uint8L) :: -// ("unk11" | uint8L) :: -// ("unk12" | uintL(12)) :: -// ignore(3) :: -// (("firstTimeEvent_length" | uint32L) >>:~ { len => -// conditional(len > 0, "firstEntry" | PacketHelpers.encodedStringAligned(5)) :: -// ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: -// ("tutorial_list" | PacketHelpers.listOfNAligned(uint32L, 0, PacketHelpers.encodedString)) :: -// ignore(207) :: + ("upperMerit" | uint32L) :: + ("middleMerit" | uint32L) :: + ("lowerMerit" | uint32L) :: + ("termOfServiceMerit" | uint32L) :: + ignore(160) :: + ("healthMax" | uint16L) :: + ("health" | uint16L) :: + ignore(1) :: + ("armor" | uint16L) :: + ignore(9) :: + ("unk4" | uint8L) :: + ignore(8) :: + ("unk5" | uint4L) :: + ("unk6" | uintL(3)) :: + ("staminaMax" | uint16L) :: + ("stamina" | uint16L) :: + ignore(149) :: + ("unk7" | uint16L) :: + ("unk8" | uint8L) :: + ("unk9" | uint8L) :: + ("unk10" | uint8L) :: + ("unk11" | uint8L) :: + ("unk12" | uintL(12)) :: + ignore(19) :: + (("firstTimeEvent_length" | uint32L) >>:~ { len => + conditional(len > 0, "firstEntry" | PacketHelpers.encodedStringAligned(5)) :: + ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: + ("tutorial_list" | PacketHelpers.listOfNAligned(uint32L, 0, PacketHelpers.encodedString)) :: + ignore(207) :: ("inventory" | bits) -// }) + }) ).as[CharacterData] } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 0dce9916..5355a495 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -193,10 +193,31 @@ class GamePacketTest extends Specification { char.unk3 mustEqual 2 char.viewPitch mustEqual 0xFF char.viewYaw mustEqual 0x6A - char.upperMerit mustEqual 0xFFFF //none - char.middleMerit mustEqual 0xFFFF //none - char.lowerMerit mustEqual 0xFFFF //none - char.termOfServiceMerit mustEqual 0xFFFF //none + char.upperMerit mustEqual 0xFFFFFFFFL //none + char.middleMerit mustEqual 0xFFFFFFFFL //none + char.lowerMerit mustEqual 0xFFFFFFFFL //none + char.termOfServiceMerit mustEqual 0xFFFFFFFFL //none + char.healthMax mustEqual 100 + char.health mustEqual 100 + char.armor mustEqual 50 //standard exosuit value + char.unk4 mustEqual 1 + char.unk5 mustEqual 7 + char.unk6 mustEqual 7 + char.staminaMax mustEqual 100 + char.stamina mustEqual 100 + char.unk7 mustEqual 28 + char.unk8 mustEqual 4 + char.unk9 mustEqual 44 + char.unk10 mustEqual 84 + char.unk11 mustEqual 104 + char.unk12 mustEqual 1900 + char.firstTimeEvent_length mustEqual 4 + char.firstEntry mustEqual Some("xpe_sanctuary_help") + char.firstTimeEvent_list.size mustEqual 3 + char.firstTimeEvent_list.head mustEqual "xpe_th_firemodes" + char.firstTimeEvent_list(1) mustEqual "used_beamer" + char.firstTimeEvent_list(2) mustEqual "map13" + char.tutorial_list.size mustEqual 0 case default => ko } From b6eed0dbbc8ffdcb164c32225d7319d6a6d5685f Mon Sep 17 00:00:00 2001 From: FateJH Date: Sat, 3 Dec 2016 20:55:19 -0500 Subject: [PATCH 10/15] split all the classes formerly in ObjectCreateMessage into separate files and placed them into their own package; this restored compiled time down to normal duration from ~20 min (seriously); test failing for known reason --- .../packet/game/ObjectCreateMessage.scala | 308 +----------------- .../game/objectcreate/AmmoBoxData.scala | 28 ++ .../game/objectcreate/CharacterData.scala | 101 ++++++ .../game/objectcreate/ConstructorData.scala | 4 + .../game/objectcreate/InternalSlot.scala | 58 ++++ .../game/objectcreate/InventoryData.scala | 21 ++ .../game/objectcreate/InventoryItem.scala | 17 + .../packet/game/objectcreate/Mold.scala | 113 +++++++ .../packet/game/objectcreate/REKData.scala | 32 ++ .../packet/game/objectcreate/RibbonBars.scala | 20 ++ .../packet/game/objectcreate/WeaponData.scala | 37 +++ common/src/test/scala/GamePacketTest.scala | 14 +- 12 files changed, 443 insertions(+), 310 deletions(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala 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 05e15926..32af6453 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -1,314 +1,12 @@ +// Copyright (c) 2016 PSForever.net to present package net.psforever.packet.game +import net.psforever.packet.game.objectcreate.Mold import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import scodec.DecodeResult -import net.psforever.types.Vector3 import scodec.bits._ import scodec.{Attempt, Codec, Err} import scodec.codecs._ -import shapeless._ - -import scala.annotation.switch - -abstract class ConstructorData - -case class AmmoBoxData(magazine : Int) extends ConstructorData - -object AmmoBoxData extends Marshallable[AmmoBoxData] { - implicit val codec : Codec[AmmoBoxData] = ( - uintL(8) :: - ignore(15) :: - ("magazine" | uint16L) - ).exmap[AmmoBoxData] ( - { - case 0xC8 :: _ :: mag :: HNil => - Attempt.successful(AmmoBoxData(mag)) - case x :: _ :: _ :: HNil => - Attempt.failure(Err("looking for 200, found "+x)) - }, - { - case AmmoBoxData(mag) => - Attempt.successful(0xC8 :: () :: mag :: HNil) - } - ).as[AmmoBoxData] -} - -case class WeaponData(unk : Int, - ammo : InternalSlot) extends ConstructorData - -object WeaponData extends Marshallable[WeaponData] { - def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData = - new WeaponData(unk, InternalSlot(cls, guid, parentSlot, Some(ammo))) - - implicit val codec : Codec[WeaponData] = ( - ("unk" | uint4L) :: - uint4L :: - ignore(20) :: - uint4L :: - ignore(16) :: - uintL(11) :: - ("ammo" | InternalSlot.codec) - ).exmap[WeaponData] ( - { - case code :: 8 :: _ :: 2 :: _ :: 0x2C0 :: ammo :: HNil => - Attempt.successful(WeaponData(code, ammo)) - case _ :: x :: _ :: y :: _ :: z :: _ :: HNil => - Attempt.failure(Err("looking for 8-2-704 pattern, found %d-%d-%d".format(x,y,z))) //TODO I actually don't know what of this is actually important - }, - { - case WeaponData(code, ammo) => - Attempt.successful(code :: 8 :: () :: 2 :: () :: 0x2C0 :: ammo :: HNil) - } - ).as[WeaponData] -} - -case class RibbonBars(upper : Int, //0xFFFF means no merit (for all ...) - middle : Int, - lower : Int, - tos : Int) - -object RibbonBars extends Marshallable[RibbonBars] { - implicit val codec : Codec[RibbonBars] = ( - ("upper" | uint16L) :: - ("middle" | uint16L) :: - ("lower" | uint16L) :: - ("tos" | uint16L) - ).as[RibbonBars] -} - -case class CharacterData(pos : Vector3, - objYaw : 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 - viewPitch : Int, - viewYaw : Int, - upperMerit : Long, //0xFFFFFFFF means no merit (for all ...) - middleMerit : Long, - lowerMerit : Long, - termOfServiceMerit : Long, - healthMax : Int, - health : Int, - armor : Int, - unk4 : Int, //1 - unk5 : Int, //7 - unk6 : Int, //7 - staminaMax : Int, - stamina : Int, - unk7 : Int, // 28 - unk8 : Int, //4 - unk9 : Int, //44 - unk10 : Int, //84 - unk11 : Int, //104 - unk12 : Int, //1900 - firstTimeEvent_length : Long, - firstEntry : Option[String], - firstTimeEvent_list : List[String], - tutorial_list : List[String], - inventory : BitVector - ) extends ConstructorData - -object CharacterData extends Marshallable[CharacterData] { - val ribbonBars : Codec[RibbonBars] = ( - ("upper" | uint16L) :: - ("middle" | uint16L) :: - ("lower" | uint16L) :: - ("tos" | uint16L) - ).as[RibbonBars] - - implicit val codec : Codec[CharacterData] = ( - ("pos" | Vector3.codec_pos) :: - ignore(16) :: - ("objYaw" | uint8L) :: - ignore(1) :: - ("faction" | uintL(2)) :: - ("bops" | bool) :: - ignore(20) :: - ("name" | PacketHelpers.encodedWideStringAligned(4)) :: - ("exosuit" | uintL(3)) :: - ignore(2) :: - ("sex" | uintL(2)) :: - ("face1" | uint4L) :: - ("face2" | uint4L) :: - ("voice" | uintL(3)) :: - ignore(22) :: - ("unk1" | uint16L) :: - ignore(42) :: - ("unk2" | uint16L) :: - ignore(30) :: - ("unk3" | uintL(4)) :: - ignore(24) :: - ("viewPitch" | uint8L) :: - ("viewYaw" | uint8L) :: - ignore(10) :: - ("upperMerit" | uint32L) :: - ("middleMerit" | uint32L) :: - ("lowerMerit" | uint32L) :: - ("termOfServiceMerit" | uint32L) :: - ignore(160) :: - ("healthMax" | uint16L) :: - ("health" | uint16L) :: - ignore(1) :: - ("armor" | uint16L) :: - ignore(9) :: - ("unk4" | uint8L) :: - ignore(8) :: - ("unk5" | uint4L) :: - ("unk6" | uintL(3)) :: - ("staminaMax" | uint16L) :: - ("stamina" | uint16L) :: - ignore(149) :: - ("unk7" | uint16L) :: - ("unk8" | uint8L) :: - ("unk9" | uint8L) :: - ("unk10" | uint8L) :: - ("unk11" | uint8L) :: - ("unk12" | uintL(12)) :: - ignore(19) :: - (("firstTimeEvent_length" | uint32L) >>:~ { len => - conditional(len > 0, "firstEntry" | PacketHelpers.encodedStringAligned(5)) :: - ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: - ("tutorial_list" | PacketHelpers.listOfNAligned(uint32L, 0, PacketHelpers.encodedString)) :: - ignore(207) :: - ("inventory" | bits) - }) - ).as[CharacterData] -} - -/** - * The same kind of data as required for a formal ObjectCreateMessage but with a required and implicit parent relationship. - * Data preceding this entry will define the existence of the parent. - * @param objectClass na - * @param guid na - * @param parentSlot na - * @param obj na - */ -case class InternalSlot(objectClass : Int, - guid : PlanetSideGUID, - parentSlot : Int, - obj : Option[ConstructorData]) - -object InternalSlot extends Marshallable[InternalSlot] { - type objPattern = Int :: PlanetSideGUID :: Int :: Option[ConstructorData] :: HNil - - implicit val codec : Codec[InternalSlot] = ( - ignore(1) :: //TODO determine what this bit does - ("objectClass" | uintL(11)) :: - ("guid" | PlanetSideGUID.codec) :: - ("parentSlot" | PacketHelpers.encodedStringSize) :: - bits - ).exmap[objPattern] ( - { - case _ :: cls :: guid :: slot :: data :: HNil => - Attempt.successful(cls :: guid :: slot :: Mold.decode(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.encode(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[InternalSlot] -} - -case class Mold(objectClass : Int, - data : BitVector) { - private var obj : Option[ConstructorData] = Mold.decode(objectClass, data) - - def isDefined : Boolean = this.obj.isDefined - - def get : ConstructorData = this.obj.get - - def set(data : ConstructorData) : Boolean = { - var ret = false - if(Some(data).isDefined) { - obj = Some(data) - ret = true - } - ret - } -} - -object Mold { - def apply(objectClass : Int, obj : ConstructorData) : Mold = - new Mold( objectClass, Mold.encode(objectClass, obj) ) - - def decode(objClass : Int, data : BitVector) : Option[ConstructorData] = { - var out : Option[ConstructorData] = None - if(!data.isEmpty) { - var outOpt : Option[DecodeResult[_]] = None - try { - (objClass : @switch) match { - case 0x79 => //avatars - outOpt = CharacterData.codec.decode(data).toOption - case 0x1C => //9mm - outOpt = AmmoBoxData.codec.decode(data).toOption - case 0x46 => //beamer - outOpt = WeaponData.codec.decode(data).toOption - case 0x159 => //gauss - outOpt = WeaponData.codec.decode(data).toOption - case _ => - } - if(outOpt.isDefined) - out = Some(outOpt.get.value.asInstanceOf[ConstructorData]) - } - catch { - case ex : ClassCastException => - //TODO generate and log wrong class error message - case ex : Exception => - //TODO generic error - } - } - out - } - - def encode(objClass : Int, obj : ConstructorData) : BitVector = { - var out = BitVector.empty - try { - var outOpt : Option[BitVector] = None - (objClass : @switch) match { - case 0x1C => //9mm - outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption - case 0x46 => //beamer - outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption - case 0x159 => //gauss - outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption - case _ => - throw new ClassCastException("cannot find object code - "+objClass) - } - if(outOpt.isDefined) - out = outOpt.get - } - catch { - case ex : ClassCastException => - //TODO generate and log wrong class error message - case ex : Exception => - //TODO generic error - } - out - } -} +import shapeless.{::, HNil} /** * The parent information of a created object.
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala new file mode 100644 index 00000000..faa582b6 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala @@ -0,0 +1,28 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +case class AmmoBoxData(magazine : Int) extends ConstructorData + +object AmmoBoxData extends Marshallable[AmmoBoxData] { + implicit val codec : Codec[AmmoBoxData] = ( + uintL(8) :: + ignore(15) :: + ("magazine" | uint16L) + ).exmap[AmmoBoxData] ( + { + case 0xC8 :: _ :: mag :: HNil => + Attempt.successful(AmmoBoxData(mag)) + case x :: _ :: _ :: HNil => + Attempt.failure(Err("looking for 200, found "+x)) + }, + { + case AmmoBoxData(mag) => + Attempt.successful(0xC8 :: () :: mag :: HNil) + } + ).as[AmmoBoxData] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala new file mode 100644 index 00000000..ddf8ca6c --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala @@ -0,0 +1,101 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.{Marshallable, PacketHelpers} +import net.psforever.types.Vector3 +import scodec.Codec +import scodec.codecs._ + +case class CharacterData(pos : Vector3, + objYaw : 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 + viewPitch : Int, + viewYaw : Int, + ribbons : RibbonBars, + healthMax : Int, + health : Int, + armor : Int, + unk4 : Int, //1 + unk5 : Int, //7 + unk6 : Int, //7 + staminaMax : Int, + stamina : Int, + unk7 : Int, // 28 + unk8 : Int, //4 + unk9 : Int, //44 + unk10 : Int, //84 + unk11 : Int, //104 + unk12 : Int, //1900 + firstTimeEvent_length : Long, + firstEntry : Option[String], + firstTimeEvent_list : List[String], + tutorial_list : List[String], + inventory : InventoryData + ) extends ConstructorData + +object CharacterData extends Marshallable[CharacterData] { + implicit val codec : Codec[CharacterData] = ( + ("pos" | Vector3.codec_pos) :: + ignore(16) :: + ("objYaw" | uint8L) :: + ignore(1) :: + ("faction" | uintL(2)) :: + ("bops" | bool) :: + ignore(20) :: + ("name" | PacketHelpers.encodedWideStringAligned(4)) :: + ("exosuit" | uintL(3)) :: + ignore(2) :: + ("sex" | uintL(2)) :: + ("face1" | uint8L) :: + ("face2" | uint4L) :: + ("voice" | uintL(3)) :: + ignore(22) :: + ("unk1" | uint16L) :: + ignore(42) :: + ("unk2" | uint16L) :: + ignore(30) :: + ("unk3" | uintL(4)) :: + ignore(24) :: + ("viewPitch" | uint8L) :: + ("viewYaw" | uint8L) :: + ignore(10) :: + ("ribbons" | RibbonBars.codec) :: + ignore(160) :: + ("healthMax" | uint16L) :: + ("health" | uint16L) :: + ignore(1) :: + ("armor" | uint16L) :: + ignore(9) :: + ("unk4" | uint8L) :: + ignore(8) :: + ("unk5" | uint4L) :: + ("unk6" | uintL(3)) :: + ("staminaMax" | uint16L) :: + ("stamina" | uint16L) :: + ignore(149) :: + ("unk7" | uint16L) :: + ("unk8" | uint8L) :: + ("unk9" | uint8L) :: + ("unk10" | uint8L) :: + ("unk11" | uint8L) :: + ("unk12" | uintL(12)) :: + ignore(19) :: + (("firstTimeEvent_length" | uint32L) >>:~ { len => + conditional(len > 0, "firstEntry" | PacketHelpers.encodedStringAligned(5)) :: + ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: + ("tutorial_list" | PacketHelpers.listOfNAligned(uint32L, 0, PacketHelpers.encodedString)) :: + ignore(207) :: + ("inventory" | InventoryData.codec) + }) + ).as[CharacterData] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala new file mode 100644 index 00000000..2b4d9cf6 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala @@ -0,0 +1,4 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +abstract class ConstructorData diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala new file mode 100644 index 00000000..704cdfe5 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala @@ -0,0 +1,58 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.{Marshallable, PacketHelpers} +import net.psforever.packet.game.PlanetSideGUID +import scodec.{Attempt, Codec, Err} +import scodec.bits.BitVector +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * The same kind of data as required for a formal ObjectCreateMessage but with a required and implicit parent relationship. + * Data preceding this entry will define the existence of the parent. + * @param objectClass na + * @param guid na + * @param parentSlot na + * @param obj na + */ +case class InternalSlot(objectClass : Int, + guid : PlanetSideGUID, + parentSlot : Int, + obj : Option[ConstructorData]) + +object InternalSlot extends Marshallable[InternalSlot] { + type objPattern = Int :: PlanetSideGUID :: Int :: Option[ConstructorData] :: HNil + + implicit val codec : Codec[InternalSlot] = ( + ignore(1) :: //TODO determine what this bit does + ("objectClass" | uintL(11)) :: + ("guid" | PlanetSideGUID.codec) :: + ("parentSlot" | PacketHelpers.encodedStringSize) :: + bits + ).exmap[objPattern] ( + { + case _ :: cls :: guid :: slot :: data :: HNil => + Attempt.successful(cls :: guid :: slot :: Mold.decode(cls, data) :: HNil) + }, + { + case cls :: guid :: slot :: None :: HNil => + Attempt.failure(Err("no constructor data could be found")) + case cls :: guid :: slot :: mold :: HNil => + Attempt.successful(() :: cls :: guid :: slot :: Mold.encode(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[InternalSlot] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala new file mode 100644 index 00000000..8e4f90e8 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala @@ -0,0 +1,21 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.{Marshallable, PacketHelpers} +import scodec.Codec +import scodec.codecs._ + +case class InventoryData(unk1 : Boolean, + size : Int, + unk2 : Boolean, + inv : List[InventoryItem]) + +object InventoryData extends Marshallable[InventoryData] { + implicit val codec : Codec[InventoryData] = ( + ("unk1" | bool) :: + (("size" | uint8L) >>:~ { len => + ("unk2" | bool) :: + ("inv" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) + }) + ).as[InventoryData] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala new file mode 100644 index 00000000..5f1a2a90 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import scodec.Codec +import scodec.codecs._ + +case class InventoryItem(item : InternalSlot, + na : Option[Boolean] = None) + +object InventoryItem extends Marshallable[InventoryItem] { + implicit val codec : Codec[InventoryItem] = ( + "item" | InternalSlot.codec >>:~ { item => + conditional(item.obj.isDefined && item.obj.get.isInstanceOf[WeaponData], bool).hlist + } + ).as[InventoryItem] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala new file mode 100644 index 00000000..f5d3437e --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala @@ -0,0 +1,113 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import scodec.DecodeResult +import scodec.bits.BitVector + +import scala.annotation.switch + +case class Mold(objectClass : Int, + data : BitVector) { + private var obj : Option[ConstructorData] = Mold.decode(objectClass, data) + + def isDefined : Boolean = this.obj.isDefined + + def get : ConstructorData = this.obj.get + + def set(data : ConstructorData) : Boolean = { + var ret = false + if(Some(data).isDefined) { + obj = Some(data) + ret = true + } + ret + } +} + +object Mold { + def apply(objectClass : Int, obj : ConstructorData) : Mold = + new Mold( objectClass, Mold.encode(objectClass, obj) ) + + def decode(objClass : Int, data : BitVector) : Option[ConstructorData] = { + var out : Option[ConstructorData] = None + if(!data.isEmpty) { + var outOpt : Option[DecodeResult[_]] = None + try { + (objClass : @switch) match { + case 0x79 => //avatars + outOpt = CharacterData.codec.decode(data).toOption + case 0x1C => //9mm + outOpt = AmmoBoxData.codec.decode(data).toOption + case 0x1D => //9mm ap + outOpt = AmmoBoxData.codec.decode(data).toOption + case 0x110 => //plasma + outOpt = AmmoBoxData.codec.decode(data).toOption + case 0x1C8 => //slot blocker? + outOpt = AmmoBoxData.codec.decode(data).toOption + case 0x21C => //forceblade (ammo) + outOpt = AmmoBoxData.codec.decode(data).toOption + case 0x46 => //beamer + outOpt = WeaponData.codec.decode(data).toOption + case 0x144 => //forceblade + outOpt = WeaponData.codec.decode(data).toOption + case 0x159 => //gauss + outOpt = WeaponData.codec.decode(data).toOption + case 0x34D => //suppressor + outOpt = WeaponData.codec.decode(data).toOption + case 0x2D8 => //rek + outOpt = REKData.codec.decode(data).toOption + case _ => + } + if(outOpt.isDefined) + out = Some(outOpt.get.value.asInstanceOf[ConstructorData]) + } + catch { + case ex : ClassCastException => + //TODO generate and log wrong class error message + case ex : Exception => + //TODO generic error + } + } + out + } + + def encode(objClass : Int, obj : ConstructorData) : BitVector = { + var out = BitVector.empty + try { + var outOpt : Option[BitVector] = None + (objClass : @switch) match { + case 0x1C => //9mm + outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption + case 0x1D => //9mm ap + outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption + case 0x110 => //plasma + outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption + case 0x1C8 => //slot blocker? + outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption + case 0x21C => //forceblade (ammo) + outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption + case 0x46 => //beamer + outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption + case 0x144 => //forceblade + outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption + case 0x159 => //gauss + outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption + case 0x34D => //suppressor + outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption + case 0x2D8 => //rek + outOpt = REKData.codec.encode(obj.asInstanceOf[REKData]).toOption + case _ => + throw new ClassCastException("cannot find object code - "+objClass) + } + if(outOpt.isDefined) + out = outOpt.get + } + catch { + case ex : ClassCastException => + //TODO generate and log wrong class error message + case ex : Exception => + //TODO generic error + } + out + } +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala new file mode 100644 index 00000000..b49786af --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +case class REKData(unk : Int) extends ConstructorData + +object REKData extends Marshallable[REKData] { + implicit val codec : Codec[REKData] = ( + ("unk" | uint4L) :: + uint4L :: + ignore(20) :: + uint4L :: + ignore(16) :: + uint4L :: + ignore(20) + ).exmap[REKData] ( + { + case code :: 8 :: _ :: 2 :: _ :: 8 :: _ :: HNil => + Attempt.successful(REKData(code)) + case _ :: x :: _ :: y :: _ :: z :: _ :: HNil => + Attempt.failure(Err("looking for 8-2-8 pattern, found %d-%d-%d".format(x,y,z))) //TODO I actually don't know what of this is actually important + }, + { + case REKData(code) => + Attempt.successful(code :: 8 :: () :: 2 :: () :: 8 :: () :: HNil) + } + ).as[REKData] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala new file mode 100644 index 00000000..7b9275a1 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala @@ -0,0 +1,20 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import scodec.Codec +import scodec.codecs._ + +case class RibbonBars(upper : Long = 0xFFFFFFFFL, //0xFFFFFFFF means no merit (for all ...) + middle : Long = 0xFFFFFFFFL, + lower : Long = 0xFFFFFFFFL, + tos : Long = 0xFFFFFFFFL) + +object RibbonBars extends Marshallable[RibbonBars] { + implicit val codec : Codec[RibbonBars] = ( + ("upper" | uint32L) :: + ("middle" | uint32L) :: + ("lower" | uint32L) :: + ("tos" | uint32L) + ).as[RibbonBars] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala new file mode 100644 index 00000000..2248bf2a --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala @@ -0,0 +1,37 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import net.psforever.packet.game.PlanetSideGUID +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import shapeless.{::, HNil} + +case class WeaponData(unk : Int, + ammo : InternalSlot) extends ConstructorData + +object WeaponData extends Marshallable[WeaponData] { + def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData = + new WeaponData(unk, InternalSlot(cls, guid, parentSlot, Some(ammo))) + + implicit val codec : Codec[WeaponData] = ( + ("unk" | uint4L) :: + uint4L :: + ignore(20) :: + uint4L :: + ignore(16) :: + uintL(11) :: + ("ammo" | InternalSlot.codec) + ).exmap[WeaponData] ( + { + case code :: 8 :: _ :: 2 :: _ :: 0x2C0 :: ammo :: HNil => + Attempt.successful(WeaponData(code, ammo)) + case _ :: x :: _ :: y :: _ :: z :: _ :: HNil => + Attempt.failure(Err("looking for 8-2-704 pattern, found %d-%d-%d".format(x,y,z))) //TODO I actually don't know what of this is actually important + }, + { + case WeaponData(code, ammo) => + Attempt.successful(code :: 8 :: () :: 2 :: () :: 0x2C0 :: ammo :: HNil) + } + ).as[WeaponData] +} diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 5355a495..f3ebd435 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -4,6 +4,7 @@ import java.net.{InetAddress, InetSocketAddress} import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ +import net.psforever.packet.game.objectcreate._ import net.psforever.types._ import scodec.Attempt.Successful import scodec.bits._ @@ -150,7 +151,7 @@ class GamePacketTest extends Specification { val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16) val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" - val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" + val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 01 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" "decode (2)" in { PacketCoding.DecodePacket(packet2).require match { @@ -193,10 +194,10 @@ class GamePacketTest extends Specification { char.unk3 mustEqual 2 char.viewPitch mustEqual 0xFF char.viewYaw mustEqual 0x6A - char.upperMerit mustEqual 0xFFFFFFFFL //none - char.middleMerit mustEqual 0xFFFFFFFFL //none - char.lowerMerit mustEqual 0xFFFFFFFFL //none - char.termOfServiceMerit mustEqual 0xFFFFFFFFL //none + char.ribbons.upper mustEqual 0xFFFFFFFFL //none + char.ribbons.middle mustEqual 0xFFFFFFFFL //none + char.ribbons.lower mustEqual 0xFFFFFFFFL //none + char.ribbons.tos mustEqual 0xFFFFFFFFL //none char.healthMax mustEqual 100 char.health mustEqual 100 char.armor mustEqual 50 //standard exosuit value @@ -218,6 +219,9 @@ class GamePacketTest extends Specification { char.firstTimeEvent_list(1) mustEqual "used_beamer" char.firstTimeEvent_list(2) mustEqual "map13" char.tutorial_list.size mustEqual 0 + char.inventory.unk1 mustEqual true + char.inventory.size mustEqual 10 + char.inventory.unk2 mustEqual false case default => ko } From d66489a572f350c96e8bd99ace006152e8b0f12b Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 5 Dec 2016 23:30:52 -0500 Subject: [PATCH 11/15] replaced hamfisted greedy Mold functionality with seamless codec functionality, though that did require a significant re-write, and I might have re-introduced slow compilation; inventory hobbled intentionally --- .../packet/game/ObjectCreateMessage.scala | 197 ++++++++++++++---- .../game/objectcreate/AmmoBoxData.scala | 20 +- .../game/objectcreate/CharacterData.scala | 46 +++- .../game/objectcreate/ConstructorData.scala | 8 +- .../game/objectcreate/InternalSlot.scala | 50 ++--- .../game/objectcreate/InventoryData.scala | 13 +- .../game/objectcreate/InventoryItem.scala | 7 +- .../packet/game/objectcreate/Mold.scala | 55 +---- .../game/objectcreate/ObjectClass.scala | 53 +++++ .../packet/game/objectcreate/REKData.scala | 20 +- .../game/objectcreate/RecoveredData.scala | 29 +++ .../packet/game/objectcreate/WeaponData.scala | 19 +- common/src/test/scala/GamePacketTest.scala | 54 ++--- 13 files changed, 399 insertions(+), 172 deletions(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/RecoveredData.scala 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 32af6453..622e1c38 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -1,9 +1,8 @@ // Copyright (c) 2016 PSForever.net to present package net.psforever.packet.game -import net.psforever.packet.game.objectcreate.Mold +import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import scodec.bits._ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} @@ -49,35 +48,37 @@ case class ObjectCreateMessageParent(guid : PlanetSideGUID, * @param objectClass the code for the type of object being constructed * @param guid the GUID this object will be assigned * @param parentInfo if defined, the relationship between this object and another object (its parent) - * @param mold the data used to construct this type of object; - * requires further object-specific processing + * @param data the data used to construct this type of object */ case class ObjectCreateMessage(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : Option[ObjectCreateMessageParent], - mold : Mold) + data : Option[ConstructorData]) extends PlanetSideGamePacket { def opcode = GamePacketOpcode.ObjectCreateMessage def encode = ObjectCreateMessage.encode(this) } object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { - type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil + type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil + type outPattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil /** * Codec for formatting around the lack of parent data in the stream. */ val noParent : Codec[Pattern] = ( - ("objectClass" | uintL(0xb)) :: //11u - ("guid" | PlanetSideGUID.codec) //16u + ("objectClass" | uintL(0xb)) >>:~ { cls => //11u + ("guid" | PlanetSideGUID.codec) :: //16u + ("data" | ObjectClass.selectDataCodec(cls)) + } ).xmap[Pattern] ( { - case cls :: guid :: HNil => - cls :: guid :: None :: HNil + case cls :: guid :: data :: HNil => + cls :: guid :: None :: data :: HNil }, { - case cls :: guid :: None :: HNil => - cls :: guid :: HNil + case cls :: guid :: None :: data :: HNil => + cls :: guid :: data :: HNil } ) @@ -86,17 +87,19 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { */ val parent : Codec[Pattern] = ( ("parentGuid" | PlanetSideGUID.codec) :: //16u - ("objectClass" | uintL(0xb)) :: //11u - ("guid" | PlanetSideGUID.codec) :: //16u - ("parentSlotIndex" | PacketHelpers.encodedStringSize) //8u or 16u + (("objectClass" | uintL(0xb)) >>:~ { cls => //11u + ("guid" | PlanetSideGUID.codec) :: //16u + ("parentSlotIndex" | PacketHelpers.encodedStringSize) :: //8u or 16u + ("data" | ObjectClass.selectDataCodec(cls)) + }) ).xmap[Pattern] ( { - case pguid :: cls :: guid :: slot :: HNil => - cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil + case pguid :: cls :: guid :: slot :: data :: HNil => + cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: data :: HNil }, { - case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil => - pguid :: cls :: guid :: slot :: HNil + case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: data :: HNil => + pguid :: cls :: guid :: slot :: data :: HNil } ) @@ -114,16 +117,16 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { * @param data the data length is indeterminate until it is read * @return the total length of the stream in bits */ - private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : BitVector) : Long = { - //known length - val first : Long = if(parentInfo.isDefined) { - if(parentInfo.get.slot > 127) 92L else 84L //60u + 16u + (8u or 16u) + private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : Option[ConstructorData]) : Long = { + //msg length + val first : Long = if(parentInfo.isDefined) { //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u)) + if(parentInfo.get.slot > 127) 92L else 84L } else { 60L } - //variant length - var second : Long = data.size + //data length + var second : Long = if(data.isDefined) data.get.bsize else 0L val secondMod4 : Long = second % 4L if(secondMod4 > 0L) { //pad to include last whole nibble second += 4L - secondMod4 @@ -133,34 +136,142 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { implicit val codec : Codec[ObjectCreateMessage] = ( ("streamLength" | uint32L) :: - (either(bool, parent, noParent).exmap[Pattern] ( + either(bool, parent, noParent).exmap[Pattern] ( { - case Left(a :: b :: Some(c) :: HNil) => - Attempt.successful(a :: b :: Some(c) :: HNil) //true, _, _, Some(c) - case Right(a :: b :: None :: HNil) => - Attempt.successful(a :: b :: None :: HNil) //false, _, _, None + case Left(a :: b :: Some(c) :: d :: HNil) => + Attempt.successful(a :: b :: Some(c) :: d :: HNil) //true, _, _, Some(c) + case Right(a :: b :: None :: d :: HNil) => + Attempt.successful(a :: b :: None :: d :: HNil) //false, _, _, None // failure cases - case Left(a :: b :: None :: HNil) => + case Left(a :: b :: None :: _ :: HNil) => Attempt.failure(Err("missing parent structure")) //true, _, _, None - case Right(a :: b :: Some(c) :: HNil) => + case Right(a :: b :: Some(c) :: _ :: HNil) => Attempt.failure(Err("unexpected parent structure")) //false, _, _, Some(c) }, { - case a :: b :: Some(c) :: HNil => - Attempt.successful(Left(a :: b :: Some(c) :: HNil)) - case a :: b :: None :: HNil => - Attempt.successful(Right(a :: b :: None :: HNil)) + case a :: b :: Some(c) :: d :: HNil => + Attempt.successful(Left(a :: b :: Some(c) :: d :: HNil)) + case a :: b :: None :: d :: HNil => + Attempt.successful(Right(a :: b :: None :: d :: HNil)) } - ) :+ - ("data" | bits) ) - ).xmap[ObjectCreateMessage] ( + ) + ).xmap[outPattern] ( { - case len :: cls :: guid :: info :: data :: HNil => - ObjectCreateMessage(len, cls, guid, info, Mold(cls, data)) + case len :: cls :: guid :: par :: data :: HNil => + len :: cls :: guid :: par :: data :: HNil }, { - case ObjectCreateMessage(_, cls, guid, info, mold) => - streamLen(info, mold.data) :: cls :: guid :: info :: mold.data :: HNil + case _ :: cls :: guid :: par :: data :: HNil => + streamLen(par, data) :: cls :: guid :: par :: data :: HNil } ).as[ObjectCreateMessage] } + +//import net.psforever.packet.game.objectcreate.Mold +//import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +//import scodec.bits._ +//import scodec.{Attempt, Codec, Err} +//import scodec.codecs._ +//import shapeless.{::, HNil} +// +//case class ObjectCreateMessageParent(guid : PlanetSideGUID, +// slot : Int) +// +//case class ObjectCreateMessage(streamLength : Long, +// objectClass : Int, +// guid : PlanetSideGUID, +// parentInfo : Option[ObjectCreateMessageParent], +// mold : Mold) +// extends PlanetSideGamePacket { +// def opcode = GamePacketOpcode.ObjectCreateMessage +// def encode = ObjectCreateMessage.encode(this) +//} +// +//object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { +// type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil +// /** +// * Codec for formatting around the lack of parent data in the stream. +// */ +// val noParent : Codec[Pattern] = ( +// ("objectClass" | uintL(0xb)) :: //11u +// ("guid" | PlanetSideGUID.codec) //16u +// ).xmap[Pattern] ( +// { +// case cls :: guid :: HNil => +// cls :: guid :: None :: HNil +// }, +// { +// case cls :: guid :: None :: HNil => +// cls :: guid :: HNil +// } +// ) +// +// /** +// * Codec for reading and formatting parent data from the stream. +// */ +// val parent : Codec[Pattern] = ( +// ("parentGuid" | PlanetSideGUID.codec) :: //16u +// ("objectClass" | uintL(0xb)) :: //11u +// ("guid" | PlanetSideGUID.codec) :: //16u +// ("parentSlotIndex" | PacketHelpers.encodedStringSize) //8u or 16u +// ).xmap[Pattern] ( +// { +// case pguid :: cls :: guid :: slot :: HNil => +// cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil +// }, +// { +// case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil => +// pguid :: cls :: guid :: slot :: HNil +// } +// ) +// +// private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : BitVector) : Long = { +// //known length +// val first : Long = if(parentInfo.isDefined) { +// if(parentInfo.get.slot > 127) 92L else 84L //60u + 16u + (8u or 16u) +// } +// else { +// 60L +// } +// //variant length +// var second : Long = data.size +// val secondMod4 : Long = second % 4L +// if(secondMod4 > 0L) { //pad to include last whole nibble +// second += 4L - secondMod4 +// } +// first + second +// } +// +// implicit val codec : Codec[ObjectCreateMessage] = ( +// ("streamLength" | uint32L) :: +// (either(bool, parent, noParent).exmap[Pattern] ( +// { +// case Left(a :: b :: Some(c) :: HNil) => +// Attempt.successful(a :: b :: Some(c) :: HNil) //true, _, _, Some(c) +// case Right(a :: b :: None :: HNil) => +// Attempt.successful(a :: b :: None :: HNil) //false, _, _, None +// // failure cases +// case Left(a :: b :: None :: HNil) => +// Attempt.failure(Err("missing parent structure")) //true, _, _, None +// case Right(a :: b :: Some(c) :: HNil) => +// Attempt.failure(Err("unexpected parent structure")) //false, _, _, Some(c) +// }, +// { +// case a :: b :: Some(c) :: HNil => +// Attempt.successful(Left(a :: b :: Some(c) :: HNil)) +// case a :: b :: None :: HNil => +// Attempt.successful(Right(a :: b :: None :: HNil)) +// } +// ) :+ +// ("data" | bits) ) +// ).xmap[ObjectCreateMessage] ( +// { +// case len :: cls :: guid :: info :: data :: HNil => +// ObjectCreateMessage(len, cls, guid, info, Mold(cls, data)) +// }, +// { +// case ObjectCreateMessage(_, cls, guid, info, mold) => +// streamLen(info, mold.data) :: cls :: guid :: info :: mold.data :: HNil +// } +// ).as[ObjectCreateMessage] +//} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala index faa582b6..6958da56 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala @@ -6,7 +6,10 @@ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} -case class AmmoBoxData(magazine : Int) extends ConstructorData +case class AmmoBoxData(magazine : Int + ) extends ConstructorData { + override def bsize : Long = 39L +} object AmmoBoxData extends Marshallable[AmmoBoxData] { implicit val codec : Codec[AmmoBoxData] = ( @@ -24,5 +27,18 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] { case AmmoBoxData(mag) => Attempt.successful(0xC8 :: () :: mag :: HNil) } - ).as[AmmoBoxData] + ) + + val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( + { + case x => + Attempt.successful(Some(x.asInstanceOf[ConstructorData])) + }, + { + case Some(x) => + Attempt.successful(x.asInstanceOf[AmmoBoxData]) + case _ => + Attempt.failure(Err("")) + } + ) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala index ddf8ca6c..91ed7b75 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala @@ -3,7 +3,7 @@ package net.psforever.packet.game.objectcreate import net.psforever.packet.{Marshallable, PacketHelpers} import net.psforever.types.Vector3 -import scodec.Codec +import scodec.{Attempt, Codec, Err} import scodec.codecs._ case class CharacterData(pos : Vector3, @@ -41,9 +41,36 @@ case class CharacterData(pos : Vector3, firstTimeEvent_list : List[String], tutorial_list : List[String], inventory : InventoryData - ) extends ConstructorData + ) extends ConstructorData { + override def bsize : Long = { + //represents static fields + val first : Long = 1194L //TODO due to changing understanding of the bit patterns in this data, this value will change + //name + val second : Long = CharacterData.stringBitSize(name, 16) + 4L //plus the padding + //fte_list + var third : Long = 32L + if(firstEntry.isDefined) { + third += CharacterData.stringBitSize(firstEntry.get) + 5L //plus the padding + for(str <- firstTimeEvent_list) { + third += CharacterData.stringBitSize(str) + } + } + //tutorial list + var fourth : Long = 32L + for(str <- tutorial_list) { + fourth += CharacterData.stringBitSize(str) + } + first + second + third + fourth + inventory.bsize + } +} object CharacterData extends Marshallable[CharacterData] { + private def stringBitSize(str : String, width : Int = 8) : Long = { + val strlen = str.length + val lenSize = if(strlen > 127) 16L else 8L + lenSize + strlen * width + } + implicit val codec : Codec[CharacterData] = ( ("pos" | Vector3.codec_pos) :: ignore(16) :: @@ -56,7 +83,7 @@ object CharacterData extends Marshallable[CharacterData] { ("exosuit" | uintL(3)) :: ignore(2) :: ("sex" | uintL(2)) :: - ("face1" | uint8L) :: + ("face1" | uint4L) :: ("face2" | uint4L) :: ("voice" | uintL(3)) :: ignore(22) :: @@ -98,4 +125,17 @@ object CharacterData extends Marshallable[CharacterData] { ("inventory" | InventoryData.codec) }) ).as[CharacterData] + + val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( + { + case x => + Attempt.successful(Some(x.asInstanceOf[ConstructorData])) + }, + { + case Some(x) => + Attempt.successful(x.asInstanceOf[CharacterData]) + case _ => + Attempt.failure(Err("")) + } + ) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala index 2b4d9cf6..9cd35252 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala @@ -1,4 +1,10 @@ // Copyright (c) 2016 PSForever.net to present package net.psforever.packet.game.objectcreate -abstract class ConstructorData +abstract class ConstructorData() { + def bsize : Long = 0L +} + +object ConstructorData { + type genericPattern = Option[ConstructorData] +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala index 704cdfe5..ed8ab873 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala @@ -3,8 +3,7 @@ package net.psforever.packet.game.objectcreate import net.psforever.packet.{Marshallable, PacketHelpers} import net.psforever.packet.game.PlanetSideGUID -import scodec.{Attempt, Codec, Err} -import scodec.bits.BitVector +import scodec.Codec import scodec.codecs._ import shapeless.{::, HNil} @@ -19,40 +18,23 @@ import shapeless.{::, HNil} case class InternalSlot(objectClass : Int, guid : PlanetSideGUID, parentSlot : Int, - obj : Option[ConstructorData]) + obj : Option[ConstructorData]) { + def bsize : Long = { + val first : Long = if(parentSlot > 127) 44L else 36L + val second : Long = if(obj.isDefined) obj.get.bsize else 0L + first + second + } +} object InternalSlot extends Marshallable[InternalSlot] { - type objPattern = Int :: PlanetSideGUID :: Int :: Option[ConstructorData] :: HNil + type objPattern = Int :: PlanetSideGUID :: Int :: ConstructorData :: HNil implicit val codec : Codec[InternalSlot] = ( ignore(1) :: //TODO determine what this bit does - ("objectClass" | uintL(11)) :: - ("guid" | PlanetSideGUID.codec) :: - ("parentSlot" | PacketHelpers.encodedStringSize) :: - bits - ).exmap[objPattern] ( - { - case _ :: cls :: guid :: slot :: data :: HNil => - Attempt.successful(cls :: guid :: slot :: Mold.decode(cls, data) :: HNil) - }, - { - case cls :: guid :: slot :: None :: HNil => - Attempt.failure(Err("no constructor data could be found")) - case cls :: guid :: slot :: mold :: HNil => - Attempt.successful(() :: cls :: guid :: slot :: Mold.encode(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[InternalSlot] -} + (("objectClass" | uintL(11)) >>:~ { obj_cls => + ("guid" | PlanetSideGUID.codec) :: + ("parentSlot" | PacketHelpers.encodedStringSize) :: + ("obj" | ObjectClass.selectDataCodec(obj_cls)) + }) + ).as[InternalSlot] +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala index 8e4f90e8..161bbf0b 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala @@ -3,19 +3,24 @@ package net.psforever.packet.game.objectcreate import net.psforever.packet.{Marshallable, PacketHelpers} import scodec.Codec +import scodec.bits.BitVector import scodec.codecs._ case class InventoryData(unk1 : Boolean, size : Int, - unk2 : Boolean, - inv : List[InventoryItem]) + unk2 : Boolean){//, + //inv : List[InventoryItem]) { + def bsize : Long = { + 10L + } +} object InventoryData extends Marshallable[InventoryData] { implicit val codec : Codec[InventoryData] = ( ("unk1" | bool) :: (("size" | uint8L) >>:~ { len => - ("unk2" | bool) :: - ("inv" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) + ("unk2" | bool).hlist// :: + //("inv" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) }) ).as[InventoryData] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala index 5f1a2a90..7c5cf9dc 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala @@ -5,13 +5,10 @@ import net.psforever.packet.Marshallable import scodec.Codec import scodec.codecs._ -case class InventoryItem(item : InternalSlot, - na : Option[Boolean] = None) +case class InventoryItem(item : InternalSlot) object InventoryItem extends Marshallable[InventoryItem] { implicit val codec : Codec[InventoryItem] = ( - "item" | InternalSlot.codec >>:~ { item => - conditional(item.obj.isDefined && item.obj.get.isInstanceOf[WeaponData], bool).hlist - } + "item" | InternalSlot.codec ).as[InventoryItem] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala index f5d3437e..eb75953c 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala @@ -4,8 +4,6 @@ package net.psforever.packet.game.objectcreate import scodec.DecodeResult import scodec.bits.BitVector -import scala.annotation.switch - case class Mold(objectClass : Int, data : BitVector) { private var obj : Option[ConstructorData] = Mold.decode(objectClass, data) @@ -31,33 +29,10 @@ object Mold { def decode(objClass : Int, data : BitVector) : Option[ConstructorData] = { var out : Option[ConstructorData] = None if(!data.isEmpty) { + val codec = ObjectClass.selectDataCodec(objClass) var outOpt : Option[DecodeResult[_]] = None try { - (objClass : @switch) match { - case 0x79 => //avatars - outOpt = CharacterData.codec.decode(data).toOption - case 0x1C => //9mm - outOpt = AmmoBoxData.codec.decode(data).toOption - case 0x1D => //9mm ap - outOpt = AmmoBoxData.codec.decode(data).toOption - case 0x110 => //plasma - outOpt = AmmoBoxData.codec.decode(data).toOption - case 0x1C8 => //slot blocker? - outOpt = AmmoBoxData.codec.decode(data).toOption - case 0x21C => //forceblade (ammo) - outOpt = AmmoBoxData.codec.decode(data).toOption - case 0x46 => //beamer - outOpt = WeaponData.codec.decode(data).toOption - case 0x144 => //forceblade - outOpt = WeaponData.codec.decode(data).toOption - case 0x159 => //gauss - outOpt = WeaponData.codec.decode(data).toOption - case 0x34D => //suppressor - outOpt = WeaponData.codec.decode(data).toOption - case 0x2D8 => //rek - outOpt = REKData.codec.decode(data).toOption - case _ => - } + outOpt = codec.decode(data).toOption if(outOpt.isDefined) out = Some(outOpt.get.value.asInstanceOf[ConstructorData]) } @@ -74,31 +49,9 @@ object Mold { def encode(objClass : Int, obj : ConstructorData) : BitVector = { var out = BitVector.empty try { + val codec = ObjectClass.selectDataCodec(objClass) var outOpt : Option[BitVector] = None - (objClass : @switch) match { - case 0x1C => //9mm - outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption - case 0x1D => //9mm ap - outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption - case 0x110 => //plasma - outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption - case 0x1C8 => //slot blocker? - outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption - case 0x21C => //forceblade (ammo) - outOpt = AmmoBoxData.codec.encode(obj.asInstanceOf[AmmoBoxData]).toOption - case 0x46 => //beamer - outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption - case 0x144 => //forceblade - outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption - case 0x159 => //gauss - outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption - case 0x34D => //suppressor - outOpt = WeaponData.codec.encode(obj.asInstanceOf[WeaponData]).toOption - case 0x2D8 => //rek - outOpt = REKData.codec.encode(obj.asInstanceOf[REKData]).toOption - case _ => - throw new ClassCastException("cannot find object code - "+objClass) - } + outOpt = codec.encode(obj.asInstanceOf[ConstructorData.genericPattern]).toOption if(outOpt.isDefined) out = outOpt.get } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala new file mode 100644 index 00000000..9e4ae3ed --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -0,0 +1,53 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import scodec.Codec + +import scala.annotation.switch + +object ObjectClass { + //character + final val PLAYER = 0x79 + //ammunition + final val BULLETS_9MM = 0x1C + final val BULLETS_9MM_AP = 0x1D + final val ENERGY_CELL = 0x110 + final val FORCE_BLADE_AMMO = 0x21C + //weapons + final val BEAMER = 0x8C + final val FORCE_BLADE = 0x144 + final val GAUSS = 0x159 + final val SUPPRESSOR = 0x34D + //tools + final val REK = 0x2D8 + //unknown + final val SLOT_BLOCKER = 0x1C8 + + def selectDataCodec(objClass : Int) : Codec[ConstructorData.genericPattern] = { + (objClass : @switch) match { + case ObjectClass.PLAYER => CharacterData.genericCodec + case ObjectClass.BULLETS_9MM => AmmoBoxData.genericCodec + case ObjectClass.BULLETS_9MM_AP => AmmoBoxData.genericCodec + case ObjectClass.ENERGY_CELL => AmmoBoxData.genericCodec + case ObjectClass.FORCE_BLADE_AMMO => AmmoBoxData.genericCodec + case ObjectClass.BEAMER => WeaponData.genericCodec + case ObjectClass.FORCE_BLADE => WeaponData.genericCodec + case ObjectClass.GAUSS => WeaponData.genericCodec + case ObjectClass.SUPPRESSOR => WeaponData.genericCodec + case ObjectClass.REK => REKData.genericCodec + case ObjectClass.SLOT_BLOCKER => AmmoBoxData.genericCodec + case _ => RecoveredData.genericCodec + } + } + +// val failureCodec : Codec[ConstructorData.genericPattern] = conditional(false, bool).exmap[ConstructorData.genericPattern] ( +// { +// case None | _ => +// Attempt.failure(Err("object class unrecognized during decoding")) +// }, +// { +// case None | _ => +// Attempt.failure(Err("object class unrecognized during encoding")) +// } +// ) +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala index b49786af..ef396c83 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala @@ -6,7 +6,10 @@ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} -case class REKData(unk : Int) extends ConstructorData +case class REKData(unk : Int + ) extends ConstructorData { + override def bsize : Long = 72L +} object REKData extends Marshallable[REKData] { implicit val codec : Codec[REKData] = ( @@ -29,4 +32,19 @@ object REKData extends Marshallable[REKData] { Attempt.successful(code :: 8 :: () :: 2 :: () :: 8 :: () :: HNil) } ).as[REKData] + + + + val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( + { + case x => + Attempt.successful(Some(x.asInstanceOf[ConstructorData])) + }, + { + case Some(x) => + Attempt.successful(x.asInstanceOf[REKData]) + case _ => + Attempt.failure(Err("")) + } + ) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RecoveredData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RecoveredData.scala new file mode 100644 index 00000000..04cda576 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/RecoveredData.scala @@ -0,0 +1,29 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.Marshallable +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ +import scodec.bits.BitVector + +case class RecoveredData(data : BitVector + ) extends ConstructorData { + override def bsize : Long = data.size +} + +object RecoveredData extends Marshallable[RecoveredData] { + implicit val codec : Codec[RecoveredData] = ( + "data" | bits + ).as[RecoveredData] + + val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( + { + case _ => + Attempt.failure(Err("un-parsed byte data preserved when decoding failed")) + }, + { + case _ => + Attempt.failure(Err("can not encode object")) + } + ) +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala index 2248bf2a..a1c21aca 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala @@ -8,7 +8,9 @@ import scodec.codecs._ import shapeless.{::, HNil} case class WeaponData(unk : Int, - ammo : InternalSlot) extends ConstructorData + ammo : InternalSlot) extends ConstructorData { + override def bsize : Long = 59L + ammo.bsize +} object WeaponData extends Marshallable[WeaponData] { def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData = @@ -34,4 +36,19 @@ object WeaponData extends Marshallable[WeaponData] { Attempt.successful(code :: 8 :: () :: 2 :: () :: 0x2C0 :: ammo :: HNil) } ).as[WeaponData] + + + + val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( + { + case x => + Attempt.successful(Some(x.asInstanceOf[ConstructorData])) + }, + { + case Some(x) => + Attempt.successful(x.asInstanceOf[WeaponData]) + case _ => + Attempt.failure(Err("")) + } + ) } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index f3ebd435..efdc3a93 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -6,6 +6,7 @@ import net.psforever.packet._ import net.psforever.packet.game._ import net.psforever.packet.game.objectcreate._ import net.psforever.types._ +import scodec.Attempt import scodec.Attempt.Successful import scodec.bits._ @@ -147,36 +148,42 @@ class GamePacketTest extends Specification { "ObjectCreateMessage" should { val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 " //fake data? - val packet2 = hex"18 F8 00 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" //fake data - val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16) + var string_inventoryItem = hex"46 04 C0 08 08 80 00 00 20 00 0C 04 10 29 A0 10 19 00 00 04 00 00" val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" - val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 01 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" + val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" + val invTest = hex"01 01 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 00 00" + val invTestWep = hex"23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 00 00" - "decode (2)" in { - PacketCoding.DecodePacket(packet2).require match { - case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => - len mustEqual 248 //60 + 188 - cls mustEqual 121 - guid mustEqual PlanetSideGUID(2497) - parent mustEqual None - mold.data mustEqual packet2Rest - mold.isDefined mustEqual false - case default => + "InventoryTest" in { + val intSlot = InternalSlot.codec.decode(invTestWep.toBitVector.drop(1)).toOption + intSlot.isDefined mustEqual true + + val invData = InventoryItem.codec.decode(invTestWep.toBitVector.drop(1)).toOption + invData.isDefined mustEqual true + + InventoryData.codec.decode(invTest.toBitVector.drop(7)).toOption match { + case Some(x) => + x.value.unk1 equals true + x.value.size mustEqual 1 + x.value.unk2 mustEqual false + //x.value.inv.head.item.objectClass mustEqual 0x8C + //x.value.inv.head.na mustEqual false + case _ => ko } } "decode (char)" in { PacketCoding.DecodePacket(string_testchar).require match { - case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => len mustEqual 3159 cls mustEqual 0x79 guid mustEqual PlanetSideGUID(75) parent.isDefined mustEqual false - mold.isDefined mustEqual true + data.isDefined mustEqual true - val char = mold.get.asInstanceOf[CharacterData] + val char = data.get.asInstanceOf[CharacterData] char.pos.x mustEqual 3674.8438f char.pos.y mustEqual 2726.789f char.pos.z mustEqual 91.15625f @@ -268,24 +275,17 @@ class GamePacketTest extends Specification { } } - "encode (2)" in { - val msg = ObjectCreateMessage(0, 121, PlanetSideGUID(2497), None, Mold(121, packet2Rest)) - val pkt = PacketCoding.EncodePacket(msg).require.toByteVector - - pkt mustEqual packet2 - } - "encode (9mm)" in { - val obj = Mold(28, AmmoBoxData(50)) - val msg = ObjectCreateMessage(0, 28, PlanetSideGUID(1280), Some(ObjectCreateMessageParent(PlanetSideGUID(75), 33)), obj) + val obj : ConstructorData = AmmoBoxData(50).asInstanceOf[ConstructorData] + val msg = ObjectCreateMessage(0, 28, PlanetSideGUID(1280), Some(ObjectCreateMessageParent(PlanetSideGUID(75), 33)), Some(obj)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_9mm } "encode (gauss)" in { - val obj = Mold(345, WeaponData(4, 28, PlanetSideGUID(1286), 0, AmmoBoxData(30))) - val msg = ObjectCreateMessage(0, 345, PlanetSideGUID(1465), Some(ObjectCreateMessageParent(PlanetSideGUID(75), 2)), obj) + val obj : ConstructorData = WeaponData(4, 28, PlanetSideGUID(1286), 0, AmmoBoxData(30)).asInstanceOf[ConstructorData] + val msg = ObjectCreateMessage(0, 345, PlanetSideGUID(1465), Some(ObjectCreateMessageParent(PlanetSideGUID(75), 2)), Some(obj)) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_gauss From 9adb077d8cb0bf5148a528878a9dc59972cf579c Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 6 Dec 2016 13:26:28 -0500 Subject: [PATCH 12/15] combining hamfisted approach and hlist approach to better manage failure cases --- .../packet/game/ObjectCreateMessage.scala | 325 ++++++++---------- .../game/objectcreate/AmmoBoxData.scala | 2 +- .../game/objectcreate/CharacterData.scala | 6 +- .../game/objectcreate/ConstructorData.scala | 2 +- .../game/objectcreate/InternalSlot.scala | 4 +- .../game/objectcreate/InventoryData.scala | 5 +- .../packet/game/objectcreate/Mold.scala | 66 ---- .../game/objectcreate/ObjectClass.scala | 28 +- .../packet/game/objectcreate/REKData.scala | 2 +- .../game/objectcreate/RecoveredData.scala | 29 -- .../packet/game/objectcreate/RibbonBars.scala | 4 +- .../packet/game/objectcreate/WeaponData.scala | 2 +- common/src/test/scala/GamePacketTest.scala | 34 +- 13 files changed, 205 insertions(+), 304 deletions(-) delete mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala delete mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/RecoveredData.scala 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 622e1c38..4af09d80 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -3,7 +3,8 @@ package net.psforever.packet.game import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import scodec.{Attempt, Codec, Err} +import scodec.bits.BitVector +import scodec.{Attempt, Codec, DecodeResult, Err} import scodec.codecs._ import shapeless.{::, HNil} @@ -40,15 +41,12 @@ case class ObjectCreateMessageParent(guid : PlanetSideGUID, * (The GM-level command `/sync` tests for objects that "do not match" between the server and the client. * It's implementation and scope are undefined.)
*
- * Knowing the object's class is essential for parsing the specific information passed by the `data` parameter.
- *
- * Exploration:
- * Can we build a `case class` "foo" that can accept the `objectClass` and the `data` and construct any valid object automatically? + * Knowing the object's class is essential for parsing the specific information passed by the `data` parameter. * @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding * @param objectClass the code for the type of object being constructed * @param guid the GUID this object will be assigned * @param parentInfo if defined, the relationship between this object and another object (its parent) - * @param data the data used to construct this type of object + * @param data if defined, the data used to construct this type of object */ case class ObjectCreateMessage(streamLength : Long, objectClass : Int, @@ -61,52 +59,133 @@ case class ObjectCreateMessage(streamLength : Long, } object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { - type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil + type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil type outPattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil /** * Codec for formatting around the lack of parent data in the stream. */ - val noParent : Codec[Pattern] = ( - ("objectClass" | uintL(0xb)) >>:~ { cls => //11u - ("guid" | PlanetSideGUID.codec) :: //16u - ("data" | ObjectClass.selectDataCodec(cls)) - } - ).xmap[Pattern] ( + private val noParent : Codec[Pattern] = ( + ("objectClass" | uintL(0xb)) :: //11u + ("guid" | PlanetSideGUID.codec) //16u + ).xmap[Pattern]( { - case cls :: guid :: data :: HNil => - cls :: guid :: None :: data :: HNil - }, - { - case cls :: guid :: None :: data :: HNil => - cls :: guid :: data :: HNil + case cls :: guid :: HNil => + cls :: guid :: None :: HNil + }, { + case cls :: guid :: None :: HNil => + cls :: guid :: HNil } ) - /** * Codec for reading and formatting parent data from the stream. */ - val parent : Codec[Pattern] = ( + private val parent : Codec[Pattern] = ( ("parentGuid" | PlanetSideGUID.codec) :: //16u - (("objectClass" | uintL(0xb)) >>:~ { cls => //11u - ("guid" | PlanetSideGUID.codec) :: //16u - ("parentSlotIndex" | PacketHelpers.encodedStringSize) :: //8u or 16u - ("data" | ObjectClass.selectDataCodec(cls)) - }) - ).xmap[Pattern] ( + ("objectClass" | uintL(0xb)) :: //11u + ("guid" | PlanetSideGUID.codec) :: //16u + ("parentSlotIndex" | PacketHelpers.encodedStringSize) //8u or 16u + ).xmap[Pattern]( { - case pguid :: cls :: guid :: slot :: data :: HNil => - cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: data :: HNil - }, - { - case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: data :: HNil => - pguid :: cls :: guid :: slot :: data :: HNil + case pguid :: cls :: guid :: slot :: HNil => + cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil + }, { + case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil => + pguid :: cls :: guid :: slot :: HNil } ) /** - * Calculate the stream length in number of bits by factoring in the two variable fields.
+ * Take bit data and transform it into an object that expresses the important information of a game piece.
*
- * Constant fields have already been factored into the results. + * This function is fail-safe because it catches errors involving bad parsing of the bitstream data. + * Generally, the `Exception` messages themselves are not useful. + * The important parts are what the packet thought the object class should be and what it actually processed. + * The bit data that failed to parse is retained for debugging at a later time. + * @param objectClass the code for the type of object being constructed + * @param data the bitstream data + * @return the optional constructed object + */ + private def decodeData(objectClass : Int, data : BitVector) : Option[ConstructorData] = { + var out : Option[ConstructorData] = None + val copy = data.drop(0) + try { + val outOpt : Option[DecodeResult[_]] = ObjectClass.selectDataCodec(objectClass).decode(copy).toOption + if(outOpt.isDefined) + out = outOpt.get.value.asInstanceOf[ConstructorData.genericPattern] + } + catch { + case ex : Exception => + //catch and release, any sort of parse error + } + out + } + + /** + * Take the important information of a game piece and transform it into bit data.
+ *
+ * This function is fail-safe because it catches errors involving bad parsing of the object data. + * Generally, the `Exception` messages themselves are not useful. + * If parsing fails, all data pertinent to debugging the failure is retained in the constructor. + * @param objClass the code for the type of object being deconstructed + * @param obj the object data + * @return the bitstream data + */ + private def encodeData(objClass : Int, obj : ConstructorData) : BitVector = { + var out = BitVector.empty + try { + val outOpt : Option[BitVector] = ObjectClass.selectDataCodec(objClass).encode(Some(obj.asInstanceOf[ConstructorData])).toOption + if(outOpt.isDefined) + out = outOpt.get + } + catch { + case ex : Exception => + //catch and release, any sort of parse error + } + out + } + + /** + * Calculate the stream length in number of bits by factoring in the whole message in two portions. + * @param parentInfo if defined, information about the parent + * @param data the data length is indeterminate until it is read + * @return the total length of the stream in bits + */ + private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : BitVector) : Long = { + //knowable length + val first : Long = commonMsgLen(parentInfo) + //data length + var second : Long = data.size + val secondMod4 : Long = second % 4L + if(secondMod4 > 0L) { + //pad to include last whole nibble + second += 4L - secondMod4 + } + first + second + } + + /** + * Calculate the stream length in number of bits by factoring in the whole message in two portions. + * @param parentInfo if defined, information about the parent + * @param data the data length is indeterminate until it is read + * @return the total length of the stream in bits + */ + private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : ConstructorData) : Long = { + //knowable length + val first : Long = commonMsgLen(parentInfo) + //data length + var second : Long = data.bitsize + val secondMod4 : Long = second % 4L + if(secondMod4 > 0L) { + //pad to include last whole nibble + second += 4L - secondMod4 + } + first + second + } + + /** + * Calculate the length (in number of bits) of the basic packet message region.
+ *
+ * Ignoring the parent data, constant field lengths have already been factored into the results. * That includes: * the length of the stream length field (32u), * the object's class (11u), @@ -114,164 +193,58 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { * and the bit to determine if there will be parent data. * In total, these fields form a known fixed length of 60u. * @param parentInfo if defined, the parentInfo adds either 24u or 32u - * @param data the data length is indeterminate until it is read - * @return the total length of the stream in bits + * @return the length, including the optional parent data */ - private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : Option[ConstructorData]) : Long = { - //msg length - val first : Long = if(parentInfo.isDefined) { //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u)) + private def commonMsgLen(parentInfo : Option[ObjectCreateMessageParent]) : Long = { + if(parentInfo.isDefined) { + //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u)) if(parentInfo.get.slot > 127) 92L else 84L } else { 60L } - //data length - var second : Long = if(data.isDefined) data.get.bsize else 0L - val secondMod4 : Long = second % 4L - if(secondMod4 > 0L) { //pad to include last whole nibble - second += 4L - secondMod4 - } - first + second } implicit val codec : Codec[ObjectCreateMessage] = ( ("streamLength" | uint32L) :: - either(bool, parent, noParent).exmap[Pattern] ( + (either(bool, parent, noParent).exmap[Pattern] ( { - case Left(a :: b :: Some(c) :: d :: HNil) => - Attempt.successful(a :: b :: Some(c) :: d :: HNil) //true, _, _, Some(c) - case Right(a :: b :: None :: d :: HNil) => - Attempt.successful(a :: b :: None :: d :: HNil) //false, _, _, None + case Left(a :: b :: Some(c) :: HNil) => + Attempt.successful(a :: b :: Some(c) :: HNil) //true, _, _, Some(c) + case Right(a :: b :: None :: HNil) => + Attempt.successful(a :: b :: None :: HNil) //false, _, _, None // failure cases - case Left(a :: b :: None :: _ :: HNil) => + case Left(a :: b :: None :: HNil) => Attempt.failure(Err("missing parent structure")) //true, _, _, None - case Right(a :: b :: Some(c) :: _ :: HNil) => + case Right(a :: b :: Some(c) :: HNil) => Attempt.failure(Err("unexpected parent structure")) //false, _, _, Some(c) - }, - { - case a :: b :: Some(c) :: d :: HNil => - Attempt.successful(Left(a :: b :: Some(c) :: d :: HNil)) - case a :: b :: None :: d :: HNil => - Attempt.successful(Right(a :: b :: None :: d :: HNil)) + }, { + case a :: b :: Some(c) :: HNil => + Attempt.successful(Left(a :: b :: Some(c) :: HNil)) + case a :: b :: None :: HNil => + Attempt.successful(Right(a :: b :: None :: HNil)) } - ) - ).xmap[outPattern] ( + ) :+ + ("data" | bits)) //greed is good + ).xmap[outPattern]( { case len :: cls :: guid :: par :: data :: HNil => - len :: cls :: guid :: par :: data :: HNil - }, + len :: cls :: guid :: par :: decodeData(cls, data) :: HNil + }, { + case _ :: cls :: guid :: par :: Some(obj) :: HNil => + streamLen(par, obj) :: cls :: guid :: par :: encodeData(cls, obj) :: HNil + case _ :: cls :: guid :: par :: None :: HNil => + streamLen(par, BitVector.empty) :: cls :: guid :: par :: BitVector.empty :: HNil + } + ).exmap[ObjectCreateMessage]( { - case _ :: cls :: guid :: par :: data :: HNil => - streamLen(par, data) :: cls :: guid :: par :: data :: HNil + case len :: cls :: guid :: par :: obj :: HNil => + Attempt.successful(ObjectCreateMessage(len, cls, guid, par, obj)) + }, { + case ObjectCreateMessage(_, _, _, _, None) => + Attempt.failure(Err("no object to encode")) + case ObjectCreateMessage(len, cls, guid, par, obj) => + Attempt.successful(len :: cls :: guid :: par :: obj :: HNil) } ).as[ObjectCreateMessage] -} - -//import net.psforever.packet.game.objectcreate.Mold -//import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -//import scodec.bits._ -//import scodec.{Attempt, Codec, Err} -//import scodec.codecs._ -//import shapeless.{::, HNil} -// -//case class ObjectCreateMessageParent(guid : PlanetSideGUID, -// slot : Int) -// -//case class ObjectCreateMessage(streamLength : Long, -// objectClass : Int, -// guid : PlanetSideGUID, -// parentInfo : Option[ObjectCreateMessageParent], -// mold : Mold) -// extends PlanetSideGamePacket { -// def opcode = GamePacketOpcode.ObjectCreateMessage -// def encode = ObjectCreateMessage.encode(this) -//} -// -//object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { -// type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil -// /** -// * Codec for formatting around the lack of parent data in the stream. -// */ -// val noParent : Codec[Pattern] = ( -// ("objectClass" | uintL(0xb)) :: //11u -// ("guid" | PlanetSideGUID.codec) //16u -// ).xmap[Pattern] ( -// { -// case cls :: guid :: HNil => -// cls :: guid :: None :: HNil -// }, -// { -// case cls :: guid :: None :: HNil => -// cls :: guid :: HNil -// } -// ) -// -// /** -// * Codec for reading and formatting parent data from the stream. -// */ -// val parent : Codec[Pattern] = ( -// ("parentGuid" | PlanetSideGUID.codec) :: //16u -// ("objectClass" | uintL(0xb)) :: //11u -// ("guid" | PlanetSideGUID.codec) :: //16u -// ("parentSlotIndex" | PacketHelpers.encodedStringSize) //8u or 16u -// ).xmap[Pattern] ( -// { -// case pguid :: cls :: guid :: slot :: HNil => -// cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil -// }, -// { -// case cls :: guid :: Some(ObjectCreateMessageParent(pguid, slot)) :: HNil => -// pguid :: cls :: guid :: slot :: HNil -// } -// ) -// -// private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : BitVector) : Long = { -// //known length -// val first : Long = if(parentInfo.isDefined) { -// if(parentInfo.get.slot > 127) 92L else 84L //60u + 16u + (8u or 16u) -// } -// else { -// 60L -// } -// //variant length -// var second : Long = data.size -// val secondMod4 : Long = second % 4L -// if(secondMod4 > 0L) { //pad to include last whole nibble -// second += 4L - secondMod4 -// } -// first + second -// } -// -// implicit val codec : Codec[ObjectCreateMessage] = ( -// ("streamLength" | uint32L) :: -// (either(bool, parent, noParent).exmap[Pattern] ( -// { -// case Left(a :: b :: Some(c) :: HNil) => -// Attempt.successful(a :: b :: Some(c) :: HNil) //true, _, _, Some(c) -// case Right(a :: b :: None :: HNil) => -// Attempt.successful(a :: b :: None :: HNil) //false, _, _, None -// // failure cases -// case Left(a :: b :: None :: HNil) => -// Attempt.failure(Err("missing parent structure")) //true, _, _, None -// case Right(a :: b :: Some(c) :: HNil) => -// Attempt.failure(Err("unexpected parent structure")) //false, _, _, Some(c) -// }, -// { -// case a :: b :: Some(c) :: HNil => -// Attempt.successful(Left(a :: b :: Some(c) :: HNil)) -// case a :: b :: None :: HNil => -// Attempt.successful(Right(a :: b :: None :: HNil)) -// } -// ) :+ -// ("data" | bits) ) -// ).xmap[ObjectCreateMessage] ( -// { -// case len :: cls :: guid :: info :: data :: HNil => -// ObjectCreateMessage(len, cls, guid, info, Mold(cls, data)) -// }, -// { -// case ObjectCreateMessage(_, cls, guid, info, mold) => -// streamLen(info, mold.data) :: cls :: guid :: info :: mold.data :: HNil -// } -// ).as[ObjectCreateMessage] -//} +} \ No newline at end of file diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala index 6958da56..054faac2 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala @@ -8,7 +8,7 @@ import shapeless.{::, HNil} case class AmmoBoxData(magazine : Int ) extends ConstructorData { - override def bsize : Long = 39L + override def bitsize : Long = 39L } object AmmoBoxData extends Marshallable[AmmoBoxData] { diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala index 91ed7b75..57f3f07c 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala @@ -42,8 +42,8 @@ case class CharacterData(pos : Vector3, tutorial_list : List[String], inventory : InventoryData ) extends ConstructorData { - override def bsize : Long = { - //represents static fields + override def bitsize : Long = { + //represents static fields (includes medals.bitsize) val first : Long = 1194L //TODO due to changing understanding of the bit patterns in this data, this value will change //name val second : Long = CharacterData.stringBitSize(name, 16) + 4L //plus the padding @@ -60,7 +60,7 @@ case class CharacterData(pos : Vector3, for(str <- tutorial_list) { fourth += CharacterData.stringBitSize(str) } - first + second + third + fourth + inventory.bsize + first + second + third + fourth + inventory.bitsize } } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala index 9cd35252..58a978bd 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala @@ -2,7 +2,7 @@ package net.psforever.packet.game.objectcreate abstract class ConstructorData() { - def bsize : Long = 0L + def bitsize : Long = 0L } object ConstructorData { diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala index ed8ab873..01b9171b 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala @@ -19,9 +19,9 @@ case class InternalSlot(objectClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : Option[ConstructorData]) { - def bsize : Long = { + def bitsize : Long = { val first : Long = if(parentSlot > 127) 44L else 36L - val second : Long = if(obj.isDefined) obj.get.bsize else 0L + val second : Long = if(obj.isDefined) obj.get.bitsize else 0L first + second } } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala index 161bbf0b..4f68764a 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala @@ -3,14 +3,13 @@ package net.psforever.packet.game.objectcreate import net.psforever.packet.{Marshallable, PacketHelpers} import scodec.Codec -import scodec.bits.BitVector import scodec.codecs._ case class InventoryData(unk1 : Boolean, size : Int, unk2 : Boolean){//, //inv : List[InventoryItem]) { - def bsize : Long = { + def bitsize : Long = { 10L } } @@ -18,7 +17,7 @@ case class InventoryData(unk1 : Boolean, object InventoryData extends Marshallable[InventoryData] { implicit val codec : Codec[InventoryData] = ( ("unk1" | bool) :: - (("size" | uint8L) >>:~ { len => + (("len" | uint8L) >>:~ { len => ("unk2" | bool).hlist// :: //("inv" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) }) diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala deleted file mode 100644 index eb75953c..00000000 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/Mold.scala +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2016 PSForever.net to present -package net.psforever.packet.game.objectcreate - -import scodec.DecodeResult -import scodec.bits.BitVector - -case class Mold(objectClass : Int, - data : BitVector) { - private var obj : Option[ConstructorData] = Mold.decode(objectClass, data) - - def isDefined : Boolean = this.obj.isDefined - - def get : ConstructorData = this.obj.get - - def set(data : ConstructorData) : Boolean = { - var ret = false - if(Some(data).isDefined) { - obj = Some(data) - ret = true - } - ret - } -} - -object Mold { - def apply(objectClass : Int, obj : ConstructorData) : Mold = - new Mold( objectClass, Mold.encode(objectClass, obj) ) - - def decode(objClass : Int, data : BitVector) : Option[ConstructorData] = { - var out : Option[ConstructorData] = None - if(!data.isEmpty) { - val codec = ObjectClass.selectDataCodec(objClass) - var outOpt : Option[DecodeResult[_]] = None - try { - outOpt = codec.decode(data).toOption - if(outOpt.isDefined) - out = Some(outOpt.get.value.asInstanceOf[ConstructorData]) - } - catch { - case ex : ClassCastException => - //TODO generate and log wrong class error message - case ex : Exception => - //TODO generic error - } - } - out - } - - def encode(objClass : Int, obj : ConstructorData) : BitVector = { - var out = BitVector.empty - try { - val codec = ObjectClass.selectDataCodec(objClass) - var outOpt : Option[BitVector] = None - outOpt = codec.encode(obj.asInstanceOf[ConstructorData.genericPattern]).toOption - if(outOpt.isDefined) - out = outOpt.get - } - catch { - case ex : ClassCastException => - //TODO generate and log wrong class error message - case ex : Exception => - //TODO generic error - } - out - } -} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala index 9e4ae3ed..dce00ef9 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -1,7 +1,8 @@ // Copyright (c) 2016 PSForever.net to present package net.psforever.packet.game.objectcreate -import scodec.Codec +import scodec.{Attempt, Codec, Err} +import scodec.codecs._ import scala.annotation.switch @@ -36,18 +37,17 @@ object ObjectClass { case ObjectClass.SUPPRESSOR => WeaponData.genericCodec case ObjectClass.REK => REKData.genericCodec case ObjectClass.SLOT_BLOCKER => AmmoBoxData.genericCodec - case _ => RecoveredData.genericCodec + //failure case + case _ => conditional(false, bool).exmap[ConstructorData.genericPattern] ( + { + case None | _ => + Attempt.failure(Err("decoding unknown object class - "+objClass)) + }, + { + case None | _ => + Attempt.failure(Err("encoding unknown object class - "+objClass)) + } + ) } } - -// val failureCodec : Codec[ConstructorData.genericPattern] = conditional(false, bool).exmap[ConstructorData.genericPattern] ( -// { -// case None | _ => -// Attempt.failure(Err("object class unrecognized during decoding")) -// }, -// { -// case None | _ => -// Attempt.failure(Err("object class unrecognized during encoding")) -// } -// ) -} \ No newline at end of file +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala index ef396c83..e1e838ea 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala @@ -8,7 +8,7 @@ import shapeless.{::, HNil} case class REKData(unk : Int ) extends ConstructorData { - override def bsize : Long = 72L + override def bitsize : Long = 72L } object REKData extends Marshallable[REKData] { diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RecoveredData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RecoveredData.scala deleted file mode 100644 index 04cda576..00000000 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/RecoveredData.scala +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2016 PSForever.net to present -package net.psforever.packet.game.objectcreate - -import net.psforever.packet.Marshallable -import scodec.{Attempt, Codec, Err} -import scodec.codecs._ -import scodec.bits.BitVector - -case class RecoveredData(data : BitVector - ) extends ConstructorData { - override def bsize : Long = data.size -} - -object RecoveredData extends Marshallable[RecoveredData] { - implicit val codec : Codec[RecoveredData] = ( - "data" | bits - ).as[RecoveredData] - - val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( - { - case _ => - Attempt.failure(Err("un-parsed byte data preserved when decoding failed")) - }, - { - case _ => - Attempt.failure(Err("can not encode object")) - } - ) -} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala index 7b9275a1..a1193299 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala @@ -8,7 +8,9 @@ import scodec.codecs._ case class RibbonBars(upper : Long = 0xFFFFFFFFL, //0xFFFFFFFF means no merit (for all ...) middle : Long = 0xFFFFFFFFL, lower : Long = 0xFFFFFFFFL, - tos : Long = 0xFFFFFFFFL) + tos : Long = 0xFFFFFFFFL) { + def bitsize : Long = 128L +} object RibbonBars extends Marshallable[RibbonBars] { implicit val codec : Codec[RibbonBars] = ( diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala index a1c21aca..245205c8 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala @@ -9,7 +9,7 @@ import shapeless.{::, HNil} case class WeaponData(unk : Int, ammo : InternalSlot) extends ConstructorData { - override def bsize : Long = 59L + ammo.bsize + override def bitsize : Long = 59L + ammo.bitsize } object WeaponData extends Marshallable[WeaponData] { diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index efdc3a93..8498e1e6 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -148,6 +148,8 @@ class GamePacketTest extends Specification { "ObjectCreateMessage" should { val packet = hex"18 CF 13 00 00 BC 87 00 0A F0 16 C3 43 A1 30 90 00 02 C0 40 00 08 70 43 00 68 00 6F 00 72 00 64 00 54 00 52 00 82 65 1F F5 9E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 00 20 27 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC CC 10 00 03 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 00 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 02 A0 00 00 12 60 78 70 65 5F 77 61 72 70 5F 67 61 74 65 5F 75 73 61 67 65 92 78 70 65 5F 69 6E 73 74 61 6E 74 5F 61 63 74 69 6F 6E 92 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 91 78 70 65 5F 62 61 74 74 6C 65 5F 72 61 6E 6B 5F 32 8E 78 70 65 5F 66 6F 72 6D 5F 73 71 75 61 64 8E 78 70 65 5F 74 68 5F 6E 6F 6E 73 61 6E 63 8B 78 70 65 5F 74 68 5F 61 6D 6D 6F 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8F 75 73 65 64 5F 63 68 61 69 6E 62 6C 61 64 65 9A 76 69 73 69 74 65 64 5F 62 72 6F 61 64 63 61 73 74 5F 77 61 72 70 67 61 74 65 8E 76 69 73 69 74 65 64 5F 6C 6F 63 6B 65 72 8D 75 73 65 64 5F 70 75 6E 69 73 68 65 72 88 75 73 65 64 5F 72 65 6B 8D 75 73 65 64 5F 72 65 70 65 61 74 65 72 9F 76 69 73 69 74 65 64 5F 64 65 63 6F 6E 73 74 72 75 63 74 69 6F 6E 5F 74 65 72 6D 69 6E 61 6C 8F 75 73 65 64 5F 73 75 70 70 72 65 73 73 6F 72 96 76 69 73 69 74 65 64 5F 6F 72 64 65 72 5F 74 65 72 6D 69 6E 61 6C 85 6D 61 70 31 35 85 6D 61 70 31 34 85 6D 61 70 31 32 85 6D 61 70 30 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 36 13 88 04 00 40 00 00 10 00 04 00 00 4D 6E 40 10 41 00 00 00 40 00 18 08 38 1C C0 20 32 00 00 07 80 15 E1 D0 02 10 20 00 00 08 00 03 01 07 13 A8 04 06 40 00 00 10 03 20 BB 00 42 E4 00 00 01 00 0E 07 70 08 6C 80 00 06 40 01 C0 F0 01 13 90 00 00 C8 00 38 1E 40 23 32 00 00 19 00 07 03 D0 05 0E 40 00 03 20 00 E8 7B 00 A4 C8 00 00 64 00 DA 4F 80 14 E1 00 00 00 40 00 18 08 38 1F 40 20 32 00 00 0A 00 08 " //fake data? + val packet2 = hex"18 F8 00 00 00 BC 8C 10 90 3B 45 C6 FA 94 00 9F F0 00 00 40 00 08 C0 44 00 69 00 66 00 66 00 45" //fake data + //val packet2Rest = packet2.bits.drop(8 + 32 + 1 + 11 + 16) var string_inventoryItem = hex"46 04 C0 08 08 80 00 00 20 00 0C 04 10 29 A0 10 19 00 00 04 00 00" val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" @@ -174,6 +176,20 @@ class GamePacketTest extends Specification { } } + "decode (2)" in { + //an invalid bit representation will fail to turn into an object + PacketCoding.DecodePacket(packet2).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 248 //60 + 188 + cls mustEqual 121 + guid mustEqual PlanetSideGUID(2497) + parent mustEqual None + data.isDefined mustEqual false + case default => + ko + } + } + "decode (char)" in { PacketCoding.DecodePacket(string_testchar).require match { case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => @@ -236,15 +252,15 @@ class GamePacketTest extends Specification { "decode (9mm)" in { PacketCoding.DecodePacket(string_9mm).require match { - case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => len mustEqual 124 cls mustEqual 28 guid mustEqual PlanetSideGUID(1280) parent.isDefined mustEqual true parent.get.guid mustEqual PlanetSideGUID(75) parent.get.slot mustEqual 33 - mold.isDefined mustEqual true - val obj = mold.get.asInstanceOf[AmmoBoxData] + data.isDefined mustEqual true + val obj = data.get.asInstanceOf[AmmoBoxData] obj.magazine mustEqual 50 case default => ko @@ -253,15 +269,15 @@ class GamePacketTest extends Specification { "decode (gauss)" in { PacketCoding.DecodePacket(string_gauss).require match { - case obj @ ObjectCreateMessage(len, cls, guid, parent, mold) => + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => len mustEqual 220 cls mustEqual 345 guid mustEqual PlanetSideGUID(1465) parent.isDefined mustEqual true parent.get.guid mustEqual PlanetSideGUID(75) parent.get.slot mustEqual 2 - mold.isDefined mustEqual true - val obj_wep = mold.get.asInstanceOf[WeaponData] + data.isDefined mustEqual true + val obj_wep = data.get.asInstanceOf[WeaponData] obj_wep.unk mustEqual 4 val obj_ammo = obj_wep.ammo//.asInstanceOf[InternalSlot] obj_ammo.objectClass mustEqual 28 @@ -275,6 +291,12 @@ class GamePacketTest extends Specification { } } + "encode (2)" in { + //the lack of an object will fail to turn into a bad bitstream + val msg = ObjectCreateMessage(0, 121, PlanetSideGUID(2497), None, None) + PacketCoding.EncodePacket(msg).isFailure mustEqual true + } + "encode (9mm)" in { val obj : ConstructorData = AmmoBoxData(50).asInstanceOf[ConstructorData] val msg = ObjectCreateMessage(0, 28, PlanetSideGUID(1280), Some(ObjectCreateMessageParent(PlanetSideGUID(75), 33)), Some(obj)) From f98a648db191c7e63697704b30c449916572f921 Mon Sep 17 00:00:00 2001 From: FateJH Date: Tue, 6 Dec 2016 21:15:29 -0500 Subject: [PATCH 13/15] removed Option from InternalSlot refactored a chunk of CharacterData as an example --- .../packet/game/ObjectCreateMessage.scala | 102 +++--- .../game/objectcreate/AmmoBoxData.scala | 16 + .../game/objectcreate/CharacterData.scala | 332 ++++++++++++++---- .../game/objectcreate/ConstructorData.scala | 21 +- .../game/objectcreate/InternalSlot.scala | 42 ++- .../game/objectcreate/InventoryData.scala | 54 ++- .../game/objectcreate/InventoryItem.scala | 53 ++- .../game/objectcreate/ObjectClass.scala | 43 ++- .../packet/game/objectcreate/REKData.scala | 15 +- .../packet/game/objectcreate/RibbonBars.scala | 18 +- .../packet/game/objectcreate/WeaponData.scala | 35 +- common/src/test/scala/GamePacketTest.scala | 159 ++++++--- 12 files changed, 672 insertions(+), 218 deletions(-) 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 4af09d80..23593447 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -15,7 +15,7 @@ import shapeless.{::, HNil} * The parent is a pre-existing object into which the (created) child is attached.
*
* The slot is encoded as a string length integer commonly used by PlanetSide. - * It is either a 0-127 eight bit number (0 = 0x80), or a 128-32767 sixteen bit number (128 = 0x0080). + * It is either a 0-127 eight bit number (0 = `0x80`), or a 128-32767 sixteen bit number (128 = `0x0080`). * @param guid the GUID of the parent object * @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent */ @@ -59,6 +59,18 @@ case class ObjectCreateMessage(streamLength : Long, } object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { + /** + * An abbreviated constructor for creating `ObjectCreateMessages`, ignoring the optional aspect of some fields. + * @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding + * @param objectClass the code for the type of object being constructed + * @param guid the GUID this object will be assigned + * @param parentInfo the relationship between this object and another object (its parent) + * @param data the data used to construct this type of object + * @return an ObjectCreateMessage + */ + def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage = + ObjectCreateMessage(streamLength, objectClass, guid, Some(parentInfo), Some(data)) + type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil type outPattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil /** @@ -146,44 +158,7 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { /** * Calculate the stream length in number of bits by factoring in the whole message in two portions. - * @param parentInfo if defined, information about the parent - * @param data the data length is indeterminate until it is read - * @return the total length of the stream in bits - */ - private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : BitVector) : Long = { - //knowable length - val first : Long = commonMsgLen(parentInfo) - //data length - var second : Long = data.size - val secondMod4 : Long = second % 4L - if(secondMod4 > 0L) { - //pad to include last whole nibble - second += 4L - secondMod4 - } - first + second - } - - /** - * Calculate the stream length in number of bits by factoring in the whole message in two portions. - * @param parentInfo if defined, information about the parent - * @param data the data length is indeterminate until it is read - * @return the total length of the stream in bits - */ - private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : ConstructorData) : Long = { - //knowable length - val first : Long = commonMsgLen(parentInfo) - //data length - var second : Long = data.bitsize - val secondMod4 : Long = second % 4L - if(secondMod4 > 0L) { - //pad to include last whole nibble - second += 4L - secondMod4 - } - first + second - } - - /** - * Calculate the length (in number of bits) of the basic packet message region.
+ * This process automates for: object encoding.
*
* Ignoring the parent data, constant field lengths have already been factored into the results. * That includes: @@ -192,17 +167,26 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { * the object's GUID (16u), * and the bit to determine if there will be parent data. * In total, these fields form a known fixed length of 60u. - * @param parentInfo if defined, the parentInfo adds either 24u or 32u - * @return the length, including the optional parent data + * @param parentInfo if defined, information about the parent adds either 24u or 32u + * @param data the data length is indeterminate until it is walked-through + * @return the total length of the stream in bits */ - private def commonMsgLen(parentInfo : Option[ObjectCreateMessageParent]) : Long = { - if(parentInfo.isDefined) { - //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u)) - if(parentInfo.get.slot > 127) 92L else 84L + private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : ConstructorData) : Long = { + //knowable length + val first : Long = if(parentInfo.isDefined) { + if(parentInfo.get.slot > 127) 92L else 84L //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u)) } else { 60L } + //object length + var second : Long = data.bitsize + val secondMod4 : Long = second % 4L + if(secondMod4 > 0L) { + //pad to include last whole nibble + second += 4L - secondMod4 + } + first + second } implicit val codec : Codec[ObjectCreateMessage] = ( @@ -226,25 +210,27 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { } ) :+ ("data" | bits)) //greed is good - ).xmap[outPattern]( + ).exmap[outPattern] ( { + case _ :: _ :: _ :: _ :: BitVector.empty :: HNil => + Attempt.failure(Err("no data to decode")) case len :: cls :: guid :: par :: data :: HNil => - len :: cls :: guid :: par :: decodeData(cls, data) :: HNil - }, { + Attempt.successful(len :: cls :: guid :: par :: decodeData(cls, data) :: HNil) + }, + { + case _ :: _ :: _ :: _ :: None :: HNil => + Attempt.failure(Err("no object to encode")) case _ :: cls :: guid :: par :: Some(obj) :: HNil => - streamLen(par, obj) :: cls :: guid :: par :: encodeData(cls, obj) :: HNil - case _ :: cls :: guid :: par :: None :: HNil => - streamLen(par, BitVector.empty) :: cls :: guid :: par :: BitVector.empty :: HNil + Attempt.successful(streamLen(par, obj) :: cls :: guid :: par :: encodeData(cls, obj) :: HNil) } - ).exmap[ObjectCreateMessage]( + ).xmap[ObjectCreateMessage] ( { case len :: cls :: guid :: par :: obj :: HNil => - Attempt.successful(ObjectCreateMessage(len, cls, guid, par, obj)) - }, { - case ObjectCreateMessage(_, _, _, _, None) => - Attempt.failure(Err("no object to encode")) + ObjectCreateMessage(len, cls, guid, par, obj) + }, + { case ObjectCreateMessage(len, cls, guid, par, obj) => - Attempt.successful(len :: cls :: guid :: par :: obj :: HNil) + len :: cls :: guid :: par :: obj :: HNil } ).as[ObjectCreateMessage] -} \ No newline at end of file +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala index 054faac2..71d46b75 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala @@ -6,8 +6,21 @@ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} +/** + * A representation of the ammunition portion of `ObjectCreateMessage` packet data. + * When alone, this data will help construct a "box" of that type of ammunition, hence the name.
+ *
+ * Exploration:
+ * This class may need to be rewritten later to support objects spawned in the world environment. + * @param magazine the number of rounds available + */ case class AmmoBoxData(magazine : Int ) extends ConstructorData { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ override def bitsize : Long = 39L } @@ -29,6 +42,9 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] { } ) + /** + * Transform between AmmoBoxData and ConstructorData. + */ val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( { case x => diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala index 57f3f07c..c58046e0 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala @@ -5,73 +5,27 @@ import net.psforever.packet.{Marshallable, PacketHelpers} import net.psforever.types.Vector3 import scodec.{Attempt, Codec, Err} import scodec.codecs._ +import shapeless.{::, HNil} -case class CharacterData(pos : Vector3, - objYaw : 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 - viewPitch : Int, - viewYaw : Int, - ribbons : RibbonBars, - healthMax : Int, - health : Int, - armor : Int, - unk4 : Int, //1 - unk5 : Int, //7 - unk6 : Int, //7 - staminaMax : Int, - stamina : Int, - unk7 : Int, // 28 - unk8 : Int, //4 - unk9 : Int, //44 - unk10 : Int, //84 - unk11 : Int, //104 - unk12 : Int, //1900 - firstTimeEvent_length : Long, - firstEntry : Option[String], - firstTimeEvent_list : List[String], - tutorial_list : List[String], - inventory : InventoryData - ) extends ConstructorData { - override def bitsize : Long = { - //represents static fields (includes medals.bitsize) - val first : Long = 1194L //TODO due to changing understanding of the bit patterns in this data, this value will change - //name - val second : Long = CharacterData.stringBitSize(name, 16) + 4L //plus the padding - //fte_list - var third : Long = 32L - if(firstEntry.isDefined) { - third += CharacterData.stringBitSize(firstEntry.get) + 5L //plus the padding - for(str <- firstTimeEvent_list) { - third += CharacterData.stringBitSize(str) - } - } - //tutorial list - var fourth : Long = 32L - for(str <- tutorial_list) { - fourth += CharacterData.stringBitSize(str) - } - first + second + third + fourth + inventory.bitsize - } -} +case class CharacterAppearanceData(pos : Vector3, + objYaw : 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 + viewPitch : Int, + viewYaw : Int, + ribbons : RibbonBars) -object CharacterData extends Marshallable[CharacterData] { - private def stringBitSize(str : String, width : Int = 8) : Long = { - val strlen = str.length - val lenSize = if(strlen > 127) 16L else 8L - lenSize + strlen * width - } - - implicit val codec : Codec[CharacterData] = ( +object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { + implicit val codec : Codec[CharacterAppearanceData] = ( ("pos" | Vector3.codec_pos) :: ignore(16) :: ("objYaw" | uint8L) :: @@ -91,12 +45,215 @@ object CharacterData extends Marshallable[CharacterData] { ignore(42) :: ("unk2" | uint16L) :: ignore(30) :: - ("unk3" | uintL(4)) :: + ("unk3" | uint4L) :: ignore(24) :: ("viewPitch" | uint8L) :: ("viewYaw" | uint8L) :: ignore(10) :: - ("ribbons" | RibbonBars.codec) :: + ("ribbons" | RibbonBars.codec) + ).as[CharacterAppearanceData] +} + +/** + * A representation of the avatar portion of `ObjectCreateMessage` packet data.
+ *
+ * This object is huge, representing the quantity of densely-encoded data in its packet. + * Although the actual organization is ill-defined, the packet can be divided into seven parts. + * The first part maintains information about the avatar as a game object in the game environment. + * The second part maintains information as an ongoing representation of the avatar. + * This includes fixed details like name and gender, though it also includes mutable aspects like exosuit type. + * The third part maintains information about career in the game. + * The fourth part maintains miscellaneous status and pose information. + * The fifth part maintains part of the statistical information about participation in the game. + * The sixth part maintains a stream of typically zero'd unknown information. + * The seventh part maintains the inventory. + * The fifth and seventh parts can inflate the size of packet significantly due to their encoding. + * The fifth, in particular, is string data that can number in the hundreds of strings(!).
+ *
+ * Ignoring the strings, lists of strings, and the inventory, the base length of the packet is currently __1138__ bits. + * Some undefined bits in the packet can change the length of the packet by being set or unset. + * This will mess with the encoding and the decoding of later fields. + * Any data that is padded for byte-alignment will also have its padding adjusted. + * Each string adds either 8 or 16, plus an additional 8 or 16 per the number of characters. + * For the name, that's 16 per character, a minimum of two characters, plus the (current) padding. + * for the first time events and tutorials, that's 8 per character, plus the (current) padding of the first entry. + * For the first time events and tutorials, however, the size of the list is always a 32-bit number. + * The formal inventory entries are preceded by 1 absolute bit.
+ *
+ * The adjusted base length is therefore __1203__ bits (1138 + 32 + 32 + 1). + * Of that, __720__ bits are unknown. + * Including the values that are defaulted, __831__ bits are perfectly unknown. + * This data is accurate as of 2016-12-07.
+ *
+ * Faction:
+ * `0 - Terran Republic`
+ * `1 - New Comglomerate`
+ * `2 - Vanu Sovereignty`
+ *
+ * Exosuit:
+ * `0 - Agile`
+ * `1 - Refinforced`
+ * `2 - Mechanized Assault`
+ * `3 - Infiltration`
+ * `4 - Standard`
+ *
+ * Sex:
+ * `1 - Male`
+ * `2 - Female`
+ *
+ * Voice:
+ * `    MALE      FEMALE`
+ * `0 - No voice  No voice`
+ * `1 - Male_1    Female_1`
+ * `2 - Male_2    Female_2`
+ * `3 - Male_3    Female_3`
+ * `4 - Male_4    Female_4`
+ * `5 - Male_5    Female_5`
+ * `6 - Female_1  No voice`
+ * `7 - Female_2  No voice` +// * @param pos the position of the character in the world environment (in three coordinates) +// * @param objYaw the angle with respect to the horizon towards which the object's front is facing; +// * every `0x1` is 2.813 degrees counter clockwise from North; +// * every `0x10` is 45-degrees; +// * it wraps at `0x80` +// * (note: references the avatar as a game object?) +// * @param faction the empire to which the avatar belongs; +// * the value scale is different from `PlanetSideEmpire`; +// * @param bops whether or not this avatar is enrolled in Black OPs +// * @param name the wide character name of the avatar +// * @param exosuit the type of exosuit the avatar will be depicted in; +// * for Black OPs, the agile exosuit and the reinforced exosuit are replaced with the Black OPs exosuits +// * @param sex whether the avatar is male or female +// * @param face1 the avatar's face, as by column number on the character creation screen +// * @param face2 the avatar's face, as by row number on the character creation screen +// * @param voice the avatar's voice selection +// * @param unk1 na; +// * defaults to `0x8080` +// * @param unk2 na; +// * defaults to `0xFFFF`; +// * may be `0x0` +// * @param unk3 na; +// * defaults to 2 +// * @param viewPitch the angle with respect to the horizon towards which the avatar is looking; +// * only supports downwards view angles; +// * `0x0` is forwards-facing; +// * `0x20` to `0xFF` is downwards-facing +// * @param viewYaw the angle with respect to the ground directions towards which the avatar is looking; +// * every `0x1` is 2.813 degrees counter clockwise from North; +// * every `0x10` is 45-degrees; +// * it wraps at `0x80` +// * @param ribbons the four merit commendation ribbon medals displayed on the avatar's left pauldron +// * @param healthMax for "x / y" of hitpoints, this is the avatar's 'y' value; +// * range is 0-65535 +// * @param health for "x / y" of hitpoints, this is the avatar's 'x' value; +// * range is 0-65535 +// * @param armor for "x / y" of armor points, this is the avatar's 'x' value; +// * range is 0-65535; +// * the avatar's "y" armor points is tied to their exosuit type +// * @param unk4 na; +// * defaults to 1 +// * @param unk5 na; +// * defaults to 7 +// * @param unk6 na; +// * defaults to 7 +// * @param staminaMax for "x / y" of stamina points, this is the avatar's 'y' value; +// * range is 0-65535 +// * @param stamina for "x / y" of stamina points, this is the avatar's 'x' value; +// * range is 0-65535 +// * @param unk7 na; +// * defaults to 28 +// * @param unk8 na; +// * defaults to 4 +// * @param unk9 na; +// * defaults to 44 +// * @param unk10 na; +// * defaults to 84 +// * @param unk11 na; +// * defaults to 104 +// * @param unk12 na; +// * defaults to 1900 +// * @param firstTimeEvent_length the total number of first time events performed by this avatar +// * @param firstTimeEvent_firstEntry the separated "first entry" of the list of first time events performed by this avatar +// * @param firstTimeEvent_list the list of first time events performed by this avatar +// * @param tutorial_length the total number of tutorials completed by this avatar +// * @param tutorial_firstEntry the separated "first entry" of the list of tutorials completed by this avatar +// * @param tutorial_list the list of tutorials completed by this avatar +// * @param inventory the avatar's inventory + */ +case class CharacterData(appearance : CharacterAppearanceData, + healthMax : Int, + health : Int, + armor : Int, + unk4 : Int, //1 + unk5 : Int, //7 + unk6 : Int, //7 + staminaMax : Int, + stamina : Int, + unk7 : Int, //28 + unk8 : Int, //4 + unk9 : Int, //44 + unk10 : Int, //84 + unk11 : Int, //104 + unk12 : Int, //1900 + firstTimeEvents : List[String], + tutorials : List[String], + inventory : InventoryData + ) extends ConstructorData { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = { +// //represents static fields (includes medals.bitsize) +// val base : Long = 1138L //TODO ongoing analysis, this value will be subject to change +// //name +// val nameSize : Long = CharacterData.stringBitSize(appearance.name, 16) + 4L //plus the current padding +// //fte_list +// var eventListSize : Long = 32L +// if(firstTimeEvent_firstEntry.isDefined) { +// eventListSize += CharacterData.stringBitSize(firstTimeEvent_firstEntry.get) + 5L //plus the current padding +// for(str <- firstTimeEvent_list) { +// eventListSize += CharacterData.stringBitSize(str) +// } +// } +// //tutorial list +// var tutorialListSize : Long = 32L +// for(str <- tutorial_list) { +// tutorialListSize += CharacterData.stringBitSize(str) +// } +// base + nameSize + eventListSize + tutorialListSize + inventory.bitsize + 0L + } +} + +object CharacterData extends Marshallable[CharacterData] { + /** + * Calculate the size of a string, including the length of the "string length" field that precedes it. + * Do not pass null-terminated strings. + * @param str a length-prefixed string + * @param width the width of the character encoding; + * defaults to the standard 8-bits + * @return the size in bits + */ + private def stringBitSize(str : String, width : Int = 8) : Long = { + val strlen = str.length + val lenSize = if(strlen > 127) 16L else 8L + lenSize + (strlen * width) + } + + private def ftePadding(len : Long) : Int = { + //TODO determine how this should be padded better + 5 + } + + private def tutListPadding(len : Long) : Int = { + //TODO determine how this should be padded when len == 0 + if(len > 0) 0 else 0 + } + + implicit val codec : Codec[CharacterData] = ( + ("appearance" | CharacterAppearanceData.codec) :: ignore(160) :: ("healthMax" | uint16L) :: ("health" | uint16L) :: @@ -118,14 +275,45 @@ object CharacterData extends Marshallable[CharacterData] { ("unk12" | uintL(12)) :: ignore(19) :: (("firstTimeEvent_length" | uint32L) >>:~ { len => - conditional(len > 0, "firstEntry" | PacketHelpers.encodedStringAligned(5)) :: + conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) :: ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: - ("tutorial_list" | PacketHelpers.listOfNAligned(uint32L, 0, PacketHelpers.encodedString)) :: - ignore(207) :: - ("inventory" | InventoryData.codec) + (("tutorial_length" | uint32L) >>:~ { len2 => + conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutListPadding(len) )) :: + ("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) :: + ignore(207) :: + ("inventory" | InventoryData.codec) + }) }) - ).as[CharacterData] + ).xmap[CharacterData] ( + { + case app :: _ :: b :: c :: _ :: d :: _ :: e :: _ :: f :: g :: h :: i :: _ :: j :: k :: l :: m :: n :: o :: _ :: p :: q :: r :: s :: t :: u :: _ :: v :: HNil => + //prepend the displaced first elements to their lists + val fteList : List[String] = if(q.isDefined) { q.get :: r } else r + val tutList : List[String] = if(t.isDefined) { t.get :: u } else u + CharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, v) + }, + { + case CharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, p) => + //shift the first elements off their lists + var fteListCopy = fteList + var firstEvent : Option[String] = None + if(fteList.nonEmpty) { + firstEvent = Some(fteList.head) + fteListCopy = fteList.drop(1) + } + var tutListCopy = tutList + var firstTutorial : Option[String] = None + if(tutList.nonEmpty) { + firstTutorial = Some(tutList.head) + tutListCopy = tutList.drop(1) + } + app :: () :: b :: c :: () :: d :: () :: e :: () :: f :: g :: h :: i :: () :: j :: k :: l :: m :: n :: o :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: p :: HNil + } + ).as[CharacterData] + /** + * Transform between CharacterData and ConstructorData. + */ val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( { case x => diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala index 58a978bd..a62d0a20 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala @@ -1,10 +1,29 @@ // Copyright (c) 2016 PSForever.net to present package net.psforever.packet.game.objectcreate +/** + * The base type for the representation of any data used to produce objects from `ObjectCreateMessage` packet data. + * There is no reason to instantiate this class as-is. + * Children of this class are expected to be able to translate through `scodec` operations into packet data.
+ *
+ * The object data is uncoupled from the object class as multiple classes use the same format for their data. + * For example, both the Suppressor and the Gauss use a weapon data format. + * For example, both 9mm Bullets and energy cells use am ammunition data format. + */ abstract class ConstructorData() { + /** + * Performs a "sizeof()" analysis of the given object. + * @return the number of bits necessary to represent this object; + * reflects the `Codec` definition rather than the parameter fields; + * defaults to `0L` + */ def bitsize : Long = 0L } object ConstructorData { + /** + * This pattern is intended to provide common conversion between all of the `Codec`s of the children of this class. + * The casting will be performed through use of `exmap`. + */ type genericPattern = Option[ConstructorData] -} \ No newline at end of file +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala index 01b9171b..4b6b5d41 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala @@ -8,27 +8,36 @@ import scodec.codecs._ import shapeless.{::, HNil} /** - * The same kind of data as required for a formal ObjectCreateMessage but with a required and implicit parent relationship. - * Data preceding this entry will define the existence of the parent. - * @param objectClass na - * @param guid na - * @param parentSlot na - * @param obj na + * The same kind of data as required for a formal `ObjectCreateMessage` but with a required and implicit parent relationship. + * Some data preceding this entry will clarify the existence of the parent.
+ *
+ * As indicated, an `InternalSlot` object is not a top-level object. + * This is true in relation between one object and another, as well as in how this object is sorted in the `ObjectCreateMessage` data. + * The data outlined by this class encompasses the same kind as the outer-most `ObjectCreateMessage`. + * By contrast, this object always has a dedicated parent object and a known slot to be attached to that parent. + * It's not optional. + * @param objectClass the code for the type of object being constructed + * @param guid the GUID this object will be assigned + * @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent + * @param obj the data used as representation of the object to be constructed */ case class InternalSlot(objectClass : Int, guid : PlanetSideGUID, parentSlot : Int, - obj : Option[ConstructorData]) { + obj : ConstructorData) { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ def bitsize : Long = { val first : Long = if(parentSlot > 127) 44L else 36L - val second : Long = if(obj.isDefined) obj.get.bitsize else 0L + val second : Long = obj.bitsize first + second } } object InternalSlot extends Marshallable[InternalSlot] { - type objPattern = Int :: PlanetSideGUID :: Int :: ConstructorData :: HNil - implicit val codec : Codec[InternalSlot] = ( ignore(1) :: //TODO determine what this bit does (("objectClass" | uintL(11)) >>:~ { obj_cls => @@ -36,5 +45,14 @@ object InternalSlot extends Marshallable[InternalSlot] { ("parentSlot" | PacketHelpers.encodedStringSize) :: ("obj" | ObjectClass.selectDataCodec(obj_cls)) }) - ).as[InternalSlot] -} \ No newline at end of file + ).xmap[InternalSlot] ( + { + case _ :: cls :: guid :: slot :: Some(obj) :: HNil => + InternalSlot(cls, guid, slot, obj) + }, + { + case InternalSlot(cls, guid, slot, obj) => + () :: cls :: guid :: slot :: Some(obj) :: HNil + } + ).as[InternalSlot] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala index 4f68764a..4a11e7d2 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala @@ -1,25 +1,59 @@ // Copyright (c) 2016 PSForever.net to present package net.psforever.packet.game.objectcreate -import net.psforever.packet.{Marshallable, PacketHelpers} +import net.psforever.packet.Marshallable import scodec.Codec import scodec.codecs._ +import shapeless.{::,HNil} +/** + * A representation of the inventory portion of `ObjectCreateMessage` packet data for avatars.
+ *
+ * Unfortunately, the inventory is a fail-fast greedy thing. + * Any format discrepancies will cause it to fail and that will cause character encoding to fail as well. + * Care should be taken that all possible item encodings are representable. + * @param unk1 na; + * always `true` to mark the start of the inventory data? + * @param unk2 na + * @param contents the actual items in the inventory; + * holster slots are 0-4; + * an inaccessible slot is 5; + * internal capacity is 6-`n`, where `n` is defined by exosuit type and is mapped into a grid + */ case class InventoryData(unk1 : Boolean, - size : Int, - unk2 : Boolean){//, - //inv : List[InventoryItem]) { + unk2 : Boolean, + contents : Vector[InventoryItem]) { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ def bitsize : Long = { - 10L + //two booleans and the 8-bit length field + val first : Long = 10L + //length of all items in inventory + var second : Long = 0L + for(item <- contents) { + second += item.bitsize + } + first + second } } object InventoryData extends Marshallable[InventoryData] { implicit val codec : Codec[InventoryData] = ( ("unk1" | bool) :: - (("len" | uint8L) >>:~ { len => - ("unk2" | bool).hlist// :: - //("inv" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) - }) - ).as[InventoryData] + ("len" | uint8L) :: + ("unk2" | bool) :: + ("contents" | vector(InventoryItem.codec)) + ).xmap[InventoryData] ( + { + case u1 :: _ :: u2 :: vector :: HNil => + InventoryData(u1, u2, vector) + }, + { + case InventoryData(u1, u2, vector) => + u1 :: vector.length :: u2 :: vector :: HNil + } + ).as[InventoryData] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala index 7c5cf9dc..4c82dbee 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala @@ -2,13 +2,60 @@ package net.psforever.packet.game.objectcreate import net.psforever.packet.Marshallable +import net.psforever.packet.game.PlanetSideGUID import scodec.Codec import scodec.codecs._ -case class InventoryItem(item : InternalSlot) +/** + * Represent an item in inventory.
+ *
+ * Note the use of `InternalSlot` to indicate the implicit parent ownership of the resulting item. + * Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible. + * @param item the object in inventory + * @param na the user should not have to worry about this potential bit; + * it follows after weapon entries, allegedly + */ +case class InventoryItem(item : InternalSlot, + na : Option[Boolean] = None) { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ + def bitsize : Long = { + //item + val first : Long = item.bitsize + //trailing bit + val second : Long = if(na.isDefined) 1L else 0L + first + second + } +} object InventoryItem extends Marshallable[InventoryItem] { + /** + * An abbreviated constructor for creating an `InventoryItem` without interacting with `InternalSlot` directly. + * @param objClass the code for the type of object (ammunition) being constructed + * @param guid the globally unique id assigned to the ammunition + * @param parentSlot the slot where the ammunition is to be installed in the weapon + * @param obj the constructor data + * @return an InventoryItem + */ + def apply(objClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : ConstructorData) : InventoryItem = { + val isWep = if(obj.isInstanceOf[WeaponData]) Some(false) else None + //TODO is this always Some(false)? + InventoryItem(InternalSlot(objClass, guid, parentSlot, obj), isWep) + } + + /** + * Determine whether the allocated item is a weapon. + * @param itm the inventory item + * @return true, if the item is a weapon; false, otherwise + */ + def wasWeapon(itm : InternalSlot) : Boolean = itm.obj.isInstanceOf[WeaponData] + implicit val codec : Codec[InventoryItem] = ( - "item" | InternalSlot.codec - ).as[InventoryItem] + ("item" | InternalSlot.codec) >>:~ { item => + conditional(wasWeapon(item), bool).hlist + } + ).as[InventoryItem] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala index dce00ef9..252b1468 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -6,37 +6,68 @@ import scodec.codecs._ import scala.annotation.switch +/** + * A reference between all object class codes and the name of the object they represent.
+ *
+ * Object classes compose a number between `0` and (probably) `2047`, always translating into an 11-bit value. + * They are recorded as little-endian hexadecimal values here. + */ object ObjectClass { //character - final val PLAYER = 0x79 + final val AVATAR = 0x79 //ammunition final val BULLETS_9MM = 0x1C final val BULLETS_9MM_AP = 0x1D final val ENERGY_CELL = 0x110 + final val JAMMER_GRENADE_AMMO = 0x1A1 final val FORCE_BLADE_AMMO = 0x21C + final val PLASMA_GRENADE_AMMO = 0x2A9 + final val BUCKSHOT = 0x2F3 //TODO apply internal name //weapons + final val SUPPRESSOR = 0x34D final val BEAMER = 0x8C + final val SWEEPER = 0x130 final val FORCE_BLADE = 0x144 final val GAUSS = 0x159 - final val SUPPRESSOR = 0x34D + final val JAMMER_GRENADE = 0x1A0 + final val PLASMA_GRENADE = 0x2A8 //tools + final val MEDKIT = 0x218 final val REK = 0x2D8 //unknown - final val SLOT_BLOCKER = 0x1C8 + final val SLOT_BLOCKER = 0x1C8 //strange item found in slot #5, between holsters and inventory + //TODO refactor this function into another object later + /** + * Given an object class, retrieve the `Codec` used to parse and translate the constructor data for that type.
+ *
+ * This function serves as a giant `switch` statement that loosely connects object data to object class. + * All entries, save the default, merely point to the `Codec` of pattern `ConstructorData.genericPattern`. + * This pattern connects all `Codec`s back to the superclass `ConstructorData`. + * The default case is a failure case for trying to either decode or encode an unknown class of object. + * @param objClass the code for the type of object being constructed + * @return the `Codec` that handles the format of data for that particular item class, or a failing codec + */ def selectDataCodec(objClass : Int) : Codec[ConstructorData.genericPattern] = { (objClass : @switch) match { - case ObjectClass.PLAYER => CharacterData.genericCodec + case ObjectClass.AVATAR => CharacterData.genericCodec + case ObjectClass.BEAMER => WeaponData.genericCodec + case ObjectClass.BUCKSHOT => AmmoBoxData.genericCodec case ObjectClass.BULLETS_9MM => AmmoBoxData.genericCodec case ObjectClass.BULLETS_9MM_AP => AmmoBoxData.genericCodec case ObjectClass.ENERGY_CELL => AmmoBoxData.genericCodec case ObjectClass.FORCE_BLADE_AMMO => AmmoBoxData.genericCodec - case ObjectClass.BEAMER => WeaponData.genericCodec case ObjectClass.FORCE_BLADE => WeaponData.genericCodec case ObjectClass.GAUSS => WeaponData.genericCodec - case ObjectClass.SUPPRESSOR => WeaponData.genericCodec + case ObjectClass.JAMMER_GRENADE => WeaponData.genericCodec + case ObjectClass.JAMMER_GRENADE_AMMO => AmmoBoxData.genericCodec + case ObjectClass.MEDKIT => AmmoBoxData.genericCodec + case ObjectClass.PLASMA_GRENADE => WeaponData.genericCodec + case ObjectClass.PLASMA_GRENADE_AMMO => AmmoBoxData.genericCodec case ObjectClass.REK => REKData.genericCodec case ObjectClass.SLOT_BLOCKER => AmmoBoxData.genericCodec + case ObjectClass.SUPPRESSOR => WeaponData.genericCodec + case ObjectClass.SWEEPER => WeaponData.genericCodec //failure case case _ => conditional(false, bool).exmap[ConstructorData.genericPattern] ( { diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala index e1e838ea..7f7adb30 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala @@ -6,8 +6,18 @@ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} +/** + * A representation of the REK portion of `ObjectCreateMessage` packet data. + * When alone, this data will help construct the "tool" called a Remote Electronics Kit. + * @param unk na + */ case class REKData(unk : Int ) extends ConstructorData { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ override def bitsize : Long = 72L } @@ -33,8 +43,9 @@ object REKData extends Marshallable[REKData] { } ).as[REKData] - - + /** + * Transform between REKData and ConstructorData. + */ val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( { case x => diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala index a1193299..2517be7b 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala @@ -5,10 +5,26 @@ import net.psforever.packet.Marshallable import scodec.Codec import scodec.codecs._ -case class RibbonBars(upper : Long = 0xFFFFFFFFL, //0xFFFFFFFF means no merit (for all ...) +/** + * Enumerate the player-displayed merit commendation awards granted for excellence (or tenacity) in combat. + * These are the medals players wish to brandish on their left pauldron.
+ *
+ * All merit commendation ribbons are represented by a 32-bit signature. + * The default "no-ribbon" value is `0xFFFFFFFF`. + * @param upper the "top" configurable merit ribbon + * @param middle the central configurable merit ribbon + * @param lower the lower configurable merit ribbon + * @param tos the automatic top-most term of service merit ribbon + */ +case class RibbonBars(upper : Long = 0xFFFFFFFFL, middle : Long = 0xFFFFFFFFL, lower : Long = 0xFFFFFFFFL, tos : Long = 0xFFFFFFFFL) { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ def bitsize : Long = 128L } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala index 245205c8..d692b20d 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala @@ -7,14 +7,42 @@ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} +/** + * A representation of the weapon portion of `ObjectCreateMessage` packet data. + * When alone, this data will help construct a "weapon" such as Suppressor.
+ *
+ * The data for the weapon also nests required default ammunition data. + * Where the ammunition is loaded is considered the "first slot." + * @param unk na + * @param ammo data regarding the currently loaded ammunition type and quantity + * @see AmmoBoxData + */ case class WeaponData(unk : Int, ammo : InternalSlot) extends ConstructorData { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @see AmmoBoxData.bitsize + * @return the number of bits necessary to represent this object + */ override def bitsize : Long = 59L + ammo.bitsize } object WeaponData extends Marshallable[WeaponData] { + /** + * An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.
+ *
+ * Exploration:
+ * This class may need to be rewritten later to support objects spawned in the world environment. + * @param unk na + * @param cls the code for the type of object (ammunition) being constructed + * @param guid the globally unique id assigned to the ammunition + * @param parentSlot the slot where the ammunition is to be installed in the weapon + * @param ammo the constructor data for the ammunition + * @return a WeaponData object + */ def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : WeaponData = - new WeaponData(unk, InternalSlot(cls, guid, parentSlot, Some(ammo))) + new WeaponData(unk, InternalSlot(cls, guid, parentSlot, ammo)) implicit val codec : Codec[WeaponData] = ( ("unk" | uint4L) :: @@ -37,8 +65,9 @@ object WeaponData extends Marshallable[WeaponData] { } ).as[WeaponData] - - + /** + * Transform between WeaponData and ConstructorData. + */ val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( { case x => diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 8498e1e6..42afd7f5 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -6,7 +6,7 @@ import net.psforever.packet._ import net.psforever.packet.game._ import net.psforever.packet.game.objectcreate._ import net.psforever.types._ -import scodec.Attempt +import scodec.{Attempt, Err} import scodec.Attempt.Successful import scodec.bits._ @@ -164,16 +164,16 @@ class GamePacketTest extends Specification { val invData = InventoryItem.codec.decode(invTestWep.toBitVector.drop(1)).toOption invData.isDefined mustEqual true - InventoryData.codec.decode(invTest.toBitVector.drop(7)).toOption match { - case Some(x) => - x.value.unk1 equals true - x.value.size mustEqual 1 - x.value.unk2 mustEqual false - //x.value.inv.head.item.objectClass mustEqual 0x8C - //x.value.inv.head.na mustEqual false - case _ => - ko - } +// InventoryData.codec.decode(invTest.toBitVector.drop(7)) match { +// case Attempt.Successful(x) => +// x.value.unk1 equals true +// x.value.size mustEqual 1 +// x.value.unk2 mustEqual false +// //x.value.inv.head.item.objectClass mustEqual 0x8C +// //x.value.inv.head.na mustEqual false +// case Attempt.Failure(x) => +// x.message mustEqual "" +// } } "decode (2)" in { @@ -190,7 +190,7 @@ class GamePacketTest extends Specification { } } - "decode (char)" in { + "decode (character)" in { PacketCoding.DecodePacket(string_testchar).require match { case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => len mustEqual 3159 @@ -200,27 +200,27 @@ class GamePacketTest extends Specification { data.isDefined mustEqual true val char = data.get.asInstanceOf[CharacterData] - char.pos.x mustEqual 3674.8438f - char.pos.y mustEqual 2726.789f - char.pos.z mustEqual 91.15625f - char.objYaw mustEqual 19 - char.faction mustEqual 2 //vs - char.bops mustEqual false - char.name mustEqual "IlllIIIlllIlIllIlllIllI" - char.exosuit mustEqual 4 //standard - char.sex mustEqual 2 //female - char.face1 mustEqual 2 - char.face2 mustEqual 9 - char.voice mustEqual 1 //female 1 - char.unk1 mustEqual 0x8080 - char.unk2 mustEqual 0xFFFF - char.unk3 mustEqual 2 - char.viewPitch mustEqual 0xFF - char.viewYaw mustEqual 0x6A - char.ribbons.upper mustEqual 0xFFFFFFFFL //none - char.ribbons.middle mustEqual 0xFFFFFFFFL //none - char.ribbons.lower mustEqual 0xFFFFFFFFL //none - char.ribbons.tos mustEqual 0xFFFFFFFFL //none + char.appearance.pos.x mustEqual 3674.8438f + char.appearance.pos.y mustEqual 2726.789f + char.appearance.pos.z mustEqual 91.15625f + char.appearance.objYaw mustEqual 19 + char.appearance.faction mustEqual 2 //vs + char.appearance.bops mustEqual false + char.appearance.name mustEqual "IlllIIIlllIlIllIlllIllI" + char.appearance.exosuit mustEqual 4 //standard + char.appearance.sex mustEqual 2 //female + char.appearance.face1 mustEqual 2 + char.appearance.face2 mustEqual 9 + char.appearance.voice mustEqual 1 //female 1 + char.appearance.unk1 mustEqual 0x8080 + char.appearance.unk2 mustEqual 0xFFFF + char.appearance.unk3 mustEqual 2 + char.appearance.viewPitch mustEqual 0xFF + char.appearance.viewYaw mustEqual 0x6A + char.appearance.ribbons.upper mustEqual 0xFFFFFFFFL //none + char.appearance.ribbons.middle mustEqual 0xFFFFFFFFL //none + char.appearance.ribbons.lower mustEqual 0xFFFFFFFFL //none + char.appearance.ribbons.tos mustEqual 0xFFFFFFFFL //none char.healthMax mustEqual 100 char.health mustEqual 100 char.armor mustEqual 50 //standard exosuit value @@ -235,16 +235,78 @@ class GamePacketTest extends Specification { char.unk10 mustEqual 84 char.unk11 mustEqual 104 char.unk12 mustEqual 1900 - char.firstTimeEvent_length mustEqual 4 - char.firstEntry mustEqual Some("xpe_sanctuary_help") - char.firstTimeEvent_list.size mustEqual 3 - char.firstTimeEvent_list.head mustEqual "xpe_th_firemodes" - char.firstTimeEvent_list(1) mustEqual "used_beamer" - char.firstTimeEvent_list(2) mustEqual "map13" - char.tutorial_list.size mustEqual 0 + char.firstTimeEvents.size mustEqual 4 + char.firstTimeEvents.head mustEqual "xpe_sanctuary_help" + char.firstTimeEvents(1) mustEqual "xpe_th_firemodes" + char.firstTimeEvents(2) mustEqual "used_beamer" + char.firstTimeEvents(3) mustEqual "map13" + char.tutorials.size mustEqual 0 char.inventory.unk1 mustEqual true - char.inventory.size mustEqual 10 char.inventory.unk2 mustEqual false + char.inventory.contents.length mustEqual 10 + val inventory = char.inventory.contents + //0 + inventory.head.item.objectClass mustEqual 0x8C //beamer + inventory.head.item.guid mustEqual PlanetSideGUID(76) + inventory.head.item.parentSlot mustEqual 0 + var wep = inventory.head.item.obj.asInstanceOf[WeaponData] + wep.ammo.objectClass mustEqual 0x110 //plasma + wep.ammo.guid mustEqual PlanetSideGUID(77) + wep.ammo.parentSlot mustEqual 0 + wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 16 + //1 + inventory(1).item.objectClass mustEqual 0x34D //suppressor + inventory(1).item.guid mustEqual PlanetSideGUID(78) + inventory(1).item.parentSlot mustEqual 2 + wep = inventory(1).item.obj.asInstanceOf[WeaponData] + wep.ammo.objectClass mustEqual 0x1C //9mm + wep.ammo.guid mustEqual PlanetSideGUID(79) + wep.ammo.parentSlot mustEqual 0 + wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 25 + //2 + inventory(2).item.objectClass mustEqual 0x144 //force blade + inventory(2).item.guid mustEqual PlanetSideGUID(80) + inventory(2).item.parentSlot mustEqual 4 + wep = inventory(2).item.obj.asInstanceOf[WeaponData] + wep.ammo.objectClass mustEqual 0x21C //force blade ammo + wep.ammo.guid mustEqual PlanetSideGUID(81) + wep.ammo.parentSlot mustEqual 0 + wep.ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1 + //3 + inventory(3).item.objectClass mustEqual 0x1C8 //thing + inventory(3).item.guid mustEqual PlanetSideGUID(82) + inventory(3).item.parentSlot mustEqual 5 + inventory(3).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1 + //4 + inventory(4).item.objectClass mustEqual 0x1C //9mm + inventory(4).item.guid mustEqual PlanetSideGUID(83) + inventory(4).item.parentSlot mustEqual 6 + inventory(4).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + //5 + inventory(5).item.objectClass mustEqual 0x1C //9mm + inventory(5).item.guid mustEqual PlanetSideGUID(84) + inventory(5).item.parentSlot mustEqual 9 + inventory(5).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + //6 + inventory(6).item.objectClass mustEqual 0x1C //9mm + inventory(6).item.guid mustEqual PlanetSideGUID(85) + inventory(6).item.parentSlot mustEqual 12 + inventory(6).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + //7 + inventory(7).item.objectClass mustEqual 0x1D //9mm ap + inventory(7).item.guid mustEqual PlanetSideGUID(86) + inventory(7).item.parentSlot mustEqual 33 + inventory(7).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + //8 + inventory(8).item.objectClass mustEqual 0x110 //plasma + inventory(8).item.guid mustEqual PlanetSideGUID(87) + inventory(8).item.parentSlot mustEqual 36 + inventory(8).item.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + //9 + inventory(9).item.objectClass mustEqual 0x2D8 //rek + inventory(9).item.guid mustEqual PlanetSideGUID(88) + inventory(9).item.parentSlot mustEqual 39 + //the rek has data but none worth testing here case default => ko } @@ -260,8 +322,7 @@ class GamePacketTest extends Specification { parent.get.guid mustEqual PlanetSideGUID(75) parent.get.slot mustEqual 33 data.isDefined mustEqual true - val obj = data.get.asInstanceOf[AmmoBoxData] - obj.magazine mustEqual 50 + data.get.asInstanceOf[AmmoBoxData].magazine mustEqual 50 case default => ko } @@ -283,9 +344,7 @@ class GamePacketTest extends Specification { obj_ammo.objectClass mustEqual 28 obj_ammo.guid mustEqual PlanetSideGUID(1286) obj_ammo.parentSlot mustEqual 0 - obj_ammo.obj.isDefined mustEqual true - val ammo = obj_ammo.obj.get.asInstanceOf[AmmoBoxData] - ammo.magazine mustEqual 30 + obj_ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30 case default => ko } @@ -298,16 +357,16 @@ class GamePacketTest extends Specification { } "encode (9mm)" in { - val obj : ConstructorData = AmmoBoxData(50).asInstanceOf[ConstructorData] - val msg = ObjectCreateMessage(0, 28, PlanetSideGUID(1280), Some(ObjectCreateMessageParent(PlanetSideGUID(75), 33)), Some(obj)) + val obj = AmmoBoxData(50) + val msg = ObjectCreateMessage(0, 28, PlanetSideGUID(1280), ObjectCreateMessageParent(PlanetSideGUID(75), 33), obj) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_9mm } "encode (gauss)" in { - val obj : ConstructorData = WeaponData(4, 28, PlanetSideGUID(1286), 0, AmmoBoxData(30)).asInstanceOf[ConstructorData] - val msg = ObjectCreateMessage(0, 345, PlanetSideGUID(1465), Some(ObjectCreateMessageParent(PlanetSideGUID(75), 2)), Some(obj)) + val obj = WeaponData(4, 28, PlanetSideGUID(1286), 0, AmmoBoxData(30)) + val msg = ObjectCreateMessage(0, 345, PlanetSideGUID(1465), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string_gauss From 3b9f3a6f33c31a93b4ab43b0415f460d68f4bfc5 Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 9 Dec 2016 19:42:20 -0500 Subject: [PATCH 14/15] exorcised bit from InventoryItem class, allowing insight on proper length of other CharacterData classes created a trait to express the ability to calculate the bit length of an instance of a class --- .../packet/game/ObjectCreateMessage.scala | 64 +-- .../game/objectcreate/AmmoBoxData.scala | 26 +- .../game/objectcreate/CharacterData.scala | 411 ++++++++++-------- .../game/objectcreate/ConstructorData.scala | 16 +- .../game/objectcreate/InternalSlot.scala | 37 +- .../game/objectcreate/InventoryData.scala | 53 ++- .../game/objectcreate/InventoryItem.scala | 40 +- .../game/objectcreate/ObjectClass.scala | 13 +- .../packet/game/objectcreate/REKData.scala | 22 +- .../packet/game/objectcreate/RibbonBars.scala | 9 +- .../game/objectcreate/StreamBitSize.scala | 17 + .../packet/game/objectcreate/WeaponData.scala | 30 +- common/src/test/scala/GamePacketTest.scala | 207 +++++---- 13 files changed, 539 insertions(+), 406 deletions(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala 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 23593447..0205775a 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -1,7 +1,7 @@ // Copyright (c) 2016 PSForever.net to present package net.psforever.packet.game -import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass} +import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass, StreamBitSize} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import scodec.bits.BitVector import scodec.{Attempt, Codec, DecodeResult, Err} @@ -12,10 +12,9 @@ import shapeless.{::, HNil} * The parent information of a created object.
*
* Rather than a created-parent with a created-child relationship, the whole of the packet still only creates the child. - * The parent is a pre-existing object into which the (created) child is attached.
- *
- * The slot is encoded as a string length integer commonly used by PlanetSide. - * It is either a 0-127 eight bit number (0 = `0x80`), or a 128-32767 sixteen bit number (128 = `0x0080`). + * The parent is a pre-existing object into which the (created) child is attached. + * The slot is encoded as a string length integer, following PlanetSide Classic convention for slot numbering. + * It is either a 0-127 eight bit number, or a 128-32767 sixteen bit number. * @param guid the GUID of the parent object * @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent */ @@ -46,7 +45,8 @@ case class ObjectCreateMessageParent(guid : PlanetSideGUID, * @param objectClass the code for the type of object being constructed * @param guid the GUID this object will be assigned * @param parentInfo if defined, the relationship between this object and another object (its parent) - * @param data if defined, the data used to construct this type of object + * @param data if defined, the data used to construct this type of object + * @see ObjectClass.selectDataCodec */ case class ObjectCreateMessage(streamLength : Long, objectClass : Int, @@ -71,6 +71,17 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage = ObjectCreateMessage(streamLength, objectClass, guid, Some(parentInfo), Some(data)) + /** + * An abbreviated constructor for creating `ObjectCreateMessages`, ignoring `parentInfo`. + * @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding + * @param objectClass the code for the type of object being constructed + * @param guid the GUID this object will be assigned + * @param data the data used to construct this type of object + * @return an ObjectCreateMessage + */ + def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateMessage = + ObjectCreateMessage(streamLength, objectClass, guid, None, Some(data)) + type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil type outPattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil /** @@ -107,21 +118,18 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { ) /** - * Take bit data and transform it into an object that expresses the important information of a game piece.
- *
+ * Take bit data and transform it into an object that expresses the important information of a game piece. * This function is fail-safe because it catches errors involving bad parsing of the bitstream data. - * Generally, the `Exception` messages themselves are not useful. + * Generally, the `Exception` messages themselves are not useful here. * The important parts are what the packet thought the object class should be and what it actually processed. - * The bit data that failed to parse is retained for debugging at a later time. * @param objectClass the code for the type of object being constructed - * @param data the bitstream data + * @param data the bitstream data * @return the optional constructed object */ private def decodeData(objectClass : Int, data : BitVector) : Option[ConstructorData] = { var out : Option[ConstructorData] = None - val copy = data.drop(0) try { - val outOpt : Option[DecodeResult[_]] = ObjectClass.selectDataCodec(objectClass).decode(copy).toOption + val outOpt : Option[DecodeResult[_]] = ObjectClass.selectDataCodec(objectClass).decode(data).toOption if(outOpt.isDefined) out = outOpt.get.value.asInstanceOf[ConstructorData.genericPattern] } @@ -133,13 +141,11 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { } /** - * Take the important information of a game piece and transform it into bit data.
- *
+ * Take the important information of a game piece and transform it into bit data. * This function is fail-safe because it catches errors involving bad parsing of the object data. - * Generally, the `Exception` messages themselves are not useful. - * If parsing fails, all data pertinent to debugging the failure is retained in the constructor. + * Generally, the `Exception` messages themselves are not useful here. * @param objClass the code for the type of object being deconstructed - * @param obj the object data + * @param obj the object data * @return the bitstream data */ private def encodeData(objClass : Int, obj : ConstructorData) : BitVector = { @@ -167,26 +173,22 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] { * the object's GUID (16u), * and the bit to determine if there will be parent data. * In total, these fields form a known fixed length of 60u. - * @param parentInfo if defined, information about the parent adds either 24u or 32u - * @param data the data length is indeterminate until it is walked-through - * @return the total length of the stream in bits + * @param parentInfo if defined, the relationship between this object and another object (its parent); + * information about the parent adds either 24u or 32u + * @param data if defined, the data used to construct this type of object; + * the data length is indeterminate until it is walked-through; + * note: the type is `StreamBitSize` as opposed to `ConstructorData` + * @return the total length of the resulting data stream in bits */ - private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : ConstructorData) : Long = { + private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : StreamBitSize) : Long = { //knowable length - val first : Long = if(parentInfo.isDefined) { + val base : Long = if(parentInfo.isDefined) { if(parentInfo.get.slot > 127) 92L else 84L //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u)) } else { 60L } - //object length - var second : Long = data.bitsize - val secondMod4 : Long = second % 4L - if(secondMod4 > 0L) { - //pad to include last whole nibble - second += 4L - secondMod4 - } - first + second + base + data.bitsize } implicit val codec : Codec[ObjectCreateMessage] = ( diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala index 71d46b75..ed8fed41 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala @@ -8,11 +8,14 @@ import shapeless.{::, HNil} /** * A representation of the ammunition portion of `ObjectCreateMessage` packet data. - * When alone, this data will help construct a "box" of that type of ammunition, hence the name.
+ * This data will help construct a "box" of that type of ammunition when standalone. + * It can also be constructed directly inside a weapon as its magazine.
*
- * Exploration:
- * This class may need to be rewritten later to support objects spawned in the world environment. + * The maximum amount of ammunition that can be stored in a single box is 65535 units. + * Regardless of the interface, however, the number will never be fully visible. + * Only the first three digits or first four digits may be represented. * @param magazine the number of rounds available + * @see WeaponData */ case class AmmoBoxData(magazine : Int ) extends ConstructorData { @@ -21,24 +24,25 @@ case class AmmoBoxData(magazine : Int * @see ConstructorData.bitsize * @return the number of bits necessary to represent this object */ - override def bitsize : Long = 39L + override def bitsize : Long = 40L } object AmmoBoxData extends Marshallable[AmmoBoxData] { implicit val codec : Codec[AmmoBoxData] = ( uintL(8) :: - ignore(15) :: - ("magazine" | uint16L) + uintL(15) :: + ("magazine" | uint16L) :: + bool ).exmap[AmmoBoxData] ( { - case 0xC8 :: _ :: mag :: HNil => + case 0xC8 :: 0 :: mag :: false :: HNil => Attempt.successful(AmmoBoxData(mag)) - case x :: _ :: _ :: HNil => - Attempt.failure(Err("looking for 200, found "+x)) + case a :: b :: _ :: d :: HNil => + Attempt.failure(Err("illegal ammunition data format")) }, { case AmmoBoxData(mag) => - Attempt.successful(0xC8 :: () :: mag :: HNil) + Attempt.successful(0xC8 :: 0 :: mag :: false:: HNil) } ) @@ -54,7 +58,7 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] { case Some(x) => Attempt.successful(x.asInstanceOf[AmmoBoxData]) case _ => - Attempt.failure(Err("")) + Attempt.failure(Err("can not encode ammo box data")) } ) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala index c58046e0..019ed856 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala @@ -7,24 +7,132 @@ import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} +/** + * A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.
+ *
+ * This partition of the data stream contains information used to represent how the player's avatar is presented. + * This appearance can be considered the avatar's obvious points beyond experience levels. + * It does not include passive exo-suit upgrades, battle rank 24 cosmetics, special postures, or current equipment. + * Those will occur later back in the main data stream.
+ *
+ * This base length of this stream is __430__ known bits, excluding the length of the name and the padding on that name. + * Of that, __203__ bits are perfectly unknown in significance. + *
+ * Faction:
+ * `0 - Terran Republic`
+ * `1 - New Conglomerate`
+ * `2 - Vanu Sovereignty`
+ *
+ * Exo-suit:
+ * `0 - Agile`
+ * `1 - Refinforced`
+ * `2 - Mechanized Assault`
+ * `3 - Infiltration`
+ * `4 - Standard`
+ *
+ * Sex:
+ * `0 - invalid`
+ * `1 - Male`
+ * `2 - Female`
+ * `3 - invalid`
+ *
+ * Voice:
+ * `    MALE      FEMALE`
+ * `0 - no voice  no voice`
+ * `1 - male_1    female_1`
+ * `2 - male_2    female_2`
+ * `3 - male_3    female_3`
+ * `4 - male_4    female_4`
+ * `5 - male_5    female_5`
+ * `6 - female_1  no voice`
+ * `7 - female_2  no voice` + * @param pos the position of the character in the world environment (in three coordinates) + * @param objYaw the angle with respect to the horizon towards which the object's front is facing; + * every `0x1` is 2.813 degrees counter clockwise from North; + * every `0x10` is 45-degrees; + * it wraps at `0x0` == `0x80` == North + * (note: references the avatar as a game object?) + * @param faction the empire to which the avatar belongs; + * the value scale is different from `PlanetSideEmpire` + * @param bops whether or not this avatar is enrolled in Black OPs + * @param unk1 na; + * defaults to 4 + * @param name the wide character name of the avatar, minimum of two characters + * @param exosuit the type of exosuit the avatar will be depicted in; + * for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits + * @param sex whether the avatar is male or female + * @param face1 the avatar's face, as by column number on the character creation screen + * @param face2 the avatar's face, as by row number on the character creation screen + * @param voice the avatar's voice selection + * @param unk2 na + * @param unk3 na; + * can be missing from the stream under certain conditions; + * see next + * @param unk4 na; + * can be missing from the stream under certain conditions; + * see previous + * @param unk5 na; + * defaults to `0x8080` + * @param unk6 na; + * defaults to `0xFFFF`; + * may be `0x0` + * @param unk7 na; + * defaults to 2 + * @param viewPitch the angle with respect to the sky and the ground towards which the avatar is looking; + * only supports downwards view angles; + * `0x0` is forwards-facing; + * `0x20` to `0xFF` is downwards-facing + * @param viewYaw the angle with respect to the horizon towards which the avatar is looking; + * every `0x1` is 2.813 degrees counter clockwise from North; + * every `0x10` is 45-degrees; + * it wraps at `0x0` == `0x80` == North + * @param unk8 na + * @param ribbons the four merit commendation ribbon medals + */ case class CharacterAppearanceData(pos : Vector3, objYaw : Int, faction : Int, bops : Boolean, + unk1 : Int, name : String, exosuit : Int, sex : Int, face1 : Int, face2 : Int, voice : Int, - unk1 : Int, //0x8080 - unk2 : Int, //0xFFFF or 0x0 - unk3 : Int, //2 + unk2 : Int, + unk3 : Int, + unk4 : Int, + unk5 : Int, + unk6 : Int, + unk7 : Int, viewPitch : Int, viewYaw : Int, - ribbons : RibbonBars) + unk8 : Int, + ribbons : RibbonBars) extends StreamBitSize { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = { + //TODO ongoing analysis, this value will be subject to change + 430L + CharacterData.stringBitSize(name, 16) + CharacterAppearanceData.namePadding + } +} object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { + /** + * Get the padding of the avatar's name. + * The padding will always be a number 0-7. + * @return the pad length in bits + */ + private def namePadding : Int = { + //TODO the parameters for this function are not correct + //TODO the proper padding length should reflect all variability in the substream prior to this point + 4 + } + implicit val codec : Codec[CharacterAppearanceData] = ( ("pos" | Vector3.codec_pos) :: ignore(16) :: @@ -32,24 +140,29 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { ignore(1) :: ("faction" | uintL(2)) :: ("bops" | bool) :: - ignore(20) :: - ("name" | PacketHelpers.encodedWideStringAligned(4)) :: + ("unk1" | uint4L) :: + ignore(16) :: + ("name" | PacketHelpers.encodedWideStringAligned( namePadding )) :: ("exosuit" | uintL(3)) :: ignore(2) :: ("sex" | uintL(2)) :: ("face1" | uint4L) :: ("face2" | uint4L) :: ("voice" | uintL(3)) :: - ignore(22) :: - ("unk1" | uint16L) :: + ("unk2" | uintL(2)) :: + ignore(4) :: + ("unk3" | uint8L) :: + ("unk4" | uint8L) :: + ("unk5" | uint16L) :: ignore(42) :: - ("unk2" | uint16L) :: + ("unk6" | uint16L) :: ignore(30) :: - ("unk3" | uint4L) :: + ("unk7" | uint4L) :: ignore(24) :: ("viewPitch" | uint8L) :: ("viewYaw" | uint8L) :: - ignore(10) :: + ("unk8" | uint4L) :: + ignore(6) :: ("ribbons" | RibbonBars.codec) ).as[CharacterAppearanceData] } @@ -58,143 +171,73 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { * A representation of the avatar portion of `ObjectCreateMessage` packet data.
*
* This object is huge, representing the quantity of densely-encoded data in its packet. - * Although the actual organization is ill-defined, the packet can be divided into seven parts. - * The first part maintains information about the avatar as a game object in the game environment. - * The second part maintains information as an ongoing representation of the avatar. - * This includes fixed details like name and gender, though it also includes mutable aspects like exosuit type. - * The third part maintains information about career in the game. - * The fourth part maintains miscellaneous status and pose information. - * The fifth part maintains part of the statistical information about participation in the game. - * The sixth part maintains a stream of typically zero'd unknown information. - * The seventh part maintains the inventory. - * The fifth and seventh parts can inflate the size of packet significantly due to their encoding. - * The fifth, in particular, is string data that can number in the hundreds of strings(!).
+ * Certain bits, when set or unset, introduce or remove other bits from the packet data as well. + * (As in: flipping a bit may create room or negate other bits from somewhere else in the data stream. + * Not accounting for this new pattern of bits will break decoding and encoding.) + * Due to the very real concern that bloating the constructor for this object with parameters could break the `apply` method, + * parameters will often be composed of nested case objects that contain a group of formal parameters. + * There are lists of byte-aligned `Strings` later-on in the packet data that will need access to these objects to calculate padding length.
*
- * Ignoring the strings, lists of strings, and the inventory, the base length of the packet is currently __1138__ bits. - * Some undefined bits in the packet can change the length of the packet by being set or unset. - * This will mess with the encoding and the decoding of later fields. - * Any data that is padded for byte-alignment will also have its padding adjusted. - * Each string adds either 8 or 16, plus an additional 8 or 16 per the number of characters. - * For the name, that's 16 per character, a minimum of two characters, plus the (current) padding. - * for the first time events and tutorials, that's 8 per character, plus the (current) padding of the first entry. - * For the first time events and tutorials, however, the size of the list is always a 32-bit number. - * The formal inventory entries are preceded by 1 absolute bit.
+ * The first subdivision of parameters concerns the avatar's basic aesthetics, mostly. + * (No other parts of the data divided up yet.) + * The final sections include two lists of accredited activity performed/completed by the player. + * The remainder of the data, following after that, can be read straight, up to and through the inventory.
*
- * The adjusted base length is therefore __1203__ bits (1138 + 32 + 32 + 1). - * Of that, __720__ bits are unknown. - * Including the values that are defaulted, __831__ bits are perfectly unknown. - * This data is accurate as of 2016-12-07.
- *
- * Faction:
- * `0 - Terran Republic`
- * `1 - New Comglomerate`
- * `2 - Vanu Sovereignty`
- *
- * Exosuit:
- * `0 - Agile`
- * `1 - Refinforced`
- * `2 - Mechanized Assault`
- * `3 - Infiltration`
- * `4 - Standard`
- *
- * Sex:
- * `1 - Male`
- * `2 - Female`
- *
- * Voice:
- * `    MALE      FEMALE`
- * `0 - No voice  No voice`
- * `1 - Male_1    Female_1`
- * `2 - Male_2    Female_2`
- * `3 - Male_3    Female_3`
- * `4 - Male_4    Female_4`
- * `5 - Male_5    Female_5`
- * `6 - Female_1  No voice`
- * `7 - Female_2  No voice` -// * @param pos the position of the character in the world environment (in three coordinates) -// * @param objYaw the angle with respect to the horizon towards which the object's front is facing; -// * every `0x1` is 2.813 degrees counter clockwise from North; -// * every `0x10` is 45-degrees; -// * it wraps at `0x80` -// * (note: references the avatar as a game object?) -// * @param faction the empire to which the avatar belongs; -// * the value scale is different from `PlanetSideEmpire`; -// * @param bops whether or not this avatar is enrolled in Black OPs -// * @param name the wide character name of the avatar -// * @param exosuit the type of exosuit the avatar will be depicted in; -// * for Black OPs, the agile exosuit and the reinforced exosuit are replaced with the Black OPs exosuits -// * @param sex whether the avatar is male or female -// * @param face1 the avatar's face, as by column number on the character creation screen -// * @param face2 the avatar's face, as by row number on the character creation screen -// * @param voice the avatar's voice selection -// * @param unk1 na; -// * defaults to `0x8080` -// * @param unk2 na; -// * defaults to `0xFFFF`; -// * may be `0x0` -// * @param unk3 na; -// * defaults to 2 -// * @param viewPitch the angle with respect to the horizon towards which the avatar is looking; -// * only supports downwards view angles; -// * `0x0` is forwards-facing; -// * `0x20` to `0xFF` is downwards-facing -// * @param viewYaw the angle with respect to the ground directions towards which the avatar is looking; -// * every `0x1` is 2.813 degrees counter clockwise from North; -// * every `0x10` is 45-degrees; -// * it wraps at `0x80` -// * @param ribbons the four merit commendation ribbon medals displayed on the avatar's left pauldron -// * @param healthMax for "x / y" of hitpoints, this is the avatar's 'y' value; -// * range is 0-65535 -// * @param health for "x / y" of hitpoints, this is the avatar's 'x' value; -// * range is 0-65535 -// * @param armor for "x / y" of armor points, this is the avatar's 'x' value; -// * range is 0-65535; -// * the avatar's "y" armor points is tied to their exosuit type -// * @param unk4 na; -// * defaults to 1 -// * @param unk5 na; -// * defaults to 7 -// * @param unk6 na; -// * defaults to 7 -// * @param staminaMax for "x / y" of stamina points, this is the avatar's 'y' value; -// * range is 0-65535 -// * @param stamina for "x / y" of stamina points, this is the avatar's 'x' value; -// * range is 0-65535 -// * @param unk7 na; -// * defaults to 28 -// * @param unk8 na; -// * defaults to 4 -// * @param unk9 na; -// * defaults to 44 -// * @param unk10 na; -// * defaults to 84 -// * @param unk11 na; -// * defaults to 104 -// * @param unk12 na; -// * defaults to 1900 -// * @param firstTimeEvent_length the total number of first time events performed by this avatar -// * @param firstTimeEvent_firstEntry the separated "first entry" of the list of first time events performed by this avatar -// * @param firstTimeEvent_list the list of first time events performed by this avatar -// * @param tutorial_length the total number of tutorials completed by this avatar -// * @param tutorial_firstEntry the separated "first entry" of the list of tutorials completed by this avatar -// * @param tutorial_list the list of tutorials completed by this avatar -// * @param inventory the avatar's inventory + * The base length of the stream is currently __1138__ bits, excluding `List`s and `String`s and inventory. + * Of that, __831__ bits are perfectly unknown. + * @param appearance data about the avatar's basic aesthetics + * @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value; + * range is 0-65535 + * @param health for `x / y` of hitpoints, this is the avatar's `x` value; + * range is 0-65535 + * @param armor for `x / y` of armor points, this is the avatar's `x` value; + * range is 0-65535; + * the avatar's `y` armor points is tied to their exo-suit type + * @param unk1 na; + * defaults to 1 + * @param unk2 na; + * defaults to 7 + * @param unk3 na; + * defaults to 7 + * @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value; + * range is 0-65535 + * @param stamina for `x / y` of stamina points, this is the avatar's `x` value; + * range is 0-65535 + * @param unk4 na; + * defaults to 28 + * @param unk5 na; + * defaults to 4 + * @param unk6 na; + * defaults to 44 + * @param unk7 na; + * defaults to 84 + * @param unk8 na; + * defaults to 104 + * @param unk9 na; + * defaults to 1900 + * @param firstTimeEvents the list of first time events performed by this avatar; + * the size field is a 32-bit number; + * the first entry may be padded + * @param tutorials the list of tutorials completed by this avatar; + * the size field is a 32-bit number; + * the first entry may be padded + * @param inventory the avatar's inventory */ case class CharacterData(appearance : CharacterAppearanceData, healthMax : Int, health : Int, armor : Int, - unk4 : Int, //1 - unk5 : Int, //7 - unk6 : Int, //7 + unk1 : Int, //1 + unk2 : Int, //7 + unk3 : Int, //7 staminaMax : Int, stamina : Int, - unk7 : Int, //28 - unk8 : Int, //4 - unk9 : Int, //44 - unk10 : Int, //84 - unk11 : Int, //104 - unk12 : Int, //1900 + unk4 : Int, //28 + unk5 : Int, //4 + unk6 : Int, //44 + unk7 : Int, //84 + unk8 : Int, //104 + unk9 : Int, //1900 firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData @@ -205,25 +248,20 @@ case class CharacterData(appearance : CharacterAppearanceData, * @return the number of bits necessary to represent this object */ override def bitsize : Long = { -// //represents static fields (includes medals.bitsize) -// val base : Long = 1138L //TODO ongoing analysis, this value will be subject to change -// //name -// val nameSize : Long = CharacterData.stringBitSize(appearance.name, 16) + 4L //plus the current padding -// //fte_list -// var eventListSize : Long = 32L -// if(firstTimeEvent_firstEntry.isDefined) { -// eventListSize += CharacterData.stringBitSize(firstTimeEvent_firstEntry.get) + 5L //plus the current padding -// for(str <- firstTimeEvent_list) { -// eventListSize += CharacterData.stringBitSize(str) -// } -// } -// //tutorial list -// var tutorialListSize : Long = 32L -// for(str <- tutorial_list) { -// tutorialListSize += CharacterData.stringBitSize(str) -// } -// base + nameSize + eventListSize + tutorialListSize + inventory.bitsize - 0L + //TODO ongoing analysis, this value will be subject to change + //fte list + val fteLen = firstTimeEvents.size + var eventListSize : Long = 32L + CharacterData.ftePadding(fteLen) + for(str <- firstTimeEvents) { + eventListSize += CharacterData.stringBitSize(str) + } + //tutorial list + val tutLen = tutorials.size + var tutorialListSize : Long = 32L + CharacterData.tutPadding(fteLen, tutLen) + for(str <- tutorials) { + tutorialListSize += CharacterData.stringBitSize(str) + } + 708L + appearance.bitsize + eventListSize + tutorialListSize + inventory.bitsize } } @@ -236,20 +274,45 @@ object CharacterData extends Marshallable[CharacterData] { * defaults to the standard 8-bits * @return the size in bits */ - private def stringBitSize(str : String, width : Int = 8) : Long = { + def stringBitSize(str : String, width : Int = 8) : Long = { val strlen = str.length val lenSize = if(strlen > 127) 16L else 8L lenSize + (strlen * width) } + /** + * Get the padding of the first entry in the first time events list. + * The padding will always be a number 0-7. + * @param len the length of the list + * @return the pad length in bits + */ private def ftePadding(len : Long) : Int = { - //TODO determine how this should be padded better - 5 + //TODO the parameters for this function are not correct + //TODO the proper padding length should reflect all variability in the stream prior to this point + if(len > 0) { + 5 + } + else + 0 } - private def tutListPadding(len : Long) : Int = { - //TODO determine how this should be padded when len == 0 - if(len > 0) 0 else 0 + /** + * Get the padding of the first entry in the completed tutorials list. + * The padding will always be a number 0-7.
+ *
+ * The tutorials list follows the first time event list and that contains byte-aligned strings too. + * While there will be more to the padding, this other list is important. + * Any elements in that list causes the automatic byte-alignment of this list's first entry. + * @param len the length of the list + * @return the pad length in bits + */ + private def tutPadding(len : Long, len2 : Long) : Int = { + if(len > 0) //automatic alignment from previous List + 0 + else if(len2 > 0) //need to align for elements + 1 + else //both lists are empty + 0 } implicit val codec : Codec[CharacterData] = ( @@ -260,25 +323,25 @@ object CharacterData extends Marshallable[CharacterData] { ignore(1) :: ("armor" | uint16L) :: ignore(9) :: - ("unk4" | uint8L) :: + ("unk1" | uint8L) :: ignore(8) :: - ("unk5" | uint4L) :: - ("unk6" | uintL(3)) :: + ("unk2" | uint4L) :: + ("unk3" | uintL(3)) :: ("staminaMax" | uint16L) :: ("stamina" | uint16L) :: ignore(149) :: - ("unk7" | uint16L) :: + ("unk4" | uint16L) :: + ("unk5" | uint8L) :: + ("unk6" | uint8L) :: + ("unk7" | uint8L) :: ("unk8" | uint8L) :: - ("unk9" | uint8L) :: - ("unk10" | uint8L) :: - ("unk11" | uint8L) :: - ("unk12" | uintL(12)) :: + ("unk9" | uintL(12)) :: ignore(19) :: (("firstTimeEvent_length" | uint32L) >>:~ { len => conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) :: ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: (("tutorial_length" | uint32L) >>:~ { len2 => - conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutListPadding(len) )) :: + conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutPadding(len, len2) )) :: ("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) :: ignore(207) :: ("inventory" | InventoryData.codec) @@ -323,7 +386,7 @@ object CharacterData extends Marshallable[CharacterData] { case Some(x) => Attempt.successful(x.asInstanceOf[CharacterData]) case _ => - Attempt.failure(Err("")) + Attempt.failure(Err("can not encode character data")) } ) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala index a62d0a20..dab33755 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala @@ -7,23 +7,15 @@ package net.psforever.packet.game.objectcreate * Children of this class are expected to be able to translate through `scodec` operations into packet data.
*
* The object data is uncoupled from the object class as multiple classes use the same format for their data. - * For example, both the Suppressor and the Gauss use a weapon data format. - * For example, both 9mm Bullets and energy cells use am ammunition data format. + * For example, both the Suppressor and the Gauss will use a "weapon data" format. + * For example, both 9mm bullets and energy cells will use an "ammunition data" format. */ -abstract class ConstructorData() { - /** - * Performs a "sizeof()" analysis of the given object. - * @return the number of bits necessary to represent this object; - * reflects the `Codec` definition rather than the parameter fields; - * defaults to `0L` - */ - def bitsize : Long = 0L -} +abstract class ConstructorData extends StreamBitSize object ConstructorData { /** * This pattern is intended to provide common conversion between all of the `Codec`s of the children of this class. - * The casting will be performed through use of `exmap`. + * The casting will be performed through use of `exmap` in the child class. */ type genericPattern = Option[ConstructorData] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala index 4b6b5d41..f634a599 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala @@ -8,51 +8,48 @@ import scodec.codecs._ import shapeless.{::, HNil} /** - * The same kind of data as required for a formal `ObjectCreateMessage` but with a required and implicit parent relationship. - * Some data preceding this entry will clarify the existence of the parent.
+ * Similar fields as required for a formal `ObjectCreateMessage` but with a required but implicit parent relationship. + * Specifically, the purpose of the packet is to start to define a new object within the definition of a previous object. + * This prior object will clarify the identity of the parent object that owns the given `parentSlot`.
*
- * As indicated, an `InternalSlot` object is not a top-level object. - * This is true in relation between one object and another, as well as in how this object is sorted in the `ObjectCreateMessage` data. - * The data outlined by this class encompasses the same kind as the outer-most `ObjectCreateMessage`. - * By contrast, this object always has a dedicated parent object and a known slot to be attached to that parent. - * It's not optional. + * An `InternalSlot` object is not a top-level object. + * Extra effort should be made to ensure the user does not have to directly construct an `InternalSlot`. * @param objectClass the code for the type of object being constructed * @param guid the GUID this object will be assigned * @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent * @param obj the data used as representation of the object to be constructed + * @see ObjectClass.selectDataCodec */ case class InternalSlot(objectClass : Int, guid : PlanetSideGUID, parentSlot : Int, - obj : ConstructorData) { + obj : ConstructorData) extends StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize * @return the number of bits necessary to represent this object */ - def bitsize : Long = { - val first : Long = if(parentSlot > 127) 44L else 36L - val second : Long = obj.bitsize - first + second + override def bitsize : Long = { + val base : Long = if(parentSlot > 127) 43L else 35L + base + obj.bitsize } } object InternalSlot extends Marshallable[InternalSlot] { implicit val codec : Codec[InternalSlot] = ( - ignore(1) :: //TODO determine what this bit does - (("objectClass" | uintL(11)) >>:~ { obj_cls => - ("guid" | PlanetSideGUID.codec) :: - ("parentSlot" | PacketHelpers.encodedStringSize) :: - ("obj" | ObjectClass.selectDataCodec(obj_cls)) - }) + ("objectClass" | uintL(11)) >>:~ { obj_cls => + ("guid" | PlanetSideGUID.codec) :: + ("parentSlot" | PacketHelpers.encodedStringSize) :: + ("obj" | ObjectClass.selectDataCodec(obj_cls)) //it's fine for this call to fail + } ).xmap[InternalSlot] ( { - case _ :: cls :: guid :: slot :: Some(obj) :: HNil => + case cls :: guid :: slot :: Some(obj) :: HNil => InternalSlot(cls, guid, slot, obj) }, { case InternalSlot(cls, guid, slot, obj) => - () :: cls :: guid :: slot :: Some(obj) :: HNil + cls :: guid :: slot :: Some(obj) :: HNil } ).as[InternalSlot] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala index 4a11e7d2..d2a597e7 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala @@ -1,20 +1,27 @@ // Copyright (c) 2016 PSForever.net to present package net.psforever.packet.game.objectcreate -import net.psforever.packet.Marshallable +import net.psforever.packet.{Marshallable, PacketHelpers} import scodec.Codec import scodec.codecs._ -import shapeless.{::,HNil} +import shapeless.{::, HNil} /** * A representation of the inventory portion of `ObjectCreateMessage` packet data for avatars.
*
- * Unfortunately, the inventory is a fail-fast greedy thing. - * Any format discrepancies will cause it to fail and that will cause character encoding to fail as well. - * Care should be taken that all possible item encodings are representable. + * The inventory is a temperamental thing. + * Items placed into the inventory must follow their proper encoding schematics to the letter. + * No values are allowed to be misplaced and no unexpected regions of data can be discovered. + * If there is even a minor failure, the whole of the inventory will fail to translate.
+ *
+ * Exploration:
+ * 4u of ignored bits are tagged onto the end of this field for purposes of finding four missing bits of stream length. + * The rest of the encoding is valid. + * Conditions must certainly decide whether these bits are present or not. * @param unk1 na; - * always `true` to mark the start of the inventory data? + * `true` to mark the start of the inventory data? * @param unk2 na + * @param unk3 na * @param contents the actual items in the inventory; * holster slots are 0-4; * an inaccessible slot is 5; @@ -22,38 +29,42 @@ import shapeless.{::,HNil} */ case class InventoryData(unk1 : Boolean, unk2 : Boolean, - contents : Vector[InventoryItem]) { + unk3 : Boolean, + contents : List[InventoryItem]) extends StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize * @return the number of bits necessary to represent this object */ - def bitsize : Long = { - //two booleans and the 8-bit length field - val first : Long = 10L + override def bitsize : Long = { + //three booleans, the 4u and the 8u length field + val base : Long = 15L //length of all items in inventory - var second : Long = 0L + var invSize : Long = 0L for(item <- contents) { - second += item.bitsize + invSize += item.bitsize } - first + second + base + invSize } } object InventoryData extends Marshallable[InventoryData] { implicit val codec : Codec[InventoryData] = ( ("unk1" | bool) :: - ("len" | uint8L) :: - ("unk2" | bool) :: - ("contents" | vector(InventoryItem.codec)) - ).xmap[InventoryData] ( + (("len" | uint8L) >>:~ { len => + ("unk2" | bool) :: + ("unk3" | bool) :: + ("contents" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) :: + ignore(4) + }) + ).xmap[InventoryData] ( { - case u1 :: _ :: u2 :: vector :: HNil => - InventoryData(u1, u2, vector) + case u1 :: _ :: u2 :: u3 :: ctnt :: _ :: HNil => + InventoryData(u1, u2, u3, ctnt) }, { - case InventoryData(u1, u2, vector) => - u1 :: vector.length :: u2 :: vector :: HNil + case InventoryData(u1, u2, u3, ctnt) => + u1 :: ctnt.size :: u2 :: u3 :: ctnt :: () :: HNil } ).as[InventoryData] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala index 4c82dbee..90fc0fc0 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala @@ -7,28 +7,22 @@ import scodec.Codec import scodec.codecs._ /** - * Represent an item in inventory.
+ * A representation of an item in an avatar's inventory. + * Reliance on `InternalSlot` indicates that this item is applicable to the same implicit parental relationship. + * (That is, its parent object will be clarified earlier on in the data stream.) + * Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible.
*
- * Note the use of `InternalSlot` to indicate the implicit parent ownership of the resulting item. - * Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible. + * This intermediary object is primarily intended to mask external use of `InternalSlot`. * @param item the object in inventory - * @param na the user should not have to worry about this potential bit; - * it follows after weapon entries, allegedly + * @see InternalSlot */ -case class InventoryItem(item : InternalSlot, - na : Option[Boolean] = None) { +case class InventoryItem(item : InternalSlot) extends StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize * @return the number of bits necessary to represent this object */ - def bitsize : Long = { - //item - val first : Long = item.bitsize - //trailing bit - val second : Long = if(na.isDefined) 1L else 0L - first + second - } + override def bitsize : Long = item.bitsize } object InventoryItem extends Marshallable[InventoryItem] { @@ -40,22 +34,10 @@ object InventoryItem extends Marshallable[InventoryItem] { * @param obj the constructor data * @return an InventoryItem */ - def apply(objClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : ConstructorData) : InventoryItem = { - val isWep = if(obj.isInstanceOf[WeaponData]) Some(false) else None - //TODO is this always Some(false)? - InventoryItem(InternalSlot(objClass, guid, parentSlot, obj), isWep) - } - - /** - * Determine whether the allocated item is a weapon. - * @param itm the inventory item - * @return true, if the item is a weapon; false, otherwise - */ - def wasWeapon(itm : InternalSlot) : Boolean = itm.obj.isInstanceOf[WeaponData] + def apply(objClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : ConstructorData) : InventoryItem = + InventoryItem(InternalSlot(objClass, guid, parentSlot, obj)) implicit val codec : Codec[InventoryItem] = ( - ("item" | InternalSlot.codec) >>:~ { item => - conditional(wasWeapon(item), bool).hlist - } + "item" | InternalSlot.codec ).as[InventoryItem] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala index 252b1468..995ee934 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -9,8 +9,9 @@ import scala.annotation.switch /** * A reference between all object class codes and the name of the object they represent.
*
- * Object classes compose a number between `0` and (probably) `2047`, always translating into an 11-bit value. + * Object classes compose a number between 0 and (probably) 2047, always translating into an 11-bit value. * They are recorded as little-endian hexadecimal values here. + * In `scodec` terms, that's a `uintL(11)` or `uintL(0xB)`. */ object ObjectClass { //character @@ -22,7 +23,7 @@ object ObjectClass { final val JAMMER_GRENADE_AMMO = 0x1A1 final val FORCE_BLADE_AMMO = 0x21C final val PLASMA_GRENADE_AMMO = 0x2A9 - final val BUCKSHOT = 0x2F3 //TODO apply internal name + final val BUCKSHOT = 0x2F3 //TODO apply internal name, eventually //weapons final val SUPPRESSOR = 0x34D final val BEAMER = 0x8C @@ -35,7 +36,7 @@ object ObjectClass { final val MEDKIT = 0x218 final val REK = 0x2D8 //unknown - final val SLOT_BLOCKER = 0x1C8 //strange item found in slot #5, between holsters and inventory + final val SLOT_BLOCKER = 0x1C8 //strange item found in inventory slot #5, between holsters and grid //TODO refactor this function into another object later /** @@ -46,7 +47,7 @@ object ObjectClass { * This pattern connects all `Codec`s back to the superclass `ConstructorData`. * The default case is a failure case for trying to either decode or encode an unknown class of object. * @param objClass the code for the type of object being constructed - * @return the `Codec` that handles the format of data for that particular item class, or a failing codec + * @return the `Codec` that handles the format of data for that particular item class, or a failing `Codec` */ def selectDataCodec(objClass : Int) : Codec[ConstructorData.genericPattern] = { (objClass : @switch) match { @@ -72,11 +73,11 @@ object ObjectClass { case _ => conditional(false, bool).exmap[ConstructorData.genericPattern] ( { case None | _ => - Attempt.failure(Err("decoding unknown object class - "+objClass)) + Attempt.failure(Err("decoding unknown object class")) }, { case None | _ => - Attempt.failure(Err("encoding unknown object class - "+objClass)) + Attempt.failure(Err("encoding unknown object class")) } ) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala index 7f7adb30..b2c32a2b 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala @@ -8,7 +8,9 @@ import shapeless.{::, HNil} /** * A representation of the REK portion of `ObjectCreateMessage` packet data. - * When alone, this data will help construct the "tool" called a Remote Electronics Kit. + * This data will help construct the "tool" called a Remote Electronics Kit.
+ *
+ * Of note is the first portion of the data which resembles the `WeaponData` format. * @param unk na */ case class REKData(unk : Int @@ -18,28 +20,28 @@ case class REKData(unk : Int * @see ConstructorData.bitsize * @return the number of bits necessary to represent this object */ - override def bitsize : Long = 72L + override def bitsize : Long = 67L } object REKData extends Marshallable[REKData] { implicit val codec : Codec[REKData] = ( ("unk" | uint4L) :: uint4L :: - ignore(20) :: + uintL(20) :: uint4L :: - ignore(16) :: + uintL(16) :: uint4L :: - ignore(20) + uintL(15) ).exmap[REKData] ( { - case code :: 8 :: _ :: 2 :: _ :: 8 :: _ :: HNil => + case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil => Attempt.successful(REKData(code)) - case _ :: x :: _ :: y :: _ :: z :: _ :: HNil => - Attempt.failure(Err("looking for 8-2-8 pattern, found %d-%d-%d".format(x,y,z))) //TODO I actually don't know what of this is actually important + case code :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => + Attempt.failure(Err("illegal rek data format")) }, { case REKData(code) => - Attempt.successful(code :: 8 :: () :: 2 :: () :: 8 :: () :: HNil) + Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil) } ).as[REKData] @@ -55,7 +57,7 @@ object REKData extends Marshallable[REKData] { case Some(x) => Attempt.successful(x.asInstanceOf[REKData]) case _ => - Attempt.failure(Err("")) + Attempt.failure(Err("can not encode rek data")) } ) } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala index 2517be7b..e4dee136 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala @@ -10,22 +10,23 @@ import scodec.codecs._ * These are the medals players wish to brandish on their left pauldron.
*
* All merit commendation ribbons are represented by a 32-bit signature. - * The default "no-ribbon" value is `0xFFFFFFFF`. + * The default "no-ribbon" value is `0xFFFFFFFF`, although some illegal values will also work. + * The term of service ribbon can not be modified by the user and will apply itself to its slot automatically when valid. * @param upper the "top" configurable merit ribbon * @param middle the central configurable merit ribbon * @param lower the lower configurable merit ribbon - * @param tos the automatic top-most term of service merit ribbon + * @param tos the top-most term of service merit ribbon */ case class RibbonBars(upper : Long = 0xFFFFFFFFL, middle : Long = 0xFFFFFFFFL, lower : Long = 0xFFFFFFFFL, - tos : Long = 0xFFFFFFFFL) { + tos : Long = 0xFFFFFFFFL) extends StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize * @return the number of bits necessary to represent this object */ - def bitsize : Long = 128L + override def bitsize : Long = 128L } object RibbonBars extends Marshallable[RibbonBars] { diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala new file mode 100644 index 00000000..9f9d562a --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala @@ -0,0 +1,17 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +/** + * Apply this trait to a class that needs to have its size in bits calculated. + */ +trait StreamBitSize { + /** + * Performs a "sizeof()" analysis of the given object. + * The calculation reflects the `scodec Codec` definition rather than the explicit parameter fields. + * For example, an `Int` is normally a 32-bit number; + * when parsed with a `uintL(7)`, it's length will be considered 7u. + * @return the number of bits necessary to represent this object; + * defaults to `0L` + */ + def bitsize : Long = 0L +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala index d692b20d..175b2547 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala @@ -9,10 +9,14 @@ import shapeless.{::, HNil} /** * A representation of the weapon portion of `ObjectCreateMessage` packet data. - * When alone, this data will help construct a "weapon" such as Suppressor.
+ * This data will help construct a "weapon" such as a Suppressor or a Gauss.
*
- * The data for the weapon also nests required default ammunition data. - * Where the ammunition is loaded is considered the "first slot." + * The data for the weapons nests information for the default (current) type and number of ammunition in its magazine. + * This ammunition data essentially is the weapon's magazine as numbered slots. + * Having said that, this format only handles one type of ammunition at a time. + * Any weapon that has two types of ammunition simultaneously loaded, e.g., a Punisher, must be handled with another `Codec`. + * This functionality is unrelated to a weapon that switches ammunition type; + * a weapon with that behavior is handled perfectly fine using this `case class`. * @param unk na * @param ammo data regarding the currently loaded ammunition type and quantity * @see AmmoBoxData @@ -25,7 +29,7 @@ case class WeaponData(unk : Int, * @see AmmoBoxData.bitsize * @return the number of bits necessary to represent this object */ - override def bitsize : Long = 59L + ammo.bitsize + override def bitsize : Long = 61L + ammo.bitsize } object WeaponData extends Marshallable[WeaponData] { @@ -47,21 +51,23 @@ object WeaponData extends Marshallable[WeaponData] { implicit val codec : Codec[WeaponData] = ( ("unk" | uint4L) :: uint4L :: - ignore(20) :: + uintL(20) :: uint4L :: - ignore(16) :: + uintL(16) :: uintL(11) :: - ("ammo" | InternalSlot.codec) + bool :: + ("ammo" | InternalSlot.codec) :: + bool ).exmap[WeaponData] ( { - case code :: 8 :: _ :: 2 :: _ :: 0x2C0 :: ammo :: HNil => + case code :: 8 :: 0 :: 2 :: 0 :: 0x2C0 :: false :: ammo :: false :: HNil => Attempt.successful(WeaponData(code, ammo)) - case _ :: x :: _ :: y :: _ :: z :: _ :: HNil => - Attempt.failure(Err("looking for 8-2-704 pattern, found %d-%d-%d".format(x,y,z))) //TODO I actually don't know what of this is actually important + case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => + Attempt.failure(Err("illegal weapon data format")) }, { case WeaponData(code, ammo) => - Attempt.successful(code :: 8 :: () :: 2 :: () :: 0x2C0 :: ammo :: HNil) + Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 0x2C0 :: false :: ammo :: false :: HNil) } ).as[WeaponData] @@ -77,7 +83,7 @@ object WeaponData extends Marshallable[WeaponData] { case Some(x) => Attempt.successful(x.asInstanceOf[WeaponData]) case _ => - Attempt.failure(Err("")) + Attempt.failure(Err("can not encode weapon data")) } ) } diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index 42afd7f5..b9b8da40 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -4,7 +4,7 @@ import java.net.{InetAddress, InetSocketAddress} import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ -import net.psforever.packet.game.objectcreate._ +import net.psforever.packet.game.objectcreate.{InventoryItem, _} import net.psforever.types._ import scodec.{Attempt, Err} import scodec.Attempt.Successful @@ -153,34 +153,14 @@ class GamePacketTest extends Specification { var string_inventoryItem = hex"46 04 C0 08 08 80 00 00 20 00 0C 04 10 29 A0 10 19 00 00 04 00 00" val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" - val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" - val invTest = hex"01 01 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 00 00" - val invTestWep = hex"23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 00 00" - - "InventoryTest" in { - val intSlot = InternalSlot.codec.decode(invTestWep.toBitVector.drop(1)).toOption - intSlot.isDefined mustEqual true - - val invData = InventoryItem.codec.decode(invTestWep.toBitVector.drop(1)).toOption - invData.isDefined mustEqual true - -// InventoryData.codec.decode(invTest.toBitVector.drop(7)) match { -// case Attempt.Successful(x) => -// x.value.unk1 equals true -// x.value.size mustEqual 1 -// x.value.unk2 mustEqual false -// //x.value.inv.head.item.objectClass mustEqual 0x8C -// //x.value.inv.head.na mustEqual false -// case Attempt.Failure(x) => -// x.message mustEqual "" -// } - } + val string_rek = hex"18 97000000 2580 6C2 9F05 81 48000002000080000" + val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" "decode (2)" in { //an invalid bit representation will fail to turn into an object PacketCoding.DecodePacket(packet2).require match { case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => - len mustEqual 248 //60 + 188 + len mustEqual 248 cls mustEqual 121 guid mustEqual PlanetSideGUID(2497) parent mustEqual None @@ -190,6 +170,60 @@ class GamePacketTest extends Specification { } } + "decode (9mm)" in { + PacketCoding.DecodePacket(string_9mm).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 124 + cls mustEqual 28 + guid mustEqual PlanetSideGUID(1280) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(75) + parent.get.slot mustEqual 33 + data.isDefined mustEqual true + data.get.asInstanceOf[AmmoBoxData].magazine mustEqual 50 + case default => + ko + } + } + + "decode (gauss)" in { + PacketCoding.DecodePacket(string_gauss).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 220 + cls mustEqual 345 + guid mustEqual PlanetSideGUID(1465) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(75) + parent.get.slot mustEqual 2 + data.isDefined mustEqual true + val obj_wep = data.get.asInstanceOf[WeaponData] + obj_wep.unk mustEqual 4 + val obj_ammo = obj_wep.ammo + obj_ammo.objectClass mustEqual 28 + obj_ammo.guid mustEqual PlanetSideGUID(1286) + obj_ammo.parentSlot mustEqual 0 + obj_ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30 + case default => + ko + } + } + + "decode (rek)" in { + PacketCoding.DecodePacket(string_rek).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 151 + cls mustEqual 0x2D8 + guid mustEqual PlanetSideGUID(1439) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(75) + parent.get.slot mustEqual 1 + data.isDefined mustEqual true + data.get.asInstanceOf[REKData].unk mustEqual 4 + case _ => + ko + } + } + "decode (character)" in { PacketCoding.DecodePacket(string_testchar).require match { case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => @@ -206,17 +240,22 @@ class GamePacketTest extends Specification { char.appearance.objYaw mustEqual 19 char.appearance.faction mustEqual 2 //vs char.appearance.bops mustEqual false + char.appearance.unk1 mustEqual 4 char.appearance.name mustEqual "IlllIIIlllIlIllIlllIllI" char.appearance.exosuit mustEqual 4 //standard char.appearance.sex mustEqual 2 //female char.appearance.face1 mustEqual 2 char.appearance.face2 mustEqual 9 char.appearance.voice mustEqual 1 //female 1 - char.appearance.unk1 mustEqual 0x8080 - char.appearance.unk2 mustEqual 0xFFFF - char.appearance.unk3 mustEqual 2 + char.appearance.unk2 mustEqual 3 + char.appearance.unk3 mustEqual 118 + char.appearance.unk4 mustEqual 30 + char.appearance.unk5 mustEqual 0x8080 + char.appearance.unk6 mustEqual 0xFFFF + char.appearance.unk7 mustEqual 2 char.appearance.viewPitch mustEqual 0xFF char.appearance.viewYaw mustEqual 0x6A + char.appearance.unk8 mustEqual 7 char.appearance.ribbons.upper mustEqual 0xFFFFFFFFL //none char.appearance.ribbons.middle mustEqual 0xFFFFFFFFL //none char.appearance.ribbons.lower mustEqual 0xFFFFFFFFL //none @@ -224,17 +263,17 @@ class GamePacketTest extends Specification { char.healthMax mustEqual 100 char.health mustEqual 100 char.armor mustEqual 50 //standard exosuit value - char.unk4 mustEqual 1 - char.unk5 mustEqual 7 - char.unk6 mustEqual 7 + char.unk1 mustEqual 1 + char.unk2 mustEqual 7 + char.unk3 mustEqual 7 char.staminaMax mustEqual 100 char.stamina mustEqual 100 - char.unk7 mustEqual 28 - char.unk8 mustEqual 4 - char.unk9 mustEqual 44 - char.unk10 mustEqual 84 - char.unk11 mustEqual 104 - char.unk12 mustEqual 1900 + char.unk4 mustEqual 28 + char.unk5 mustEqual 4 + char.unk6 mustEqual 44 + char.unk7 mustEqual 84 + char.unk8 mustEqual 104 + char.unk9 mustEqual 1900 char.firstTimeEvents.size mustEqual 4 char.firstTimeEvents.head mustEqual "xpe_sanctuary_help" char.firstTimeEvents(1) mustEqual "xpe_th_firemodes" @@ -243,7 +282,7 @@ class GamePacketTest extends Specification { char.tutorials.size mustEqual 0 char.inventory.unk1 mustEqual true char.inventory.unk2 mustEqual false - char.inventory.contents.length mustEqual 10 + char.inventory.contents.size mustEqual 10 val inventory = char.inventory.contents //0 inventory.head.item.objectClass mustEqual 0x8C //beamer @@ -306,45 +345,7 @@ class GamePacketTest extends Specification { inventory(9).item.objectClass mustEqual 0x2D8 //rek inventory(9).item.guid mustEqual PlanetSideGUID(88) inventory(9).item.parentSlot mustEqual 39 - //the rek has data but none worth testing here - case default => - ko - } - } - - "decode (9mm)" in { - PacketCoding.DecodePacket(string_9mm).require match { - case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => - len mustEqual 124 - cls mustEqual 28 - guid mustEqual PlanetSideGUID(1280) - parent.isDefined mustEqual true - parent.get.guid mustEqual PlanetSideGUID(75) - parent.get.slot mustEqual 33 - data.isDefined mustEqual true - data.get.asInstanceOf[AmmoBoxData].magazine mustEqual 50 - case default => - ko - } - } - - "decode (gauss)" in { - PacketCoding.DecodePacket(string_gauss).require match { - case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => - len mustEqual 220 - cls mustEqual 345 - guid mustEqual PlanetSideGUID(1465) - parent.isDefined mustEqual true - parent.get.guid mustEqual PlanetSideGUID(75) - parent.get.slot mustEqual 2 - data.isDefined mustEqual true - val obj_wep = data.get.asInstanceOf[WeaponData] - obj_wep.unk mustEqual 4 - val obj_ammo = obj_wep.ammo//.asInstanceOf[InternalSlot] - obj_ammo.objectClass mustEqual 28 - obj_ammo.guid mustEqual PlanetSideGUID(1286) - obj_ammo.parentSlot mustEqual 0 - obj_ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30 + //the rek has data but none worth testing here case default => ko } @@ -371,6 +372,60 @@ class GamePacketTest extends Specification { pkt mustEqual string_gauss } + + "encode (rek)" in { + val obj = REKData(4) + val msg = ObjectCreateMessage(0, 0x2D8, PlanetSideGUID(1439), ObjectCreateMessageParent(PlanetSideGUID(75), 1), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_rek + } + + "encode (character)" in { + val app = CharacterAppearanceData( + Vector3(3674.8438f, 2726.789f, 91.15625f), + 19, + 2, + false, + 4, + "IlllIIIlllIlIllIlllIllI", + 4, + 2, + 2,9, + 1, + 3, 118,30, 0x8080, 0xFFFF, 2, + 255, 106, 7, + RibbonBars() + ) + val inv = InventoryItem(0x8C, PlanetSideGUID(76), 0, WeaponData(8, 0x110, PlanetSideGUID(77), 0, AmmoBoxData(16))) :: + InventoryItem(0x34D, PlanetSideGUID(78), 2, WeaponData(8, 0x1C, PlanetSideGUID(79), 0, AmmoBoxData(25))) :: + InventoryItem(0x144, PlanetSideGUID(80), 4, WeaponData(8, 0x21C, PlanetSideGUID(81), 0, AmmoBoxData(1))) :: + InventoryItem(0x1C8, PlanetSideGUID(82), 5, AmmoBoxData(1)) :: + InventoryItem(0x1C, PlanetSideGUID(83), 6, AmmoBoxData(50)) :: + InventoryItem(0x1C, PlanetSideGUID(84), 9, AmmoBoxData(50)) :: + InventoryItem(0x1C, PlanetSideGUID(85), 12, AmmoBoxData(50)) :: + InventoryItem(0x1D, PlanetSideGUID(86), 33, AmmoBoxData(50)) :: + InventoryItem(0x110, PlanetSideGUID(87), 36, AmmoBoxData(50)) :: + InventoryItem(0x2D8, PlanetSideGUID(88), 39, REKData(8)) :: + Nil + val obj = CharacterData( + app, + 100, 100, + 50, + 1, 7, 7, + 100, 100, + 28, 4, 44, 84, 104, 1900, + "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, + List.empty, + InventoryData( + true, false, false, inv + ) + ) + val msg = ObjectCreateMessage(0, 0x79, PlanetSideGUID(75), obj) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_testchar + } } "ChatMsg" should { From 509ab8ec57aa582e00473471cf01a8c708a554ca Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 12 Dec 2016 20:05:42 -0500 Subject: [PATCH 15/15] working encoding and decoding of some objects for ObjectCreateMessage; check tests and WSActor --- .../packet/game/ObjectCreateMessage.scala | 17 +-- .../game/objectcreate/AmmoBoxData.scala | 23 +++- .../game/objectcreate/CharacterData.scala | 92 +++++++-------- .../ConcurrentFeedWeaponData.scala | 106 ++++++++++++++++++ .../game/objectcreate/InternalSlot.scala | 18 +-- .../game/objectcreate/InventoryData.scala | 32 +++--- .../game/objectcreate/InventoryItem.scala | 8 +- .../game/objectcreate/ObjectClass.scala | 4 + .../packet/game/objectcreate/REKData.scala | 7 +- .../packet/game/objectcreate/RibbonBars.scala | 8 +- .../game/objectcreate/StreamBitSize.scala | 3 +- .../packet/game/objectcreate/WeaponData.scala | 26 ++--- common/src/test/scala/GamePacketTest.scala | 36 ++++++ .../src/main/scala/WorldSessionActor.scala | 54 ++++++++- 14 files changed, 320 insertions(+), 114 deletions(-) create mode 100644 common/src/main/scala/net/psforever/packet/game/objectcreate/ConcurrentFeedWeaponData.scala 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 0205775a..9db2022d 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala @@ -18,8 +18,8 @@ import shapeless.{::, HNil} * @param guid the GUID of the parent object * @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent */ -case class ObjectCreateMessageParent(guid : PlanetSideGUID, - slot : Int) +final case class ObjectCreateMessageParent(guid : PlanetSideGUID, + slot : Int) /** * Communicate with the client that a certain object with certain properties is to be created. @@ -45,14 +45,15 @@ case class ObjectCreateMessageParent(guid : PlanetSideGUID, * @param objectClass the code for the type of object being constructed * @param guid the GUID this object will be assigned * @param parentInfo if defined, the relationship between this object and another object (its parent) - * @param data if defined, the data used to construct this type of object + * @param data the data used to construct this type of object; + * on decoding, set to `None` if the process failed * @see ObjectClass.selectDataCodec */ -case class ObjectCreateMessage(streamLength : Long, - objectClass : Int, - guid : PlanetSideGUID, - parentInfo : Option[ObjectCreateMessageParent], - data : Option[ConstructorData]) +final case class ObjectCreateMessage(streamLength : Long, + objectClass : Int, + guid : PlanetSideGUID, + parentInfo : Option[ObjectCreateMessageParent], + data : Option[ConstructorData]) extends PlanetSideGamePacket { def opcode = GamePacketOpcode.ObjectCreateMessage def encode = ObjectCreateMessage.encode(this) diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala index ed8fed41..2d33df7a 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala @@ -2,6 +2,7 @@ package net.psforever.packet.game.objectcreate import net.psforever.packet.Marshallable +import net.psforever.packet.game.PlanetSideGUID import scodec.{Attempt, Codec, Err} import scodec.codecs._ import shapeless.{::, HNil} @@ -13,12 +14,11 @@ import shapeless.{::, HNil} *
* The maximum amount of ammunition that can be stored in a single box is 65535 units. * Regardless of the interface, however, the number will never be fully visible. - * Only the first three digits or first four digits may be represented. + * Only the first three digits or the first four digits may be represented. * @param magazine the number of rounds available * @see WeaponData */ -case class AmmoBoxData(magazine : Int - ) extends ConstructorData { +final case class AmmoBoxData(magazine : Int) extends ConstructorData { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize @@ -28,9 +28,20 @@ case class AmmoBoxData(magazine : Int } object AmmoBoxData extends Marshallable[AmmoBoxData] { + /** + * An abbreviated constructor for creating `WeaponData` while masking use of `InternalSlot`. + * @param cls the code for the type of object being constructed + * @param guid the GUID this object will be assigned + * @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent + * @param ammo the `AmmoBoxData` + * @return an `InternalSlot` object that encapsulates `AmmoBoxData` + */ + def apply(cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : InternalSlot = + new InternalSlot(cls, guid, parentSlot, ammo) + implicit val codec : Codec[AmmoBoxData] = ( - uintL(8) :: - uintL(15) :: + uint8L :: + uint(15) :: ("magazine" | uint16L) :: bool ).exmap[AmmoBoxData] ( @@ -38,7 +49,7 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] { case 0xC8 :: 0 :: mag :: false :: HNil => Attempt.successful(AmmoBoxData(mag)) case a :: b :: _ :: d :: HNil => - Attempt.failure(Err("illegal ammunition data format")) + Attempt.failure(Err("invalid ammunition data format")) }, { case AmmoBoxData(mag) => diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala index 019ed856..b614fc70 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala @@ -89,27 +89,27 @@ import shapeless.{::, HNil} * @param unk8 na * @param ribbons the four merit commendation ribbon medals */ -case class CharacterAppearanceData(pos : Vector3, - objYaw : Int, - faction : Int, - bops : Boolean, - unk1 : Int, - name : String, - exosuit : Int, - sex : Int, - face1 : Int, - face2 : Int, - voice : Int, - unk2 : Int, - unk3 : Int, - unk4 : Int, - unk5 : Int, - unk6 : Int, - unk7 : Int, - viewPitch : Int, - viewYaw : Int, - unk8 : Int, - ribbons : RibbonBars) extends StreamBitSize { +final case class CharacterAppearanceData(pos : Vector3, + objYaw : Int, + faction : Int, + bops : Boolean, + unk1 : Int, + name : String, + exosuit : Int, + sex : Int, + face1 : Int, + face2 : Int, + voice : Int, + unk2 : Int, + unk3 : Int, + unk4 : Int, + unk5 : Int, + unk6 : Int, + unk7 : Int, + viewPitch : Int, + viewYaw : Int, + unk8 : Int, + ribbons : RibbonBars) extends StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize @@ -138,18 +138,18 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { ignore(16) :: ("objYaw" | uint8L) :: ignore(1) :: - ("faction" | uintL(2)) :: + ("faction" | uint2L) :: ("bops" | bool) :: ("unk1" | uint4L) :: ignore(16) :: ("name" | PacketHelpers.encodedWideStringAligned( namePadding )) :: ("exosuit" | uintL(3)) :: ignore(2) :: - ("sex" | uintL(2)) :: + ("sex" | uint2L) :: ("face1" | uint4L) :: ("face2" | uint4L) :: ("voice" | uintL(3)) :: - ("unk2" | uintL(2)) :: + ("unk2" | uint2L) :: ignore(4) :: ("unk3" | uint8L) :: ("unk4" | uint8L) :: @@ -219,29 +219,29 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { * the size field is a 32-bit number; * the first entry may be padded * @param tutorials the list of tutorials completed by this avatar; - * the size field is a 32-bit number; - * the first entry may be padded + * the size field is a 32-bit number; + * the first entry may be padded * @param inventory the avatar's inventory */ -case class CharacterData(appearance : CharacterAppearanceData, - healthMax : Int, - health : Int, - armor : Int, - unk1 : Int, //1 - unk2 : Int, //7 - unk3 : Int, //7 - staminaMax : Int, - stamina : Int, - unk4 : Int, //28 - unk5 : Int, //4 - unk6 : Int, //44 - unk7 : Int, //84 - unk8 : Int, //104 - unk9 : Int, //1900 - firstTimeEvents : List[String], - tutorials : List[String], - inventory : InventoryData - ) extends ConstructorData { +final case class CharacterData(appearance : CharacterAppearanceData, + healthMax : Int, + health : Int, + armor : Int, + unk1 : Int, //1 + unk2 : Int, //7 + unk3 : Int, //7 + staminaMax : Int, + stamina : Int, + unk4 : Int, //28 + unk5 : Int, //4 + unk6 : Int, //44 + unk7 : Int, //84 + unk8 : Int, //104 + unk9 : Int, //1900 + firstTimeEvents : List[String], + tutorials : List[String], + inventory : InventoryData + ) extends ConstructorData { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize @@ -310,7 +310,7 @@ object CharacterData extends Marshallable[CharacterData] { if(len > 0) //automatic alignment from previous List 0 else if(len2 > 0) //need to align for elements - 1 + 5 else //both lists are empty 0 } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConcurrentFeedWeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConcurrentFeedWeaponData.scala new file mode 100644 index 00000000..ae151e0f --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConcurrentFeedWeaponData.scala @@ -0,0 +1,106 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game.objectcreate + +import net.psforever.packet.game.PlanetSideGUID +import net.psforever.packet.{Marshallable, PacketHelpers} +import scodec.codecs._ +import scodec.{Attempt, Codec, Err} +import shapeless.{::, HNil} + +/** + * A representation of a class of weapons that can be created using `ObjectCreateMessage` packet data. + * A "concurrent feed weapon" refers to a weapon system that can chamber multiple types of ammunition simultaneously. + * This data will help construct a "weapon" such as a Punisher.
+ *
+ * The data for the weapons nests information for the default (current) type and number of ammunition in its magazine. + * This ammunition data essentially is the weapon's magazines as numbered slots. + * @param unk na + * @param ammo `List` data regarding the currently loaded ammunition types and quantities + * @see WeaponData + * @see AmmoBoxData + */ +final case class ConcurrentFeedWeaponData(unk : Int, + ammo : List[InternalSlot]) extends ConstructorData { + /** + * Performs a "sizeof()" analysis of the given object. + * @see ConstructorData.bitsize + * @see InternalSlot.bitsize + * @see AmmoBoxData.bitsize + * @return the number of bits necessary to represent this object + */ + override def bitsize : Long = { + var bitsize : Long = 0L + for(o <- ammo) { + bitsize += o.bitsize + } + 61L + bitsize + } +} + +object ConcurrentFeedWeaponData extends Marshallable[ConcurrentFeedWeaponData] { + /** + * An abbreviated constructor for creating `ConcurrentFeedWeaponData` while masking use of `InternalSlot` for its `AmmoBoxData`.
+ *
+ * Exploration:
+ * This class may need to be rewritten later to support objects spawned in the world environment. + * @param unk na + * @param cls the code for the type of object (ammunition) being constructed + * @param guid the globally unique id assigned to the ammunition + * @param parentSlot the slot where the ammunition is to be installed in the weapon + * @param ammo the constructor data for the ammunition + * @return a WeaponData object + */ + def apply(unk : Int, cls : Int, guid : PlanetSideGUID, parentSlot : Int, ammo : AmmoBoxData) : ConcurrentFeedWeaponData = + new ConcurrentFeedWeaponData(unk, InternalSlot(cls, guid, parentSlot, ammo) :: Nil) + + implicit val codec : Codec[ConcurrentFeedWeaponData] = ( + ("unk" | uint4L) :: + uint4L :: + uint24 :: + uint16 :: + uint2L :: + (uint8L >>:~ { size => + uint2L :: + ("ammo" | PacketHelpers.listOfNSized(size, InternalSlot.codec)) :: + bool + }) + ).exmap[ConcurrentFeedWeaponData] ( + { + case code :: 8 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil => + if(size != ammo.size) + Attempt.failure(Err("weapon encodes wrong number of ammunition")) + else if(size == 0) + Attempt.failure(Err("weapon needs to encode at least one type of ammunition")) + else + Attempt.successful(ConcurrentFeedWeaponData(code, ammo)) + case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => + Attempt.failure(Err("invalid weapon data format")) + }, + { + case ConcurrentFeedWeaponData(code, ammo) => + val size = ammo.size + if(size == 0) + Attempt.failure(Err("weapon needs to encode at least one type of ammunition")) + else if(size >= 255) + Attempt.failure(Err("weapon has too much ammunition (255+ types!)")) + else + Attempt.successful(code :: 8 :: 2 :: 0 :: 3 :: size :: 0 :: ammo :: false :: HNil) + } + ).as[ConcurrentFeedWeaponData] + + /** + * Transform between WeaponData and ConstructorData. + */ + val genericCodec : Codec[ConstructorData.genericPattern] = codec.exmap[ConstructorData.genericPattern] ( + { + case x => + Attempt.successful(Some(x.asInstanceOf[ConstructorData])) + }, + { + case Some(x) => + Attempt.successful(x.asInstanceOf[ConcurrentFeedWeaponData]) + case _ => + Attempt.failure(Err("can not encode weapon data")) + } + ) +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala index f634a599..4fef95ef 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala @@ -8,22 +8,22 @@ import scodec.codecs._ import shapeless.{::, HNil} /** - * Similar fields as required for a formal `ObjectCreateMessage` but with a required but implicit parent relationship. - * Specifically, the purpose of the packet is to start to define a new object within the definition of a previous object. - * This prior object will clarify the identity of the parent object that owns the given `parentSlot`.
+ * An intermediate class for the primary fields of `ObjectCreateMessage` with an implicit parent-child relationship.
*
- * An `InternalSlot` object is not a top-level object. - * Extra effort should be made to ensure the user does not have to directly construct an `InternalSlot`. + * Any object that is contained in a "slot" of another object will use `InternalSlot` to hold the anchoring data. + * This prior object will clarify the identity of the "parent" object that owns the given `parentSlot`.
+ *
+ * Try to avoid exposing `InternalSlot` in the process of implementing code. * @param objectClass the code for the type of object being constructed * @param guid the GUID this object will be assigned * @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent * @param obj the data used as representation of the object to be constructed * @see ObjectClass.selectDataCodec */ -case class InternalSlot(objectClass : Int, - guid : PlanetSideGUID, - parentSlot : Int, - obj : ConstructorData) extends StreamBitSize { +final case class InternalSlot(objectClass : Int, + guid : PlanetSideGUID, + parentSlot : Int, + obj : ConstructorData) extends StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala index d2a597e7..9a7232e8 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala @@ -14,12 +14,18 @@ import shapeless.{::, HNil} * No values are allowed to be misplaced and no unexpected regions of data can be discovered. * If there is even a minor failure, the whole of the inventory will fail to translate.
*
+ * Under the official servers, when a new character was generated, the inventory encoded as `0x1C`. + * This inventory had no size field, no contents, and an indeterminate number of values. + * This format is no longer supported. + * Going forward, an empty inventory - approximately `0x10000` - should be used as substitute.
+ *
* Exploration:
- * 4u of ignored bits are tagged onto the end of this field for purposes of finding four missing bits of stream length. - * The rest of the encoding is valid. - * Conditions must certainly decide whether these bits are present or not. + * 4u of ignored bits have been added to the end of the inventory to make up for missing stream length. + * They do not actually seem to be part of the inventory. + * Are these bits always at the end of the packet data and what is the significance? * @param unk1 na; * `true` to mark the start of the inventory data? + * is explicitly declaring the bit necessary when it always seems to be `true`? * @param unk2 na * @param unk3 na * @param contents the actual items in the inventory; @@ -27,17 +33,17 @@ import shapeless.{::, HNil} * an inaccessible slot is 5; * internal capacity is 6-`n`, where `n` is defined by exosuit type and is mapped into a grid */ -case class InventoryData(unk1 : Boolean, - unk2 : Boolean, - unk3 : Boolean, - contents : List[InventoryItem]) extends StreamBitSize { +final case class InventoryData(unk1 : Boolean, + unk2 : Boolean, + unk3 : Boolean, + contents : List[InventoryItem]) extends StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize * @return the number of bits necessary to represent this object */ override def bitsize : Long = { - //three booleans, the 4u and the 8u length field + //three booleans, the 4u extra, and the 8u length field val base : Long = 15L //length of all items in inventory var invSize : Long = 0L @@ -57,14 +63,14 @@ object InventoryData extends Marshallable[InventoryData] { ("contents" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) :: ignore(4) }) - ).xmap[InventoryData] ( + ).xmap[InventoryData] ( { - case u1 :: _ :: u2 :: u3 :: ctnt :: _ :: HNil => - InventoryData(u1, u2, u3, ctnt) + case u1 :: _ :: a :: b :: ctnt :: _ :: HNil => + InventoryData(u1, a, b, ctnt) }, { - case InventoryData(u1, u2, u3, ctnt) => - u1 :: ctnt.size :: u2 :: u3 :: ctnt :: () :: HNil + case InventoryData(u1, a, b, ctnt) => + u1 :: ctnt.size :: a :: b :: ctnt :: () :: HNil } ).as[InventoryData] } diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala index 90fc0fc0..25f17abe 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala @@ -8,15 +8,15 @@ import scodec.codecs._ /** * A representation of an item in an avatar's inventory. - * Reliance on `InternalSlot` indicates that this item is applicable to the same implicit parental relationship. - * (That is, its parent object will be clarified earlier on in the data stream.) + * Reliance on `InternalSlot` indicates that this item is applicable to the same implicit parent-child relationship. + * (That is, its parent object will be clarified by the containing element, e.g., the inventory or its owner.) * Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible.
*
- * This intermediary object is primarily intended to mask external use of `InternalSlot`. + * This intermediary object is primarily intended to mask external use of `InternalSlot`, as specified by the class. * @param item the object in inventory * @see InternalSlot */ -case class InventoryItem(item : InternalSlot) extends StreamBitSize { +final case class InventoryItem(item : InternalSlot) extends StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala index 995ee934..e351bc3d 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala @@ -20,6 +20,7 @@ object ObjectClass { final val BULLETS_9MM = 0x1C final val BULLETS_9MM_AP = 0x1D final val ENERGY_CELL = 0x110 + final val JAMMER_GRENADE_PACK = 0x19D final val JAMMER_GRENADE_AMMO = 0x1A1 final val FORCE_BLADE_AMMO = 0x21C final val PLASMA_GRENADE_AMMO = 0x2A9 @@ -32,6 +33,7 @@ object ObjectClass { final val GAUSS = 0x159 final val JAMMER_GRENADE = 0x1A0 final val PLASMA_GRENADE = 0x2A8 + final val PUNISHER = 0x2C2 //tools final val MEDKIT = 0x218 final val REK = 0x2D8 @@ -62,9 +64,11 @@ object ObjectClass { case ObjectClass.GAUSS => WeaponData.genericCodec case ObjectClass.JAMMER_GRENADE => WeaponData.genericCodec case ObjectClass.JAMMER_GRENADE_AMMO => AmmoBoxData.genericCodec + case ObjectClass.JAMMER_GRENADE_PACK => AmmoBoxData.genericCodec case ObjectClass.MEDKIT => AmmoBoxData.genericCodec case ObjectClass.PLASMA_GRENADE => WeaponData.genericCodec case ObjectClass.PLASMA_GRENADE_AMMO => AmmoBoxData.genericCodec + case ObjectClass.PUNISHER => ConcurrentFeedWeaponData.genericCodec case ObjectClass.REK => REKData.genericCodec case ObjectClass.SLOT_BLOCKER => AmmoBoxData.genericCodec case ObjectClass.SUPPRESSOR => WeaponData.genericCodec diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala index b2c32a2b..89078a59 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala @@ -13,8 +13,7 @@ import shapeless.{::, HNil} * Of note is the first portion of the data which resembles the `WeaponData` format. * @param unk na */ -case class REKData(unk : Int - ) extends ConstructorData { +final case class REKData(unk : Int) extends ConstructorData { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize @@ -29,7 +28,7 @@ object REKData extends Marshallable[REKData] { uint4L :: uintL(20) :: uint4L :: - uintL(16) :: + uint16L :: uint4L :: uintL(15) ).exmap[REKData] ( @@ -37,7 +36,7 @@ object REKData extends Marshallable[REKData] { case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil => Attempt.successful(REKData(code)) case code :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => - Attempt.failure(Err("illegal rek data format")) + Attempt.failure(Err("invalid rek data format")) }, { case REKData(code) => diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala index e4dee136..28358a97 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala @@ -17,10 +17,10 @@ import scodec.codecs._ * @param lower the lower configurable merit ribbon * @param tos the top-most term of service merit ribbon */ -case class RibbonBars(upper : Long = 0xFFFFFFFFL, - middle : Long = 0xFFFFFFFFL, - lower : Long = 0xFFFFFFFFL, - tos : Long = 0xFFFFFFFFL) extends StreamBitSize { +final case class RibbonBars(upper : Long = 0xFFFFFFFFL, + middle : Long = 0xFFFFFFFFL, + lower : Long = 0xFFFFFFFFL, + tos : Long = 0xFFFFFFFFL) extends StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala index 9f9d562a..bbdd12ce 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala @@ -8,8 +8,9 @@ trait StreamBitSize { /** * Performs a "sizeof()" analysis of the given object. * The calculation reflects the `scodec Codec` definition rather than the explicit parameter fields. - * For example, an `Int` is normally a 32-bit number; + * For example, an `Int` is normally a 32u number; * when parsed with a `uintL(7)`, it's length will be considered 7u. + * (Note: being permanently signed, an `scodec` 32u value must fit into a `Long` type.) * @return the number of bits necessary to represent this object; * defaults to `0L` */ diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala index 175b2547..41dded1d 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala @@ -8,11 +8,11 @@ import scodec.codecs._ import shapeless.{::, HNil} /** - * A representation of the weapon portion of `ObjectCreateMessage` packet data. - * This data will help construct a "weapon" such as a Suppressor or a Gauss.
+ * A representation of a class of weapons that can be created using `ObjectCreateMessage` packet data. + * This data will help construct a "loaded weapon" such as a Suppressor or a Gauss.
*
* The data for the weapons nests information for the default (current) type and number of ammunition in its magazine. - * This ammunition data essentially is the weapon's magazine as numbered slots. + * This ammunition data essentially is the weapon's magazines as numbered slots. * Having said that, this format only handles one type of ammunition at a time. * Any weapon that has two types of ammunition simultaneously loaded, e.g., a Punisher, must be handled with another `Codec`. * This functionality is unrelated to a weapon that switches ammunition type; @@ -21,8 +21,8 @@ import shapeless.{::, HNil} * @param ammo data regarding the currently loaded ammunition type and quantity * @see AmmoBoxData */ -case class WeaponData(unk : Int, - ammo : InternalSlot) extends ConstructorData { +final case class WeaponData(unk : Int, + ammo : InternalSlot) extends ConstructorData { /** * Performs a "sizeof()" analysis of the given object. * @see ConstructorData.bitsize @@ -51,23 +51,23 @@ object WeaponData extends Marshallable[WeaponData] { implicit val codec : Codec[WeaponData] = ( ("unk" | uint4L) :: uint4L :: - uintL(20) :: - uint4L :: - uintL(16) :: - uintL(11) :: - bool :: + uint24 :: + uint16L :: + uint2 :: + uint8 :: //size = 1 type of ammunition loaded + uint2 :: ("ammo" | InternalSlot.codec) :: bool ).exmap[WeaponData] ( { - case code :: 8 :: 0 :: 2 :: 0 :: 0x2C0 :: false :: ammo :: false :: HNil => + case code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil => Attempt.successful(WeaponData(code, ammo)) case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil => - Attempt.failure(Err("illegal weapon data format")) + Attempt.failure(Err("invalid weapon data format")) }, { case WeaponData(code, ammo) => - Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 0x2C0 :: false :: ammo :: false :: HNil) + Attempt.successful(code :: 8 :: 2 :: 0 :: 3 :: 1 :: 0 :: ammo :: false :: HNil) } ).as[WeaponData] diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala index b9b8da40..8ecccd69 100644 --- a/common/src/test/scala/GamePacketTest.scala +++ b/common/src/test/scala/GamePacketTest.scala @@ -153,6 +153,7 @@ class GamePacketTest extends Specification { var string_inventoryItem = hex"46 04 C0 08 08 80 00 00 20 00 0C 04 10 29 A0 10 19 00 00 04 00 00" val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000" val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" + val string_punisher = hex"18 27010000 2580 612 a706 82 080000020000c08 1c13a0d01900000780 13a4701a072000000800" val string_rek = hex"18 97000000 2580 6C2 9F05 81 48000002000080000" val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" @@ -208,6 +209,33 @@ class GamePacketTest extends Specification { } } + "decode (punisher)" in { + PacketCoding.DecodePacket(string_punisher).require match { + case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => + len mustEqual 295 + cls mustEqual 706 + guid mustEqual PlanetSideGUID(1703) + parent.isDefined mustEqual true + parent.get.guid mustEqual PlanetSideGUID(75) + parent.get.slot mustEqual 2 + data.isDefined mustEqual true + val obj_wep = data.get.asInstanceOf[ConcurrentFeedWeaponData] + obj_wep.unk mustEqual 0 + val obj_ammo = obj_wep.ammo + obj_ammo.size mustEqual 2 + obj_ammo.head.objectClass mustEqual 28 + obj_ammo.head.guid mustEqual PlanetSideGUID(1693) + obj_ammo.head.parentSlot mustEqual 0 + obj_ammo.head.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30 + obj_ammo(1).objectClass mustEqual 413 + obj_ammo(1).guid mustEqual PlanetSideGUID(1564) + obj_ammo(1).parentSlot mustEqual 1 + obj_ammo(1).obj.asInstanceOf[AmmoBoxData].magazine mustEqual 1 + case _ => + ko + } + } + "decode (rek)" in { PacketCoding.DecodePacket(string_rek).require match { case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => @@ -373,6 +401,14 @@ class GamePacketTest extends Specification { pkt mustEqual string_gauss } + "encode (punisher)" in { + val obj = ConcurrentFeedWeaponData(0, AmmoBoxData(28, PlanetSideGUID(1693), 0, AmmoBoxData(30)) :: AmmoBoxData(413, PlanetSideGUID(1564), 1, AmmoBoxData(1)) :: Nil) + val msg = ObjectCreateMessage(0, 706, PlanetSideGUID(1703), ObjectCreateMessageParent(PlanetSideGUID(75), 2), obj) + var pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_punisher + } + "encode (rek)" in { val obj = REKData(4) val msg = ObjectCreateMessage(0, 0x2D8, PlanetSideGUID(1439), ObjectCreateMessageParent(PlanetSideGUID(75), 1), obj) diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 27c607b2..de389012 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -9,7 +9,8 @@ import scodec.Attempt.{Failure, Successful} import scodec.bits._ import org.log4s.MDC import MDCContextAware.Implicits._ -import net.psforever.types.ChatMessageType +import net.psforever.packet.game.objectcreate._ +import net.psforever.types.{ChatMessageType, Vector3} class WorldSessionActor extends Actor with MDCContextAware { private[this] val log = org.log4s.getLogger @@ -107,8 +108,49 @@ class WorldSessionActor extends Actor with MDCContextAware { } } - // XXX: hard coded ObjectCreateMessage - val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00 " + //val objectHex = hex"18 57 0C 00 00 BC 84 B0 06 C2 D7 65 53 5C A1 60 00 01 34 40 00 09 70 49 00 6C 00 6C 00 6C 00 49 00 49 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 49 00 6C 00 6C 00 49 00 6C 00 6C 00 6C 00 49 00 6C 00 6C 00 49 00 84 52 70 76 1E 80 80 00 00 00 00 00 3F FF C0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" + //currently, the character's starting BEP is discarded due to unknown bit format + val app = CharacterAppearanceData( + Vector3(3674.8438f, 2726.789f, 91.15625f), + 19, + 2, + false, + 4, + "IlllIIIlllIlIllIlllIllI", + 4, + 2, + 2, 9, + 1, + 3, 118, 30, 0x8080, 0xFFFF, 2, + 255, 106, 7, + RibbonBars() + ) + val inv = + InventoryItem(ObjectClass.BEAMER, PlanetSideGUID(76), 0, WeaponData(8, ObjectClass.ENERGY_CELL, PlanetSideGUID(77), 0, AmmoBoxData(16))) :: + InventoryItem(ObjectClass.SUPPRESSOR, PlanetSideGUID(78), 2, WeaponData(8, ObjectClass.BULLETS_9MM, PlanetSideGUID(79), 0, AmmoBoxData(25))) :: + InventoryItem(ObjectClass.FORCE_BLADE, PlanetSideGUID(80), 4, WeaponData(8, ObjectClass.FORCE_BLADE_AMMO, PlanetSideGUID(81), 0, AmmoBoxData(1))) :: + InventoryItem(ObjectClass.SLOT_BLOCKER, PlanetSideGUID(82), 5, AmmoBoxData(1)) :: + InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(83), 6, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(84), 9, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.BULLETS_9MM, PlanetSideGUID(85), 12, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.BULLETS_9MM_AP, PlanetSideGUID(86), 33, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.ENERGY_CELL, PlanetSideGUID(87), 36, AmmoBoxData(50)) :: + InventoryItem(ObjectClass.REK, PlanetSideGUID(88), 39, REKData(8)) :: + Nil + val obj = CharacterData( + app, + 100, 100, + 50, + 1, 7, 7, + 100, 100, + 28, 4, 44, 84, 104, 1900, + "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, + List.empty, + InventoryData( + true, false, false, inv + ) + ) + val objectHex = ObjectCreateMessage(0, ObjectClass.AVATAR, PlanetSideGUID(75), obj) def handleGamePkt(pkt : PlanetSideGamePacket) = pkt match { case ConnectToWorldRequestMessage(server, token, majorVersion, minorVersion, revision, buildDate, unk) => @@ -118,7 +160,7 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"New world login to ${server} with Token:${token}. ${clientVersion}") // ObjectCreateMessage - sendRawResponse(objectHex) + sendResponse(PacketCoding.CreateGamePacket(0, objectHex)) // XXX: hard coded message sendRawResponse(hex"14 0F 00 00 00 10 27 00 00 C1 D8 7A 02 4B 00 26 5C B0 80 00 ") @@ -132,14 +174,14 @@ class WorldSessionActor extends Actor with MDCContextAware { case CharacterRequestAction.Delete => sendResponse(PacketCoding.CreateGamePacket(0, ActionResultMessage(false, Some(1)))) case CharacterRequestAction.Select => - PacketCoding.DecodeGamePacket(objectHex).require match { + objectHex match { case obj @ ObjectCreateMessage(len, cls, guid, _, _) => log.debug("Object: " + obj) // LoadMapMessage 13714 in mossy .gcap // XXX: hardcoded shit sendResponse(PacketCoding.CreateGamePacket(0, LoadMapMessage("map13","home3",40100,25,true,3770441820L))) //VS Sanctuary sendResponse(PacketCoding.CreateGamePacket(0, ZonePopulationUpdateMessage(PlanetSideGUID(13), 414, 138, 0, 138, 0, 138, 0, 138, 0))) - sendRawResponse(objectHex) + sendResponse(PacketCoding.CreateGamePacket(0, objectHex)) // These object_guids are specfic to VS Sanc sendResponse(PacketCoding.CreateGamePacket(0, SetEmpireMessage(PlanetSideGUID(2), PlanetSideEmpire.VS))) //HART building C