diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala
index 23593447..0205775a 100644
--- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateMessage.scala
@@ -1,7 +1,7 @@
// Copyright (c) 2016 PSForever.net to present
package net.psforever.packet.game
-import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass}
+import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectClass, StreamBitSize}
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.bits.BitVector
import scodec.{Attempt, Codec, DecodeResult, Err}
@@ -12,10 +12,9 @@ import shapeless.{::, HNil}
* The parent information of a created object.
*
* Rather than a created-parent with a created-child relationship, the whole of the packet still only creates the child.
- * The parent is a pre-existing object into which the (created) child is attached.
- *
- * The slot is encoded as a string length integer commonly used by PlanetSide.
- * It is either a 0-127 eight bit number (0 = `0x80`), or a 128-32767 sixteen bit number (128 = `0x0080`).
+ * The parent is a pre-existing object into which the (created) child is attached.
+ * The slot is encoded as a string length integer, following PlanetSide Classic convention for slot numbering.
+ * It is either a 0-127 eight bit number, or a 128-32767 sixteen bit number.
* @param guid the GUID of the parent object
* @param slot a parent-defined slot identifier that explains where the child is to be attached to the parent
*/
@@ -46,7 +45,8 @@ case class ObjectCreateMessageParent(guid : PlanetSideGUID,
* @param objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param parentInfo if defined, the relationship between this object and another object (its parent)
- * @param data if defined, the data used to construct this type of object
+ * @param data if defined, the data used to construct this type of object
+ * @see ObjectClass.selectDataCodec
*/
case class ObjectCreateMessage(streamLength : Long,
objectClass : Int,
@@ -71,6 +71,17 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage =
ObjectCreateMessage(streamLength, objectClass, guid, Some(parentInfo), Some(data))
+ /**
+ * An abbreviated constructor for creating `ObjectCreateMessages`, ignoring `parentInfo`.
+ * @param streamLength the total length of the data that composes this packet in bits, excluding the opcode and end padding
+ * @param objectClass the code for the type of object being constructed
+ * @param guid the GUID this object will be assigned
+ * @param data the data used to construct this type of object
+ * @return an ObjectCreateMessage
+ */
+ def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateMessage =
+ ObjectCreateMessage(streamLength, objectClass, guid, None, Some(data))
+
type Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil
type outPattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: HNil
/**
@@ -107,21 +118,18 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
)
/**
- * Take bit data and transform it into an object that expresses the important information of a game piece.
- *
+ * Take bit data and transform it into an object that expresses the important information of a game piece.
* This function is fail-safe because it catches errors involving bad parsing of the bitstream data.
- * Generally, the `Exception` messages themselves are not useful.
+ * Generally, the `Exception` messages themselves are not useful here.
* The important parts are what the packet thought the object class should be and what it actually processed.
- * The bit data that failed to parse is retained for debugging at a later time.
* @param objectClass the code for the type of object being constructed
- * @param data the bitstream data
+ * @param data the bitstream data
* @return the optional constructed object
*/
private def decodeData(objectClass : Int, data : BitVector) : Option[ConstructorData] = {
var out : Option[ConstructorData] = None
- val copy = data.drop(0)
try {
- val outOpt : Option[DecodeResult[_]] = ObjectClass.selectDataCodec(objectClass).decode(copy).toOption
+ val outOpt : Option[DecodeResult[_]] = ObjectClass.selectDataCodec(objectClass).decode(data).toOption
if(outOpt.isDefined)
out = outOpt.get.value.asInstanceOf[ConstructorData.genericPattern]
}
@@ -133,13 +141,11 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
}
/**
- * Take the important information of a game piece and transform it into bit data.
- *
+ * Take the important information of a game piece and transform it into bit data.
* This function is fail-safe because it catches errors involving bad parsing of the object data.
- * Generally, the `Exception` messages themselves are not useful.
- * If parsing fails, all data pertinent to debugging the failure is retained in the constructor.
+ * Generally, the `Exception` messages themselves are not useful here.
* @param objClass the code for the type of object being deconstructed
- * @param obj the object data
+ * @param obj the object data
* @return the bitstream data
*/
private def encodeData(objClass : Int, obj : ConstructorData) : BitVector = {
@@ -167,26 +173,22 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
* the object's GUID (16u),
* and the bit to determine if there will be parent data.
* In total, these fields form a known fixed length of 60u.
- * @param parentInfo if defined, information about the parent adds either 24u or 32u
- * @param data the data length is indeterminate until it is walked-through
- * @return the total length of the stream in bits
+ * @param parentInfo if defined, the relationship between this object and another object (its parent);
+ * information about the parent adds either 24u or 32u
+ * @param data if defined, the data used to construct this type of object;
+ * the data length is indeterminate until it is walked-through;
+ * note: the type is `StreamBitSize` as opposed to `ConstructorData`
+ * @return the total length of the resulting data stream in bits
*/
- private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : ConstructorData) : Long = {
+ private def streamLen(parentInfo : Option[ObjectCreateMessageParent], data : StreamBitSize) : Long = {
//knowable length
- val first : Long = if(parentInfo.isDefined) {
+ val base : Long = if(parentInfo.isDefined) {
if(parentInfo.get.slot > 127) 92L else 84L //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u))
}
else {
60L
}
- //object length
- var second : Long = data.bitsize
- val secondMod4 : Long = second % 4L
- if(secondMod4 > 0L) {
- //pad to include last whole nibble
- second += 4L - secondMod4
- }
- first + second
+ base + data.bitsize
}
implicit val codec : Codec[ObjectCreateMessage] = (
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala
index 71d46b75..ed8fed41 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/AmmoBoxData.scala
@@ -8,11 +8,14 @@ import shapeless.{::, HNil}
/**
* A representation of the ammunition portion of `ObjectCreateMessage` packet data.
- * When alone, this data will help construct a "box" of that type of ammunition, hence the name.
+ * This data will help construct a "box" of that type of ammunition when standalone.
+ * It can also be constructed directly inside a weapon as its magazine.
*
- * Exploration:
- * This class may need to be rewritten later to support objects spawned in the world environment.
+ * The maximum amount of ammunition that can be stored in a single box is 65535 units.
+ * Regardless of the interface, however, the number will never be fully visible.
+ * Only the first three digits or first four digits may be represented.
* @param magazine the number of rounds available
+ * @see WeaponData
*/
case class AmmoBoxData(magazine : Int
) extends ConstructorData {
@@ -21,24 +24,25 @@ case class AmmoBoxData(magazine : Int
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
- override def bitsize : Long = 39L
+ override def bitsize : Long = 40L
}
object AmmoBoxData extends Marshallable[AmmoBoxData] {
implicit val codec : Codec[AmmoBoxData] = (
uintL(8) ::
- ignore(15) ::
- ("magazine" | uint16L)
+ uintL(15) ::
+ ("magazine" | uint16L) ::
+ bool
).exmap[AmmoBoxData] (
{
- case 0xC8 :: _ :: mag :: HNil =>
+ case 0xC8 :: 0 :: mag :: false :: HNil =>
Attempt.successful(AmmoBoxData(mag))
- case x :: _ :: _ :: HNil =>
- Attempt.failure(Err("looking for 200, found "+x))
+ case a :: b :: _ :: d :: HNil =>
+ Attempt.failure(Err("illegal ammunition data format"))
},
{
case AmmoBoxData(mag) =>
- Attempt.successful(0xC8 :: () :: mag :: HNil)
+ Attempt.successful(0xC8 :: 0 :: mag :: false:: HNil)
}
)
@@ -54,7 +58,7 @@ object AmmoBoxData extends Marshallable[AmmoBoxData] {
case Some(x) =>
Attempt.successful(x.asInstanceOf[AmmoBoxData])
case _ =>
- Attempt.failure(Err(""))
+ Attempt.failure(Err("can not encode ammo box data"))
}
)
}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
index c58046e0..019ed856 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
@@ -7,24 +7,132 @@ import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
+/**
+ * A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.
+ *
+ * This partition of the data stream contains information used to represent how the player's avatar is presented.
+ * This appearance can be considered the avatar's obvious points beyond experience levels.
+ * It does not include passive exo-suit upgrades, battle rank 24 cosmetics, special postures, or current equipment.
+ * Those will occur later back in the main data stream.
+ *
+ * This base length of this stream is __430__ known bits, excluding the length of the name and the padding on that name.
+ * Of that, __203__ bits are perfectly unknown in significance.
+ *
+ * Faction:
+ * `0 - Terran Republic`
+ * `1 - New Conglomerate`
+ * `2 - Vanu Sovereignty`
+ *
+ * Exo-suit:
+ * `0 - Agile`
+ * `1 - Refinforced`
+ * `2 - Mechanized Assault`
+ * `3 - Infiltration`
+ * `4 - Standard`
+ *
+ * Sex:
+ * `0 - invalid`
+ * `1 - Male`
+ * `2 - Female`
+ * `3 - invalid`
+ *
+ * Voice:
+ * ` MALE FEMALE`
+ * `0 - no voice no voice`
+ * `1 - male_1 female_1`
+ * `2 - male_2 female_2`
+ * `3 - male_3 female_3`
+ * `4 - male_4 female_4`
+ * `5 - male_5 female_5`
+ * `6 - female_1 no voice`
+ * `7 - female_2 no voice`
+ * @param pos the position of the character in the world environment (in three coordinates)
+ * @param objYaw the angle with respect to the horizon towards which the object's front is facing;
+ * every `0x1` is 2.813 degrees counter clockwise from North;
+ * every `0x10` is 45-degrees;
+ * it wraps at `0x0` == `0x80` == North
+ * (note: references the avatar as a game object?)
+ * @param faction the empire to which the avatar belongs;
+ * the value scale is different from `PlanetSideEmpire`
+ * @param bops whether or not this avatar is enrolled in Black OPs
+ * @param unk1 na;
+ * defaults to 4
+ * @param name the wide character name of the avatar, minimum of two characters
+ * @param exosuit the type of exosuit the avatar will be depicted in;
+ * for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits
+ * @param sex whether the avatar is male or female
+ * @param face1 the avatar's face, as by column number on the character creation screen
+ * @param face2 the avatar's face, as by row number on the character creation screen
+ * @param voice the avatar's voice selection
+ * @param unk2 na
+ * @param unk3 na;
+ * can be missing from the stream under certain conditions;
+ * see next
+ * @param unk4 na;
+ * can be missing from the stream under certain conditions;
+ * see previous
+ * @param unk5 na;
+ * defaults to `0x8080`
+ * @param unk6 na;
+ * defaults to `0xFFFF`;
+ * may be `0x0`
+ * @param unk7 na;
+ * defaults to 2
+ * @param viewPitch the angle with respect to the sky and the ground towards which the avatar is looking;
+ * only supports downwards view angles;
+ * `0x0` is forwards-facing;
+ * `0x20` to `0xFF` is downwards-facing
+ * @param viewYaw the angle with respect to the horizon towards which the avatar is looking;
+ * every `0x1` is 2.813 degrees counter clockwise from North;
+ * every `0x10` is 45-degrees;
+ * it wraps at `0x0` == `0x80` == North
+ * @param unk8 na
+ * @param ribbons the four merit commendation ribbon medals
+ */
case class CharacterAppearanceData(pos : Vector3,
objYaw : Int,
faction : Int,
bops : Boolean,
+ unk1 : Int,
name : String,
exosuit : Int,
sex : Int,
face1 : Int,
face2 : Int,
voice : Int,
- unk1 : Int, //0x8080
- unk2 : Int, //0xFFFF or 0x0
- unk3 : Int, //2
+ unk2 : Int,
+ unk3 : Int,
+ unk4 : Int,
+ unk5 : Int,
+ unk6 : Int,
+ unk7 : Int,
viewPitch : Int,
viewYaw : Int,
- ribbons : RibbonBars)
+ unk8 : Int,
+ ribbons : RibbonBars) extends StreamBitSize {
+ /**
+ * Performs a "sizeof()" analysis of the given object.
+ * @see ConstructorData.bitsize
+ * @return the number of bits necessary to represent this object
+ */
+ override def bitsize : Long = {
+ //TODO ongoing analysis, this value will be subject to change
+ 430L + CharacterData.stringBitSize(name, 16) + CharacterAppearanceData.namePadding
+ }
+}
object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
+ /**
+ * Get the padding of the avatar's name.
+ * The padding will always be a number 0-7.
+ * @return the pad length in bits
+ */
+ private def namePadding : Int = {
+ //TODO the parameters for this function are not correct
+ //TODO the proper padding length should reflect all variability in the substream prior to this point
+ 4
+ }
+
implicit val codec : Codec[CharacterAppearanceData] = (
("pos" | Vector3.codec_pos) ::
ignore(16) ::
@@ -32,24 +140,29 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
ignore(1) ::
("faction" | uintL(2)) ::
("bops" | bool) ::
- ignore(20) ::
- ("name" | PacketHelpers.encodedWideStringAligned(4)) ::
+ ("unk1" | uint4L) ::
+ ignore(16) ::
+ ("name" | PacketHelpers.encodedWideStringAligned( namePadding )) ::
("exosuit" | uintL(3)) ::
ignore(2) ::
("sex" | uintL(2)) ::
("face1" | uint4L) ::
("face2" | uint4L) ::
("voice" | uintL(3)) ::
- ignore(22) ::
- ("unk1" | uint16L) ::
+ ("unk2" | uintL(2)) ::
+ ignore(4) ::
+ ("unk3" | uint8L) ::
+ ("unk4" | uint8L) ::
+ ("unk5" | uint16L) ::
ignore(42) ::
- ("unk2" | uint16L) ::
+ ("unk6" | uint16L) ::
ignore(30) ::
- ("unk3" | uint4L) ::
+ ("unk7" | uint4L) ::
ignore(24) ::
("viewPitch" | uint8L) ::
("viewYaw" | uint8L) ::
- ignore(10) ::
+ ("unk8" | uint4L) ::
+ ignore(6) ::
("ribbons" | RibbonBars.codec)
).as[CharacterAppearanceData]
}
@@ -58,143 +171,73 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
* A representation of the avatar portion of `ObjectCreateMessage` packet data.
*
* This object is huge, representing the quantity of densely-encoded data in its packet.
- * Although the actual organization is ill-defined, the packet can be divided into seven parts.
- * The first part maintains information about the avatar as a game object in the game environment.
- * The second part maintains information as an ongoing representation of the avatar.
- * This includes fixed details like name and gender, though it also includes mutable aspects like exosuit type.
- * The third part maintains information about career in the game.
- * The fourth part maintains miscellaneous status and pose information.
- * The fifth part maintains part of the statistical information about participation in the game.
- * The sixth part maintains a stream of typically zero'd unknown information.
- * The seventh part maintains the inventory.
- * The fifth and seventh parts can inflate the size of packet significantly due to their encoding.
- * The fifth, in particular, is string data that can number in the hundreds of strings(!).
+ * Certain bits, when set or unset, introduce or remove other bits from the packet data as well.
+ * (As in: flipping a bit may create room or negate other bits from somewhere else in the data stream.
+ * Not accounting for this new pattern of bits will break decoding and encoding.)
+ * Due to the very real concern that bloating the constructor for this object with parameters could break the `apply` method,
+ * parameters will often be composed of nested case objects that contain a group of formal parameters.
+ * There are lists of byte-aligned `Strings` later-on in the packet data that will need access to these objects to calculate padding length.
*
- * Ignoring the strings, lists of strings, and the inventory, the base length of the packet is currently __1138__ bits.
- * Some undefined bits in the packet can change the length of the packet by being set or unset.
- * This will mess with the encoding and the decoding of later fields.
- * Any data that is padded for byte-alignment will also have its padding adjusted.
- * Each string adds either 8 or 16, plus an additional 8 or 16 per the number of characters.
- * For the name, that's 16 per character, a minimum of two characters, plus the (current) padding.
- * for the first time events and tutorials, that's 8 per character, plus the (current) padding of the first entry.
- * For the first time events and tutorials, however, the size of the list is always a 32-bit number.
- * The formal inventory entries are preceded by 1 absolute bit.
+ * The first subdivision of parameters concerns the avatar's basic aesthetics, mostly.
+ * (No other parts of the data divided up yet.)
+ * The final sections include two lists of accredited activity performed/completed by the player.
+ * The remainder of the data, following after that, can be read straight, up to and through the inventory.
*
- * The adjusted base length is therefore __1203__ bits (1138 + 32 + 32 + 1).
- * Of that, __720__ bits are unknown.
- * Including the values that are defaulted, __831__ bits are perfectly unknown.
- * This data is accurate as of 2016-12-07.
- *
- * Faction:
- * `0 - Terran Republic`
- * `1 - New Comglomerate`
- * `2 - Vanu Sovereignty`
- *
- * Exosuit:
- * `0 - Agile`
- * `1 - Refinforced`
- * `2 - Mechanized Assault`
- * `3 - Infiltration`
- * `4 - Standard`
- *
- * Sex:
- * `1 - Male`
- * `2 - Female`
- *
- * Voice:
- * ` MALE FEMALE`
- * `0 - No voice No voice`
- * `1 - Male_1 Female_1`
- * `2 - Male_2 Female_2`
- * `3 - Male_3 Female_3`
- * `4 - Male_4 Female_4`
- * `5 - Male_5 Female_5`
- * `6 - Female_1 No voice`
- * `7 - Female_2 No voice`
-// * @param pos the position of the character in the world environment (in three coordinates)
-// * @param objYaw the angle with respect to the horizon towards which the object's front is facing;
-// * every `0x1` is 2.813 degrees counter clockwise from North;
-// * every `0x10` is 45-degrees;
-// * it wraps at `0x80`
-// * (note: references the avatar as a game object?)
-// * @param faction the empire to which the avatar belongs;
-// * the value scale is different from `PlanetSideEmpire`;
-// * @param bops whether or not this avatar is enrolled in Black OPs
-// * @param name the wide character name of the avatar
-// * @param exosuit the type of exosuit the avatar will be depicted in;
-// * for Black OPs, the agile exosuit and the reinforced exosuit are replaced with the Black OPs exosuits
-// * @param sex whether the avatar is male or female
-// * @param face1 the avatar's face, as by column number on the character creation screen
-// * @param face2 the avatar's face, as by row number on the character creation screen
-// * @param voice the avatar's voice selection
-// * @param unk1 na;
-// * defaults to `0x8080`
-// * @param unk2 na;
-// * defaults to `0xFFFF`;
-// * may be `0x0`
-// * @param unk3 na;
-// * defaults to 2
-// * @param viewPitch the angle with respect to the horizon towards which the avatar is looking;
-// * only supports downwards view angles;
-// * `0x0` is forwards-facing;
-// * `0x20` to `0xFF` is downwards-facing
-// * @param viewYaw the angle with respect to the ground directions towards which the avatar is looking;
-// * every `0x1` is 2.813 degrees counter clockwise from North;
-// * every `0x10` is 45-degrees;
-// * it wraps at `0x80`
-// * @param ribbons the four merit commendation ribbon medals displayed on the avatar's left pauldron
-// * @param healthMax for "x / y" of hitpoints, this is the avatar's 'y' value;
-// * range is 0-65535
-// * @param health for "x / y" of hitpoints, this is the avatar's 'x' value;
-// * range is 0-65535
-// * @param armor for "x / y" of armor points, this is the avatar's 'x' value;
-// * range is 0-65535;
-// * the avatar's "y" armor points is tied to their exosuit type
-// * @param unk4 na;
-// * defaults to 1
-// * @param unk5 na;
-// * defaults to 7
-// * @param unk6 na;
-// * defaults to 7
-// * @param staminaMax for "x / y" of stamina points, this is the avatar's 'y' value;
-// * range is 0-65535
-// * @param stamina for "x / y" of stamina points, this is the avatar's 'x' value;
-// * range is 0-65535
-// * @param unk7 na;
-// * defaults to 28
-// * @param unk8 na;
-// * defaults to 4
-// * @param unk9 na;
-// * defaults to 44
-// * @param unk10 na;
-// * defaults to 84
-// * @param unk11 na;
-// * defaults to 104
-// * @param unk12 na;
-// * defaults to 1900
-// * @param firstTimeEvent_length the total number of first time events performed by this avatar
-// * @param firstTimeEvent_firstEntry the separated "first entry" of the list of first time events performed by this avatar
-// * @param firstTimeEvent_list the list of first time events performed by this avatar
-// * @param tutorial_length the total number of tutorials completed by this avatar
-// * @param tutorial_firstEntry the separated "first entry" of the list of tutorials completed by this avatar
-// * @param tutorial_list the list of tutorials completed by this avatar
-// * @param inventory the avatar's inventory
+ * The base length of the stream is currently __1138__ bits, excluding `List`s and `String`s and inventory.
+ * Of that, __831__ bits are perfectly unknown.
+ * @param appearance data about the avatar's basic aesthetics
+ * @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value;
+ * range is 0-65535
+ * @param health for `x / y` of hitpoints, this is the avatar's `x` value;
+ * range is 0-65535
+ * @param armor for `x / y` of armor points, this is the avatar's `x` value;
+ * range is 0-65535;
+ * the avatar's `y` armor points is tied to their exo-suit type
+ * @param unk1 na;
+ * defaults to 1
+ * @param unk2 na;
+ * defaults to 7
+ * @param unk3 na;
+ * defaults to 7
+ * @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value;
+ * range is 0-65535
+ * @param stamina for `x / y` of stamina points, this is the avatar's `x` value;
+ * range is 0-65535
+ * @param unk4 na;
+ * defaults to 28
+ * @param unk5 na;
+ * defaults to 4
+ * @param unk6 na;
+ * defaults to 44
+ * @param unk7 na;
+ * defaults to 84
+ * @param unk8 na;
+ * defaults to 104
+ * @param unk9 na;
+ * defaults to 1900
+ * @param firstTimeEvents the list of first time events performed by this avatar;
+ * the size field is a 32-bit number;
+ * the first entry may be padded
+ * @param tutorials the list of tutorials completed by this avatar;
+ * the size field is a 32-bit number;
+ * the first entry may be padded
+ * @param inventory the avatar's inventory
*/
case class CharacterData(appearance : CharacterAppearanceData,
healthMax : Int,
health : Int,
armor : Int,
- unk4 : Int, //1
- unk5 : Int, //7
- unk6 : Int, //7
+ unk1 : Int, //1
+ unk2 : Int, //7
+ unk3 : Int, //7
staminaMax : Int,
stamina : Int,
- unk7 : Int, //28
- unk8 : Int, //4
- unk9 : Int, //44
- unk10 : Int, //84
- unk11 : Int, //104
- unk12 : Int, //1900
+ unk4 : Int, //28
+ unk5 : Int, //4
+ unk6 : Int, //44
+ unk7 : Int, //84
+ unk8 : Int, //104
+ unk9 : Int, //1900
firstTimeEvents : List[String],
tutorials : List[String],
inventory : InventoryData
@@ -205,25 +248,20 @@ case class CharacterData(appearance : CharacterAppearanceData,
* @return the number of bits necessary to represent this object
*/
override def bitsize : Long = {
-// //represents static fields (includes medals.bitsize)
-// val base : Long = 1138L //TODO ongoing analysis, this value will be subject to change
-// //name
-// val nameSize : Long = CharacterData.stringBitSize(appearance.name, 16) + 4L //plus the current padding
-// //fte_list
-// var eventListSize : Long = 32L
-// if(firstTimeEvent_firstEntry.isDefined) {
-// eventListSize += CharacterData.stringBitSize(firstTimeEvent_firstEntry.get) + 5L //plus the current padding
-// for(str <- firstTimeEvent_list) {
-// eventListSize += CharacterData.stringBitSize(str)
-// }
-// }
-// //tutorial list
-// var tutorialListSize : Long = 32L
-// for(str <- tutorial_list) {
-// tutorialListSize += CharacterData.stringBitSize(str)
-// }
-// base + nameSize + eventListSize + tutorialListSize + inventory.bitsize
- 0L
+ //TODO ongoing analysis, this value will be subject to change
+ //fte list
+ val fteLen = firstTimeEvents.size
+ var eventListSize : Long = 32L + CharacterData.ftePadding(fteLen)
+ for(str <- firstTimeEvents) {
+ eventListSize += CharacterData.stringBitSize(str)
+ }
+ //tutorial list
+ val tutLen = tutorials.size
+ var tutorialListSize : Long = 32L + CharacterData.tutPadding(fteLen, tutLen)
+ for(str <- tutorials) {
+ tutorialListSize += CharacterData.stringBitSize(str)
+ }
+ 708L + appearance.bitsize + eventListSize + tutorialListSize + inventory.bitsize
}
}
@@ -236,20 +274,45 @@ object CharacterData extends Marshallable[CharacterData] {
* defaults to the standard 8-bits
* @return the size in bits
*/
- private def stringBitSize(str : String, width : Int = 8) : Long = {
+ def stringBitSize(str : String, width : Int = 8) : Long = {
val strlen = str.length
val lenSize = if(strlen > 127) 16L else 8L
lenSize + (strlen * width)
}
+ /**
+ * Get the padding of the first entry in the first time events list.
+ * The padding will always be a number 0-7.
+ * @param len the length of the list
+ * @return the pad length in bits
+ */
private def ftePadding(len : Long) : Int = {
- //TODO determine how this should be padded better
- 5
+ //TODO the parameters for this function are not correct
+ //TODO the proper padding length should reflect all variability in the stream prior to this point
+ if(len > 0) {
+ 5
+ }
+ else
+ 0
}
- private def tutListPadding(len : Long) : Int = {
- //TODO determine how this should be padded when len == 0
- if(len > 0) 0 else 0
+ /**
+ * Get the padding of the first entry in the completed tutorials list.
+ * The padding will always be a number 0-7.
+ *
+ * The tutorials list follows the first time event list and that contains byte-aligned strings too.
+ * While there will be more to the padding, this other list is important.
+ * Any elements in that list causes the automatic byte-alignment of this list's first entry.
+ * @param len the length of the list
+ * @return the pad length in bits
+ */
+ private def tutPadding(len : Long, len2 : Long) : Int = {
+ if(len > 0) //automatic alignment from previous List
+ 0
+ else if(len2 > 0) //need to align for elements
+ 1
+ else //both lists are empty
+ 0
}
implicit val codec : Codec[CharacterData] = (
@@ -260,25 +323,25 @@ object CharacterData extends Marshallable[CharacterData] {
ignore(1) ::
("armor" | uint16L) ::
ignore(9) ::
- ("unk4" | uint8L) ::
+ ("unk1" | uint8L) ::
ignore(8) ::
- ("unk5" | uint4L) ::
- ("unk6" | uintL(3)) ::
+ ("unk2" | uint4L) ::
+ ("unk3" | uintL(3)) ::
("staminaMax" | uint16L) ::
("stamina" | uint16L) ::
ignore(149) ::
- ("unk7" | uint16L) ::
+ ("unk4" | uint16L) ::
+ ("unk5" | uint8L) ::
+ ("unk6" | uint8L) ::
+ ("unk7" | uint8L) ::
("unk8" | uint8L) ::
- ("unk9" | uint8L) ::
- ("unk10" | uint8L) ::
- ("unk11" | uint8L) ::
- ("unk12" | uintL(12)) ::
+ ("unk9" | uintL(12)) ::
ignore(19) ::
(("firstTimeEvent_length" | uint32L) >>:~ { len =>
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) ::
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
(("tutorial_length" | uint32L) >>:~ { len2 =>
- conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutListPadding(len) )) ::
+ conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutPadding(len, len2) )) ::
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
ignore(207) ::
("inventory" | InventoryData.codec)
@@ -323,7 +386,7 @@ object CharacterData extends Marshallable[CharacterData] {
case Some(x) =>
Attempt.successful(x.asInstanceOf[CharacterData])
case _ =>
- Attempt.failure(Err(""))
+ Attempt.failure(Err("can not encode character data"))
}
)
}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala
index a62d0a20..dab33755 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ConstructorData.scala
@@ -7,23 +7,15 @@ package net.psforever.packet.game.objectcreate
* Children of this class are expected to be able to translate through `scodec` operations into packet data.
*
* The object data is uncoupled from the object class as multiple classes use the same format for their data.
- * For example, both the Suppressor and the Gauss use a weapon data format.
- * For example, both 9mm Bullets and energy cells use am ammunition data format.
+ * For example, both the Suppressor and the Gauss will use a "weapon data" format.
+ * For example, both 9mm bullets and energy cells will use an "ammunition data" format.
*/
-abstract class ConstructorData() {
- /**
- * Performs a "sizeof()" analysis of the given object.
- * @return the number of bits necessary to represent this object;
- * reflects the `Codec` definition rather than the parameter fields;
- * defaults to `0L`
- */
- def bitsize : Long = 0L
-}
+abstract class ConstructorData extends StreamBitSize
object ConstructorData {
/**
* This pattern is intended to provide common conversion between all of the `Codec`s of the children of this class.
- * The casting will be performed through use of `exmap`.
+ * The casting will be performed through use of `exmap` in the child class.
*/
type genericPattern = Option[ConstructorData]
}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala
index 4b6b5d41..f634a599 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InternalSlot.scala
@@ -8,51 +8,48 @@ import scodec.codecs._
import shapeless.{::, HNil}
/**
- * The same kind of data as required for a formal `ObjectCreateMessage` but with a required and implicit parent relationship.
- * Some data preceding this entry will clarify the existence of the parent.
+ * Similar fields as required for a formal `ObjectCreateMessage` but with a required but implicit parent relationship.
+ * Specifically, the purpose of the packet is to start to define a new object within the definition of a previous object.
+ * This prior object will clarify the identity of the parent object that owns the given `parentSlot`.
*
- * As indicated, an `InternalSlot` object is not a top-level object.
- * This is true in relation between one object and another, as well as in how this object is sorted in the `ObjectCreateMessage` data.
- * The data outlined by this class encompasses the same kind as the outer-most `ObjectCreateMessage`.
- * By contrast, this object always has a dedicated parent object and a known slot to be attached to that parent.
- * It's not optional.
+ * An `InternalSlot` object is not a top-level object.
+ * Extra effort should be made to ensure the user does not have to directly construct an `InternalSlot`.
* @param objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned
* @param parentSlot a parent-defined slot identifier that explains where the child is to be attached to the parent
* @param obj the data used as representation of the object to be constructed
+ * @see ObjectClass.selectDataCodec
*/
case class InternalSlot(objectClass : Int,
guid : PlanetSideGUID,
parentSlot : Int,
- obj : ConstructorData) {
+ obj : ConstructorData) extends StreamBitSize {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
- def bitsize : Long = {
- val first : Long = if(parentSlot > 127) 44L else 36L
- val second : Long = obj.bitsize
- first + second
+ override def bitsize : Long = {
+ val base : Long = if(parentSlot > 127) 43L else 35L
+ base + obj.bitsize
}
}
object InternalSlot extends Marshallable[InternalSlot] {
implicit val codec : Codec[InternalSlot] = (
- ignore(1) :: //TODO determine what this bit does
- (("objectClass" | uintL(11)) >>:~ { obj_cls =>
- ("guid" | PlanetSideGUID.codec) ::
- ("parentSlot" | PacketHelpers.encodedStringSize) ::
- ("obj" | ObjectClass.selectDataCodec(obj_cls))
- })
+ ("objectClass" | uintL(11)) >>:~ { obj_cls =>
+ ("guid" | PlanetSideGUID.codec) ::
+ ("parentSlot" | PacketHelpers.encodedStringSize) ::
+ ("obj" | ObjectClass.selectDataCodec(obj_cls)) //it's fine for this call to fail
+ }
).xmap[InternalSlot] (
{
- case _ :: cls :: guid :: slot :: Some(obj) :: HNil =>
+ case cls :: guid :: slot :: Some(obj) :: HNil =>
InternalSlot(cls, guid, slot, obj)
},
{
case InternalSlot(cls, guid, slot, obj) =>
- () :: cls :: guid :: slot :: Some(obj) :: HNil
+ cls :: guid :: slot :: Some(obj) :: HNil
}
).as[InternalSlot]
}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala
index 4a11e7d2..d2a597e7 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryData.scala
@@ -1,20 +1,27 @@
// Copyright (c) 2016 PSForever.net to present
package net.psforever.packet.game.objectcreate
-import net.psforever.packet.Marshallable
+import net.psforever.packet.{Marshallable, PacketHelpers}
import scodec.Codec
import scodec.codecs._
-import shapeless.{::,HNil}
+import shapeless.{::, HNil}
/**
* A representation of the inventory portion of `ObjectCreateMessage` packet data for avatars.
*
- * Unfortunately, the inventory is a fail-fast greedy thing.
- * Any format discrepancies will cause it to fail and that will cause character encoding to fail as well.
- * Care should be taken that all possible item encodings are representable.
+ * The inventory is a temperamental thing.
+ * Items placed into the inventory must follow their proper encoding schematics to the letter.
+ * No values are allowed to be misplaced and no unexpected regions of data can be discovered.
+ * If there is even a minor failure, the whole of the inventory will fail to translate.
+ *
+ * Exploration:
+ * 4u of ignored bits are tagged onto the end of this field for purposes of finding four missing bits of stream length.
+ * The rest of the encoding is valid.
+ * Conditions must certainly decide whether these bits are present or not.
* @param unk1 na;
- * always `true` to mark the start of the inventory data?
+ * `true` to mark the start of the inventory data?
* @param unk2 na
+ * @param unk3 na
* @param contents the actual items in the inventory;
* holster slots are 0-4;
* an inaccessible slot is 5;
@@ -22,38 +29,42 @@ import shapeless.{::,HNil}
*/
case class InventoryData(unk1 : Boolean,
unk2 : Boolean,
- contents : Vector[InventoryItem]) {
+ unk3 : Boolean,
+ contents : List[InventoryItem]) extends StreamBitSize {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
- def bitsize : Long = {
- //two booleans and the 8-bit length field
- val first : Long = 10L
+ override def bitsize : Long = {
+ //three booleans, the 4u and the 8u length field
+ val base : Long = 15L
//length of all items in inventory
- var second : Long = 0L
+ var invSize : Long = 0L
for(item <- contents) {
- second += item.bitsize
+ invSize += item.bitsize
}
- first + second
+ base + invSize
}
}
object InventoryData extends Marshallable[InventoryData] {
implicit val codec : Codec[InventoryData] = (
("unk1" | bool) ::
- ("len" | uint8L) ::
- ("unk2" | bool) ::
- ("contents" | vector(InventoryItem.codec))
- ).xmap[InventoryData] (
+ (("len" | uint8L) >>:~ { len =>
+ ("unk2" | bool) ::
+ ("unk3" | bool) ::
+ ("contents" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) ::
+ ignore(4)
+ })
+ ).xmap[InventoryData] (
{
- case u1 :: _ :: u2 :: vector :: HNil =>
- InventoryData(u1, u2, vector)
+ case u1 :: _ :: u2 :: u3 :: ctnt :: _ :: HNil =>
+ InventoryData(u1, u2, u3, ctnt)
},
{
- case InventoryData(u1, u2, vector) =>
- u1 :: vector.length :: u2 :: vector :: HNil
+ case InventoryData(u1, u2, u3, ctnt) =>
+ u1 :: ctnt.size :: u2 :: u3 :: ctnt :: () :: HNil
}
).as[InventoryData]
}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala
index 4c82dbee..90fc0fc0 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/InventoryItem.scala
@@ -7,28 +7,22 @@ import scodec.Codec
import scodec.codecs._
/**
- * Represent an item in inventory.
+ * A representation of an item in an avatar's inventory.
+ * Reliance on `InternalSlot` indicates that this item is applicable to the same implicit parental relationship.
+ * (That is, its parent object will be clarified earlier on in the data stream.)
+ * Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible.
*
- * Note the use of `InternalSlot` to indicate the implicit parent ownership of the resulting item.
- * Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible.
+ * This intermediary object is primarily intended to mask external use of `InternalSlot`.
* @param item the object in inventory
- * @param na the user should not have to worry about this potential bit;
- * it follows after weapon entries, allegedly
+ * @see InternalSlot
*/
-case class InventoryItem(item : InternalSlot,
- na : Option[Boolean] = None) {
+case class InventoryItem(item : InternalSlot) extends StreamBitSize {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
- def bitsize : Long = {
- //item
- val first : Long = item.bitsize
- //trailing bit
- val second : Long = if(na.isDefined) 1L else 0L
- first + second
- }
+ override def bitsize : Long = item.bitsize
}
object InventoryItem extends Marshallable[InventoryItem] {
@@ -40,22 +34,10 @@ object InventoryItem extends Marshallable[InventoryItem] {
* @param obj the constructor data
* @return an InventoryItem
*/
- def apply(objClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : ConstructorData) : InventoryItem = {
- val isWep = if(obj.isInstanceOf[WeaponData]) Some(false) else None
- //TODO is this always Some(false)?
- InventoryItem(InternalSlot(objClass, guid, parentSlot, obj), isWep)
- }
-
- /**
- * Determine whether the allocated item is a weapon.
- * @param itm the inventory item
- * @return true, if the item is a weapon; false, otherwise
- */
- def wasWeapon(itm : InternalSlot) : Boolean = itm.obj.isInstanceOf[WeaponData]
+ def apply(objClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : ConstructorData) : InventoryItem =
+ InventoryItem(InternalSlot(objClass, guid, parentSlot, obj))
implicit val codec : Codec[InventoryItem] = (
- ("item" | InternalSlot.codec) >>:~ { item =>
- conditional(wasWeapon(item), bool).hlist
- }
+ "item" | InternalSlot.codec
).as[InventoryItem]
}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala
index 252b1468..995ee934 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/ObjectClass.scala
@@ -9,8 +9,9 @@ import scala.annotation.switch
/**
* A reference between all object class codes and the name of the object they represent.
*
- * Object classes compose a number between `0` and (probably) `2047`, always translating into an 11-bit value.
+ * Object classes compose a number between 0 and (probably) 2047, always translating into an 11-bit value.
* They are recorded as little-endian hexadecimal values here.
+ * In `scodec` terms, that's a `uintL(11)` or `uintL(0xB)`.
*/
object ObjectClass {
//character
@@ -22,7 +23,7 @@ object ObjectClass {
final val JAMMER_GRENADE_AMMO = 0x1A1
final val FORCE_BLADE_AMMO = 0x21C
final val PLASMA_GRENADE_AMMO = 0x2A9
- final val BUCKSHOT = 0x2F3 //TODO apply internal name
+ final val BUCKSHOT = 0x2F3 //TODO apply internal name, eventually
//weapons
final val SUPPRESSOR = 0x34D
final val BEAMER = 0x8C
@@ -35,7 +36,7 @@ object ObjectClass {
final val MEDKIT = 0x218
final val REK = 0x2D8
//unknown
- final val SLOT_BLOCKER = 0x1C8 //strange item found in slot #5, between holsters and inventory
+ final val SLOT_BLOCKER = 0x1C8 //strange item found in inventory slot #5, between holsters and grid
//TODO refactor this function into another object later
/**
@@ -46,7 +47,7 @@ object ObjectClass {
* This pattern connects all `Codec`s back to the superclass `ConstructorData`.
* The default case is a failure case for trying to either decode or encode an unknown class of object.
* @param objClass the code for the type of object being constructed
- * @return the `Codec` that handles the format of data for that particular item class, or a failing codec
+ * @return the `Codec` that handles the format of data for that particular item class, or a failing `Codec`
*/
def selectDataCodec(objClass : Int) : Codec[ConstructorData.genericPattern] = {
(objClass : @switch) match {
@@ -72,11 +73,11 @@ object ObjectClass {
case _ => conditional(false, bool).exmap[ConstructorData.genericPattern] (
{
case None | _ =>
- Attempt.failure(Err("decoding unknown object class - "+objClass))
+ Attempt.failure(Err("decoding unknown object class"))
},
{
case None | _ =>
- Attempt.failure(Err("encoding unknown object class - "+objClass))
+ Attempt.failure(Err("encoding unknown object class"))
}
)
}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala
index 7f7adb30..b2c32a2b 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/REKData.scala
@@ -8,7 +8,9 @@ import shapeless.{::, HNil}
/**
* A representation of the REK portion of `ObjectCreateMessage` packet data.
- * When alone, this data will help construct the "tool" called a Remote Electronics Kit.
+ * This data will help construct the "tool" called a Remote Electronics Kit.
+ *
+ * Of note is the first portion of the data which resembles the `WeaponData` format.
* @param unk na
*/
case class REKData(unk : Int
@@ -18,28 +20,28 @@ case class REKData(unk : Int
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
- override def bitsize : Long = 72L
+ override def bitsize : Long = 67L
}
object REKData extends Marshallable[REKData] {
implicit val codec : Codec[REKData] = (
("unk" | uint4L) ::
uint4L ::
- ignore(20) ::
+ uintL(20) ::
uint4L ::
- ignore(16) ::
+ uintL(16) ::
uint4L ::
- ignore(20)
+ uintL(15)
).exmap[REKData] (
{
- case code :: 8 :: _ :: 2 :: _ :: 8 :: _ :: HNil =>
+ case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil =>
Attempt.successful(REKData(code))
- case _ :: x :: _ :: y :: _ :: z :: _ :: HNil =>
- Attempt.failure(Err("looking for 8-2-8 pattern, found %d-%d-%d".format(x,y,z))) //TODO I actually don't know what of this is actually important
+ case code :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
+ Attempt.failure(Err("illegal rek data format"))
},
{
case REKData(code) =>
- Attempt.successful(code :: 8 :: () :: 2 :: () :: 8 :: () :: HNil)
+ Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil)
}
).as[REKData]
@@ -55,7 +57,7 @@ object REKData extends Marshallable[REKData] {
case Some(x) =>
Attempt.successful(x.asInstanceOf[REKData])
case _ =>
- Attempt.failure(Err(""))
+ Attempt.failure(Err("can not encode rek data"))
}
)
}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala
index 2517be7b..e4dee136 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala
@@ -10,22 +10,23 @@ import scodec.codecs._
* These are the medals players wish to brandish on their left pauldron.
*
* All merit commendation ribbons are represented by a 32-bit signature.
- * The default "no-ribbon" value is `0xFFFFFFFF`.
+ * The default "no-ribbon" value is `0xFFFFFFFF`, although some illegal values will also work.
+ * The term of service ribbon can not be modified by the user and will apply itself to its slot automatically when valid.
* @param upper the "top" configurable merit ribbon
* @param middle the central configurable merit ribbon
* @param lower the lower configurable merit ribbon
- * @param tos the automatic top-most term of service merit ribbon
+ * @param tos the top-most term of service merit ribbon
*/
case class RibbonBars(upper : Long = 0xFFFFFFFFL,
middle : Long = 0xFFFFFFFFL,
lower : Long = 0xFFFFFFFFL,
- tos : Long = 0xFFFFFFFFL) {
+ tos : Long = 0xFFFFFFFFL) extends StreamBitSize {
/**
* Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object
*/
- def bitsize : Long = 128L
+ override def bitsize : Long = 128L
}
object RibbonBars extends Marshallable[RibbonBars] {
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala
new file mode 100644
index 00000000..9f9d562a
--- /dev/null
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/StreamBitSize.scala
@@ -0,0 +1,17 @@
+// Copyright (c) 2016 PSForever.net to present
+package net.psforever.packet.game.objectcreate
+
+/**
+ * Apply this trait to a class that needs to have its size in bits calculated.
+ */
+trait StreamBitSize {
+ /**
+ * Performs a "sizeof()" analysis of the given object.
+ * The calculation reflects the `scodec Codec` definition rather than the explicit parameter fields.
+ * For example, an `Int` is normally a 32-bit number;
+ * when parsed with a `uintL(7)`, it's length will be considered 7u.
+ * @return the number of bits necessary to represent this object;
+ * defaults to `0L`
+ */
+ def bitsize : Long = 0L
+}
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala
index d692b20d..175b2547 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/WeaponData.scala
@@ -9,10 +9,14 @@ import shapeless.{::, HNil}
/**
* A representation of the weapon portion of `ObjectCreateMessage` packet data.
- * When alone, this data will help construct a "weapon" such as Suppressor.
+ * This data will help construct a "weapon" such as a Suppressor or a Gauss.
*
- * The data for the weapon also nests required default ammunition data.
- * Where the ammunition is loaded is considered the "first slot."
+ * The data for the weapons nests information for the default (current) type and number of ammunition in its magazine.
+ * This ammunition data essentially is the weapon's magazine as numbered slots.
+ * Having said that, this format only handles one type of ammunition at a time.
+ * Any weapon that has two types of ammunition simultaneously loaded, e.g., a Punisher, must be handled with another `Codec`.
+ * This functionality is unrelated to a weapon that switches ammunition type;
+ * a weapon with that behavior is handled perfectly fine using this `case class`.
* @param unk na
* @param ammo data regarding the currently loaded ammunition type and quantity
* @see AmmoBoxData
@@ -25,7 +29,7 @@ case class WeaponData(unk : Int,
* @see AmmoBoxData.bitsize
* @return the number of bits necessary to represent this object
*/
- override def bitsize : Long = 59L + ammo.bitsize
+ override def bitsize : Long = 61L + ammo.bitsize
}
object WeaponData extends Marshallable[WeaponData] {
@@ -47,21 +51,23 @@ object WeaponData extends Marshallable[WeaponData] {
implicit val codec : Codec[WeaponData] = (
("unk" | uint4L) ::
uint4L ::
- ignore(20) ::
+ uintL(20) ::
uint4L ::
- ignore(16) ::
+ uintL(16) ::
uintL(11) ::
- ("ammo" | InternalSlot.codec)
+ bool ::
+ ("ammo" | InternalSlot.codec) ::
+ bool
).exmap[WeaponData] (
{
- case code :: 8 :: _ :: 2 :: _ :: 0x2C0 :: ammo :: HNil =>
+ case code :: 8 :: 0 :: 2 :: 0 :: 0x2C0 :: false :: ammo :: false :: HNil =>
Attempt.successful(WeaponData(code, ammo))
- case _ :: x :: _ :: y :: _ :: z :: _ :: HNil =>
- Attempt.failure(Err("looking for 8-2-704 pattern, found %d-%d-%d".format(x,y,z))) //TODO I actually don't know what of this is actually important
+ case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil =>
+ Attempt.failure(Err("illegal weapon data format"))
},
{
case WeaponData(code, ammo) =>
- Attempt.successful(code :: 8 :: () :: 2 :: () :: 0x2C0 :: ammo :: HNil)
+ Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 0x2C0 :: false :: ammo :: false :: HNil)
}
).as[WeaponData]
@@ -77,7 +83,7 @@ object WeaponData extends Marshallable[WeaponData] {
case Some(x) =>
Attempt.successful(x.asInstanceOf[WeaponData])
case _ =>
- Attempt.failure(Err(""))
+ Attempt.failure(Err("can not encode weapon data"))
}
)
}
diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala
index 42afd7f5..b9b8da40 100644
--- a/common/src/test/scala/GamePacketTest.scala
+++ b/common/src/test/scala/GamePacketTest.scala
@@ -4,7 +4,7 @@ import java.net.{InetAddress, InetSocketAddress}
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game._
-import net.psforever.packet.game.objectcreate._
+import net.psforever.packet.game.objectcreate.{InventoryItem, _}
import net.psforever.types._
import scodec.{Attempt, Err}
import scodec.Attempt.Successful
@@ -153,34 +153,14 @@ class GamePacketTest extends Specification {
var string_inventoryItem = hex"46 04 C0 08 08 80 00 00 20 00 0C 04 10 29 A0 10 19 00 00 04 00 00"
val string_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000"
val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000"
- val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00"
- val invTest = hex"01 01 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 00 00"
- val invTestWep = hex"23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 00 00"
-
- "InventoryTest" in {
- val intSlot = InternalSlot.codec.decode(invTestWep.toBitVector.drop(1)).toOption
- intSlot.isDefined mustEqual true
-
- val invData = InventoryItem.codec.decode(invTestWep.toBitVector.drop(1)).toOption
- invData.isDefined mustEqual true
-
-// InventoryData.codec.decode(invTest.toBitVector.drop(7)) match {
-// case Attempt.Successful(x) =>
-// x.value.unk1 equals true
-// x.value.size mustEqual 1
-// x.value.unk2 mustEqual false
-// //x.value.inv.head.item.objectClass mustEqual 0x8C
-// //x.value.inv.head.na mustEqual false
-// case Attempt.Failure(x) =>
-// x.message mustEqual ""
-// }
- }
+ val string_rek = hex"18 97000000 2580 6C2 9F05 81 48000002000080000"
+ val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FC 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00"
"decode (2)" in {
//an invalid bit representation will fail to turn into an object
PacketCoding.DecodePacket(packet2).require match {
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
- len mustEqual 248 //60 + 188
+ len mustEqual 248
cls mustEqual 121
guid mustEqual PlanetSideGUID(2497)
parent mustEqual None
@@ -190,6 +170,60 @@ class GamePacketTest extends Specification {
}
}
+ "decode (9mm)" in {
+ PacketCoding.DecodePacket(string_9mm).require match {
+ case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
+ len mustEqual 124
+ cls mustEqual 28
+ guid mustEqual PlanetSideGUID(1280)
+ parent.isDefined mustEqual true
+ parent.get.guid mustEqual PlanetSideGUID(75)
+ parent.get.slot mustEqual 33
+ data.isDefined mustEqual true
+ data.get.asInstanceOf[AmmoBoxData].magazine mustEqual 50
+ case default =>
+ ko
+ }
+ }
+
+ "decode (gauss)" in {
+ PacketCoding.DecodePacket(string_gauss).require match {
+ case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
+ len mustEqual 220
+ cls mustEqual 345
+ guid mustEqual PlanetSideGUID(1465)
+ parent.isDefined mustEqual true
+ parent.get.guid mustEqual PlanetSideGUID(75)
+ parent.get.slot mustEqual 2
+ data.isDefined mustEqual true
+ val obj_wep = data.get.asInstanceOf[WeaponData]
+ obj_wep.unk mustEqual 4
+ val obj_ammo = obj_wep.ammo
+ obj_ammo.objectClass mustEqual 28
+ obj_ammo.guid mustEqual PlanetSideGUID(1286)
+ obj_ammo.parentSlot mustEqual 0
+ obj_ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30
+ case default =>
+ ko
+ }
+ }
+
+ "decode (rek)" in {
+ PacketCoding.DecodePacket(string_rek).require match {
+ case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
+ len mustEqual 151
+ cls mustEqual 0x2D8
+ guid mustEqual PlanetSideGUID(1439)
+ parent.isDefined mustEqual true
+ parent.get.guid mustEqual PlanetSideGUID(75)
+ parent.get.slot mustEqual 1
+ data.isDefined mustEqual true
+ data.get.asInstanceOf[REKData].unk mustEqual 4
+ case _ =>
+ ko
+ }
+ }
+
"decode (character)" in {
PacketCoding.DecodePacket(string_testchar).require match {
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
@@ -206,17 +240,22 @@ class GamePacketTest extends Specification {
char.appearance.objYaw mustEqual 19
char.appearance.faction mustEqual 2 //vs
char.appearance.bops mustEqual false
+ char.appearance.unk1 mustEqual 4
char.appearance.name mustEqual "IlllIIIlllIlIllIlllIllI"
char.appearance.exosuit mustEqual 4 //standard
char.appearance.sex mustEqual 2 //female
char.appearance.face1 mustEqual 2
char.appearance.face2 mustEqual 9
char.appearance.voice mustEqual 1 //female 1
- char.appearance.unk1 mustEqual 0x8080
- char.appearance.unk2 mustEqual 0xFFFF
- char.appearance.unk3 mustEqual 2
+ char.appearance.unk2 mustEqual 3
+ char.appearance.unk3 mustEqual 118
+ char.appearance.unk4 mustEqual 30
+ char.appearance.unk5 mustEqual 0x8080
+ char.appearance.unk6 mustEqual 0xFFFF
+ char.appearance.unk7 mustEqual 2
char.appearance.viewPitch mustEqual 0xFF
char.appearance.viewYaw mustEqual 0x6A
+ char.appearance.unk8 mustEqual 7
char.appearance.ribbons.upper mustEqual 0xFFFFFFFFL //none
char.appearance.ribbons.middle mustEqual 0xFFFFFFFFL //none
char.appearance.ribbons.lower mustEqual 0xFFFFFFFFL //none
@@ -224,17 +263,17 @@ class GamePacketTest extends Specification {
char.healthMax mustEqual 100
char.health mustEqual 100
char.armor mustEqual 50 //standard exosuit value
- char.unk4 mustEqual 1
- char.unk5 mustEqual 7
- char.unk6 mustEqual 7
+ char.unk1 mustEqual 1
+ char.unk2 mustEqual 7
+ char.unk3 mustEqual 7
char.staminaMax mustEqual 100
char.stamina mustEqual 100
- char.unk7 mustEqual 28
- char.unk8 mustEqual 4
- char.unk9 mustEqual 44
- char.unk10 mustEqual 84
- char.unk11 mustEqual 104
- char.unk12 mustEqual 1900
+ char.unk4 mustEqual 28
+ char.unk5 mustEqual 4
+ char.unk6 mustEqual 44
+ char.unk7 mustEqual 84
+ char.unk8 mustEqual 104
+ char.unk9 mustEqual 1900
char.firstTimeEvents.size mustEqual 4
char.firstTimeEvents.head mustEqual "xpe_sanctuary_help"
char.firstTimeEvents(1) mustEqual "xpe_th_firemodes"
@@ -243,7 +282,7 @@ class GamePacketTest extends Specification {
char.tutorials.size mustEqual 0
char.inventory.unk1 mustEqual true
char.inventory.unk2 mustEqual false
- char.inventory.contents.length mustEqual 10
+ char.inventory.contents.size mustEqual 10
val inventory = char.inventory.contents
//0
inventory.head.item.objectClass mustEqual 0x8C //beamer
@@ -306,45 +345,7 @@ class GamePacketTest extends Specification {
inventory(9).item.objectClass mustEqual 0x2D8 //rek
inventory(9).item.guid mustEqual PlanetSideGUID(88)
inventory(9).item.parentSlot mustEqual 39
- //the rek has data but none worth testing here
- case default =>
- ko
- }
- }
-
- "decode (9mm)" in {
- PacketCoding.DecodePacket(string_9mm).require match {
- case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
- len mustEqual 124
- cls mustEqual 28
- guid mustEqual PlanetSideGUID(1280)
- parent.isDefined mustEqual true
- parent.get.guid mustEqual PlanetSideGUID(75)
- parent.get.slot mustEqual 33
- data.isDefined mustEqual true
- data.get.asInstanceOf[AmmoBoxData].magazine mustEqual 50
- case default =>
- ko
- }
- }
-
- "decode (gauss)" in {
- PacketCoding.DecodePacket(string_gauss).require match {
- case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
- len mustEqual 220
- cls mustEqual 345
- guid mustEqual PlanetSideGUID(1465)
- parent.isDefined mustEqual true
- parent.get.guid mustEqual PlanetSideGUID(75)
- parent.get.slot mustEqual 2
- data.isDefined mustEqual true
- val obj_wep = data.get.asInstanceOf[WeaponData]
- obj_wep.unk mustEqual 4
- val obj_ammo = obj_wep.ammo//.asInstanceOf[InternalSlot]
- obj_ammo.objectClass mustEqual 28
- obj_ammo.guid mustEqual PlanetSideGUID(1286)
- obj_ammo.parentSlot mustEqual 0
- obj_ammo.obj.asInstanceOf[AmmoBoxData].magazine mustEqual 30
+ //the rek has data but none worth testing here
case default =>
ko
}
@@ -371,6 +372,60 @@ class GamePacketTest extends Specification {
pkt mustEqual string_gauss
}
+
+ "encode (rek)" in {
+ val obj = REKData(4)
+ val msg = ObjectCreateMessage(0, 0x2D8, PlanetSideGUID(1439), ObjectCreateMessageParent(PlanetSideGUID(75), 1), obj)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_rek
+ }
+
+ "encode (character)" in {
+ val app = CharacterAppearanceData(
+ Vector3(3674.8438f, 2726.789f, 91.15625f),
+ 19,
+ 2,
+ false,
+ 4,
+ "IlllIIIlllIlIllIlllIllI",
+ 4,
+ 2,
+ 2,9,
+ 1,
+ 3, 118,30, 0x8080, 0xFFFF, 2,
+ 255, 106, 7,
+ RibbonBars()
+ )
+ val inv = InventoryItem(0x8C, PlanetSideGUID(76), 0, WeaponData(8, 0x110, PlanetSideGUID(77), 0, AmmoBoxData(16))) ::
+ InventoryItem(0x34D, PlanetSideGUID(78), 2, WeaponData(8, 0x1C, PlanetSideGUID(79), 0, AmmoBoxData(25))) ::
+ InventoryItem(0x144, PlanetSideGUID(80), 4, WeaponData(8, 0x21C, PlanetSideGUID(81), 0, AmmoBoxData(1))) ::
+ InventoryItem(0x1C8, PlanetSideGUID(82), 5, AmmoBoxData(1)) ::
+ InventoryItem(0x1C, PlanetSideGUID(83), 6, AmmoBoxData(50)) ::
+ InventoryItem(0x1C, PlanetSideGUID(84), 9, AmmoBoxData(50)) ::
+ InventoryItem(0x1C, PlanetSideGUID(85), 12, AmmoBoxData(50)) ::
+ InventoryItem(0x1D, PlanetSideGUID(86), 33, AmmoBoxData(50)) ::
+ InventoryItem(0x110, PlanetSideGUID(87), 36, AmmoBoxData(50)) ::
+ InventoryItem(0x2D8, PlanetSideGUID(88), 39, REKData(8)) ::
+ Nil
+ val obj = CharacterData(
+ app,
+ 100, 100,
+ 50,
+ 1, 7, 7,
+ 100, 100,
+ 28, 4, 44, 84, 104, 1900,
+ "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil,
+ List.empty,
+ InventoryData(
+ true, false, false, inv
+ )
+ )
+ val msg = ObjectCreateMessage(0, 0x79, PlanetSideGUID(75), obj)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ pkt mustEqual string_testchar
+ }
}
"ChatMsg" should {