added documentation and refined comments; corrected stream length calculation issues where padding lengths were not being properly retained or updated; working tests

This commit is contained in:
FateJH 2018-06-01 14:17:36 -04:00
parent 4e41468cd0
commit 389d0b4d82
6 changed files with 442 additions and 98 deletions

View file

@ -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.<br>
* <br>
* 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.<br>
* <br>
* Voice:<br>
* `&nbsp;&nbsp;&nbsp;&nbsp;MALE&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;FEMALE`<br>
* `0 - no voice &nbsp;no voice`<br>
* `1 - male_1 &nbsp;&nbsp; female_1`<br>
* `2 - male_2 &nbsp;&nbsp; female_2`<br>
* `3 - male_3 &nbsp;&nbsp; female_3`<br>
* `4 - male_4 &nbsp;&nbsp; female_4`<br>
* `5 - male_5 &nbsp;&nbsp; female_5`<br>
* `6 - female_1 &nbsp;no voice`<br>
* `7 - female_2 &nbsp;no voice`
* This appearance coincides with the data available from the `CharacterCreateRequestMessage` packet.
* @see `PlanetSideEmpire`<br>
* `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

View file

@ -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`<br>
* `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 _ =>

View file

@ -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.<br>
* <br>
* 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.<br>
* <br>
* 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`<br>
* `InventoryData`<br>
@ -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`<br>
* `CharacterAppearanceData.name`<br>
* `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
}
)

View file

@ -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`<br>
* `VehicleData.InitialStreamLengthToSeatEntries`<br>
* `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)
}
}

View file

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

View file

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