diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala
index a16de51e..e590def6 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala
@@ -7,24 +7,36 @@ import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
+/**
+ * The voice used by the player character, from a selection of ten divided between five male voices and five female voices.
+ * The first entry (0) is no voice.
+ * While it is technically not valid to have a wrong-gendered voice,
+ * unlisted sixth and seventh entries would give a male character a female voice;
+ * a female character with either entry would become mute.
+ * @see `CharacterGender`
+ */
+object CharacterVoice extends Enumeration {
+ type Type = Value
+
+ val
+ Mute,
+ Voice1, //grizzled, tough
+ Voice2, //greenhorn, clueless
+ Voice3, //roughneck, gruff
+ Voice4, //stalwart, smooth
+ Voice5 //daredevil, calculating
+ = Value
+
+ implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(3))
+}
+
/**
* A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.
*
* This partition of the data stream contains information used to represent how the player's avatar is presented.
- * This appearance coincides with the data available from the `CharacterCreateRequestMessage` packet.
- *
- * Voice:
- * ` MALE FEMALE`
- * `0 - no voice no voice`
- * `1 - male_1 female_1`
- * `2 - male_2 female_2`
- * `3 - male_3 female_3`
- * `4 - male_4 female_4`
- * `5 - male_5 female_5`
- * `6 - female_1 no voice`
- * `7 - female_2 no voice`
+ * This appearance coincides with the data available from the `CharacterCreateRequestMessage` packet.
* @see `PlanetSideEmpire`
- * `CharacaterGender`
+ * `CharacterGender`
* @param name the unique name of the avatar;
* minimum of two characters
* @param faction the empire to which the avatar belongs
@@ -111,7 +123,7 @@ final case class CharacterAppearanceData(app : BasicCharacterData,
override def bitsize : Long = {
//factor guard bool values into the base size, not its corresponding optional field
- val nameStringSize : Long = StreamBitSize.stringBitSize(app.name, 16) + CharacterAppearanceData.namePaddingRule(name_padding)
+ val nameStringSize : Long = StreamBitSize.stringBitSize(app.name, 16) + name_padding
val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) + CharacterAppearanceData.outfitNamePadding
val altModelSize = CharacterAppearanceData.altModelBit(this).getOrElse(0)
335L + nameStringSize + outfitStringSize + altModelSize
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
index fae8ab44..d89e6d37 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
@@ -55,9 +55,12 @@ object UniformStyle extends Enumeration {
* any user would find this character ill-equipped.
* @param health the amount of health the player has, as a percentage of a filled bar;
* the bar has 85 states, with 3 points for each state;
- * when 0% (less than 3 of 255), the player will collapse into a death pose on the ground
+ * when 0% (less than 3 of 255), the player will collapse into a death pose on the ground;
+ * while `is_corpse == true`, `health` will always report as 0;
+ * while `is_seated == true`, `health` will (try to) report as 100
* @param armor the amount of armor the player has, as a percentage of a filled bar;
- * the bar has 85 states, with 3 points for each state
+ * the bar has 85 states, with 3 points for each state;
+ * while `is_seated == true`, `armor` will always report as 0
* @param uniform_upgrade the level of upgrade to apply to the player's base uniform
* @param command_rank the player's command rank as a number from 0 to 5;
* cosmetic armor associated with the command rank will be applied automatically
@@ -66,7 +69,13 @@ object UniformStyle extends Enumeration {
* @param cosmetics optional decorative features that are added to the player's head model by console/chat commands;
* they become available at battle rank 24, but here they require the third uniform upgrade (rank 25);
* these flags do not exist if they are not applicable
- * @see `DetailedCharacterData`
+ * @param is_backpack this player character should be depicted as a corpse;
+ * corpses are either coffins (defunct), backpacks (normal), or a pastry (festive);
+ * the alternate model bit should be flipped
+ * @param is_seated this player character is seated in a vehicle or mounted to some other object;
+ * alternate format for data parsing applies
+ * @see `DetailedCharacterData`
+ * `CharacterAppearanceData`
*/
final case class CharacterData(health : Int,
armor : Int,
@@ -75,13 +84,15 @@ final case class CharacterData(health : Int,
command_rank : Int,
implant_effects : Option[ImplantEffects.Value],
cosmetics : Option[Cosmetics])
- (is_backpack : Boolean) extends ConstructorData {
+ (is_backpack : Boolean,
+ is_seated : Boolean) extends ConstructorData {
override def bitsize : Long = {
//factor guard bool values into the base size, not its corresponding optional field
+ val seatedSize = if(is_seated) { 0 } else { 16 }
val effectsSize : Long = if(implant_effects.isDefined) { 4L } else { 0L }
val cosmeticsSize : Long = if(cosmetics.isDefined) { cosmetics.get.bitsize } else { 0L }
- 27L + effectsSize + cosmeticsSize
+ 11L + seatedSize + effectsSize + cosmeticsSize
}
}
@@ -94,11 +105,9 @@ object CharacterData extends Marshallable[CharacterData] {
* @param cr the player's command rank as a number from 0 to 5
* @param implant_effects the effects of implants that can be seen on a player's character
* @param cosmetics optional decorative features that are added to the player's head model by console/chat commands
- * //@param inv the avatar's inventory
- * //@param drawn_slot the holster that is initially drawn
* @return a `CharacterData` object
*/
- def apply(health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : Option[ImplantEffects.Value], cosmetics : Option[Cosmetics]) : (Boolean)=>CharacterData =
+ def apply(health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : Option[ImplantEffects.Value], cosmetics : Option[Cosmetics]) : (Boolean,Boolean)=>CharacterData =
CharacterData(health, armor, uniform, 0, cr, implant_effects, cosmetics)
def codec(is_backpack : Boolean) : Codec[CharacterData] = (
@@ -107,7 +116,7 @@ object CharacterData extends Marshallable[CharacterData] {
(("uniform_upgrade" | UniformStyle.codec) >>:~ { style =>
ignore(3) :: //unknown
("command_rank" | uintL(3)) ::
- bool :: //stream misalignment when != 1
+ bool :: //misalignment when == 1
optional(bool, "implant_effects" | ImplantEffects.codec) ::
conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec)
})
@@ -115,7 +124,7 @@ object CharacterData extends Marshallable[CharacterData] {
{
case health :: armor :: uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: HNil =>
val newHealth = if(is_backpack) { 0 } else { health }
- Attempt.Successful(CharacterData(newHealth, armor, uniform, 0, cr, implant_effects, cosmetics)(is_backpack))
+ Attempt.Successful(CharacterData(newHealth, armor, uniform, 0, cr, implant_effects, cosmetics)(is_backpack, false))
case _ =>
Attempt.Failure(Err("invalid character data; can not encode"))
@@ -141,13 +150,13 @@ object CharacterData extends Marshallable[CharacterData] {
).exmap[CharacterData] (
{
case uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: HNil =>
- Attempt.Successful(new CharacterData(100, 0, uniform, 0, cr, implant_effects, cosmetics)(is_backpack))
+ Attempt.Successful(new CharacterData(100, 0, uniform, 0, cr, implant_effects, cosmetics)(is_backpack, true))
case _ =>
Attempt.Failure(Err("invalid character data; can not encode"))
},
{
- case CharacterData(_, _, uniform, _, cr, implant_effects, cosmetics) =>
+ case obj @ CharacterData(_, _, uniform, _, cr, implant_effects, cosmetics) =>
Attempt.Successful(uniform :: () :: cr :: false :: implant_effects :: cosmetics :: HNil)
case _ =>
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala
index 8045b069..636df525 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/PlayerData.scala
@@ -8,7 +8,7 @@ import scodec.Codec
import shapeless.{::, HNil}
/**
- * A representation of another player's character for the `ObjectCreateDetailedMessage` packet.
+ * A representation of another player's character for the `ObjectCreateMessage` packet.
* In general, this packet is used to describe other players.
*
* Divisions exist to make the data more manageable.
@@ -17,10 +17,12 @@ import shapeless.{::, HNil}
* that are shared by both the `ObjectCreateDetailedMessage` version of a controlled player character
* and the `ObjectCreateMessage` version of a player character (this).
* The third field provides further information on the appearance of the player character, albeit condensed.
+ * The fourth field involves the player's `Equipment` holsters and their inventory.
+ * The hand that the player has exposed is last.
* One of the most compact forms of a player character description is transcribed using this information.
*
* The presence or absence of position data as the first division creates a cascading effect
- * causing all of fields in the other two divisions to gain offsets.
+ * causing all of fields in the other two divisions to gain offset values.
* These offsets exist in the form of `String` and `List` padding.
* @see `CharacterData`
* `InventoryData`
@@ -40,7 +42,8 @@ final case class PlayerData(pos : Option[PlacementData],
drawn_slot : DrawnSlot.Value)
(position_defined : Boolean) extends ConstructorData {
override def bitsize : Long = {
- val posSize : Long = if(pos.isDefined) { pos.get.bitsize } else { 0 }
+ //factor guard bool values into the base size, not its corresponding optional field
+ val posSize : Long = if(pos.isDefined) { pos.get.bitsize } else { 0L }
val appSize : Long = basic_appearance.bitsize
val charSize = character_data.bitsize
val inventorySize : Long = if(inventory.isDefined) { inventory.get.bitsize } else { 0L }
@@ -52,24 +55,39 @@ object PlayerData extends Marshallable[PlayerData] {
/**
* Overloaded constructor that ignores the coordinate information.
* It passes information between the three major divisions for the purposes of offset calculations.
+ * This constructor should be used for players that are mounted.
* @param basic_appearance a curried function for the common fields regarding the the character's appearance
* @param character_data a curried function for the class-specific data that explains about the character
* @param inventory the player's inventory
* @param drawn_slot the holster that is initially drawn
+ * @param accumulative the input position for the stream up to which this entry;
+ * used to calculate the padding value for the player's name in `CharacterAppearanceData`
* @return a `PlayerData` object
*/
- def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type) : PlayerData = {
- val appearance = basic_appearance(0)
- PlayerData(None, appearance, character_data(appearance.backpack), Some(inventory), drawn_slot)(false)
- }
- /** */
- def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean)=>CharacterData, hand_held : DrawnSlot.Type) : PlayerData = {
- val appearance = basic_appearance(0)
- PlayerData(None, appearance, character_data(appearance.backpack), None, hand_held)(false)
+ def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type, accumulative : Long) : PlayerData = {
+ val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative))
+ PlayerData(None, appearance, character_data(appearance.backpack, true), Some(inventory), drawn_slot)(false)
}
/**
- * Overloaded constructor that includes the coordinate information.
+ * Overloaded constructor that ignores the coordinate information and the inventory.
* It passes information between the three major divisions for the purposes of offset calculations.
+ * This constructor should be used for players that are mounted.
+ * @param basic_appearance a curried function for the common fields regarding the the character's appearance
+ * @param character_data a curried function for the class-specific data that explains about the character
+ * @param drawn_slot the holster that is initially drawn
+ * @param accumulative the input position for the stream up to which this entry;
+ * used to calculate the padding value for the player's name in `CharacterAppearanceData`
+ * @return a `PlayerData` object
+ */
+ def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, drawn_slot : DrawnSlot.Type, accumulative : Long) : PlayerData = {
+ val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative))
+ PlayerData(None, appearance, character_data(appearance.backpack, true), None, drawn_slot)(false)
+ }
+
+ /**
+ * Overloaded constructor.
+ * It passes information between the three major divisions for the purposes of offset calculations.
+ * This constructor should be used for players that are standing apart from other containers.
* @param pos the optional position of the character in the world environment
* @param basic_appearance a curried function for the common fields regarding the the character's appearance
* @param character_data a curried function for the class-specific data that explains about the character
@@ -77,20 +95,54 @@ object PlayerData extends Marshallable[PlayerData] {
* @param drawn_slot the holster that is initially drawn
* @return a `PlayerData` object
*/
- def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type) : PlayerData = {
+ def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type) : PlayerData = {
val appearance = basic_appearance( placementOffset(Some(pos)) )
- PlayerData(Some(pos), appearance, character_data(appearance.backpack), Some(inventory), drawn_slot)(true)
+ PlayerData(Some(pos), appearance, character_data(appearance.backpack, false), Some(inventory), drawn_slot)(true)
}
- /** */
- def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean)=>CharacterData, hand_held : DrawnSlot.Type) : PlayerData = {
+ /**
+ * Overloaded constructor that ignores the inventory.
+ * It passes information between the three major divisions for the purposes of offset calculations.
+ * This constructor should be used for players that are standing apart from other containers.
+ * @param pos the optional position of the character in the world environment
+ * @param basic_appearance a curried function for the common fields regarding the the character's appearance
+ * @param character_data a curried function for the class-specific data that explains about the character
+ * @param drawn_slot the holster that is initially drawn
+ * @return a `PlayerData` object
+ */
+ def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, drawn_slot : DrawnSlot.Type) : PlayerData = {
val appearance = basic_appearance( placementOffset(Some(pos)) )
- PlayerData(Some(pos), appearance, character_data(appearance.backpack), None, hand_held)(true)
+ PlayerData(Some(pos), appearance, character_data(appearance.backpack, false), None, drawn_slot)(true)
+ }
+
+ /**
+ * Calculate the padding value for the next mounted player character's name `String`.
+ * Due to the depth of seated player characters, the `name` field can have a variable amount of padding
+ * between the string size field and the first character.
+ * Specifically, the padding value is the number of bits after the size field
+ * that would cause the first character of the name to be aligned to the first bit of the next byte.
+ * The 35 counts the object class, unique identifier, and slot fields of the enclosing `InternalSlot`.
+ * The 23 counts all of the fields before the player's `name` field in `CharacterAppearanceData`.
+ * @see `InternalSlot`
+ * `CharacterAppearanceData.name`
+ * `VehicleData.InitialStreamLengthToSeatEntries`
+ * @param accumulative current entry stream offset (start of this player's entry)
+ * @return the padding value, 0-7 bits
+ */
+ def CumulativeSeatedPlayerNamePadding(accumulative : Long) : Int = {
+ val offset = accumulative + 23 + 35
+ val pad = ((offset - math.floor(offset / 8) * 8) % 8).toInt
+ if(pad > 0) {
+ 8 - pad
+ }
+ else {
+ 0
+ }
}
/**
* Determine the padding offset for a subsequent field given the existence of `PlacementData`.
* The padding will always be a number 0-7.
- * @see `PlacemtnData`
+ * @see `PlacementData`
* @param pos the optional `PlacementData` object that creates the shift in bits
* @return the pad length in bits
*/
@@ -103,6 +155,14 @@ object PlayerData extends Marshallable[PlayerData] {
}
}
+ /**
+ * This `Codec` is generic.
+ * However, it should not be used to translate a `Player` object
+ * in the middle of translating that `Player`'s mounting object.
+ * The offset value is calculated internally.
+ * @param position_defined this entry has `PlacementData` that defines position, orientation, and, optionally, motion
+ * @return a `Codec` that translates a `PlayerData` object
+ */
def codec(position_defined : Boolean) : Codec[PlayerData] = (
conditional(position_defined, "pos" | PlacementData.codec) >>:~ { pos =>
("basic_appearance" | CharacterAppearanceData.codec(placementOffset(pos))) >>:~ { app =>
@@ -124,26 +184,29 @@ object PlayerData extends Marshallable[PlayerData] {
}
)
-
-
- def codec(position_defined : Boolean, offset : Int) : Codec[PlayerData] = (
- conditional(position_defined, "pos" | PlacementData.codec) >>:~ { pos =>
- ("basic_appearance" | CharacterAppearanceData.codec(offset)) >>:~ { app =>
- ("character_data" | newcodecs.binary_choice(position_defined,
- CharacterData.codec(app.backpack),
- CharacterData.codec_seated(app.backpack))) ::
- optional(bool, "inventory" | InventoryData.codec) ::
- ("drawn_slot" | DrawnSlot.codec) ::
- bool //usually false
+ /**
+ * This `Codec` is exclusively for translating a `Player` object
+ * while that `Player` object is encountered in the process of translating its mounting object.
+ * In other words, the player is "seated" or "mounted."
+ * @see `CharacterAppearanceData.codec`
+ * @param offset the padding for the player's name field
+ * @return a `Codec` that translates a `PlayerData` object
+ */
+ def codec(offset : Int) : Codec[PlayerData] = (
+ ("basic_appearance" | CharacterAppearanceData.codec(offset)) >>:~ { app =>
+ ("character_data" | CharacterData.codec_seated(app.backpack)) ::
+ optional(bool, "inventory" | InventoryData.codec) ::
+ ("drawn_slot" | DrawnSlot.codec) ::
+ bool //usually false
}
- }).xmap[PlayerData] (
+ ).xmap[PlayerData] (
{
- case pos :: app :: data :: inv :: hand :: _ :: HNil =>
- PlayerData(pos, app, data, inv, hand)(pos.isDefined)
+ case app :: data :: inv :: hand :: _ :: HNil =>
+ PlayerData(None, app, data, inv, hand)(false)
},
{
- case PlayerData(pos, app, data, inv, hand) =>
- pos :: app :: data :: inv :: hand :: false :: HNil
+ case PlayerData(None, app, data, inv, hand) =>
+ app :: data :: inv :: hand :: false :: HNil
}
)
diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala
index 412517e4..56753c64 100644
--- a/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala
+++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/VehicleData.scala
@@ -212,7 +212,7 @@ object VehicleData extends Marshallable[VehicleData] {
("unk4" | bool) ::
("cloak" | bool) :: //cloak as wraith, phantasm
conditional(vehicle_type != VehicleFormat.Normal, "unk5" | selectFormatReader(vehicle_type)) :: //padding?
- optional(bool, "inventory" | custom_inventory_codec(InitialStreamLengthToSeatEntries(com, vehicle_type)))
+ optional(bool, "inventory" | custom_inventory_codec(InitialStreamLengthToSeatEntries(com.pos.vel.isDefined, vehicle_type)))
}
).exmap[VehicleData] (
{
@@ -245,9 +245,19 @@ object VehicleData extends Marshallable[VehicleData] {
)
}
- private def InitialStreamLengthToSeatEntries(com : CommonFieldData, format : VehicleFormat.Type) : Long = {
+ /**
+ * Distance from the length field of a vehicle creation packet up until the start of the vehicle's inventory data.
+ * The only field excluded belongs to the original opcode for the packet.
+ * The parameters outline reasons why the length of the stream would be different
+ * and are used to determine the exact difference value.
+ * @see `ObjectCreateMessage`
+ * @param hasVelocity the presence of a velocity field - `vel` - in the `PlacementData` object for this vehicle
+ * @param format the `Codec` subtype for this vehicle
+ * @return the length of the bitstream
+ */
+ def InitialStreamLengthToSeatEntries(hasVelocity : Boolean, format : VehicleFormat.Type) : Long = {
198 +
- (if(com.pos.vel.isDefined) { 42 } else { 0 }) +
+ (if(hasVelocity) { 42 } else { 0 }) +
(format match {
case VehicleFormat.Utility => 6
case VehicleFormat.Variant => 8
@@ -255,6 +265,33 @@ object VehicleData extends Marshallable[VehicleData] {
})
}
+ /**
+ * Increment the distance to the next mounted player's `name` field with the length of the previous entry,
+ * then calculate the new padding value for that next entry's `name` field.
+ * @param base the original distance to the last entry
+ * @param next the length of the last entry, if one was parsed
+ * @return the padding value, 0-7 bits
+ */
+ def CumulativeSeatedPlayerNamePadding(base : Long, next : Option[StreamBitSize]) : Int = {
+ PlayerData.CumulativeSeatedPlayerNamePadding(base + (next match {
+ case Some(o) => o.bitsize
+ case None => 0
+ }))
+ }
+
+ /**
+ * A special method of handling mounted players within the same inventory space as normal `Equipment` can be encountered.
+ * Due to variable-length fields within `PlayerData` extracted from the input,
+ * the distance of the bit(stream) vector to the initial inventory entry is calculated
+ * to produce the initial value for padding the `PlayerData` object's name field.
+ * After player-related entries have been extracted and processed in isolation,
+ * the remainder of the inventory must be handled as standard inventory
+ * and finally both groups must be repackaged into a single standard `InventoryData` object.
+ * Due to the unique value for the mounted players that must be updated for each entry processed,
+ * the entries are temporarily formatted into a linked list before being put back into a normal `List`.
+ * @param length the distance in bits to the first inventory entry
+ * @return a `Codec` that translates `InventoryData`
+ */
private def custom_inventory_codec(length : Long) : Codec[InventoryData] = {
import shapeless.::
(
@@ -262,16 +299,7 @@ object VehicleData extends Marshallable[VehicleData] {
uint2 ::
(inventory_seat_codec(
length, //length of stream until current seat
- { //calculated offset of name field in next seat
- val next = length + 23 + 35 //in bits: InternalSlot lead + length of CharacterAppearanceData~>name
- val pad =((next - math.floor(next / 8) * 8) % 8).toInt
- if(pad > 0) {
- 8 - pad
- }
- else {
- 0
- }
- }
+ PlayerData.CumulativeSeatedPlayerNamePadding(length) //calculated offset of name field in next seat
) >>:~ { seats =>
PacketHelpers.listOfNSized(size - countSeats(seats), InternalSlot.codec).hlist
})
@@ -292,8 +320,22 @@ object VehicleData extends Marshallable[VehicleData] {
)
}
+ /**
+ * The format for the linked list of extracted mounted `PlayerData`.
+ * @param seat data for this entry extracted via `PlayerData`
+ * @param next the next entry
+ */
private case class InventorySeat(seat : Option[InternalSlot], next : Option[InventorySeat])
+ /**
+ * Look ahead at the next value to determine if it is an example of a player character
+ * and would be processed as a `PlayerData` object.
+ * Update the stream read position with each extraction.
+ * Continue to process values so long as they represent player character data.
+ * @param length the distance in bits to the current inventory entry
+ * @param offset the padding value for this entry's player character's `name` field
+ * @return a recursive `Codec` that translates subsequent `PlayerData` entries until exhausted
+ */
private def inventory_seat_codec(length : Long, offset : Int) : Codec[Option[InventorySeat]] = {
import shapeless.::
(
@@ -306,20 +348,9 @@ object VehicleData extends Marshallable[VehicleData] {
case None => 0
})
},
- { //calculated offset of name field in next seat
- val next = length + 23 + 35 + (seat match {
- case Some(o) => o.bitsize
- case None => 0
- })
- val pad =((next - math.floor(next / 8) * 8) % 8).toInt
- if(pad > 0) {
- 8 - pad
- }
- else {
- 0
- }
- })).hlist
- }
+ VehicleData.CumulativeSeatedPlayerNamePadding(length, seat) //calculated offset of name field in next seat
+ )).hlist
+ }
}
).exmap[Option[InventorySeat]] (
{
@@ -342,13 +373,24 @@ object VehicleData extends Marshallable[VehicleData] {
)
}
+ /**
+ * Translate data the is verified to involve a player who is seated (mounted) to the parent object at a given slot.
+ * The operation performed by this `Codec` is very similar to `InternalSlot.codec`.
+ * @param pad the padding offset for the player's name;
+ * 0-7 bits;
+ * this padding value must recalculate for each represented seat
+ * @see `CharacterAppearanceData`
+ * `VehicleData.InitialStreamLengthToSeatEntries`
+ * `PlayerData.CumulativeSeatedPlayerNamePadding`
+ * @return a `Codec` that translates `PlayerData`
+ */
private def seat_codec(pad : Int) : Codec[InternalSlot] = {
import shapeless.::
(
("objectClass" | uintL(11)) ::
("guid" | PlanetSideGUID.codec) ::
("parentSlot" | PacketHelpers.encodedStringSize) ::
- ("obj" | PlayerData.codec(false, pad))
+ ("obj" | PlayerData.codec(pad))
).xmap[InternalSlot] (
{
case objectClass :: guid :: parentSlot :: obj :: HNil =>
@@ -361,38 +403,67 @@ object VehicleData extends Marshallable[VehicleData] {
)
}
+ /**
+ * Count the number of entries in a linked list.
+ * @param chain the head of the linked list
+ * @return the number of entries
+ */
private def countSeats(chain : Option[InventorySeat]) : Int = {
chain match {
+ case Some(_) =>
+ var curr = chain
+ var count = 0
+ do {
+ val link = curr.get
+ count += (if(link.seat.nonEmpty) { 1 } else { 0 })
+ curr = link.next
+ }
+ while(curr.nonEmpty)
+ count
+
case None =>
0
- case Some(link) =>
- if(link.seat.isDefined) { 1 } else { 0 } + countSeats(link.next)
}
}
+ /**
+ * Transform a linked list of `InventorySlot` slot objects into a formal list of `InternalSlot` objects.
+ * @param chain the head of the linked list
+ * @return a proper list of the contents of the input linked list
+ */
private def unlinkSeats(chain : Option[InventorySeat]) : List[InternalSlot] = {
var curr = chain
val out = new ListBuffer[InternalSlot]
while(curr.isDefined) {
- curr.get.seat match {
+ val link = curr.get
+ link.seat match {
case None =>
curr = None
case Some(seat) =>
out += seat
- curr = curr.get.next
+ curr = link.next
}
}
out.toList
}
+ /**
+ * Transform a formal list of `InternalSlot` objects into a linked list of `InventorySlot` slot objects.
+ * @param list a proper list of objects
+ * @return a linked list composed of the contents of the input list
+ */
private def chainSeats(list : List[InternalSlot]) : Option[InventorySeat] = {
list match {
case Nil =>
None
case x :: Nil =>
Some(InventorySeat(Some(x), None))
- case x :: xs =>
- Some(InventorySeat(Some(x), chainSeats(xs)))
+ case _ :: _ =>
+ var link = InventorySeat(Some(list.last), None)
+ list.reverse.drop(1).foreach(seat => {
+ link = InventorySeat(Some(seat), Some(link))
+ })
+ Some(link)
}
}
diff --git a/common/src/test/scala/game/objectcreate/CharacterDataTest.scala b/common/src/test/scala/game/objectcreate/CharacterDataTest.scala
index 843e61a2..06805d20 100644
--- a/common/src/test/scala/game/objectcreate/CharacterDataTest.scala
+++ b/common/src/test/scala/game/objectcreate/CharacterDataTest.scala
@@ -200,7 +200,7 @@ class CharacterDataTest extends Specification {
MeritCommendation.SixYearTR
)
)
- val char : (Boolean)=>CharacterData = CharacterData(
+ val char : (Boolean,Boolean)=>CharacterData = CharacterData(
255, 253,
UniformStyle.ThirdUpgrade,
5,
@@ -215,7 +215,7 @@ class CharacterDataTest extends Specification {
InventoryItemData(ObjectClass.chainblade, PlanetSideGUID(4088), 4, WeaponData(0, 0, 1, ObjectClass.melee_ammo, PlanetSideGUID(3279), 0, AmmoBoxData())) ::
Nil
)
- val obj = PlayerData.apply(pos, app, char, inv, DrawnSlot.Rifle1)
+ val obj = PlayerData(pos, app, char, inv, DrawnSlot.Rifle1)
val msg = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(3902), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
@@ -260,14 +260,14 @@ class CharacterDataTest extends Specification {
MeritCommendation.SixYearVS
)
)
- val char : (Boolean)=>CharacterData = CharacterData(
+ val char : (Boolean,Boolean)=>CharacterData = CharacterData(
0, 0,
UniformStyle.ThirdUpgrade,
2,
None,
Some(Cosmetics(true, true, true, true, false))
)
- val obj = PlayerData.apply(pos, app, char, DrawnSlot.Pistol1)
+ val obj = PlayerData(pos, app, char, DrawnSlot.Pistol1)
val msg = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(3380), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
diff --git a/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala b/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala
new file mode 100644
index 00000000..938d6e6a
--- /dev/null
+++ b/common/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala
@@ -0,0 +1,189 @@
+// Copyright (c) 2017 PSForever
+package game.objectcreatevehicle
+
+import net.psforever.packet._
+import net.psforever.packet.game.{ObjectCreateMessage, PlanetSideGUID}
+import net.psforever.packet.game.objectcreate._
+import net.psforever.types._
+import org.specs2.mutable._
+import scodec.bits._
+
+class MountedVehiclesTest extends Specification {
+ val string_mosquito_seated =
+ hex"17c70700009e2d410d8ed818f1a4017047f7ffbc6390ffbe01801cff00003c08791801d00000002340530063007200610077006e00790052" ++
+ hex"006f006e006e0069006500020b7e67b540404001000000000022b50100268042006c00610063006b00200042006500720065007400200041" ++
+ hex"0072006d006f007500720065006400200043006f00720070007300170040030050040003bc00000234040001a00400027a7a0809a6910800" ++
+ hex"00000008090a6403603000001082202e040000000202378ae0e80c00000162710b82000000008083837032030000015e2583210000000020" ++
+ hex"20e21c0c80c000007722120e81c0000000808063483603000000"
+
+ "decode (Scrawny Ronnie's mosquito)" in {
+ PacketCoding.DecodePacket(string_mosquito_seated).require match {
+ case ObjectCreateMessage(len, cls, guid, parent, data) =>
+ len mustEqual 1991
+ cls mustEqual ObjectClass.mosquito
+ guid mustEqual PlanetSideGUID(4308)
+ parent mustEqual None
+ data match {
+ case Some(vdata : VehicleData) =>
+ vdata.basic.pos.coord mustEqual Vector3(4571.6875f, 5602.1875f, 93)
+ vdata.basic.pos.orient mustEqual Vector3(11.25f, 2.8125f, 92.8125f)
+ vdata.basic.pos.vel mustEqual Some(Vector3(31.71875f, 8.875f, -0.03125f))
+ vdata.basic.faction mustEqual PlanetSideEmpire.TR
+ vdata.basic.bops mustEqual false
+ vdata.basic.destroyed mustEqual false
+ vdata.basic.jammered mustEqual false
+ vdata.basic.player_guid mustEqual PlanetSideGUID(1888)
+ vdata.unk1 mustEqual 0
+ vdata.health mustEqual 255
+ vdata.unk2 mustEqual false
+ vdata.no_mount_points mustEqual false
+ vdata.driveState mustEqual DriveState.Mobile
+ vdata.unk3 mustEqual false
+ vdata.unk5 mustEqual false
+ vdata.cloak mustEqual false
+ vdata.unk4 mustEqual Some(VariantVehicleData(7))
+ vdata.inventory match {
+ case Some(InventoryData(list)) =>
+ list.head.objectClass mustEqual ObjectClass.avatar
+ list.head.guid mustEqual PlanetSideGUID(3776)
+ list.head.parentSlot mustEqual 0
+ list.head.obj match {
+ case PlayerData(pos, app, char, Some(InventoryData(inv)), hand) =>
+ pos mustEqual None
+ app.app.name mustEqual "ScrawnyRonnie"
+ app.app.faction mustEqual PlanetSideEmpire.TR
+ app.app.sex mustEqual CharacterGender.Male
+ app.app.head mustEqual 5
+ app.app.voice mustEqual 5
+ app.voice2 mustEqual 3
+ app.black_ops mustEqual false
+ app.lfs mustEqual false
+ app.outfit_name mustEqual "Black Beret Armoured Corps"
+ app.outfit_logo mustEqual 23
+ app.facingPitch mustEqual 354.375f
+ app.facingYawUpper mustEqual 0.0f
+ app.altModelBit mustEqual None
+ app.charging_pose mustEqual false
+ app.on_zipline mustEqual false
+ app.backpack mustEqual false
+ app.ribbons.upper mustEqual MeritCommendation.MarkovVeteran
+ app.ribbons.middle mustEqual MeritCommendation.HeavyInfantry4
+ app.ribbons.lower mustEqual MeritCommendation.TankBuster7
+ app.ribbons.tos mustEqual MeritCommendation.SixYearTR
+ char.health mustEqual 100
+ char.armor mustEqual 0
+ char.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade
+ char.command_rank mustEqual 5
+ char.implant_effects mustEqual None
+ char.cosmetics mustEqual Some(Cosmetics(true, true, true, true, false))
+ inv.size mustEqual 4
+ inv.head.objectClass mustEqual ObjectClass.medicalapplicator
+ inv.head.parentSlot mustEqual 0
+ inv(1).objectClass mustEqual ObjectClass.bank
+ inv(1).parentSlot mustEqual 1
+ inv(2).objectClass mustEqual ObjectClass.mini_chaingun
+ inv(2).parentSlot mustEqual 2
+ inv(3).objectClass mustEqual ObjectClass.chainblade
+ inv(3).parentSlot mustEqual 4
+ hand mustEqual DrawnSlot.None
+ case _ =>
+ ko
+ }
+ list(1).objectClass mustEqual ObjectClass.rotarychaingun_mosquito
+ list(1).parentSlot mustEqual 1
+ case None =>
+ ko
+ }
+ case _ =>
+ ko
+ }
+ case _ =>
+ ko
+ }
+ }
+
+ "encode (Scrawny Ronnie's mosquito)" in {
+ val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
+ BasicCharacterData("ScrawnyRonnie", PlanetSideEmpire.TR, CharacterGender.Male, 5, 5),
+ 3,
+ false, false,
+ ExoSuitType.Agile,
+ "Black Beret Armoured Corps",
+ 23,
+ false,
+ 354.375f, 0.0f,
+ false,
+ GrenadeState.None, false, false, false,
+ RibbonBars(
+ MeritCommendation.MarkovVeteran,
+ MeritCommendation.HeavyInfantry4,
+ MeritCommendation.TankBuster7,
+ MeritCommendation.SixYearTR
+ )
+ )
+ val char : (Boolean,Boolean)=>CharacterData = CharacterData(
+ 100, 0,
+ UniformStyle.ThirdUpgrade,
+ 0,
+ 5,
+ None,
+ Some(Cosmetics(true, true, true, true, false))
+ )
+ val inv : InventoryData = InventoryData(
+ List(
+ InternalSlot(ObjectClass.medicalapplicator, PlanetSideGUID(4201), 0,
+ WeaponData(0, 0, 0, List(InternalSlot(ObjectClass.health_canister, PlanetSideGUID(3472), 0, AmmoBoxData(0))))
+ ),
+ InternalSlot(ObjectClass.bank, PlanetSideGUID(2952), 1,
+ WeaponData(0, 0, 0, List(InternalSlot(ObjectClass.armor_canister, PlanetSideGUID(3758), 0, AmmoBoxData(0))))
+ ),
+ InternalSlot(ObjectClass.mini_chaingun, PlanetSideGUID(2929), 2,
+ WeaponData(0, 0, 0, List(InternalSlot(ObjectClass.bullet_9mm, PlanetSideGUID(3292), 0, AmmoBoxData(0))))
+ ),
+ InternalSlot(ObjectClass.chainblade, PlanetSideGUID(3222), 4,
+ WeaponData(0, 0, 0, List(InternalSlot(ObjectClass.melee_ammo, PlanetSideGUID(3100), 0, AmmoBoxData(0))))
+ )
+ )
+ )
+ val player = PlayerData.apply(app, char, inv, DrawnSlot.None, VehicleData.InitialStreamLengthToSeatEntries(true, VehicleFormat.Variant))
+ val obj = VehicleData(
+ CommonFieldData(
+ PlacementData(
+ Vector3(4571.6875f, 5602.1875f, 93),
+ Vector3(11.25f, 2.8125f, 92.8125f),
+ Some(Vector3(31.71875f, 8.875f, -0.03125f))
+ ),
+ PlanetSideEmpire.TR,
+ false, false, 0, false,
+ PlanetSideGUID(1888)
+ ),
+ 0, 255,
+ false, false,
+ DriveState.Mobile,
+ false, false, false,
+ Some(VariantVehicleData(7)),
+ Some(
+ InventoryData(
+ List(
+ InternalSlot(ObjectClass.avatar, PlanetSideGUID(3776), 0, player),
+ InternalSlot(ObjectClass.rotarychaingun_mosquito, PlanetSideGUID(3602), 1,
+ WeaponData(6, 0, 0, List(InternalSlot(ObjectClass.bullet_12mm, PlanetSideGUID(3538), 0, AmmoBoxData(0))))
+ )
+ )
+ )
+ )
+ )(VehicleFormat.Variant)
+ val msg = ObjectCreateMessage(ObjectClass.mosquito, PlanetSideGUID(4308), obj)
+ val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
+
+ val pkt_bitv = pkt.toBitVector
+ val ori_bitv = string_mosquito_seated.toBitVector
+ pkt_bitv.take(555) mustEqual ori_bitv.take(555) //skip 126
+ pkt_bitv.drop(681).take(512) mustEqual ori_bitv.drop(681).take(512) //renew
+ pkt_bitv.drop(1193).take(88) mustEqual ori_bitv.drop(1193).take(88) //skip 3
+ pkt_bitv.drop(1284).take(512) mustEqual ori_bitv.drop(1284).take(512) //renew
+ pkt_bitv.drop(1796) mustEqual ori_bitv.drop(1796)
+ //TODO work on CharacterData to make this pass as a single stream
+ }
+}
+