mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
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:
parent
f98a648db1
commit
3b9f3a6f33
|
|
@ -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] = (
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
* ` MALE FEMALE`<br>
|
||||
* `0 - no voice no voice`<br>
|
||||
* `1 - male_1 female_1`<br>
|
||||
* `2 - male_2 female_2`<br>
|
||||
* `3 - male_3 female_3`<br>
|
||||
* `4 - male_4 female_4`<br>
|
||||
* `5 - male_5 female_5`<br>
|
||||
* `6 - female_1 no voice`<br>
|
||||
* `7 - female_2 no voice`
|
||||
* @param pos the position of the character in the world environment (in three coordinates)
|
||||
* @param objYaw the angle with respect to the horizon towards which the object's front is facing;
|
||||
* every `0x1` is 2.813 degrees counter clockwise from North;
|
||||
* every `0x10` is 45-degrees;
|
||||
* it wraps at `0x0` == `0x80` == North
|
||||
* (note: references the avatar as a game object?)
|
||||
* @param faction the empire to which the avatar belongs;
|
||||
* the value scale is different from `PlanetSideEmpire`
|
||||
* @param bops whether or not this avatar is enrolled in Black OPs
|
||||
* @param unk1 na;
|
||||
* defaults to 4
|
||||
* @param name the wide character name of the avatar, minimum of two characters
|
||||
* @param exosuit the type of exosuit the avatar will be depicted in;
|
||||
* for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits
|
||||
* @param sex whether the avatar is male or female
|
||||
* @param face1 the avatar's face, as by column number on the character creation screen
|
||||
* @param face2 the avatar's face, as by row number on the character creation screen
|
||||
* @param voice the avatar's voice selection
|
||||
* @param unk2 na
|
||||
* @param unk3 na;
|
||||
* can be missing from the stream under certain conditions;
|
||||
* see next
|
||||
* @param unk4 na;
|
||||
* can be missing from the stream under certain conditions;
|
||||
* see previous
|
||||
* @param unk5 na;
|
||||
* defaults to `0x8080`
|
||||
* @param unk6 na;
|
||||
* defaults to `0xFFFF`;
|
||||
* may be `0x0`
|
||||
* @param unk7 na;
|
||||
* defaults to 2
|
||||
* @param viewPitch the angle with respect to the sky and the ground towards which the avatar is looking;
|
||||
* only supports downwards view angles;
|
||||
* `0x0` is forwards-facing;
|
||||
* `0x20` to `0xFF` is downwards-facing
|
||||
* @param viewYaw the angle with respect to the horizon towards which the avatar is looking;
|
||||
* every `0x1` is 2.813 degrees counter clockwise from North;
|
||||
* every `0x10` is 45-degrees;
|
||||
* it wraps at `0x0` == `0x80` == North
|
||||
* @param unk8 na
|
||||
* @param ribbons the four merit commendation ribbon medals
|
||||
*/
|
||||
case class CharacterAppearanceData(pos : Vector3,
|
||||
objYaw : Int,
|
||||
faction : Int,
|
||||
bops : Boolean,
|
||||
unk1 : Int,
|
||||
name : String,
|
||||
exosuit : Int,
|
||||
sex : Int,
|
||||
face1 : Int,
|
||||
face2 : Int,
|
||||
voice : Int,
|
||||
unk1 : Int, //0x8080
|
||||
unk2 : Int, //0xFFFF or 0x0
|
||||
unk3 : Int, //2
|
||||
unk2 : Int,
|
||||
unk3 : Int,
|
||||
unk4 : Int,
|
||||
unk5 : Int,
|
||||
unk6 : Int,
|
||||
unk7 : Int,
|
||||
viewPitch : Int,
|
||||
viewYaw : Int,
|
||||
ribbons : RibbonBars)
|
||||
unk8 : Int,
|
||||
ribbons : RibbonBars) extends StreamBitSize {
|
||||
/**
|
||||
* Performs a "sizeof()" analysis of the given object.
|
||||
* @see ConstructorData.bitsize
|
||||
* @return the number of bits necessary to represent this object
|
||||
*/
|
||||
override def bitsize : Long = {
|
||||
//TODO ongoing analysis, this value will be subject to change
|
||||
430L + CharacterData.stringBitSize(name, 16) + CharacterAppearanceData.namePadding
|
||||
}
|
||||
}
|
||||
|
||||
object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
|
||||
/**
|
||||
* Get the padding of the avatar's name.
|
||||
* The padding will always be a number 0-7.
|
||||
* @return the pad length in bits
|
||||
*/
|
||||
private def namePadding : Int = {
|
||||
//TODO the parameters for this function are not correct
|
||||
//TODO the proper padding length should reflect all variability in the substream prior to this point
|
||||
4
|
||||
}
|
||||
|
||||
implicit val codec : Codec[CharacterAppearanceData] = (
|
||||
("pos" | Vector3.codec_pos) ::
|
||||
ignore(16) ::
|
||||
|
|
@ -32,24 +140,29 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
|
|||
ignore(1) ::
|
||||
("faction" | uintL(2)) ::
|
||||
("bops" | bool) ::
|
||||
ignore(20) ::
|
||||
("name" | PacketHelpers.encodedWideStringAligned(4)) ::
|
||||
("unk1" | uint4L) ::
|
||||
ignore(16) ::
|
||||
("name" | PacketHelpers.encodedWideStringAligned( namePadding )) ::
|
||||
("exosuit" | uintL(3)) ::
|
||||
ignore(2) ::
|
||||
("sex" | uintL(2)) ::
|
||||
("face1" | uint4L) ::
|
||||
("face2" | uint4L) ::
|
||||
("voice" | uintL(3)) ::
|
||||
ignore(22) ::
|
||||
("unk1" | uint16L) ::
|
||||
("unk2" | uintL(2)) ::
|
||||
ignore(4) ::
|
||||
("unk3" | uint8L) ::
|
||||
("unk4" | uint8L) ::
|
||||
("unk5" | uint16L) ::
|
||||
ignore(42) ::
|
||||
("unk2" | uint16L) ::
|
||||
("unk6" | uint16L) ::
|
||||
ignore(30) ::
|
||||
("unk3" | uint4L) ::
|
||||
("unk7" | uint4L) ::
|
||||
ignore(24) ::
|
||||
("viewPitch" | uint8L) ::
|
||||
("viewYaw" | uint8L) ::
|
||||
ignore(10) ::
|
||||
("unk8" | uint4L) ::
|
||||
ignore(6) ::
|
||||
("ribbons" | RibbonBars.codec)
|
||||
).as[CharacterAppearanceData]
|
||||
}
|
||||
|
|
@ -58,143 +171,73 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
|
|||
* A representation of the avatar portion of `ObjectCreateMessage` packet data.<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>
|
||||
* ` MALE FEMALE`<br>
|
||||
* `0 - No voice No voice`<br>
|
||||
* `1 - Male_1 Female_1`<br>
|
||||
* `2 - Male_2 Female_2`<br>
|
||||
* `3 - Male_3 Female_3`<br>
|
||||
* `4 - Male_4 Female_4`<br>
|
||||
* `5 - Male_5 Female_5`<br>
|
||||
* `6 - Female_1 No voice`<br>
|
||||
* `7 - Female_2 No voice`
|
||||
// * @param pos the position of the character in the world environment (in three coordinates)
|
||||
// * @param objYaw the angle with respect to the horizon towards which the object's front is facing;
|
||||
// * every `0x1` is 2.813 degrees counter clockwise from North;
|
||||
// * every `0x10` is 45-degrees;
|
||||
// * it wraps at `0x80`
|
||||
// * (note: references the avatar as a game object?)
|
||||
// * @param faction the empire to which the avatar belongs;
|
||||
// * the value scale is different from `PlanetSideEmpire`;
|
||||
// * @param bops whether or not this avatar is enrolled in Black OPs
|
||||
// * @param name the wide character name of the avatar
|
||||
// * @param exosuit the type of exosuit the avatar will be depicted in;
|
||||
// * for Black OPs, the agile exosuit and the reinforced exosuit are replaced with the Black OPs exosuits
|
||||
// * @param sex whether the avatar is male or female
|
||||
// * @param face1 the avatar's face, as by column number on the character creation screen
|
||||
// * @param face2 the avatar's face, as by row number on the character creation screen
|
||||
// * @param voice the avatar's voice selection
|
||||
// * @param unk1 na;
|
||||
// * defaults to `0x8080`
|
||||
// * @param unk2 na;
|
||||
// * defaults to `0xFFFF`;
|
||||
// * may be `0x0`
|
||||
// * @param unk3 na;
|
||||
// * defaults to 2
|
||||
// * @param viewPitch the angle with respect to the horizon towards which the avatar is looking;
|
||||
// * only supports downwards view angles;
|
||||
// * `0x0` is forwards-facing;
|
||||
// * `0x20` to `0xFF` is downwards-facing
|
||||
// * @param viewYaw the angle with respect to the ground directions towards which the avatar is looking;
|
||||
// * every `0x1` is 2.813 degrees counter clockwise from North;
|
||||
// * every `0x10` is 45-degrees;
|
||||
// * it wraps at `0x80`
|
||||
// * @param ribbons the four merit commendation ribbon medals displayed on the avatar's left pauldron
|
||||
// * @param healthMax for "x / y" of hitpoints, this is the avatar's 'y' value;
|
||||
// * range is 0-65535
|
||||
// * @param health for "x / y" of hitpoints, this is the avatar's 'x' value;
|
||||
// * range is 0-65535
|
||||
// * @param armor for "x / y" of armor points, this is the avatar's 'x' value;
|
||||
// * range is 0-65535;
|
||||
// * the avatar's "y" armor points is tied to their exosuit type
|
||||
// * @param unk4 na;
|
||||
// * defaults to 1
|
||||
// * @param unk5 na;
|
||||
// * defaults to 7
|
||||
// * @param unk6 na;
|
||||
// * defaults to 7
|
||||
// * @param staminaMax for "x / y" of stamina points, this is the avatar's 'y' value;
|
||||
// * range is 0-65535
|
||||
// * @param stamina for "x / y" of stamina points, this is the avatar's 'x' value;
|
||||
// * range is 0-65535
|
||||
// * @param unk7 na;
|
||||
// * defaults to 28
|
||||
// * @param unk8 na;
|
||||
// * defaults to 4
|
||||
// * @param unk9 na;
|
||||
// * defaults to 44
|
||||
// * @param unk10 na;
|
||||
// * defaults to 84
|
||||
// * @param unk11 na;
|
||||
// * defaults to 104
|
||||
// * @param unk12 na;
|
||||
// * defaults to 1900
|
||||
// * @param firstTimeEvent_length the total number of first time events performed by this avatar
|
||||
// * @param firstTimeEvent_firstEntry the separated "first entry" of the list of first time events performed by this avatar
|
||||
// * @param firstTimeEvent_list the list of first time events performed by this avatar
|
||||
// * @param tutorial_length the total number of tutorials completed by this avatar
|
||||
// * @param tutorial_firstEntry the separated "first entry" of the list of tutorials completed by this avatar
|
||||
// * @param tutorial_list the list of tutorials completed by this avatar
|
||||
// * @param inventory the avatar's inventory
|
||||
* The base length of the stream is currently __1138__ bits, excluding `List`s and `String`s and inventory.
|
||||
* Of that, __831__ bits are perfectly unknown.
|
||||
* @param appearance data about the avatar's basic aesthetics
|
||||
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value;
|
||||
* range is 0-65535
|
||||
* @param health for `x / y` of hitpoints, this is the avatar's `x` value;
|
||||
* range is 0-65535
|
||||
* @param armor for `x / y` of armor points, this is the avatar's `x` value;
|
||||
* range is 0-65535;
|
||||
* the avatar's `y` armor points is tied to their exo-suit type
|
||||
* @param unk1 na;
|
||||
* defaults to 1
|
||||
* @param unk2 na;
|
||||
* defaults to 7
|
||||
* @param unk3 na;
|
||||
* defaults to 7
|
||||
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value;
|
||||
* range is 0-65535
|
||||
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value;
|
||||
* range is 0-65535
|
||||
* @param unk4 na;
|
||||
* defaults to 28
|
||||
* @param unk5 na;
|
||||
* defaults to 4
|
||||
* @param unk6 na;
|
||||
* defaults to 44
|
||||
* @param unk7 na;
|
||||
* defaults to 84
|
||||
* @param unk8 na;
|
||||
* defaults to 104
|
||||
* @param unk9 na;
|
||||
* defaults to 1900
|
||||
* @param firstTimeEvents the list of first time events performed by this avatar;
|
||||
* the size field is a 32-bit number;
|
||||
* the first entry may be padded
|
||||
* @param tutorials the list of tutorials completed by this avatar;
|
||||
* the size field is a 32-bit number;
|
||||
* the first entry may be padded
|
||||
* @param inventory the avatar's inventory
|
||||
*/
|
||||
case class CharacterData(appearance : CharacterAppearanceData,
|
||||
healthMax : Int,
|
||||
health : Int,
|
||||
armor : Int,
|
||||
unk4 : Int, //1
|
||||
unk5 : Int, //7
|
||||
unk6 : Int, //7
|
||||
unk1 : Int, //1
|
||||
unk2 : Int, //7
|
||||
unk3 : Int, //7
|
||||
staminaMax : Int,
|
||||
stamina : Int,
|
||||
unk7 : Int, //28
|
||||
unk8 : Int, //4
|
||||
unk9 : Int, //44
|
||||
unk10 : Int, //84
|
||||
unk11 : Int, //104
|
||||
unk12 : Int, //1900
|
||||
unk4 : Int, //28
|
||||
unk5 : Int, //4
|
||||
unk6 : Int, //44
|
||||
unk7 : Int, //84
|
||||
unk8 : Int, //104
|
||||
unk9 : Int, //1900
|
||||
firstTimeEvents : List[String],
|
||||
tutorials : List[String],
|
||||
inventory : InventoryData
|
||||
|
|
@ -205,25 +248,20 @@ case class CharacterData(appearance : CharacterAppearanceData,
|
|||
* @return the number of bits necessary to represent this object
|
||||
*/
|
||||
override def bitsize : Long = {
|
||||
// //represents static fields (includes medals.bitsize)
|
||||
// val base : Long = 1138L //TODO ongoing analysis, this value will be subject to change
|
||||
// //name
|
||||
// val nameSize : Long = CharacterData.stringBitSize(appearance.name, 16) + 4L //plus the current padding
|
||||
// //fte_list
|
||||
// var eventListSize : Long = 32L
|
||||
// if(firstTimeEvent_firstEntry.isDefined) {
|
||||
// eventListSize += CharacterData.stringBitSize(firstTimeEvent_firstEntry.get) + 5L //plus the current padding
|
||||
// for(str <- firstTimeEvent_list) {
|
||||
// eventListSize += CharacterData.stringBitSize(str)
|
||||
// }
|
||||
// }
|
||||
// //tutorial list
|
||||
// var tutorialListSize : Long = 32L
|
||||
// for(str <- tutorial_list) {
|
||||
// tutorialListSize += CharacterData.stringBitSize(str)
|
||||
// }
|
||||
// base + nameSize + eventListSize + tutorialListSize + inventory.bitsize
|
||||
0L
|
||||
//TODO ongoing analysis, this value will be subject to change
|
||||
//fte list
|
||||
val fteLen = firstTimeEvents.size
|
||||
var eventListSize : Long = 32L + CharacterData.ftePadding(fteLen)
|
||||
for(str <- firstTimeEvents) {
|
||||
eventListSize += CharacterData.stringBitSize(str)
|
||||
}
|
||||
//tutorial list
|
||||
val tutLen = tutorials.size
|
||||
var tutorialListSize : Long = 32L + CharacterData.tutPadding(fteLen, tutLen)
|
||||
for(str <- tutorials) {
|
||||
tutorialListSize += CharacterData.stringBitSize(str)
|
||||
}
|
||||
708L + appearance.bitsize + eventListSize + tutorialListSize + inventory.bitsize
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -236,20 +274,45 @@ object CharacterData extends Marshallable[CharacterData] {
|
|||
* defaults to the standard 8-bits
|
||||
* @return the size in bits
|
||||
*/
|
||||
private def stringBitSize(str : String, width : Int = 8) : Long = {
|
||||
def stringBitSize(str : String, width : Int = 8) : Long = {
|
||||
val strlen = str.length
|
||||
val lenSize = if(strlen > 127) 16L else 8L
|
||||
lenSize + (strlen * width)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the padding of the first entry in the first time events list.
|
||||
* The padding will always be a number 0-7.
|
||||
* @param len the length of the list
|
||||
* @return the pad length in bits
|
||||
*/
|
||||
private def ftePadding(len : Long) : Int = {
|
||||
//TODO determine how this should be padded better
|
||||
5
|
||||
//TODO the parameters for this function are not correct
|
||||
//TODO the proper padding length should reflect all variability in the stream prior to this point
|
||||
if(len > 0) {
|
||||
5
|
||||
}
|
||||
else
|
||||
0
|
||||
}
|
||||
|
||||
private def tutListPadding(len : Long) : Int = {
|
||||
//TODO determine how this should be padded when len == 0
|
||||
if(len > 0) 0 else 0
|
||||
/**
|
||||
* Get the padding of the first entry in the completed tutorials list.
|
||||
* The padding will always be a number 0-7.<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"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue