mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-07-14 07:55:07 +00:00
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:
parent
4e41468cd0
commit
389d0b4d82
6 changed files with 442 additions and 98 deletions
|
|
@ -7,24 +7,36 @@ import scodec.{Attempt, Codec, Err}
|
||||||
import scodec.codecs._
|
import scodec.codecs._
|
||||||
import shapeless.{::, HNil}
|
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>
|
* A part of a representation of the avatar portion of `ObjectCreateMessage` packet data.<br>
|
||||||
* <br>
|
* <br>
|
||||||
* This partition of the data stream contains information used to represent how the player's avatar is presented.
|
* 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>
|
* This appearance coincides with the data available from the `CharacterCreateRequestMessage` packet.
|
||||||
* <br>
|
|
||||||
* Voice:<br>
|
|
||||||
* ` MALE FEMALE`<br>
|
|
||||||
* `0 - no voice no voice`<br>
|
|
||||||
* `1 - male_1 female_1`<br>
|
|
||||||
* `2 - male_2 female_2`<br>
|
|
||||||
* `3 - male_3 female_3`<br>
|
|
||||||
* `4 - male_4 female_4`<br>
|
|
||||||
* `5 - male_5 female_5`<br>
|
|
||||||
* `6 - female_1 no voice`<br>
|
|
||||||
* `7 - female_2 no voice`
|
|
||||||
* @see `PlanetSideEmpire`<br>
|
* @see `PlanetSideEmpire`<br>
|
||||||
* `CharacaterGender`
|
* `CharacterGender`
|
||||||
* @param name the unique name of the avatar;
|
* @param name the unique name of the avatar;
|
||||||
* minimum of two characters
|
* minimum of two characters
|
||||||
* @param faction the empire to which the avatar belongs
|
* @param faction the empire to which the avatar belongs
|
||||||
|
|
@ -111,7 +123,7 @@ final case class CharacterAppearanceData(app : BasicCharacterData,
|
||||||
|
|
||||||
override def bitsize : Long = {
|
override def bitsize : Long = {
|
||||||
//factor guard bool values into the base size, not its corresponding optional field
|
//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 outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) + CharacterAppearanceData.outfitNamePadding
|
||||||
val altModelSize = CharacterAppearanceData.altModelBit(this).getOrElse(0)
|
val altModelSize = CharacterAppearanceData.altModelBit(this).getOrElse(0)
|
||||||
335L + nameStringSize + outfitStringSize + altModelSize
|
335L + nameStringSize + outfitStringSize + altModelSize
|
||||||
|
|
|
||||||
|
|
@ -55,9 +55,12 @@ object UniformStyle extends Enumeration {
|
||||||
* any user would find this character ill-equipped.
|
* any user would find this character ill-equipped.
|
||||||
* @param health the amount of health the player has, as a percentage of a filled bar;
|
* @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;
|
* 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;
|
* @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 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;
|
* @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
|
* 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;
|
* @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);
|
* 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
|
* 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,
|
final case class CharacterData(health : Int,
|
||||||
armor : Int,
|
armor : Int,
|
||||||
|
|
@ -75,13 +84,15 @@ final case class CharacterData(health : Int,
|
||||||
command_rank : Int,
|
command_rank : Int,
|
||||||
implant_effects : Option[ImplantEffects.Value],
|
implant_effects : Option[ImplantEffects.Value],
|
||||||
cosmetics : Option[Cosmetics])
|
cosmetics : Option[Cosmetics])
|
||||||
(is_backpack : Boolean) extends ConstructorData {
|
(is_backpack : Boolean,
|
||||||
|
is_seated : Boolean) extends ConstructorData {
|
||||||
|
|
||||||
override def bitsize : Long = {
|
override def bitsize : Long = {
|
||||||
//factor guard bool values into the base size, not its corresponding optional field
|
//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 effectsSize : Long = if(implant_effects.isDefined) { 4L } else { 0L }
|
||||||
val cosmeticsSize : Long = if(cosmetics.isDefined) { cosmetics.get.bitsize } 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 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 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 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
|
* @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)
|
CharacterData(health, armor, uniform, 0, cr, implant_effects, cosmetics)
|
||||||
|
|
||||||
def codec(is_backpack : Boolean) : Codec[CharacterData] = (
|
def codec(is_backpack : Boolean) : Codec[CharacterData] = (
|
||||||
|
|
@ -107,7 +116,7 @@ object CharacterData extends Marshallable[CharacterData] {
|
||||||
(("uniform_upgrade" | UniformStyle.codec) >>:~ { style =>
|
(("uniform_upgrade" | UniformStyle.codec) >>:~ { style =>
|
||||||
ignore(3) :: //unknown
|
ignore(3) :: //unknown
|
||||||
("command_rank" | uintL(3)) ::
|
("command_rank" | uintL(3)) ::
|
||||||
bool :: //stream misalignment when != 1
|
bool :: //misalignment when == 1
|
||||||
optional(bool, "implant_effects" | ImplantEffects.codec) ::
|
optional(bool, "implant_effects" | ImplantEffects.codec) ::
|
||||||
conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.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 =>
|
case health :: armor :: uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: HNil =>
|
||||||
val newHealth = if(is_backpack) { 0 } else { health }
|
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 _ =>
|
case _ =>
|
||||||
Attempt.Failure(Err("invalid character data; can not encode"))
|
Attempt.Failure(Err("invalid character data; can not encode"))
|
||||||
|
|
@ -141,13 +150,13 @@ object CharacterData extends Marshallable[CharacterData] {
|
||||||
).exmap[CharacterData] (
|
).exmap[CharacterData] (
|
||||||
{
|
{
|
||||||
case uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: HNil =>
|
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 _ =>
|
case _ =>
|
||||||
Attempt.Failure(Err("invalid character data; can not encode"))
|
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)
|
Attempt.Successful(uniform :: () :: cr :: false :: implant_effects :: cosmetics :: HNil)
|
||||||
|
|
||||||
case _ =>
|
case _ =>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import scodec.Codec
|
||||||
import shapeless.{::, HNil}
|
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>
|
* In general, this packet is used to describe other players.<br>
|
||||||
* <br>
|
* <br>
|
||||||
* Divisions exist to make the data more manageable.
|
* 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
|
* that are shared by both the `ObjectCreateDetailedMessage` version of a controlled player character
|
||||||
* and the `ObjectCreateMessage` version of a player character (this).
|
* 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 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>
|
* One of the most compact forms of a player character description is transcribed using this information.<br>
|
||||||
* <br>
|
* <br>
|
||||||
* The presence or absence of position data as the first division creates a cascading effect
|
* 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.
|
* These offsets exist in the form of `String` and `List` padding.
|
||||||
* @see `CharacterData`<br>
|
* @see `CharacterData`<br>
|
||||||
* `InventoryData`<br>
|
* `InventoryData`<br>
|
||||||
|
|
@ -40,7 +42,8 @@ final case class PlayerData(pos : Option[PlacementData],
|
||||||
drawn_slot : DrawnSlot.Value)
|
drawn_slot : DrawnSlot.Value)
|
||||||
(position_defined : Boolean) extends ConstructorData {
|
(position_defined : Boolean) extends ConstructorData {
|
||||||
override def bitsize : Long = {
|
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 appSize : Long = basic_appearance.bitsize
|
||||||
val charSize = character_data.bitsize
|
val charSize = character_data.bitsize
|
||||||
val inventorySize : Long = if(inventory.isDefined) { inventory.get.bitsize } else { 0L }
|
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.
|
* Overloaded constructor that ignores the coordinate information.
|
||||||
* It passes information between the three major divisions for the purposes of offset calculations.
|
* 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 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 character_data a curried function for the class-specific data that explains about the character
|
||||||
* @param inventory the player's inventory
|
* @param inventory the player's inventory
|
||||||
* @param drawn_slot the holster that is initially drawn
|
* @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
|
* @return a `PlayerData` object
|
||||||
*/
|
*/
|
||||||
def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type) : PlayerData = {
|
def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type, accumulative : Long) : PlayerData = {
|
||||||
val appearance = basic_appearance(0)
|
val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative))
|
||||||
PlayerData(None, appearance, character_data(appearance.backpack), Some(inventory), drawn_slot)(false)
|
PlayerData(None, appearance, character_data(appearance.backpack, true), 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)
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 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.
|
* 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 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 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 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
|
* @param drawn_slot the holster that is initially drawn
|
||||||
* @return a `PlayerData` object
|
* @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)) )
|
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)) )
|
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`.
|
* Determine the padding offset for a subsequent field given the existence of `PlacementData`.
|
||||||
* The padding will always be a number 0-7.
|
* 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
|
* @param pos the optional `PlacementData` object that creates the shift in bits
|
||||||
* @return the pad length 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] = (
|
def codec(position_defined : Boolean) : Codec[PlayerData] = (
|
||||||
conditional(position_defined, "pos" | PlacementData.codec) >>:~ { pos =>
|
conditional(position_defined, "pos" | PlacementData.codec) >>:~ { pos =>
|
||||||
("basic_appearance" | CharacterAppearanceData.codec(placementOffset(pos))) >>:~ { app =>
|
("basic_appearance" | CharacterAppearanceData.codec(placementOffset(pos))) >>:~ { app =>
|
||||||
|
|
@ -124,26 +184,29 @@ object PlayerData extends Marshallable[PlayerData] {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This `Codec` is exclusively for translating a `Player` object
|
||||||
def codec(position_defined : Boolean, offset : Int) : Codec[PlayerData] = (
|
* while that `Player` object is encountered in the process of translating its mounting object.
|
||||||
conditional(position_defined, "pos" | PlacementData.codec) >>:~ { pos =>
|
* In other words, the player is "seated" or "mounted."
|
||||||
("basic_appearance" | CharacterAppearanceData.codec(offset)) >>:~ { app =>
|
* @see `CharacterAppearanceData.codec`
|
||||||
("character_data" | newcodecs.binary_choice(position_defined,
|
* @param offset the padding for the player's name field
|
||||||
CharacterData.codec(app.backpack),
|
* @return a `Codec` that translates a `PlayerData` object
|
||||||
CharacterData.codec_seated(app.backpack))) ::
|
*/
|
||||||
optional(bool, "inventory" | InventoryData.codec) ::
|
def codec(offset : Int) : Codec[PlayerData] = (
|
||||||
("drawn_slot" | DrawnSlot.codec) ::
|
("basic_appearance" | CharacterAppearanceData.codec(offset)) >>:~ { app =>
|
||||||
bool //usually false
|
("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 =>
|
case app :: data :: inv :: hand :: _ :: HNil =>
|
||||||
PlayerData(pos, app, data, inv, hand)(pos.isDefined)
|
PlayerData(None, app, data, inv, hand)(false)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
case PlayerData(pos, app, data, inv, hand) =>
|
case PlayerData(None, app, data, inv, hand) =>
|
||||||
pos :: app :: data :: inv :: hand :: false :: HNil
|
app :: data :: inv :: hand :: false :: HNil
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ object VehicleData extends Marshallable[VehicleData] {
|
||||||
("unk4" | bool) ::
|
("unk4" | bool) ::
|
||||||
("cloak" | bool) :: //cloak as wraith, phantasm
|
("cloak" | bool) :: //cloak as wraith, phantasm
|
||||||
conditional(vehicle_type != VehicleFormat.Normal, "unk5" | selectFormatReader(vehicle_type)) :: //padding?
|
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] (
|
).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 +
|
198 +
|
||||||
(if(com.pos.vel.isDefined) { 42 } else { 0 }) +
|
(if(hasVelocity) { 42 } else { 0 }) +
|
||||||
(format match {
|
(format match {
|
||||||
case VehicleFormat.Utility => 6
|
case VehicleFormat.Utility => 6
|
||||||
case VehicleFormat.Variant => 8
|
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] = {
|
private def custom_inventory_codec(length : Long) : Codec[InventoryData] = {
|
||||||
import shapeless.::
|
import shapeless.::
|
||||||
(
|
(
|
||||||
|
|
@ -262,16 +299,7 @@ object VehicleData extends Marshallable[VehicleData] {
|
||||||
uint2 ::
|
uint2 ::
|
||||||
(inventory_seat_codec(
|
(inventory_seat_codec(
|
||||||
length, //length of stream until current seat
|
length, //length of stream until current seat
|
||||||
{ //calculated offset of name field in next seat
|
PlayerData.CumulativeSeatedPlayerNamePadding(length) //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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) >>:~ { seats =>
|
) >>:~ { seats =>
|
||||||
PacketHelpers.listOfNSized(size - countSeats(seats), InternalSlot.codec).hlist
|
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])
|
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]] = {
|
private def inventory_seat_codec(length : Long, offset : Int) : Codec[Option[InventorySeat]] = {
|
||||||
import shapeless.::
|
import shapeless.::
|
||||||
(
|
(
|
||||||
|
|
@ -306,20 +348,9 @@ object VehicleData extends Marshallable[VehicleData] {
|
||||||
case None => 0
|
case None => 0
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
{ //calculated offset of name field in next seat
|
VehicleData.CumulativeSeatedPlayerNamePadding(length, seat) //calculated offset of name field in next seat
|
||||||
val next = length + 23 + 35 + (seat match {
|
)).hlist
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
).exmap[Option[InventorySeat]] (
|
).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] = {
|
private def seat_codec(pad : Int) : Codec[InternalSlot] = {
|
||||||
import shapeless.::
|
import shapeless.::
|
||||||
(
|
(
|
||||||
("objectClass" | uintL(11)) ::
|
("objectClass" | uintL(11)) ::
|
||||||
("guid" | PlanetSideGUID.codec) ::
|
("guid" | PlanetSideGUID.codec) ::
|
||||||
("parentSlot" | PacketHelpers.encodedStringSize) ::
|
("parentSlot" | PacketHelpers.encodedStringSize) ::
|
||||||
("obj" | PlayerData.codec(false, pad))
|
("obj" | PlayerData.codec(pad))
|
||||||
).xmap[InternalSlot] (
|
).xmap[InternalSlot] (
|
||||||
{
|
{
|
||||||
case objectClass :: guid :: parentSlot :: obj :: HNil =>
|
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 = {
|
private def countSeats(chain : Option[InventorySeat]) : Int = {
|
||||||
chain match {
|
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 =>
|
case None =>
|
||||||
0
|
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] = {
|
private def unlinkSeats(chain : Option[InventorySeat]) : List[InternalSlot] = {
|
||||||
var curr = chain
|
var curr = chain
|
||||||
val out = new ListBuffer[InternalSlot]
|
val out = new ListBuffer[InternalSlot]
|
||||||
while(curr.isDefined) {
|
while(curr.isDefined) {
|
||||||
curr.get.seat match {
|
val link = curr.get
|
||||||
|
link.seat match {
|
||||||
case None =>
|
case None =>
|
||||||
curr = None
|
curr = None
|
||||||
case Some(seat) =>
|
case Some(seat) =>
|
||||||
out += seat
|
out += seat
|
||||||
curr = curr.get.next
|
curr = link.next
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.toList
|
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] = {
|
private def chainSeats(list : List[InternalSlot]) : Option[InventorySeat] = {
|
||||||
list match {
|
list match {
|
||||||
case Nil =>
|
case Nil =>
|
||||||
None
|
None
|
||||||
case x :: Nil =>
|
case x :: Nil =>
|
||||||
Some(InventorySeat(Some(x), None))
|
Some(InventorySeat(Some(x), None))
|
||||||
case x :: xs =>
|
case _ :: _ =>
|
||||||
Some(InventorySeat(Some(x), chainSeats(xs)))
|
var link = InventorySeat(Some(list.last), None)
|
||||||
|
list.reverse.drop(1).foreach(seat => {
|
||||||
|
link = InventorySeat(Some(seat), Some(link))
|
||||||
|
})
|
||||||
|
Some(link)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ class CharacterDataTest extends Specification {
|
||||||
MeritCommendation.SixYearTR
|
MeritCommendation.SixYearTR
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val char : (Boolean)=>CharacterData = CharacterData(
|
val char : (Boolean,Boolean)=>CharacterData = CharacterData(
|
||||||
255, 253,
|
255, 253,
|
||||||
UniformStyle.ThirdUpgrade,
|
UniformStyle.ThirdUpgrade,
|
||||||
5,
|
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())) ::
|
InventoryItemData(ObjectClass.chainblade, PlanetSideGUID(4088), 4, WeaponData(0, 0, 1, ObjectClass.melee_ammo, PlanetSideGUID(3279), 0, AmmoBoxData())) ::
|
||||||
Nil
|
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 msg = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(3902), obj)
|
||||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||||
|
|
@ -260,14 +260,14 @@ class CharacterDataTest extends Specification {
|
||||||
MeritCommendation.SixYearVS
|
MeritCommendation.SixYearVS
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val char : (Boolean)=>CharacterData = CharacterData(
|
val char : (Boolean,Boolean)=>CharacterData = CharacterData(
|
||||||
0, 0,
|
0, 0,
|
||||||
UniformStyle.ThirdUpgrade,
|
UniformStyle.ThirdUpgrade,
|
||||||
2,
|
2,
|
||||||
None,
|
None,
|
||||||
Some(Cosmetics(true, true, true, true, false))
|
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 msg = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(3380), obj)
|
||||||
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue