exorcised bit from InventoryItem class, allowing insight on proper length of other CharacterData classes

created a trait to express the ability to calculate the bit length of an instance of a class
This commit is contained in:
FateJH 2016-12-09 19:42:20 -05:00
parent f98a648db1
commit 3b9f3a6f33
13 changed files with 539 additions and 406 deletions

View file

@ -1,7 +1,7 @@
// Copyright (c) 2016 PSForever.net to present // Copyright (c) 2016 PSForever.net to present
package net.psforever.packet.game 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 net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import scodec.bits.BitVector import scodec.bits.BitVector
import scodec.{Attempt, Codec, DecodeResult, Err} import scodec.{Attempt, Codec, DecodeResult, Err}
@ -12,10 +12,9 @@ import shapeless.{::, HNil}
* The parent information of a created object.<br> * The parent information of a created object.<br>
* <br> * <br>
* Rather than a created-parent with a created-child relationship, the whole of the packet still only creates the child. * 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.<br> * The parent is a pre-existing object into which the (created) child is attached.
* <br> * The slot is encoded as a string length integer, following PlanetSide Classic convention for slot numbering.
* The slot is encoded as a string length integer commonly used by PlanetSide. * It is either a 0-127 eight bit number, or a 128-32767 sixteen bit number.
* 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 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 * @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 objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned * @param guid the GUID this object will be assigned
* @param parentInfo if defined, the relationship between this object and another object (its parent) * @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, case class ObjectCreateMessage(streamLength : Long,
objectClass : Int, objectClass : Int,
@ -71,6 +71,17 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage = def apply(streamLength : Long, objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateMessage =
ObjectCreateMessage(streamLength, objectClass, guid, Some(parentInfo), Some(data)) 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 Pattern = Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: HNil
type outPattern = Long :: Int :: PlanetSideGUID :: Option[ObjectCreateMessageParent] :: Option[ConstructorData] :: 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.<br> * Take bit data and transform it into an object that expresses the important information of a game piece.
* <br>
* This function is fail-safe because it catches errors involving bad parsing of the bitstream data. * 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 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 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 * @return the optional constructed object
*/ */
private def decodeData(objectClass : Int, data : BitVector) : Option[ConstructorData] = { private def decodeData(objectClass : Int, data : BitVector) : Option[ConstructorData] = {
var out : Option[ConstructorData] = None var out : Option[ConstructorData] = None
val copy = data.drop(0)
try { try {
val outOpt : Option[DecodeResult[_]] = ObjectClass.selectDataCodec(objectClass).decode(copy).toOption val outOpt : Option[DecodeResult[_]] = ObjectClass.selectDataCodec(objectClass).decode(data).toOption
if(outOpt.isDefined) if(outOpt.isDefined)
out = outOpt.get.value.asInstanceOf[ConstructorData.genericPattern] 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.<br> * Take the important information of a game piece and transform it into bit data.
* <br>
* This function is fail-safe because it catches errors involving bad parsing of the object 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. * Generally, the `Exception` messages themselves are not useful here.
* If parsing fails, all data pertinent to debugging the failure is retained in the constructor.
* @param objClass the code for the type of object being deconstructed * @param objClass the code for the type of object being deconstructed
* @param obj the object data * @param obj the object data
* @return the bitstream data * @return the bitstream data
*/ */
private def encodeData(objClass : Int, obj : ConstructorData) : BitVector = { private def encodeData(objClass : Int, obj : ConstructorData) : BitVector = {
@ -167,26 +173,22 @@ object ObjectCreateMessage extends Marshallable[ObjectCreateMessage] {
* the object's GUID (16u), * the object's GUID (16u),
* and the bit to determine if there will be parent data. * and the bit to determine if there will be parent data.
* In total, these fields form a known fixed length of 60u. * 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 parentInfo if defined, the relationship between this object and another object (its parent);
* @param data the data length is indeterminate until it is walked-through * information about the parent adds either 24u or 32u
* @return the total length of the stream in bits * @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 //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)) if(parentInfo.get.slot > 127) 92L else 84L //(32u + 1u + 11u + 16u) ?+ (16u + (8u | 16u))
} }
else { else {
60L 60L
} }
//object length base + data.bitsize
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] = ( implicit val codec : Codec[ObjectCreateMessage] = (

View file

@ -8,11 +8,14 @@ import shapeless.{::, HNil}
/** /**
* A representation of the ammunition portion of `ObjectCreateMessage` packet data. * 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.<br> * 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.<br>
* <br> * <br>
* Exploration:<br> * The maximum amount of ammunition that can be stored in a single box is 65535 units.
* This class may need to be rewritten later to support objects spawned in the world environment. * 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 * @param magazine the number of rounds available
* @see WeaponData
*/ */
case class AmmoBoxData(magazine : Int case class AmmoBoxData(magazine : Int
) extends ConstructorData { ) extends ConstructorData {
@ -21,24 +24,25 @@ case class AmmoBoxData(magazine : Int
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object * @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] { object AmmoBoxData extends Marshallable[AmmoBoxData] {
implicit val codec : Codec[AmmoBoxData] = ( implicit val codec : Codec[AmmoBoxData] = (
uintL(8) :: uintL(8) ::
ignore(15) :: uintL(15) ::
("magazine" | uint16L) ("magazine" | uint16L) ::
bool
).exmap[AmmoBoxData] ( ).exmap[AmmoBoxData] (
{ {
case 0xC8 :: _ :: mag :: HNil => case 0xC8 :: 0 :: mag :: false :: HNil =>
Attempt.successful(AmmoBoxData(mag)) Attempt.successful(AmmoBoxData(mag))
case x :: _ :: _ :: HNil => case a :: b :: _ :: d :: HNil =>
Attempt.failure(Err("looking for 200, found "+x)) Attempt.failure(Err("illegal ammunition data format"))
}, },
{ {
case AmmoBoxData(mag) => 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) => case Some(x) =>
Attempt.successful(x.asInstanceOf[AmmoBoxData]) Attempt.successful(x.asInstanceOf[AmmoBoxData])
case _ => case _ =>
Attempt.failure(Err("")) Attempt.failure(Err("can not encode ammo box data"))
} }
) )
} }

View file

@ -7,24 +7,132 @@ import scodec.{Attempt, Codec, Err}
import scodec.codecs._ import scodec.codecs._
import shapeless.{::, HNil} import shapeless.{::, HNil}
/**
* A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.<br>
* <br>
* 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.<br>
* <br>
* 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.
* <br>
* Faction:<br>
* `0 - Terran Republic`<br>
* `1 - New Conglomerate`<br>
* `2 - Vanu Sovereignty`<br>
* <br>
* Exo-suit:<br>
* `0 - Agile`<br>
* `1 - Refinforced`<br>
* `2 - Mechanized Assault`<br>
* `3 - Infiltration`<br>
* `4 - Standard`<br>
* <br>
* Sex:<br>
* `0 - invalid`<br>
* `1 - Male`<br>
* `2 - Female`<br>
* `3 - invalid`<br>
* <br>
* Voice:<br>
* `&nbsp;&nbsp;&nbsp;&nbsp;MALE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FEMALE`<br>
* `0 - no voice &nbsp;no voice`<br>
* `1 - male_1 &nbsp;&nbsp; female_1`<br>
* `2 - male_2 &nbsp;&nbsp; female_2`<br>
* `3 - male_3 &nbsp;&nbsp; female_3`<br>
* `4 - male_4 &nbsp;&nbsp; female_4`<br>
* `5 - male_5 &nbsp;&nbsp; female_5`<br>
* `6 - female_1 &nbsp;no voice`<br>
* `7 - female_2 &nbsp;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, case class CharacterAppearanceData(pos : Vector3,
objYaw : Int, objYaw : Int,
faction : Int, faction : Int,
bops : Boolean, bops : Boolean,
unk1 : Int,
name : String, name : String,
exosuit : Int, exosuit : Int,
sex : Int, sex : Int,
face1 : Int, face1 : Int,
face2 : Int, face2 : Int,
voice : Int, voice : Int,
unk1 : Int, //0x8080 unk2 : Int,
unk2 : Int, //0xFFFF or 0x0 unk3 : Int,
unk3 : Int, //2 unk4 : Int,
unk5 : Int,
unk6 : Int,
unk7 : Int,
viewPitch : Int, viewPitch : Int,
viewYaw : 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] { 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] = ( implicit val codec : Codec[CharacterAppearanceData] = (
("pos" | Vector3.codec_pos) :: ("pos" | Vector3.codec_pos) ::
ignore(16) :: ignore(16) ::
@ -32,24 +140,29 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
ignore(1) :: ignore(1) ::
("faction" | uintL(2)) :: ("faction" | uintL(2)) ::
("bops" | bool) :: ("bops" | bool) ::
ignore(20) :: ("unk1" | uint4L) ::
("name" | PacketHelpers.encodedWideStringAligned(4)) :: ignore(16) ::
("name" | PacketHelpers.encodedWideStringAligned( namePadding )) ::
("exosuit" | uintL(3)) :: ("exosuit" | uintL(3)) ::
ignore(2) :: ignore(2) ::
("sex" | uintL(2)) :: ("sex" | uintL(2)) ::
("face1" | uint4L) :: ("face1" | uint4L) ::
("face2" | uint4L) :: ("face2" | uint4L) ::
("voice" | uintL(3)) :: ("voice" | uintL(3)) ::
ignore(22) :: ("unk2" | uintL(2)) ::
("unk1" | uint16L) :: ignore(4) ::
("unk3" | uint8L) ::
("unk4" | uint8L) ::
("unk5" | uint16L) ::
ignore(42) :: ignore(42) ::
("unk2" | uint16L) :: ("unk6" | uint16L) ::
ignore(30) :: ignore(30) ::
("unk3" | uint4L) :: ("unk7" | uint4L) ::
ignore(24) :: ignore(24) ::
("viewPitch" | uint8L) :: ("viewPitch" | uint8L) ::
("viewYaw" | uint8L) :: ("viewYaw" | uint8L) ::
ignore(10) :: ("unk8" | uint4L) ::
ignore(6) ::
("ribbons" | RibbonBars.codec) ("ribbons" | RibbonBars.codec)
).as[CharacterAppearanceData] ).as[CharacterAppearanceData]
} }
@ -58,143 +171,73 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
* A representation of the avatar portion of `ObjectCreateMessage` packet data.<br> * A representation of the avatar portion of `ObjectCreateMessage` packet data.<br>
* <br> * <br>
* This object is huge, representing the quantity of densely-encoded data in its packet. * 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. * Certain bits, when set or unset, introduce or remove other bits from the packet data as well.
* The first part maintains information about the avatar as a game object in the game environment. * (As in: flipping a bit may create room or negate other bits from somewhere else in the data stream.
* The second part maintains information as an ongoing representation of the avatar. * Not accounting for this new pattern of bits will break decoding and encoding.)
* This includes fixed details like name and gender, though it also includes mutable aspects like exosuit type. * Due to the very real concern that bloating the constructor for this object with parameters could break the `apply` method,
* The third part maintains information about career in the game. * parameters will often be composed of nested case objects that contain a group of formal parameters.
* The fourth part maintains miscellaneous status and pose information. * There are lists of byte-aligned `Strings` later-on in the packet data that will need access to these objects to calculate padding length.<br>
* 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(!).<br>
* <br> * <br>
* Ignoring the strings, lists of strings, and the inventory, the base length of the packet is currently __1138__ bits. * The first subdivision of parameters concerns the avatar's basic aesthetics, mostly.
* Some undefined bits in the packet can change the length of the packet by being set or unset. * (No other parts of the data divided up yet.)
* This will mess with the encoding and the decoding of later fields. * The final sections include two lists of accredited activity performed/completed by the player.
* Any data that is padded for byte-alignment will also have its padding adjusted. * The remainder of the data, following after that, can be read straight, up to and through the inventory.<br>
* 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.<br>
* <br> * <br>
* The adjusted base length is therefore __1203__ bits (1138 + 32 + 32 + 1). * The base length of the stream is currently __1138__ bits, excluding `List`s and `String`s and inventory.
* Of that, __720__ bits are unknown. * Of that, __831__ bits are perfectly unknown.
* Including the values that are defaulted, __831__ bits are perfectly unknown. * @param appearance data about the avatar's basic aesthetics
* This data is accurate as of 2016-12-07.<br> * @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value;
* <br> * range is 0-65535
* Faction:<br> * @param health for `x / y` of hitpoints, this is the avatar's `x` value;
* `0 - Terran Republic`<br> * range is 0-65535
* `1 - New Comglomerate`<br> * @param armor for `x / y` of armor points, this is the avatar's `x` value;
* `2 - Vanu Sovereignty`<br> * range is 0-65535;
* <br> * the avatar's `y` armor points is tied to their exo-suit type
* Exosuit:<br> * @param unk1 na;
* `0 - Agile`<br> * defaults to 1
* `1 - Refinforced`<br> * @param unk2 na;
* `2 - Mechanized Assault`<br> * defaults to 7
* `3 - Infiltration`<br> * @param unk3 na;
* `4 - Standard`<br> * defaults to 7
* <br> * @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value;
* Sex:<br> * range is 0-65535
* `1 - Male`<br> * @param stamina for `x / y` of stamina points, this is the avatar's `x` value;
* `2 - Female`<br> * range is 0-65535
* <br> * @param unk4 na;
* Voice:<br> * defaults to 28
* `&nbsp;&nbsp;&nbsp;&nbsp;MALE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FEMALE`<br> * @param unk5 na;
* `0 - No voice &nbsp;No voice`<br> * defaults to 4
* `1 - Male_1 &nbsp;&nbsp; Female_1`<br> * @param unk6 na;
* `2 - Male_2 &nbsp;&nbsp; Female_2`<br> * defaults to 44
* `3 - Male_3 &nbsp;&nbsp; Female_3`<br> * @param unk7 na;
* `4 - Male_4 &nbsp;&nbsp; Female_4`<br> * defaults to 84
* `5 - Male_5 &nbsp;&nbsp; Female_5`<br> * @param unk8 na;
* `6 - Female_1 &nbsp;No voice`<br> * defaults to 104
* `7 - Female_2 &nbsp;No voice` * @param unk9 na;
// * @param pos the position of the character in the world environment (in three coordinates) * defaults to 1900
// * @param objYaw the angle with respect to the horizon towards which the object's front is facing; * @param firstTimeEvents the list of first time events performed by this avatar;
// * every `0x1` is 2.813 degrees counter clockwise from North; * the size field is a 32-bit number;
// * every `0x10` is 45-degrees; * the first entry may be padded
// * it wraps at `0x80` * @param tutorials the list of tutorials completed by this avatar;
// * (note: references the avatar as a game object?) * the size field is a 32-bit number;
// * @param faction the empire to which the avatar belongs; * the first entry may be padded
// * the value scale is different from `PlanetSideEmpire`; * @param inventory the avatar's inventory
// * @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, case class CharacterData(appearance : CharacterAppearanceData,
healthMax : Int, healthMax : Int,
health : Int, health : Int,
armor : Int, armor : Int,
unk4 : Int, //1 unk1 : Int, //1
unk5 : Int, //7 unk2 : Int, //7
unk6 : Int, //7 unk3 : Int, //7
staminaMax : Int, staminaMax : Int,
stamina : Int, stamina : Int,
unk7 : Int, //28 unk4 : Int, //28
unk8 : Int, //4 unk5 : Int, //4
unk9 : Int, //44 unk6 : Int, //44
unk10 : Int, //84 unk7 : Int, //84
unk11 : Int, //104 unk8 : Int, //104
unk12 : Int, //1900 unk9 : Int, //1900
firstTimeEvents : List[String], firstTimeEvents : List[String],
tutorials : List[String], tutorials : List[String],
inventory : InventoryData inventory : InventoryData
@ -205,25 +248,20 @@ case class CharacterData(appearance : CharacterAppearanceData,
* @return the number of bits necessary to represent this object * @return the number of bits necessary to represent this object
*/ */
override def bitsize : Long = { override def bitsize : Long = {
// //represents static fields (includes medals.bitsize) //TODO ongoing analysis, this value will be subject to change
// val base : Long = 1138L //TODO ongoing analysis, this value will be subject to change //fte list
// //name val fteLen = firstTimeEvents.size
// val nameSize : Long = CharacterData.stringBitSize(appearance.name, 16) + 4L //plus the current padding var eventListSize : Long = 32L + CharacterData.ftePadding(fteLen)
// //fte_list for(str <- firstTimeEvents) {
// var eventListSize : Long = 32L eventListSize += CharacterData.stringBitSize(str)
// if(firstTimeEvent_firstEntry.isDefined) { }
// eventListSize += CharacterData.stringBitSize(firstTimeEvent_firstEntry.get) + 5L //plus the current padding //tutorial list
// for(str <- firstTimeEvent_list) { val tutLen = tutorials.size
// eventListSize += CharacterData.stringBitSize(str) var tutorialListSize : Long = 32L + CharacterData.tutPadding(fteLen, tutLen)
// } for(str <- tutorials) {
// } tutorialListSize += CharacterData.stringBitSize(str)
// //tutorial list }
// var tutorialListSize : Long = 32L 708L + appearance.bitsize + eventListSize + tutorialListSize + inventory.bitsize
// for(str <- tutorial_list) {
// tutorialListSize += CharacterData.stringBitSize(str)
// }
// base + nameSize + eventListSize + tutorialListSize + inventory.bitsize
0L
} }
} }
@ -236,20 +274,45 @@ object CharacterData extends Marshallable[CharacterData] {
* defaults to the standard 8-bits * defaults to the standard 8-bits
* @return the size in 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 strlen = str.length
val lenSize = if(strlen > 127) 16L else 8L val lenSize = if(strlen > 127) 16L else 8L
lenSize + (strlen * width) 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 = { private def ftePadding(len : Long) : Int = {
//TODO determine how this should be padded better //TODO the parameters for this function are not correct
5 //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 * Get the padding of the first entry in the completed tutorials list.
if(len > 0) 0 else 0 * The padding will always be a number 0-7.<br>
* <br>
* 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] = ( implicit val codec : Codec[CharacterData] = (
@ -260,25 +323,25 @@ object CharacterData extends Marshallable[CharacterData] {
ignore(1) :: ignore(1) ::
("armor" | uint16L) :: ("armor" | uint16L) ::
ignore(9) :: ignore(9) ::
("unk4" | uint8L) :: ("unk1" | uint8L) ::
ignore(8) :: ignore(8) ::
("unk5" | uint4L) :: ("unk2" | uint4L) ::
("unk6" | uintL(3)) :: ("unk3" | uintL(3)) ::
("staminaMax" | uint16L) :: ("staminaMax" | uint16L) ::
("stamina" | uint16L) :: ("stamina" | uint16L) ::
ignore(149) :: ignore(149) ::
("unk7" | uint16L) :: ("unk4" | uint16L) ::
("unk5" | uint8L) ::
("unk6" | uint8L) ::
("unk7" | uint8L) ::
("unk8" | uint8L) :: ("unk8" | uint8L) ::
("unk9" | uint8L) :: ("unk9" | uintL(12)) ::
("unk10" | uint8L) ::
("unk11" | uint8L) ::
("unk12" | uintL(12)) ::
ignore(19) :: ignore(19) ::
(("firstTimeEvent_length" | uint32L) >>:~ { len => (("firstTimeEvent_length" | uint32L) >>:~ { len =>
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) :: conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) ::
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
(("tutorial_length" | uint32L) >>:~ { len2 => (("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)) :: ("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
ignore(207) :: ignore(207) ::
("inventory" | InventoryData.codec) ("inventory" | InventoryData.codec)
@ -323,7 +386,7 @@ object CharacterData extends Marshallable[CharacterData] {
case Some(x) => case Some(x) =>
Attempt.successful(x.asInstanceOf[CharacterData]) Attempt.successful(x.asInstanceOf[CharacterData])
case _ => case _ =>
Attempt.failure(Err("")) Attempt.failure(Err("can not encode character data"))
} }
) )
} }

View file

@ -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.<br> * Children of this class are expected to be able to translate through `scodec` operations into packet data.<br>
* <br> * <br>
* The object data is uncoupled from the object class as multiple classes use the same format for their 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 the Suppressor and the Gauss will use a "weapon data" format.
* For example, both 9mm Bullets and energy cells use am ammunition data format. * For example, both 9mm bullets and energy cells will use an "ammunition data" format.
*/ */
abstract class ConstructorData() { abstract class ConstructorData extends StreamBitSize
/**
* 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 { object ConstructorData {
/** /**
* This pattern is intended to provide common conversion between all of the `Codec`s of the children of this class. * 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] type genericPattern = Option[ConstructorData]
} }

View file

@ -8,51 +8,48 @@ import scodec.codecs._
import shapeless.{::, HNil} import shapeless.{::, HNil}
/** /**
* The same kind of data as required for a formal `ObjectCreateMessage` but with a required and implicit parent relationship. * Similar fields as required for a formal `ObjectCreateMessage` but with a required but implicit parent relationship.
* Some data preceding this entry will clarify the existence of the parent.<br> * 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`.<br>
* <br> * <br>
* As indicated, an `InternalSlot` object is not a top-level object. * 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. * Extra effort should be made to ensure the user does not have to directly construct an `InternalSlot`.
* 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 objectClass the code for the type of object being constructed
* @param guid the GUID this object will be assigned * @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 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 * @param obj the data used as representation of the object to be constructed
* @see ObjectClass.selectDataCodec
*/ */
case class InternalSlot(objectClass : Int, case class InternalSlot(objectClass : Int,
guid : PlanetSideGUID, guid : PlanetSideGUID,
parentSlot : Int, parentSlot : Int,
obj : ConstructorData) { obj : ConstructorData) extends StreamBitSize {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object * @return the number of bits necessary to represent this object
*/ */
def bitsize : Long = { override def bitsize : Long = {
val first : Long = if(parentSlot > 127) 44L else 36L val base : Long = if(parentSlot > 127) 43L else 35L
val second : Long = obj.bitsize base + obj.bitsize
first + second
} }
} }
object InternalSlot extends Marshallable[InternalSlot] { object InternalSlot extends Marshallable[InternalSlot] {
implicit val codec : Codec[InternalSlot] = ( implicit val codec : Codec[InternalSlot] = (
ignore(1) :: //TODO determine what this bit does ("objectClass" | uintL(11)) >>:~ { obj_cls =>
(("objectClass" | uintL(11)) >>:~ { obj_cls => ("guid" | PlanetSideGUID.codec) ::
("guid" | PlanetSideGUID.codec) :: ("parentSlot" | PacketHelpers.encodedStringSize) ::
("parentSlot" | PacketHelpers.encodedStringSize) :: ("obj" | ObjectClass.selectDataCodec(obj_cls)) //it's fine for this call to fail
("obj" | ObjectClass.selectDataCodec(obj_cls)) }
})
).xmap[InternalSlot] ( ).xmap[InternalSlot] (
{ {
case _ :: cls :: guid :: slot :: Some(obj) :: HNil => case cls :: guid :: slot :: Some(obj) :: HNil =>
InternalSlot(cls, guid, slot, obj) InternalSlot(cls, guid, slot, obj)
}, },
{ {
case 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] ).as[InternalSlot]
} }

View file

@ -1,20 +1,27 @@
// Copyright (c) 2016 PSForever.net to present // Copyright (c) 2016 PSForever.net to present
package net.psforever.packet.game.objectcreate package net.psforever.packet.game.objectcreate
import net.psforever.packet.Marshallable import net.psforever.packet.{Marshallable, PacketHelpers}
import scodec.Codec import scodec.Codec
import scodec.codecs._ import scodec.codecs._
import shapeless.{::,HNil} import shapeless.{::, HNil}
/** /**
* A representation of the inventory portion of `ObjectCreateMessage` packet data for avatars.<br> * A representation of the inventory portion of `ObjectCreateMessage` packet data for avatars.<br>
* <br> * <br>
* Unfortunately, the inventory is a fail-fast greedy thing. * The inventory is a temperamental thing.
* Any format discrepancies will cause it to fail and that will cause character encoding to fail as well. * Items placed into the inventory must follow their proper encoding schematics to the letter.
* Care should be taken that all possible item encodings are representable. * 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.<br>
* <br>
* Exploration:<br>
* 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; * @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 unk2 na
* @param unk3 na
* @param contents the actual items in the inventory; * @param contents the actual items in the inventory;
* holster slots are 0-4; * holster slots are 0-4;
* an inaccessible slot is 5; * an inaccessible slot is 5;
@ -22,38 +29,42 @@ import shapeless.{::,HNil}
*/ */
case class InventoryData(unk1 : Boolean, case class InventoryData(unk1 : Boolean,
unk2 : Boolean, unk2 : Boolean,
contents : Vector[InventoryItem]) { unk3 : Boolean,
contents : List[InventoryItem]) extends StreamBitSize {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object * @return the number of bits necessary to represent this object
*/ */
def bitsize : Long = { override def bitsize : Long = {
//two booleans and the 8-bit length field //three booleans, the 4u and the 8u length field
val first : Long = 10L val base : Long = 15L
//length of all items in inventory //length of all items in inventory
var second : Long = 0L var invSize : Long = 0L
for(item <- contents) { for(item <- contents) {
second += item.bitsize invSize += item.bitsize
} }
first + second base + invSize
} }
} }
object InventoryData extends Marshallable[InventoryData] { object InventoryData extends Marshallable[InventoryData] {
implicit val codec : Codec[InventoryData] = ( implicit val codec : Codec[InventoryData] = (
("unk1" | bool) :: ("unk1" | bool) ::
("len" | uint8L) :: (("len" | uint8L) >>:~ { len =>
("unk2" | bool) :: ("unk2" | bool) ::
("contents" | vector(InventoryItem.codec)) ("unk3" | bool) ::
).xmap[InventoryData] ( ("contents" | PacketHelpers.listOfNSized(len, InventoryItem.codec)) ::
ignore(4)
})
).xmap[InventoryData] (
{ {
case u1 :: _ :: u2 :: vector :: HNil => case u1 :: _ :: u2 :: u3 :: ctnt :: _ :: HNil =>
InventoryData(u1, u2, vector) InventoryData(u1, u2, u3, ctnt)
}, },
{ {
case InventoryData(u1, u2, vector) => case InventoryData(u1, u2, u3, ctnt) =>
u1 :: vector.length :: u2 :: vector :: HNil u1 :: ctnt.size :: u2 :: u3 :: ctnt :: () :: HNil
} }
).as[InventoryData] ).as[InventoryData]
} }

View file

@ -7,28 +7,22 @@ import scodec.Codec
import scodec.codecs._ import scodec.codecs._
/** /**
* Represent an item in inventory.<br> * 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.<br>
* <br> * <br>
* Note the use of `InternalSlot` to indicate the implicit parent ownership of the resulting item. * This intermediary object is primarily intended to mask external use of `InternalSlot`.
* Unwinding inventory items into individual standard `ObjectCreateMessage` packet data is entirely possible.
* @param item the object in inventory * @param item the object in inventory
* @param na the user should not have to worry about this potential bit; * @see InternalSlot
* it follows after weapon entries, allegedly
*/ */
case class InventoryItem(item : InternalSlot, case class InventoryItem(item : InternalSlot) extends StreamBitSize {
na : Option[Boolean] = None) {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object * @return the number of bits necessary to represent this object
*/ */
def bitsize : Long = { override def bitsize : Long = item.bitsize
//item
val first : Long = item.bitsize
//trailing bit
val second : Long = if(na.isDefined) 1L else 0L
first + second
}
} }
object InventoryItem extends Marshallable[InventoryItem] { object InventoryItem extends Marshallable[InventoryItem] {
@ -40,22 +34,10 @@ object InventoryItem extends Marshallable[InventoryItem] {
* @param obj the constructor data * @param obj the constructor data
* @return an InventoryItem * @return an InventoryItem
*/ */
def apply(objClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : ConstructorData) : InventoryItem = { def apply(objClass : Int, guid : PlanetSideGUID, parentSlot : Int, obj : ConstructorData) : InventoryItem =
val isWep = if(obj.isInstanceOf[WeaponData]) Some(false) else None InventoryItem(InternalSlot(objClass, guid, parentSlot, obj))
//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] = ( implicit val codec : Codec[InventoryItem] = (
("item" | InternalSlot.codec) >>:~ { item => "item" | InternalSlot.codec
conditional(wasWeapon(item), bool).hlist
}
).as[InventoryItem] ).as[InventoryItem]
} }

View file

@ -9,8 +9,9 @@ import scala.annotation.switch
/** /**
* A reference between all object class codes and the name of the object they represent.<br> * A reference between all object class codes and the name of the object they represent.<br>
* <br> * <br>
* 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. * They are recorded as little-endian hexadecimal values here.
* In `scodec` terms, that's a `uintL(11)` or `uintL(0xB)`.
*/ */
object ObjectClass { object ObjectClass {
//character //character
@ -22,7 +23,7 @@ object ObjectClass {
final val JAMMER_GRENADE_AMMO = 0x1A1 final val JAMMER_GRENADE_AMMO = 0x1A1
final val FORCE_BLADE_AMMO = 0x21C final val FORCE_BLADE_AMMO = 0x21C
final val PLASMA_GRENADE_AMMO = 0x2A9 final val PLASMA_GRENADE_AMMO = 0x2A9
final val BUCKSHOT = 0x2F3 //TODO apply internal name final val BUCKSHOT = 0x2F3 //TODO apply internal name, eventually
//weapons //weapons
final val SUPPRESSOR = 0x34D final val SUPPRESSOR = 0x34D
final val BEAMER = 0x8C final val BEAMER = 0x8C
@ -35,7 +36,7 @@ object ObjectClass {
final val MEDKIT = 0x218 final val MEDKIT = 0x218
final val REK = 0x2D8 final val REK = 0x2D8
//unknown //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 //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`. * 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. * 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 * @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] = { def selectDataCodec(objClass : Int) : Codec[ConstructorData.genericPattern] = {
(objClass : @switch) match { (objClass : @switch) match {
@ -72,11 +73,11 @@ object ObjectClass {
case _ => conditional(false, bool).exmap[ConstructorData.genericPattern] ( case _ => conditional(false, bool).exmap[ConstructorData.genericPattern] (
{ {
case None | _ => case None | _ =>
Attempt.failure(Err("decoding unknown object class - "+objClass)) Attempt.failure(Err("decoding unknown object class"))
}, },
{ {
case None | _ => case None | _ =>
Attempt.failure(Err("encoding unknown object class - "+objClass)) Attempt.failure(Err("encoding unknown object class"))
} }
) )
} }

View file

@ -8,7 +8,9 @@ import shapeless.{::, HNil}
/** /**
* A representation of the REK portion of `ObjectCreateMessage` packet data. * 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.<br>
* <br>
* Of note is the first portion of the data which resembles the `WeaponData` format.
* @param unk na * @param unk na
*/ */
case class REKData(unk : Int case class REKData(unk : Int
@ -18,28 +20,28 @@ case class REKData(unk : Int
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object * @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] { object REKData extends Marshallable[REKData] {
implicit val codec : Codec[REKData] = ( implicit val codec : Codec[REKData] = (
("unk" | uint4L) :: ("unk" | uint4L) ::
uint4L :: uint4L ::
ignore(20) :: uintL(20) ::
uint4L :: uint4L ::
ignore(16) :: uintL(16) ::
uint4L :: uint4L ::
ignore(20) uintL(15)
).exmap[REKData] ( ).exmap[REKData] (
{ {
case code :: 8 :: _ :: 2 :: _ :: 8 :: _ :: HNil => case code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil =>
Attempt.successful(REKData(code)) Attempt.successful(REKData(code))
case _ :: x :: _ :: y :: _ :: z :: _ :: HNil => case code :: _ :: _ :: _ :: _ :: _ :: _ :: 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 Attempt.failure(Err("illegal rek data format"))
}, },
{ {
case REKData(code) => case REKData(code) =>
Attempt.successful(code :: 8 :: () :: 2 :: () :: 8 :: () :: HNil) Attempt.successful(code :: 8 :: 0 :: 2 :: 0 :: 8 :: 0 :: HNil)
} }
).as[REKData] ).as[REKData]
@ -55,7 +57,7 @@ object REKData extends Marshallable[REKData] {
case Some(x) => case Some(x) =>
Attempt.successful(x.asInstanceOf[REKData]) Attempt.successful(x.asInstanceOf[REKData])
case _ => case _ =>
Attempt.failure(Err("")) Attempt.failure(Err("can not encode rek data"))
} }
) )
} }

View file

@ -10,22 +10,23 @@ import scodec.codecs._
* These are the medals players wish to brandish on their left pauldron.<br> * These are the medals players wish to brandish on their left pauldron.<br>
* <br> * <br>
* All merit commendation ribbons are represented by a 32-bit signature. * 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 upper the "top" configurable merit ribbon
* @param middle the central configurable merit ribbon * @param middle the central configurable merit ribbon
* @param lower the lower 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, case class RibbonBars(upper : Long = 0xFFFFFFFFL,
middle : Long = 0xFFFFFFFFL, middle : Long = 0xFFFFFFFFL,
lower : Long = 0xFFFFFFFFL, lower : Long = 0xFFFFFFFFL,
tos : Long = 0xFFFFFFFFL) { tos : Long = 0xFFFFFFFFL) extends StreamBitSize {
/** /**
* Performs a "sizeof()" analysis of the given object. * Performs a "sizeof()" analysis of the given object.
* @see ConstructorData.bitsize * @see ConstructorData.bitsize
* @return the number of bits necessary to represent this object * @return the number of bits necessary to represent this object
*/ */
def bitsize : Long = 128L override def bitsize : Long = 128L
} }
object RibbonBars extends Marshallable[RibbonBars] { object RibbonBars extends Marshallable[RibbonBars] {

View file

@ -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
}

View file

@ -9,10 +9,14 @@ import shapeless.{::, HNil}
/** /**
* A representation of the weapon portion of `ObjectCreateMessage` packet data. * A representation of the weapon portion of `ObjectCreateMessage` packet data.
* When alone, this data will help construct a "weapon" such as Suppressor.<br> * This data will help construct a "weapon" such as a Suppressor or a Gauss.<br>
* <br> * <br>
* The data for the weapon also nests required default ammunition data. * The data for the weapons nests information for the default (current) type and number of ammunition in its magazine.
* Where the ammunition is loaded is considered the "first slot." * 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 unk na
* @param ammo data regarding the currently loaded ammunition type and quantity * @param ammo data regarding the currently loaded ammunition type and quantity
* @see AmmoBoxData * @see AmmoBoxData
@ -25,7 +29,7 @@ case class WeaponData(unk : Int,
* @see AmmoBoxData.bitsize * @see AmmoBoxData.bitsize
* @return the number of bits necessary to represent this object * @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] { object WeaponData extends Marshallable[WeaponData] {
@ -47,21 +51,23 @@ object WeaponData extends Marshallable[WeaponData] {
implicit val codec : Codec[WeaponData] = ( implicit val codec : Codec[WeaponData] = (
("unk" | uint4L) :: ("unk" | uint4L) ::
uint4L :: uint4L ::
ignore(20) :: uintL(20) ::
uint4L :: uint4L ::
ignore(16) :: uintL(16) ::
uintL(11) :: uintL(11) ::
("ammo" | InternalSlot.codec) bool ::
("ammo" | InternalSlot.codec) ::
bool
).exmap[WeaponData] ( ).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)) Attempt.successful(WeaponData(code, ammo))
case _ :: x :: _ :: y :: _ :: z :: _ :: HNil => case code :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: 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 Attempt.failure(Err("illegal weapon data format"))
}, },
{ {
case WeaponData(code, ammo) => 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] ).as[WeaponData]
@ -77,7 +83,7 @@ object WeaponData extends Marshallable[WeaponData] {
case Some(x) => case Some(x) =>
Attempt.successful(x.asInstanceOf[WeaponData]) Attempt.successful(x.asInstanceOf[WeaponData])
case _ => case _ =>
Attempt.failure(Err("")) Attempt.failure(Err("can not encode weapon data"))
} }
) )
} }

View file

@ -4,7 +4,7 @@ import java.net.{InetAddress, InetSocketAddress}
import org.specs2.mutable._ import org.specs2.mutable._
import net.psforever.packet._ import net.psforever.packet._
import net.psforever.packet.game._ import net.psforever.packet.game._
import net.psforever.packet.game.objectcreate._ import net.psforever.packet.game.objectcreate.{InventoryItem, _}
import net.psforever.types._ import net.psforever.types._
import scodec.{Attempt, Err} import scodec.{Attempt, Err}
import scodec.Attempt.Successful 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" 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_9mm = hex"18 7C000000 2580 0E0 0005 A1 C8000064000"
val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000" val string_gauss = hex"18 DC000000 2580 2C9 B905 82 480000020000C04 1C00C0B0190000078000"
val string_testchar = hex"18 570C0000 BC8 4B00 6C2D7 65535 CA16 0 00 01 34 40 00 0970 49006C006C006C004900490049006C006C006C0049006C0049006C006C0049006C006C006C0049006C006C004900 84 52 70 76 1E 80 80 00 00 00 00 00 3FFFC 0 00 00 00 20 00 00 0F F6 A7 03 FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FD 90 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 90 01 90 00 64 00 00 01 00 7E C8 00 C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 C0 00 42 C5 46 86 C7 00 00 00 80 00 00 12 40 78 70 65 5F 73 61 6E 63 74 75 61 72 79 5F 68 65 6C 70 90 78 70 65 5F 74 68 5F 66 69 72 65 6D 6F 64 65 73 8B 75 73 65 64 5F 62 65 61 6D 65 72 85 6D 61 70 31 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 0A 23 02 60 04 04 40 00 00 10 00 06 02 08 14 D0 08 0C 80 00 02 00 02 6B 4E 00 82 88 00 00 02 00 00 C0 41 C0 9E 01 01 90 00 00 64 00 44 2A 00 10 91 00 00 00 40 00 18 08 38 94 40 20 32 00 00 00 80 19 05 48 02 17 20 00 00 08 00 70 29 80 43 64 00 00 32 00 0E 05 40 08 9C 80 00 06 40 01 C0 AA 01 19 90 00 00 C8 00 3A 15 80 28 72 00 00 19 00 04 0A B8 05 26 40 00 03 20 06 C2 58 00 A7 88 00 00 02 00 00 80 00 00" val string_rek = hex"18 97000000 2580 6C2 9F05 81 48000002000080000"
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 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"
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 ""
// }
}
"decode (2)" in { "decode (2)" in {
//an invalid bit representation will fail to turn into an object //an invalid bit representation will fail to turn into an object
PacketCoding.DecodePacket(packet2).require match { PacketCoding.DecodePacket(packet2).require match {
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
len mustEqual 248 //60 + 188 len mustEqual 248
cls mustEqual 121 cls mustEqual 121
guid mustEqual PlanetSideGUID(2497) guid mustEqual PlanetSideGUID(2497)
parent mustEqual None 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 { "decode (character)" in {
PacketCoding.DecodePacket(string_testchar).require match { PacketCoding.DecodePacket(string_testchar).require match {
case obj @ ObjectCreateMessage(len, cls, guid, parent, data) => case obj @ ObjectCreateMessage(len, cls, guid, parent, data) =>
@ -206,17 +240,22 @@ class GamePacketTest extends Specification {
char.appearance.objYaw mustEqual 19 char.appearance.objYaw mustEqual 19
char.appearance.faction mustEqual 2 //vs char.appearance.faction mustEqual 2 //vs
char.appearance.bops mustEqual false char.appearance.bops mustEqual false
char.appearance.unk1 mustEqual 4
char.appearance.name mustEqual "IlllIIIlllIlIllIlllIllI" char.appearance.name mustEqual "IlllIIIlllIlIllIlllIllI"
char.appearance.exosuit mustEqual 4 //standard char.appearance.exosuit mustEqual 4 //standard
char.appearance.sex mustEqual 2 //female char.appearance.sex mustEqual 2 //female
char.appearance.face1 mustEqual 2 char.appearance.face1 mustEqual 2
char.appearance.face2 mustEqual 9 char.appearance.face2 mustEqual 9
char.appearance.voice mustEqual 1 //female 1 char.appearance.voice mustEqual 1 //female 1
char.appearance.unk1 mustEqual 0x8080 char.appearance.unk2 mustEqual 3
char.appearance.unk2 mustEqual 0xFFFF char.appearance.unk3 mustEqual 118
char.appearance.unk3 mustEqual 2 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.viewPitch mustEqual 0xFF
char.appearance.viewYaw mustEqual 0x6A char.appearance.viewYaw mustEqual 0x6A
char.appearance.unk8 mustEqual 7
char.appearance.ribbons.upper mustEqual 0xFFFFFFFFL //none char.appearance.ribbons.upper mustEqual 0xFFFFFFFFL //none
char.appearance.ribbons.middle mustEqual 0xFFFFFFFFL //none char.appearance.ribbons.middle mustEqual 0xFFFFFFFFL //none
char.appearance.ribbons.lower mustEqual 0xFFFFFFFFL //none char.appearance.ribbons.lower mustEqual 0xFFFFFFFFL //none
@ -224,17 +263,17 @@ class GamePacketTest extends Specification {
char.healthMax mustEqual 100 char.healthMax mustEqual 100
char.health mustEqual 100 char.health mustEqual 100
char.armor mustEqual 50 //standard exosuit value char.armor mustEqual 50 //standard exosuit value
char.unk4 mustEqual 1 char.unk1 mustEqual 1
char.unk5 mustEqual 7 char.unk2 mustEqual 7
char.unk6 mustEqual 7 char.unk3 mustEqual 7
char.staminaMax mustEqual 100 char.staminaMax mustEqual 100
char.stamina mustEqual 100 char.stamina mustEqual 100
char.unk7 mustEqual 28 char.unk4 mustEqual 28
char.unk8 mustEqual 4 char.unk5 mustEqual 4
char.unk9 mustEqual 44 char.unk6 mustEqual 44
char.unk10 mustEqual 84 char.unk7 mustEqual 84
char.unk11 mustEqual 104 char.unk8 mustEqual 104
char.unk12 mustEqual 1900 char.unk9 mustEqual 1900
char.firstTimeEvents.size mustEqual 4 char.firstTimeEvents.size mustEqual 4
char.firstTimeEvents.head mustEqual "xpe_sanctuary_help" char.firstTimeEvents.head mustEqual "xpe_sanctuary_help"
char.firstTimeEvents(1) mustEqual "xpe_th_firemodes" char.firstTimeEvents(1) mustEqual "xpe_th_firemodes"
@ -243,7 +282,7 @@ class GamePacketTest extends Specification {
char.tutorials.size mustEqual 0 char.tutorials.size mustEqual 0
char.inventory.unk1 mustEqual true char.inventory.unk1 mustEqual true
char.inventory.unk2 mustEqual false char.inventory.unk2 mustEqual false
char.inventory.contents.length mustEqual 10 char.inventory.contents.size mustEqual 10
val inventory = char.inventory.contents val inventory = char.inventory.contents
//0 //0
inventory.head.item.objectClass mustEqual 0x8C //beamer 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.objectClass mustEqual 0x2D8 //rek
inventory(9).item.guid mustEqual PlanetSideGUID(88) inventory(9).item.guid mustEqual PlanetSideGUID(88)
inventory(9).item.parentSlot mustEqual 39 inventory(9).item.parentSlot mustEqual 39
//the rek has data but none worth testing here //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
case default => case default =>
ko ko
} }
@ -371,6 +372,60 @@ class GamePacketTest extends Specification {
pkt mustEqual string_gauss 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 { "ChatMsg" should {