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