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
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.<br>
* <br>
* 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>
* <br>
* 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.<br>
* <br>
* 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.<br>
* <br>
* 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] = (

View file

@ -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.<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>
* Exploration:<br>
* 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"))
}
)
}

View file

@ -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.<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,
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.<br>
* <br>
* 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(!).<br>
* 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.<br>
* <br>
* 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.<br>
* 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.<br>
* <br>
* 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.<br>
* <br>
* Faction:<br>
* `0 - Terran Republic`<br>
* `1 - New Comglomerate`<br>
* `2 - Vanu Sovereignty`<br>
* <br>
* Exosuit:<br>
* `0 - Agile`<br>
* `1 - Refinforced`<br>
* `2 - Mechanized Assault`<br>
* `3 - Infiltration`<br>
* `4 - Standard`<br>
* <br>
* Sex:<br>
* `1 - Male`<br>
* `2 - Female`<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 `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.<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] = (
@ -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"))
}
)
}

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>
* <br>
* 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]
}

View file

@ -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.<br>
* 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`.<br>
* <br>
* 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]
}

View file

@ -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.<br>
* <br>
* 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.<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;
* 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]
}

View file

@ -7,28 +7,22 @@ import scodec.Codec
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>
* 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]
}

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>
* <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.
* 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"))
}
)
}

View file

@ -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.<br>
* <br>
* 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"))
}
)
}

View file

@ -10,22 +10,23 @@ import scodec.codecs._
* These are the medals players wish to brandish on their left pauldron.<br>
* <br>
* 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] {

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.
* 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>
* 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"))
}
)
}

View file

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