From 3b9f3a6f33c31a93b4ab43b0415f460d68f4bfc5 Mon Sep 17 00:00:00 2001 From: FateJH Date: Fri, 9 Dec 2016 19:42:20 -0500 Subject: [PATCH] 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 235934472..0205775aa 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 71d46b75d..ed8fed414 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 c58046e0e..019ed856f 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 a62d0a20f..dab33755e 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 4b6b5d41b..f634a5992 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 4a11e7d28..d2a597e7c 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 4c82dbeed..90fc0fc08 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 252b14686..995ee9347 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 7f7adb304..b2c32a2b4 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 2517be7b9..e4dee1366 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 000000000..9f9d562a9 --- /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 d692b20d5..175b25478 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 42afd7f5a..b9b8da40b 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 {