mirror of
https://github.com/2revoemag/PSF-BotServer.git
synced 2026-03-04 20:50:20 +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 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>
|
||||
* ` 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`
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -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 _ =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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