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