Merge pull request #234 from Fate-JH/char-app-data

Character Bitstream Data
This commit is contained in:
Fate-JH 2018-11-05 23:45:08 -05:00 committed by GitHub
commit 5a67fcd88d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 2896 additions and 788 deletions

View file

@ -64,58 +64,92 @@ object AvatarConverter {
* @return the resulting `CharacterAppearanceData`
*/
def MakeAppearanceData(obj : Player) : (Int)=>CharacterAppearanceData = {
CharacterAppearanceData(
val alt_model_flag : Boolean = obj.isBackpack
val aa : Int=>CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, obj.Voice),
voice2 = 0,
black_ops = false,
alt_model_flag,
false,
None,
jammered = false,
obj.ExoSuit,
None,
0,
0,
0L,
0,
0,
0,
0
)
val ab : (Boolean,Int)=>CharacterAppearanceB = CharacterAppearanceB(
0L,
outfit_name = "",
outfit_logo = 0,
false,
obj.isBackpack,
false,
false,
false,
facingPitch = obj.Orientation.y,
facingYawUpper = obj.FacingYawUpper,
lfs = true,
GrenadeState.None,
is_cloaking = false,
obj.Cloaked,
false,
false,
charging_pose = false,
on_zipline = false,
RibbonBars()
false,
on_zipline = None
)
CharacterAppearanceData(aa, ab, RibbonBars())
}
def MakeCharacterData(obj : Player) : (Boolean,Boolean)=>CharacterData = {
val MaxArmor = obj.MaxArmor
CharacterData(
255 * obj.Health / obj.MaxHealth, //TODO not precise
255 * obj.Health / obj.MaxHealth,
if(MaxArmor == 0) {
0
}
else {
255 * obj.Armor / MaxArmor
}, //TODO not precise
},
DressBattleRank(obj),
0,
DressCommandRank(obj),
recursiveMakeImplantEffects(obj.Implants.iterator),
MakeImplantEffectList(obj.Implants),
MakeCosmetics(obj.BEP)
)
}
def MakeDetailedCharacterData(obj : Player) : (Option[Int])=>DetailedCharacterData = {
DetailedCharacterData(
obj.BEP,
val bep = obj.BEP
val ba : DetailedCharacterA = DetailedCharacterA(
bep,
obj.CEP,
obj.MaxHealth,
obj.Health,
0L, 0L, 0L,
obj.MaxHealth, obj.Health,
false,
obj.Armor,
obj.MaxStamina,
obj.Stamina,
obj.Certifications.toList.sortBy(_.id), //TODO is sorting necessary?
0L,
obj.MaxStamina, obj.Stamina,
0, 0, 0L,
List(0, 0, 0, 0, 0, 0),
obj.Certifications.toList.sortBy(_.id) //TODO is sorting necessary?
)
val bb : (Long, Option[Int])=>DetailedCharacterB = DetailedCharacterB(
None,
MakeImplantEntries(obj),
Nil, Nil,
firstTimeEvents = List.empty[String], //TODO fte list
tutorials = List.empty[String], //TODO tutorial list
MakeCosmetics(obj.BEP)
0L, 0L, 0L, 0L, 0L,
Some(DCDExtra2(0, 0)),
Nil, Nil, false,
MakeCosmetics(bep)
)
(pad_length : Option[Int]) => DetailedCharacterData(ba, bb(bep, pad_length))(pad_length)
}
def MakeInventoryData(obj : Player) : InventoryData = {
@ -185,7 +219,7 @@ object AvatarConverter {
private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = {
val numImplants : Int = DetailedCharacterData.numberOfImplantSlots(obj.BEP)
val implants = obj.Implants
obj.Implants.map({ case(implant, initialization, active) =>
obj.Implants.map({ case(implant, initialization, _) =>
if(initialization == 0) {
ImplantEntry(implant, None)
}
@ -196,34 +230,24 @@ object AvatarConverter {
}
/**
* Find an active implant whose effect will be displayed on this player.
* @param iter an `Iterator` of `ImplantSlot` objects
* Find and encode implants whose effect will be displayed on this player.
* @param implants a `Sequence` of `ImplantSlot` objects
* @return the effect of an active implant
*/
@tailrec private def recursiveMakeImplantEffects(iter : Iterator[(ImplantType.Value, Long, Boolean)]) : Option[ImplantEffects.Value] = {
if(!iter.hasNext) {
None
}
else {
val(implant, _, active) = iter.next
if(active) {
private def MakeImplantEffectList(implants : Seq[(ImplantType.Value, Long, Boolean)]) : List[ImplantEffects.Value] = {
implants.collect {
case ((implant,_,true)) =>
implant match {
case ImplantType.AdvancedRegen =>
Some(ImplantEffects.RegenEffects)
ImplantEffects.RegenEffects
case ImplantType.DarklightVision =>
Some(ImplantEffects.DarklightEffects)
ImplantEffects.DarklightEffects
case ImplantType.PersonalShield =>
Some(ImplantEffects.PersonalShieldEffects)
ImplantEffects.PersonalShieldEffects
case ImplantType.Surge =>
Some(ImplantEffects.SurgeEffects)
case _ =>
recursiveMakeImplantEffects(iter)
ImplantEffects.SurgeEffects
}
}
else {
recursiveMakeImplantEffects(iter)
}
}
}.toList
}
/**

View file

@ -4,7 +4,7 @@ package net.psforever.objects.definition.converter
import net.psforever.objects.{EquipmentSlot, Player}
import net.psforever.objects.equipment.Equipment
import net.psforever.packet.game.objectcreate._
import net.psforever.types.{CharacterVoice, GrenadeState, ImplantType}
import net.psforever.types.{CertificationType, CharacterVoice, GrenadeState, ImplantType}
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}
@ -22,20 +22,7 @@ class CharacterSelectConverter extends AvatarConverter {
DetailedPlayerData.apply(
PlacementData(0, 0, 0),
MakeAppearanceData(obj),
DetailedCharacterData(
obj.BEP,
obj.CEP,
healthMax = 1,
health = 1,
armor = 0,
staminaMax = 1,
stamina = 1,
certs = Nil,
MakeImplantEntries(obj), //necessary for correct stream length
firstTimeEvents = Nil,
tutorials = Nil,
AvatarConverter.MakeCosmetics(obj.BEP)
),
MakeDetailedCharacterData(obj),
InventoryData(recursiveMakeHolsters(obj.Holsters().iterator)),
AvatarConverter.GetDrawnSlot(obj)
)
@ -49,24 +36,73 @@ class CharacterSelectConverter extends AvatarConverter {
* @return the resulting `CharacterAppearanceData`
*/
private def MakeAppearanceData(obj : Player) : (Int)=>CharacterAppearanceData = {
CharacterAppearanceData(
val aa : Int=>CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, CharacterVoice.Mute),
voice2 = 0,
black_ops = false,
false,
false,
None,
jammered = false,
obj.ExoSuit,
None,
0,
0,
0L,
0,
0,
0,
0
)
val ab : (Boolean,Int)=>CharacterAppearanceB = CharacterAppearanceB(
0L,
outfit_name = "",
outfit_logo = 0,
false,
backpack = false,
false,
false,
false,
facingPitch = 0,
facingYawUpper = 0,
lfs = true,
lfs = false,
GrenadeState.None,
is_cloaking = false,
obj.Cloaked,
false,
false,
charging_pose = false,
on_zipline = false,
RibbonBars()
false,
on_zipline = None
)
CharacterAppearanceData(aa, ab, RibbonBars())
}
private def MakeDetailedCharacterData(obj : Player) : (Option[Int]=>DetailedCharacterData) = {
val bep = obj.BEP
val ba : DetailedCharacterA = DetailedCharacterA(
bep,
obj.CEP,
0L, 0L, 0L,
1, 1,
false,
0,
0L,
1, 1,
0, 0, 0L,
List(0, 0, 0, 0, 0, 0),
certs = List.empty[CertificationType.Value]
)
val bb : (Long, Option[Int])=>DetailedCharacterB = DetailedCharacterB(
None,
MakeImplantEntries(obj), //necessary for correct stream length
Nil, Nil,
firstTimeEvents = List.empty[String],
tutorials = List.empty[String],
0L, 0L, 0L, 0L, 0L,
Some(DCDExtra2(0, 0)),
Nil, Nil, false,
AvatarConverter.MakeCosmetics(bep)
)
(pad_length : Option[Int]) => DetailedCharacterData(ba, bb(bep, pad_length))(pad_length)
}
/**

View file

@ -4,7 +4,7 @@ package net.psforever.objects.definition.converter
import net.psforever.objects.{EquipmentSlot, Player}
import net.psforever.objects.equipment.Equipment
import net.psforever.packet.game.objectcreate._
import net.psforever.types.{CharacterGender, CharacterVoice, GrenadeState, Vector3}
import net.psforever.types._
import scala.annotation.tailrec
import scala.util.{Failure, Success, Try}
@ -18,20 +18,7 @@ class CorpseConverter extends AvatarConverter {
DetailedPlayerData.apply(
PlacementData(obj.Position, Vector3(0,0, obj.Orientation.z)),
MakeAppearanceData(obj),
DetailedCharacterData(
bep = 0,
cep = 0,
healthMax = 0,
health = 0,
armor = 0,
staminaMax = 0,
stamina = 0,
certs = Nil,
implants = Nil,
firstTimeEvents = Nil,
tutorials = Nil,
cosmetics = None
),
MakeDetailedCharacterData(obj),
InventoryData((MakeHolsters(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)),
DrawnSlot.None
)
@ -44,24 +31,72 @@ class CorpseConverter extends AvatarConverter {
* @return the resulting `CharacterAppearanceData`
*/
private def MakeAppearanceData(obj : Player) : (Int)=>CharacterAppearanceData = {
CharacterAppearanceData(
val aa : Int=>CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(obj.Name, obj.Faction, CharacterGender.Male, 0, CharacterVoice.Mute),
voice2 = 0,
black_ops = false,
altModel = true,
false,
None,
jammered = false,
obj.ExoSuit,
None,
0,
0,
0L,
0,
0,
0,
0
)
val ab : (Boolean,Int)=>CharacterAppearanceB = CharacterAppearanceB(
0L,
outfit_name = "",
outfit_logo = 0,
false,
backpack = true,
facingPitch = obj.Orientation.y, //TODO is this important?
false,
false,
false,
facingPitch = 0,
facingYawUpper = 0,
lfs = true,
lfs = false,
GrenadeState.None,
is_cloaking = false,
false,
false,
charging_pose = false,
on_zipline = false,
RibbonBars()
false,
on_zipline = None
)
CharacterAppearanceData(aa, ab, RibbonBars())
}
private def MakeDetailedCharacterData(obj : Player) : (Option[Int]=>DetailedCharacterData) = {
val ba : DetailedCharacterA = DetailedCharacterA(
bep = 0L,
cep = 0L,
0L, 0L, 0L,
0, 0,
false,
0,
0L,
0, 0,
0, 0, 0L,
List(0, 0, 0, 0, 0, 0),
certs = List.empty[CertificationType.Value]
)
val bb : (Long, Option[Int])=>DetailedCharacterB = DetailedCharacterB(
None,
implants = List.empty[ImplantEntry],
Nil, Nil,
firstTimeEvents = List.empty[String],
tutorials = List.empty[String],
0L, 0L, 0L, 0L, 0L,
Some(DCDExtra2(0, 0)),
Nil, Nil, false,
cosmetics = None
)
(pad_length : Option[Int]) => DetailedCharacterData(ba, bb(0, pad_length))(pad_length)
}
/**

View file

@ -16,16 +16,7 @@ abstract class IdentifiableEntity extends Identifiable {
private val container : GUIDContainable = GUIDContainer()
private var current : GUIDContainable = IdentifiableEntity.noGUIDContainer
def HasGUID : Boolean = {
try {
GUID
true
}
catch {
case _ : NoGUIDException =>
false
}
}
def HasGUID : Boolean = current ne IdentifiableEntity.noGUIDContainer
def GUID : PlanetSideGUID = current.GUID

View file

@ -44,17 +44,15 @@ class UniqueNumberSystem(private val guid : NumberPoolHub, private val poolActor
def receive : Receive = {
case Register(obj, Some(pname), None, call) =>
val callback = call.getOrElse(sender())
try {
obj.GUID //stop if object already has a GUID; sometimes this happens
if(obj.HasGUID) {
AlreadyRegistered(obj, pname)
callback ! Success(obj)
}
catch {
case _ : Exception =>
val id : Long = index
index += 1
requestQueue += id -> UniqueNumberSystem.GUIDRequest(obj, pname, callback)
RegistrationProcess(pname, id)
else {
val id : Long = index
index += 1
requestQueue += id -> UniqueNumberSystem.GUIDRequest(obj, pname, callback)
RegistrationProcess(pname, id)
}
//this message is automatically sent by NumberPoolActor

View file

@ -7,6 +7,96 @@ import scodec.{Attempt, Codec, Err}
import scodec.codecs._
import shapeless.{::, HNil}
/**
* A part of a representation of the avatar portion of `ObjectCreateDetailedMessage` packet data.
* @see `CharacterData`
* @see `DetailedCharacterData`
* @see `ExoSuitType`
* @param app the player's cardinal appearance settings
* @param black_ops whether or not this avatar is enrolled in Black OPs
* @param jammered the player has been caught in an EMP blast recently;
* creates a jammered sound effect that follows the player around and can be heard by others
* @param exosuit the type of exo-suit the avatar will be depicted in;
* for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits
*/
final case class CharacterAppearanceA(app : BasicCharacterData,
black_ops : Boolean,
altModel : Boolean,
unk1 : Boolean,
unk2 : Option[CharacterAppearanceData.ExtraData],
jammered : Boolean,
exosuit : ExoSuitType.Value,
unk3 : Option[Int],
unk4 : Int,
unk5 : Int,
unk6 : Long,
unk7 : Int,
unk8 : Int,
unk9 : Int,
unkA : Int)
(name_padding : Int) extends StreamBitSize {
override def bitsize : Long = {
//factor guard bool values into the base size, not its corresponding optional field
val unk2Size : Long = unk2 match { case Some(n) => n.bitsize ; case None => 0L }
val nameStringSize : Long = StreamBitSize.stringBitSize(app.name, 16) + name_padding
val unk3Size : Long = unk3 match { case Some(_) => 32L ; case None => 0L }
137L + unk2Size + nameStringSize + unk3Size
}
}
/**
* A part of a representation of the avatar portion of `ObjectCreateDetailedMessage` packet data.
* @see `CharacterData`
* @see `DetailedCharacterData`
* @see `ExoSuitType`
* @see `GrenadeState`
* @see `RibbonBars`
* @see `http://www.planetside-universe.com/p-outfit-decals-31.htm`
* @param outfit_name the name of the outfit to which this player belongs;
* if the option is selected, allies with see either "[`outfit_name`]" or "{No Outfit}" under the player's name
* @param outfit_logo the decal seen on the player's exo-suit (and beret and cap) associated with the player's outfit;
* if there is a variable color for that decal, the faction-appropriate one is selected
* @param facingPitch a "pitch" angle
* @param facingYawUpper a "yaw" angle that represents the angle of the avatar's upper body with respect to its forward-facing direction;
* this number is normally 0 for forward facing;
* the range is limited between approximately 61 degrees of center turned to left or right
* @param lfs this player is looking for a squad;
* all allies will see the phrase "[Looking for Squad]" under the player's name
* @param is_cloaking avatar is cloaked by virtue of an Infiltration Suit
* @param grenade_state if the player has a grenade `Primed`;
* should be `GrenadeStateState.None` if nothing special
* @param charging_pose animation pose for both charging modules and BFR imprinting
* @param on_zipline player's model is changed into a faction-color ball of energy, as if on a zip line
*/
final case class CharacterAppearanceB(unk0 : Long,
outfit_name : String,
outfit_logo : Int,
unk1 : Boolean,
backpack : Boolean,
unk2 : Boolean,
unk3 : Boolean,
unk4 : Boolean,
facingPitch : Float,
facingYawUpper : Float,
lfs : Boolean,
grenade_state : GrenadeState.Value,
is_cloaking : Boolean,
unk5 : Boolean,
unk6 : Boolean,
charging_pose : Boolean,
unk7 : Boolean,
on_zipline : Option[CharacterAppearanceData.ZiplineData])
(alt_model : Boolean, name_padding : Int) extends StreamBitSize {
override def bitsize : Long = {
//factor guard bool values into the base size, not its corresponding optional field
val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) +
CharacterAppearanceData.outfitNamePadding //even if the outfit_name is blank, string is always padded
val backpackSize = if(backpack) { 1L } else { 0L }
val onZiplineSize : Long = on_zipline match { case Some(n) => n.bitsize; case None => 0 }
70L + outfitStringSize + backpackSize + onZiplineSize
}
}
/**
* A part of a representation of the avatar portion of `ObjectCreateDetailedMessage` packet data.<br>
* <br>
@ -28,64 +118,16 @@ import shapeless.{::, HNil}
* <br>
* Exploration:<br>
* How do I crouch?
* @see `CharacterData`<br>
* `DetailedCharacterData`<br>
* `ExoSuitType`<br>
* `GrenadeState`<br>
* `RibbonBars`
* @see `http://www.planetside-universe.com/p-outfit-decals-31.htm`
* @param app the player's cardinal appearance settings
* @param voice2 na;
* affects the frequency by which the character's voice is heard (somehow);
* commonly 3 for best results
* @param black_ops whether or not this avatar is enrolled in Black OPs
* @param jammered the player has been caught in an EMP blast recently;
* creates a jammered sound effect that follows the player around and can be heard by others
* @param exosuit the type of exo-suit the avatar will be depicted in;
* for Black OPs, the agile exo-suit and the reinforced exo-suit are replaced with the Black OPs exo-suits
* @param outfit_name the name of the outfit to which this player belongs;
* if the option is selected, allies with see either "[`outfit_name`]" or "{No Outfit}" under the player's name
* @param outfit_logo the decal seen on the player's exo-suit (and beret and cap) associated with the player's outfit;
* if there is a variable color for that decal, the faction-appropriate one is selected
* @param facingPitch a "pitch" angle
* @param facingYawUpper a "yaw" angle that represents the angle of the avatar's upper body with respect to its forward-facing direction;
* this number is normally 0 for forward facing;
* the range is limited between approximately 61 degrees of center turned to left or right
* @param lfs this player is looking for a squad;
* all allies will see the phrase "[Looking for Squad]" under the player's name
* @param is_cloaking avatar is cloaked by virtue of an Infiltration Suit
* @param grenade_state if the player has a grenade `Primed`;
* should be `GrenadeStateState.None` if nothing special
* @param charging_pose animation pose for both charging modules and BFR imprinting
* @param on_zipline player's model is changed into a faction-color ball of energy, as if on a zip line
* @see `CharacterData`
* @see `DetailedCharacterData`
* @param ribbons the four merit commendation ribbon medals
*/
final case class CharacterAppearanceData(app : BasicCharacterData,
voice2 : Int,
black_ops : Boolean,
jammered : Boolean,
exosuit : ExoSuitType.Value,
outfit_name : String,
outfit_logo : Int,
backpack : Boolean,
facingPitch : Float,
facingYawUpper : Float,
lfs : Boolean,
grenade_state : GrenadeState.Value,
is_cloaking : Boolean,
charging_pose : Boolean,
on_zipline : Boolean,
final case class CharacterAppearanceData(a : CharacterAppearanceA,
b : CharacterAppearanceB,
ribbons : RibbonBars)
(name_padding : Int) extends StreamBitSize {
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) + name_padding
val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) +
CharacterAppearanceData.outfitNamePadding //even if the outfit_name is blank, string is always padded
val altModelSize = CharacterAppearanceData.altModelBit(this).getOrElse(0)
335L + nameStringSize + outfitStringSize + altModelSize //base value includes the ribbons
}
override def bitsize : Long = 128L + a.bitsize + b.bitsize
/**
* External access to the value padding on the name field.
@ -102,6 +144,91 @@ final case class CharacterAppearanceData(app : BasicCharacterData,
}
object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
def apply(app : BasicCharacterData,
black_ops : Boolean,
jammered : Boolean,
exosuit : ExoSuitType.Value,
outfit_name : String,
outfit_logo : Int,
backpack : Boolean,
facingPitch : Float,
facingYawUpper : Float,
lfs : Boolean,
grenade_state : GrenadeState.Value,
is_cloaking : Boolean,
charging_pose : Boolean,
on_zipline : Option[ZiplineData],
ribbons : RibbonBars)(name_padding : Int) : CharacterAppearanceData = {
val altModel : Boolean = backpack || on_zipline.isDefined
val a = CharacterAppearanceA(
app,
black_ops,
altModel,
false,
None,
jammered,
exosuit,
None,
0,
0,
0,
0,
0,
0,
0
)(name_padding)
val b = CharacterAppearanceB(
outfit_name.length,
outfit_name : String,
outfit_logo : Int,
false,
backpack,
false,
false,
false,
facingPitch : Float,
facingYawUpper : Float,
lfs : Boolean,
grenade_state : GrenadeState.Value,
is_cloaking : Boolean,
false,
false,
charging_pose : Boolean,
false,
on_zipline
)(altModel, name_padding)
new CharacterAppearanceData(
a,
b,
ribbons
)(name_padding)
}
def apply(a : Int=>CharacterAppearanceA, b : (Boolean,Int)=>CharacterAppearanceB, ribbons : RibbonBars)(name_padding : Int) : CharacterAppearanceData = {
val first = a(name_padding)
CharacterAppearanceData(a(name_padding), b(first.altModel, name_padding), ribbons)(name_padding)
}
/**
* na
* @param unk1 na
* @param unk2 na
*/
final case class ExtraData(unk1 : Boolean,
unk2 : Boolean) extends StreamBitSize {
override def bitsize : Long = 2L
}
/**
* na
* @param unk1 na
* @param unk2 na
*/
final case class ZiplineData(unk1 : Long,
unk2 : Boolean) extends StreamBitSize {
override def bitsize : Long = 33L
}
/**
* When a player is released-dead or attached to a zipline, their basic infantry model is replaced with a different one.
* In the former case, a backpack.
@ -110,13 +237,26 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
* @param app the appearance
* @return the length of the variable field that exists when using alternate models
*/
def altModelBit(app : CharacterAppearanceData) : Option[Int] = if(app.backpack || app.on_zipline) {
def altModelBit(app : CharacterAppearanceData) : Option[Int] = if(app.b.backpack || app.b.on_zipline.isDefined) {
Some(1)
}
else {
None
}
def namePadding(inheritPad : Int, pad : Option[ExtraData]) : Int = {
pad match {
case Some(n) =>
val bitsize = n.bitsize.toInt % 8
if(inheritPad > bitsize)
inheritPad - bitsize
else
8 - bitsize
case None =>
inheritPad
}
}
/**
* Get the padding of the outfit's name.
* The padding will always be a number 0-7.
@ -126,72 +266,60 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
6
}
def codec(name_padding : Int) : Codec[CharacterAppearanceData] = (
private val extra_codec : Codec[ExtraData] = (
("unk1" | bool) ::
("unk2" | bool)
).as[ExtraData]
private val zipline_codec : Codec[ZiplineData] = (
("unk1" | uint32L) ::
("unk2" | bool)
).as[ZiplineData]
/**
* na
* @param name_padding na
* @return na
*/
def a_codec(name_padding : Int) : Codec[CharacterAppearanceA] = (
("faction" | PlanetSideEmpire.codec) ::
("black_ops" | bool) ::
(("alt_model" | bool) >>:~ { alt_model => //modifies stream format (to display alternate player models)
ignore(1) :: //unknown
("jammered" | bool) ::
bool :: //crashes client
uint(16) :: //unknown, but usually 0
("name" | PacketHelpers.encodedWideStringAligned(name_padding)) ::
("exosuit" | ExoSuitType.codec) ::
ignore(2) :: //unknown
("sex" | CharacterGender.codec) ::
("head" | uint8L) ::
("voice" | CharacterVoice.codec) ::
("voice2" | uint2L) ::
ignore(78) :: //unknown
uint16L :: //usually either 0 or 65535
uint32L :: //for outfit_name (below) to be visible in-game, this value should be non-zero
("outfit_name" | PacketHelpers.encodedWideStringAligned( outfitNamePadding )) ::
("outfit_logo" | uint8L) ::
ignore(1) :: //unknown
("backpack" | bool) :: //requires alt_model flag (does NOT require health == 0)
bool :: //stream misalignment when set
("facingPitch" | Angular.codec_pitch) ::
("facingYawUpper" | Angular.codec_yaw(0f)) ::
ignore(1) :: //unknown
conditional(alt_model, bool) :: //alt_model flag adds a bit before lfs
ignore(1) :: //an alternate lfs?
("lfs" | bool) ::
("grenade_state" | GrenadeState.codec_2u) :: //note: bin10 and bin11 are neutral (bin00 is not defined)
("is_cloaking" | bool) ::
ignore(1) :: //unknown
bool :: //stream misalignment when set
("charging_pose" | bool) ::
ignore(1) :: //alternate charging pose?
("on_zipline" | bool) :: //requires alt_model flag
("ribbons" | RibbonBars.codec)
("unk1" | bool) :: //serves a different internal purpose depending on the state of alt_model
(conditional(false, "unk2" | extra_codec) >>:~ { extra => //TODO not sure what causes this branch
("jammered" | bool) ::
optional(bool, "unk3" | uint16L) ::
("unk4" | uint16L) ::
("name" | PacketHelpers.encodedWideStringAligned(namePadding(name_padding, extra))) ::
("exosuit" | ExoSuitType.codec) ::
("unk5" | uint2) :: //unknown
("sex" | CharacterGender.codec) ::
("head" | uint8L) ::
("voice" | CharacterVoice.codec) ::
("unk6" | uint32L) ::
("unk7" | uint16L) ::
("unk8" | uint16L) ::
("unk9" | uint16L) ::
("unkA" | uint16L) //usually either 0 or 65535
})
})
).exmap[CharacterAppearanceData] (
).exmap[CharacterAppearanceA] (
{
case _ :: _ :: false :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: true :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil |
_ :: _ :: false :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: true :: _ :: HNil =>
Attempt.Failure(Err("invalid character appearance data; can not encode alternate model without required bit set"))
case faction :: bops :: _ :: _ :: jamd :: false :: 0 :: name :: suit :: _ :: sex :: head :: v1 :: v2 :: _ :: _ :: _/*has_outfit_name*/ :: outfit :: logo :: _ :: bpack :: false :: facingPitch :: facingYawUpper :: _ :: _ :: _ :: lfs :: gstate :: cloaking :: _ :: false :: charging :: _ :: zipline :: ribbons :: HNil =>
case faction :: bops :: alt :: u1 :: u2 :: jamd :: u3 :: u4 :: name :: suit :: u5 :: sex :: head :: v1 :: u6 :: u7 :: u8 :: u9 :: uA :: HNil =>
Attempt.successful(
CharacterAppearanceData(BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons)(name_padding)
CharacterAppearanceA(BasicCharacterData(name, faction, sex, head, v1), bops, alt, u1, u2, jamd, suit, u3, u4, u5, u6, u7, u8, u9, uA)(name_padding)
)
case _ =>
Attempt.Failure(Err("invalid character appearance data; can not encode"))
},
{
case CharacterAppearanceData(BasicCharacterData(name, PlanetSideEmpire.NEUTRAL, _, _, _), _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) =>
case CharacterAppearanceA(BasicCharacterData(name, PlanetSideEmpire.NEUTRAL, _, _, _), _, _, _, _, _, _, _, _, _, _, _, _, _, _) =>
Attempt.failure(Err(s"character $name's faction can not declare as neutral"))
case CharacterAppearanceData(BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons) =>
val has_outfit_name : Long = outfit.length.toLong //TODO this is a kludge
var alt_model : Boolean = false
var alt_model_extrabit : Option[Boolean] = None
if(zipline || bpack) {
alt_model = true
alt_model_extrabit = Some(false)
}
case CharacterAppearanceA(BasicCharacterData(name, faction, sex, head, v1), bops, alt, u1, u2, jamd, suit, u3, u4, u5, u6, u7, u8, u9, uA) =>
Attempt.successful(
faction :: bops :: alt_model :: () :: jamd :: false :: 0 :: name :: suit :: () :: sex :: head :: v1 :: v2 :: () :: 0 :: has_outfit_name :: outfit :: logo :: () :: bpack :: false :: facingPitch :: facingYawUpper :: () :: alt_model_extrabit :: () :: lfs :: gstate :: cloaking :: () :: false :: charging :: () :: zipline :: ribbons :: HNil
faction :: bops :: alt :: u1 :: u2 :: jamd :: u3 :: u4 :: name :: suit :: u5 :: sex :: head :: v1 :: u6 :: u7 :: u8 :: u9 :: uA :: HNil
)
case _ =>
@ -199,5 +327,77 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
}
)
/**
* na
* @param alt_model na
* @param name_padding na
* @return na
*/
def b_codec(alt_model : Boolean, name_padding : Int) : Codec[CharacterAppearanceB] = (
("unk0" | uint32L) :: //for outfit_name (below) to be visible in-game, this value should be non-zero
("outfit_name" | PacketHelpers.encodedWideStringAligned(outfitNamePadding)) ::
("outfit_logo" | uint8L) ::
("unk1" | bool) :: //unknown
conditional(alt_model, "backpack" | bool) :: //alt_model flag adds this bit; see ps.c:line#1069587
("unk2" | bool) :: //requires alt_model flag (does NOT require health == 0)
("unk3" | bool) :: //stream misalignment when set
("unk4" | bool) :: //unknown
("facingPitch" | Angular.codec_pitch) ::
("facingYawUpper" | Angular.codec_yaw(0f)) ::
("lfs" | uint2) ::
("grenade_state" | GrenadeState.codec_2u) :: //note: bin10 and bin11 are neutral (bin00 is not defined)
("is_cloaking" | bool) ::
("unk5" | bool) :: //unknown
("unk6" | bool) :: //stream misalignment when set
("charging_pose" | bool) ::
("unk7" | bool) :: //alternate charging pose?
optional(bool, "on_zipline" | zipline_codec)
).exmap[CharacterAppearanceB] (
{
case u0 :: outfit :: logo :: u1 :: bpack :: u2 :: u3 :: u4 :: facingPitch :: facingYawUpper :: lfs :: gstate :: cloaking :: u5 :: u6 :: charging :: u7 :: zipline :: HNil =>
val lfsBool = if(lfs == 0) false else true
val bpackBool = bpack match { case Some(_) => alt_model ; case None => false }
Attempt.successful(
CharacterAppearanceB(u0, outfit, logo, u1, bpackBool, u2, u3, u4, facingPitch, facingYawUpper, lfsBool, gstate, cloaking, u5, u6, charging, u7, zipline)(alt_model, name_padding)
)
},
{
case CharacterAppearanceB(u0, outfit, logo, u1, bpack, u2, u3, u4, facingPitch, facingYawUpper, lfs, gstate, cloaking, u5, u6, charging, u7, zipline) =>
val u0Long = if(u0 == 0 && outfit.nonEmpty) {
outfit.length.toLong
}
else {
u0
} //TODO this is a kludge; unk0 must be (some) non-zero if outfit_name is defined
val (bpackOpt, zipOpt) = if(alt_model) {
val bpackOpt = if(bpack) { Some(true) } else { None }
(bpackOpt, zipline)
}
else {
(None, None)
} //alt_model must be set for either of the other two to be valid
val lfsInt = if(lfs) { 1 } else { 0 }
Attempt.successful(
u0Long :: outfit :: logo :: u1 :: bpackOpt :: u2 :: u3 :: u4 :: facingPitch :: facingYawUpper :: lfsInt :: gstate :: cloaking :: u5 :: u6 :: charging :: u7 :: zipOpt :: HNil
)
}
)
def codec(name_padding : Int) : Codec[CharacterAppearanceData] = (
("a" | a_codec(name_padding)) >>:~ { a =>
("b" | b_codec(a.altModel, name_padding)) ::
("ribbons" | RibbonBars.codec)
}
).xmap[CharacterAppearanceData] (
{
case a :: b :: ribbons :: HNil =>
CharacterAppearanceData(a, b, ribbons)(name_padding)
},
{
case CharacterAppearanceData(a, b, ribbons) =>
a :: b :: ribbons :: HNil
}
)
implicit val codec : Codec[CharacterAppearanceData] = codec(0)
}

View file

@ -8,11 +8,10 @@ import shapeless.{::, HNil}
/**
* Values for the implant effects on a character model.
* The effects can not be activated simultaneously.
* In at least one case, attempting to activate multiple effects will cause the PlanetSide client to crash.<br>
* The effects are not additive and this value is not a bitmask.<br>
* <br>
* `RegenEffects` is a reverse-flagged item - inactive when the corresponding bit is set.
* For that reason, every other effect is `n`+1, while `NoEffects` is 1 and `RegenEffects` is 0.
* For that reason, every other effect is `n + 1`, while `NoEffects` is `1` and `RegenEffects` is `0`.
*/
object ImplantEffects extends Enumeration {
type Type = Value
@ -44,19 +43,20 @@ object UniformStyle extends Enumeration {
/**
* A representation of a portion of an avatar's `ObjectCreateDetailedMessage` packet data.<br>
* <br>
* This densely-packed information outlines most of the specifics required to depict some other player's character.
* This information outlines most of the specifics required to depict some other player's character.
* Someone else decides how that character is behaving and the server tells each client how to depict that behavior.
* For that reason, the character is mostly for presentation purposes, rather than really being fleshed-out.
* Of the inventory for this character, only the initial five weapon slots are defined.<br>
* <br>
* Of the inventory for this character, only the initial five weapon slots are defined.
* In the "backend of the client," the character produced by this data is no different
* from the kind of character that could be declared a given player's avatar.
* In terms of equipment and complicated features common to an avatar character, however,
* any user would find this character ill-equipped.
* @see `DetailedCharacterData`
* @see `Cosmetics`
* @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;
* while `is_corpse == true`, `health` will always report as 0;
* while `is_backpack == 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;
@ -65,7 +65,8 @@ object UniformStyle extends Enumeration {
* @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
* @param implant_effects the effects of implants that can be seen on a player's character;
* though many implants can be used simultaneously, only one implant effect can be applied here
* the number of entries equates to the number of effects applied;
* the maximu number of effects is three
* @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
@ -74,23 +75,20 @@ object UniformStyle extends Enumeration {
* 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,
uniform_upgrade : UniformStyle.Value,
unk : Int,
command_rank : Int,
implant_effects : Option[ImplantEffects.Value],
implant_effects : List[ImplantEffects.Value],
cosmetics : Option[Cosmetics])
(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 effectsSize : Long = implant_effects.length * 4L
val cosmeticsSize : Long = if(cosmetics.isDefined) { cosmetics.get.bitsize } else { 0L }
11L + seatedSize + effectsSize + cosmeticsSize
}
@ -98,7 +96,7 @@ final case class CharacterData(health : Int,
object CharacterData extends Marshallable[CharacterData] {
/**
* An overloaded constructor for `CharacterData` that allows for a not-optional inventory.
* An overloaded constructor for `CharacterData`.
* @param health the amount of health 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
* @param uniform the level of upgrade to apply to the player's base uniform
@ -107,32 +105,31 @@ object CharacterData extends Marshallable[CharacterData] {
* @param cosmetics optional decorative features that are added to the player's head model by console/chat commands
* @return a `CharacterData` object
*/
def apply(health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : Option[ImplantEffects.Value], cosmetics : Option[Cosmetics]) : (Boolean,Boolean)=>CharacterData =
def apply(health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : List[ImplantEffects.Value], cosmetics : Option[Cosmetics]) : (Boolean,Boolean)=>CharacterData =
CharacterData(health, armor, uniform, 0, cr, implant_effects, cosmetics)
def codec(is_backpack : Boolean) : Codec[CharacterData] = (
("health" | uint8L) :: //dead state when health == 0
("armor" | uint8L) ::
(("uniform_upgrade" | UniformStyle.codec) >>:~ { style =>
ignore(3) :: //unknown
uint(3) :: //uniform_upgrade is actually interpreted as a 6u field, but the lower 3u seems to be discarded
("command_rank" | uintL(3)) ::
bool :: //misalignment when == 1
optional(bool, "implant_effects" | ImplantEffects.codec) ::
listOfN(uint2, "implant_effects" | ImplantEffects.codec) ::
conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec)
})
).exmap[CharacterData] (
{
case health :: armor :: uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: HNil =>
case health :: armor :: uniform :: unk :: cr :: 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, false))
Attempt.Successful(CharacterData(newHealth, armor, uniform, unk, cr, implant_effects, cosmetics)(is_backpack, false))
case _ =>
Attempt.Failure(Err("invalid character data; can not encode"))
},
{
case CharacterData(health, armor, uniform, _, cr, implant_effects, cosmetics) =>
case CharacterData(health, armor, uniform, unk, cr, implant_effects, cosmetics) =>
val newHealth = if(is_backpack) { 0 } else { health }
Attempt.Successful(newHealth :: armor :: uniform :: () :: cr :: false :: implant_effects :: cosmetics :: HNil)
Attempt.Successful(newHealth :: armor :: uniform :: unk :: cr :: implant_effects :: cosmetics :: HNil)
case _ =>
Attempt.Failure(Err("invalid character data; can not decode"))
@ -141,23 +138,22 @@ object CharacterData extends Marshallable[CharacterData] {
def codec_seated(is_backpack : Boolean) : Codec[CharacterData] = (
("uniform_upgrade" | UniformStyle.codec) >>:~ { style =>
ignore(3) :: //unknown
uint(3) :: //uniform_upgrade is actually interpreted as a 6u field, but the lower 3u seems to be discarded
("command_rank" | uintL(3)) ::
bool :: //stream misalignment when != 1
optional(bool, "implant_effects" | ImplantEffects.codec) ::
listOfN(uint2, "implant_effects" | ImplantEffects.codec) ::
conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec)
}
).exmap[CharacterData] (
{
case uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: HNil =>
Attempt.Successful(new CharacterData(100, 0, uniform, 0, cr, implant_effects, cosmetics)(is_backpack, true))
case uniform :: unk :: cr :: implant_effects :: cosmetics :: HNil =>
Attempt.Successful(new CharacterData(100, 0, uniform, unk, cr, implant_effects, cosmetics)(is_backpack, true))
case _ =>
Attempt.Failure(Err("invalid character data; can not encode"))
},
{
case obj @ CharacterData(_, _, uniform, _, cr, implant_effects, cosmetics) =>
Attempt.Successful(uniform :: () :: cr :: false :: implant_effects :: cosmetics :: HNil)
case CharacterData(_, _, uniform, unk, cr, implant_effects, cosmetics) =>
Attempt.Successful(uniform :: unk :: cr :: implant_effects :: cosmetics :: HNil)
case _ =>
Attempt.Failure(Err("invalid character data; can not decode"))

View file

@ -12,29 +12,52 @@ import scala.annotation.tailrec
/**
* An entry in the `List` of valid implant slots in `DetailedCharacterData`.
* `activation`, if defined, indicates the time remaining (in seconds?) before an implant becomes usable.
* @param implant the type of implant
* @param activation the activation timer;
* technically, this is "unconfirmed"
* @param initialization the amount of time necessary until this implant is ready to be activated;
* technically, this is unconfirmed
* @param active whether this implant is turned on;
* technically, this is unconfirmed
* @see `ImplantType`
*/
final case class ImplantEntry(implant : ImplantType.Value,
activation : Option[Int]) extends StreamBitSize {
initialization : Option[Int],
active : Boolean) extends StreamBitSize {
override def bitsize : Long = {
val activationSize = if(activation.isDefined) { 12L } else { 5L }
5L + activationSize
val timerSize = initialization match { case Some(_) => 8L ; case None => 1L }
9L + timerSize
}
}
object ImplantEntry {
def apply(implant : ImplantType.Value, initialization : Option[Int]) : ImplantEntry = {
ImplantEntry(implant, initialization, false)
}
}
/**
* A representation of a portion of an avatar's `ObjectCreateDetailedMessage` packet data.<br>
* <br>
* This densely-packed information outlines most of the specifics required to depict a character as an avatar.
* It goes into depth about information related to the given character in-game career that is not revealed to other players.
* To be specific, it passes more thorough data about the character that the client can display to the owner of the client.
* For example, health is a full number, rather than a percentage.
* Just as prominent is the list of first time events and the list of completed tutorials.
* Additionally, a full inventory, as opposed to the initial five weapon slots.
* na
* @param unk1 na
* @param unk2 na
*/
final case class DCDExtra1(unk1 : String,
unk2 : Int) extends StreamBitSize {
override def bitsize : Long = 16L + StreamBitSize.stringBitSize(unk1)
}
/**
* na
* @param unk1 an
* @param unk2 na
*/
final case class DCDExtra2(unk1 : Int,
unk2 : Int) extends StreamBitSize {
override def bitsize : Long = 13L
}
/**
* A representation of a portion of an avatar's `ObjectCreateDetailedMessage` packet data.
* @see `CharacterData`
* @see `CertificationType`
* @param bep the avatar's battle experience points, which determines his Battle Rank
* @param cep the avatar's command experience points, which determines his Command Rank
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value;
@ -44,17 +67,39 @@ final case class ImplantEntry(implant : ImplantType.Value,
* @param armor for `x / y` of armor points, this is the avatar's `x` value;
* range is 0-65535;
* the avatar's `y` armor points is tied to their exo-suit type
* @param unk1 na;
* defaults to 1
* @param unk2 na;
* defaults to 7
* @param unk3 na;
* defaults to 7
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value;
* range is 0-65535
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value;
* range is 0-65535
* @param certs the `List` of active certifications
* @param certs the `List` of certifications
*/
final case class DetailedCharacterA(bep : Long,
cep : Long,
unk1 : Long,
unk2 : Long,
unk3 : Long,
healthMax : Int,
health : Int,
unk4 : Boolean,
armor : Int,
unk5 : Long,
staminaMax : Int,
stamina : Int,
unk6 : Int,
unk7 : Int,
unk8 : Long,
unk9 : List[Int],
certs : List[CertificationType.Value]) extends StreamBitSize {
override def bitsize : Long = {
val certSize : Long = certs.length * 8
428L + certSize
}
}
/**
* A representation of a portion of an avatar's `ObjectCreateDetailedMessage` packet data.
* @see `CharacterData`
* @see `Cosmetics`
* @param implants the `List` of implant slots currently possessed by this avatar
* @param firstTimeEvents the list of first time events performed by this avatar;
* the size field is a 32-bit number;
@ -65,92 +110,160 @@ final case class ImplantEntry(implant : ImplantType.Value,
* @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;
* these flags do not exist if they are not applicable
* @see `CharacterData`<br>
* `CertificationType`
*/
final case class DetailedCharacterData(bep : Long,
cep : Long,
healthMax : Int,
health : Int,
armor : Int,
unk1 : Int, //1
unk2 : Int, //7
unk3 : Int, //7
staminaMax : Int,
stamina : Int,
certs : List[CertificationType.Value],
implants : List[ImplantEntry],
firstTimeEvents : List[String],
tutorials : List[String],
cosmetics : Option[Cosmetics])
(pad_length : Option[Int]) extends ConstructorData {
final case class DetailedCharacterB(unk1 : Option[Long],
implants : List[ImplantEntry],
unk2 : List[DCDExtra1],
unk3 : List[DCDExtra1],
firstTimeEvents : List[String],
tutorials : List[String],
unk4 : Long,
unk5 : Long,
unk6 : Long,
unk7 : Long,
unk8 : Long,
unk9 : Option[DCDExtra2],
unkA : List[Long],
unkB : List[String],
unkC : Boolean,
cosmetics : Option[Cosmetics])
(bep : Long,
pad_length : Option[Int]) extends StreamBitSize {
override def bitsize : Long = {
//factor guard bool values into the base size, not corresponding optional fields, unless contained or enumerated
val certSize = (certs.length + 1) * 8 //cert list
var implantSize : Long = 0L //implant list
for(entry <- implants) {
implantSize += entry.bitsize
//unk1
val unk1Size = unk1 match { case Some(_) => 32L ; case None => 0L }
//implant list
val implantSize : Long = implants.foldLeft(0L)(_ + _.bitsize)
//fte list
val eventListSize : Long = firstTimeEvents.foldLeft(0L)(_ + StreamBitSize.stringBitSize(_))
//tutorial list
val tutorialListSize : Long = tutorials.foldLeft(0L)(_ + StreamBitSize.stringBitSize(_))
val unk2Len = unk2.size
val unk3Len = unk3.size
val unkAllLen = unk2Len + unk3Len
val unk2_3ListSize : Long = if(unk2Len > 0) {
unkAllLen * unk2.head.bitsize
}
val implantPadding = DetailedCharacterData.implantFieldPadding(implants, pad_length)
val fteLen = firstTimeEvents.size //fte list
var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen, implantPadding)
for(str <- firstTimeEvents) {
eventListSize += StreamBitSize.stringBitSize(str)
else if(unk3Len > 0) {
unkAllLen * unk3.head.bitsize
}
val tutLen = tutorials.size //tutorial list
var tutorialListSize : Long = 32L + DetailedCharacterData.tutPadding(fteLen, tutLen, implantPadding)
for(str <- tutorials) {
tutorialListSize += StreamBitSize.stringBitSize(str)
else {
0L
}
val br24 = DetailedCharacterData.isBR24(bep) //character is at least BR24
val extraBitSize : Long = if(br24) { 33L } else { 46L }
//character is at least BR24
val br24 = DetailedCharacterData.isBR24(bep)
val unk9Size : Long = if(br24) { 0L } else { 13L }
val unkASize : Long = unkA.length * 32L
val unkBSize : Long = unkB.foldLeft(0L)(_ + StreamBitSize.stringBitSize(_))
val cosmeticsSize : Long = if(br24) { cosmetics.get.bitsize } else { 0L }
598L + certSize + implantSize + eventListSize + extraBitSize + cosmeticsSize + tutorialListSize
val paddingSize : Int =
DetailedCharacterData.paddingCalculations(pad_length, implants, Nil)(unk2Len) + /* unk2 */
DetailedCharacterData.paddingCalculations(pad_length, implants, List(unk2))(unk3Len) + /* unk3 */
DetailedCharacterData.paddingCalculations(pad_length, implants, List(unk3, unk2))(firstTimeEvents.length) + /* firstTimeEvents */
DetailedCharacterData.paddingCalculations(pad_length, implants, List(firstTimeEvents, unk3, unk2))(tutorials.size) + /* tutorials */
DetailedCharacterData.paddingCalculations(
DetailedCharacterData.displaceByUnk9(pad_length, unk9, 5),
implants,
List(DetailedCharacterData.optToList(unk9), tutorials, firstTimeEvents, unk3, unk2)
)(unkB.length) /* unkB */
275L + unk1Size + implantSize + eventListSize + unk2_3ListSize + tutorialListSize + unk9Size + unkASize + unkBSize + cosmeticsSize + paddingSize
}
}
/**
* A representation of a portion of an avatar's `ObjectCreateDetailedMessage` packet data.<br>
* <br>
* This densely-packed information outlines most of the specifics required to depict a character as an avatar.
* It goes into depth about information related to the given character in-game career that is not revealed to other players.
* To be specific, it passes more thorough data about the character that the client can display to the owner of the client.
* For example, health is a full number, rather than a percentage, as is the case with `CharacterData`.
* Just as prominent is the list of first time events and the list of completed tutorials.
* Additionally, a full inventory, as opposed to the initial five weapon slots.
* @see `CharacterData`
*/
final case class DetailedCharacterData(a : DetailedCharacterA,
b : DetailedCharacterB)
(pad_length : Option[Int]) extends ConstructorData {
override def bitsize : Long = a.bitsize + b.bitsize
}
object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
/**
* Overloaded constructor for `DetailedCharacterData` that requires an inventory and drops unknown values.
* @param bep the avatar's battle experience points, which determines his Battle Rank
* @param cep the avatar's command experience points, which determines his Command Rank
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value
* @param health for `x / y` of hitpoints, this is the avatar's `x` value
* @param armor for `x / y` of armor points, this is the avatar's `x` value
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value
* @param certs the `List` of active certifications
* @param implants the `List` of implant slots currently possessed by this avatar
* @param firstTimeEvents the list of first time events performed by this avatar
* @param tutorials the list of tutorials completed by this avatar
* @return a `DetailedCharacterData` object
*/
def apply(bep : Long, cep : Long, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, certs : List[CertificationType.Value], implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], cosmetics : Option[Cosmetics]) : (Option[Int])=>DetailedCharacterData =
DetailedCharacterData(bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, cosmetics)
def apply(bep : Long,
cep : Long,
healthMax : Int,
health : Int,
armor : Int,
staminaMax : Int,
stamina : Int,
certs : List[CertificationType.Value],
implants : List[ImplantEntry],
firstTimeEvents : List[String],
tutorials : List[String],
cosmetics : Option[Cosmetics]) : Option[Int]=>DetailedCharacterData = {
val a = DetailedCharacterA(
bep,
cep,
0L,
0L,
0L,
healthMax, health,
false,
armor,
0L,
staminaMax, stamina,
0,
0,
0L,
List(0,0,0,0,0,0),
certs
)
val b : (Long, Option[Int]) => DetailedCharacterB = DetailedCharacterB(
None,
implants,
Nil,
Nil,
firstTimeEvents,
tutorials,
0L,
0L,
0L,
0L,
0L,
None,
Nil,
Nil,
false,
cosmetics
)
(pad_length : Option[Int]) => DetailedCharacterData(a, b(a.bep, pad_length))(pad_length)
}
/**
* `Codec` for entries in the `List` of implants.
*/
private val implant_entry_codec : Codec[ImplantEntry] = (
("implant" | ImplantType.codec) ::
("implant" | uint8L) ::
(bool >>:~ { guard =>
newcodecs.binary_choice(guard, uintL(5), uintL(12)).hlist
newcodecs.binary_choice(guard, uint(1), uint8L).hlist
})
).xmap[ImplantEntry] (
{
case implant :: true :: _ :: HNil =>
ImplantEntry(implant, None)
case implant :: true :: n :: HNil => //initialized (no timer), active/inactive?
val activeBool : Boolean = n != 0
ImplantEntry(ImplantType(implant), None, activeBool) //TODO catch potential NoSuchElementException?
case implant :: false :: extra :: HNil =>
ImplantEntry(implant, Some(extra))
case implant :: false :: extra :: HNil => //uninitialized (timer), inactive
ImplantEntry(ImplantType(implant), Some(extra), false) //TODO catch potential NoSuchElementException?
},
{
case ImplantEntry(implant, None) =>
implant :: true :: 0 :: HNil
case ImplantEntry(implant, None, n) => //initialized (no timer), active/inactive?
val activeInt : Int = if(n) { 1 } else { 0 }
implant.id :: true :: activeInt :: HNil
case ImplantEntry(implant, Some(extra)) =>
implant :: false :: extra :: HNil
case ImplantEntry(implant, Some(extra), _) => //uninitialized (timer), inactive
implant.id :: false :: extra :: HNil
}
)
@ -176,21 +289,215 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
}
/**
* The padding value of the first entry in either of two byte-aligned `List` structures.
* @param implants implant entries
* @return the pad length in bits `0 <= n < 8`
* `Codec` for a `List` of `DCDExtra1` objects.
* The first entry contains a padded `String` so it must be processed different from the remainder.
*/
private def implantFieldPadding(implants : List[ImplantEntry], varBit : Option[Int] = None) : Int = {
val base : Int = 5 //the offset with no implant entries
val baseOffset : Int = base - varBit.getOrElse(0)
val resultA = if(baseOffset < 0) { 8 - baseOffset } else { baseOffset % 8 }
private def dcd_list_codec(padFunc : (Long)=>Int) : Codec[List[DCDExtra1]] = (
uint8 >>:~ { size =>
conditional(size > 0, dcd_extra1_codec(padFunc(size))) ::
PacketHelpers.listOfNSized(size - 1, dcd_extra1_codec(0))
}
).xmap[List[DCDExtra1]] (
{
case _ :: Some(first) :: Nil :: HNil =>
List(first)
case _ :: Some(first) :: rest :: HNil =>
first +: rest
case _ :: None :: _ :: HNil =>
List()
},
{
case List() =>
0 :: None :: Nil :: HNil
case contents =>
contents.length :: contents.headOption :: contents.tail :: HNil
}
)
var implantOffset : Int = 0
implants.foreach({entry =>
implantOffset += entry.bitsize.toInt
})
val resultB : Int = resultA - (implantOffset % 8)
if(resultB < 0) { 8 + resultB } else { resultB }
/**
* `Codec` for entries in the `List` of `DCDExtra1` objects.
* The first entry's size of 80 characters is hard-set by the client.
*/
private def dcd_extra1_codec(pad : Int) : Codec[DCDExtra1] = (
("unk1" | PacketHelpers.encodedStringAligned(pad)) ::
("unk2" | uint16L)
).xmap[DCDExtra1] (
{
case unk1 :: unk2 :: HNil =>
DCDExtra1(unk1, unk2)
},
{
case DCDExtra1(unk1, unk2) =>
unk1.slice(0, 80) :: unk2 :: HNil //max 80 characters
}
)
/**
* A common `Codec` for a `List` of `String` objects
* used for first time events list and for the tutorials list.
* The first entry contains a padded `String` so it must be processed different from the remainder.
* @param padFunc a curried function awaiting the extracted length of the current `List`
*/
private def eventsListCodec(padFunc : (Long)=>Int) : Codec[List[String]] = (
uint32L >>:~ { size =>
conditional(size > 0, PacketHelpers.encodedStringAligned(padFunc(size))) ::
PacketHelpers.listOfNSized(size - 1, PacketHelpers.encodedString)
}
).xmap[List[String]] (
{
case _ :: Some(first) :: Nil :: HNil =>
List(first)
case _ :: Some(first) :: rest :: HNil =>
first +: rest
case _ :: None :: _ :: HNil =>
List()
},
{
case List() =>
0 :: None :: Nil :: HNil
case contents =>
contents.length :: contents.headOption :: contents.tail :: HNil
}
)
/**
* `Codec` for a `DCDExtra2` object.
*/
private val dcd_extra2_codec : Codec[DCDExtra2] = (
uint(5) ::
uint8L
).as[DCDExtra2]
/**
* `Codec` for a `List` of `String` objects.
* The first entry contains a padded `String` so it must be processed different from the remainder.
* The padding length is the conclusion of the summation of all the bits up until the point of this `String` object.
* Additionally, the length of this current string is also a necessary consideration.
* @see `paddingCalculations`
* @param padFunc a curried function awaiting the extracted length of the current `List` and will count the padding bits
*/
private def unkBCodec(padFunc : (Long)=>Int) : Codec[List[String]] = (
uint16L >>:~ { size =>
conditional(size > 0, PacketHelpers.encodedStringAligned(padFunc(size))) ::
PacketHelpers.listOfNSized(size - 1, PacketHelpers.encodedString)
}
).xmap[List[String]] (
{
case _ :: Some(first) :: Nil :: HNil =>
List(first)
case _ :: Some(first) :: rest :: HNil =>
first +: rest
case _ :: None :: _ :: HNil =>
List()
},
{
case List() =>
0 :: None :: Nil :: HNil
case contents =>
contents.length :: contents.headOption :: contents.tail :: HNil
}
)
/**
* Suport function that obtains the "absolute list value" of an `Option` object.
* @param opt the `Option` object
* @return if defined, returns a `List` of the `Option` object's contents;
* if undefined (`None`), returns an empty list
*/
def optToList(opt : Option[Any]) : List[Any] = opt match {
case Some(o) => List(o)
case None => Nil
}
/**
* A very specific `Option` object addition function.
* If a condition is met, the current `Optional` value is incremented by a specific amount.
* @param start the original amount
* @param test the test on whether to add to `start`
* @param value how much to add to `start`
* @return the amount after testing
*/
def displaceByUnk9(start : Option[Int], test : Option[Any], value : Int) : Option[Int] = test match {
case Some(_) =>
Some(start.getOrElse(0) + value)
case None =>
start
}
/**
* A `List` of bit distances between different sets of `String` objects in the `DetailedCharacterData` `Codec`
* in reverse order of encountered `String` fields (later to earlier).
* The distances are not the actual lengths but are modulo eight.
* Specific strings include (the contents of):<br>
* - `unk9` (as a `List` object)<br>
* - `tutorials`<br>
* - `firstTimeEvents`<br>
* - `unk3`<br>
* - `unk2`
*/
private val displacementPerEntry : List[Int] = List(7, 0, 0, 0, 0)
/**
* A curried function to calculate a cumulative padding value
* for whichever of the groups of `List` objects of `String` objects are found in a `DetailedCharacterData` object.
* Defines the expected base value - the starting value for determining the padding.
* The specific `String` object being considered is determined by the number of input lists.
* @see `paddingCalculations(Int, Option[Int], List[ImplantEntry], List[List[Any]])(Long)`
* @param contextOffset an inherited modification of the `base` padding value
* @param implants the list of implants in the stream
* @param prevLists all of the important previous lists
* @param currListLen the length of the current list
* @return the padding value for the target list
*/
def paddingCalculations(contextOffset : Option[Int], implants : List[ImplantEntry], prevLists : List[List[Any]])(currListLen : Long) : Int = {
paddingCalculations(3, contextOffset, implants, prevLists)(currListLen)
}
/**
* A curried function to calculate a cumulative padding value
* for whichever of the groups of `List` objects of `String` objects are found in a `DetailedCharacterData` object.
* The specific `String` object being considered is determined by the number of input lists.
* @see `paddingCalculations(Option[Int], List[ImplantEntry], List[List[Any/]/])(Long)`
* @param base the starting value with no implant entries, or bits from context
* @param contextOffset an inherited modification of the `base` padding value
* @param implants the list of implants in the stream
* @param prevLists all of the important previous lists
* @param currListLen the length of the current list
* @throws Exception if the number of input lists (`prevLists`) exceeds the number of expected bit distances between known lists
* @return the padding value for the target list;
* a value clamped between 0 and 7
*/
def paddingCalculations(base : Int, contextOffset : Option[Int], implants : List[ImplantEntry], prevLists : List[List[Any]])(currListLen : Long) : Int = {
if(prevLists.length > displacementPerEntry.length) {
throw new Exception("mismatched number of input lists compared to bit distances")
}
else if(currListLen > 0) {
//displacement into next byte of the content field of the first relevant string without padding
val baseResult : Int = base + contextOffset.getOrElse(0) + implants.foldLeft(0L)(_ + _.bitsize).toInt
val displacementResult : Int = (if(prevLists.isEmpty) {
baseResult
}
else {
//isolate the displacements that are important
val sequentialEmptyLists : List[List[Any]] = prevLists.takeWhile(_.isEmpty)
val offsetSlice : List[Int] = displacementPerEntry.drop(displacementPerEntry.length - sequentialEmptyLists.length)
if(prevLists.length == sequentialEmptyLists.length) { //if all lists are empty, factor in the base displacement
baseResult + offsetSlice.sum
}
else {
offsetSlice.sum
}
}) % 8
if(displacementResult != 0) {
8 - displacementResult
}
else {
0
}
}
else {
0 //if the current list has no length, there's no need to pad it
}
}
/**
@ -212,92 +519,86 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
}
/**
* Get the padding of the first entry in the first time events list.
* @param len the length of the first time events list
* @param implantPadding the padding that resulted from implant entries
* @return the pad length in bits `0 <= n < 8`
* By comparing the battle experience points to a fixed number of points,
* determine if the player is at least battle rank 24.
* Important things happen if the player is at least battle rank 24 ...
* @param bep the battle experience points being compared
* @return `true`, if the battle experience points are enough to be a player of the esteemed battle rank
*/
private def ftePadding(len : Long, implantPadding : Int) : Int = {
//TODO the proper padding length should reflect all variability in the stream prior to this point
if(len > 0) {
implantPadding
}
else {
0
}
}
/**
* Get the padding of the first entry in the completed tutorials list.<br>
* <br>
* The tutorials list follows the first time event list and also contains byte-aligned strings.
* If the both lists are populated or empty at the same time, the first entry will not need padding.
* If the first time events list is unpopulated, but this list is populated, the first entry will need padding bits.
* @param len the length of the first time events list
* @param len2 the length of the tutorial list
* @param implantPadding the padding that resulted from implant entries
* @return the pad length in bits `n < 8`
*/
private def tutPadding(len : Long, len2 : Long, implantPadding : Int) : Int = {
if(len > 0) {
0 //automatic alignment from previous List
}
else if(len2 > 0) {
implantPadding //need to align for elements
}
else {
0 //both lists are empty
}
}
def isBR24(bep : Long) : Boolean = bep > 2286230
def codec(pad_length : Option[Int]) : Codec[DetailedCharacterData] = (
("bep" | uint32L) >>:~ { bep =>
val a_codec : Codec[DetailedCharacterA] = (
("bep" | uint32L) ::
("cep" | uint32L) ::
ignore(96) ::
("healthMax" | uint16L) ::
("health" | uint16L) ::
ignore(1) ::
("armor" | uint16L) ::
ignore(9) ::
("unk1" | uint8L) ::
ignore(8) ::
("unk2" | uint4L) ::
("unk3" | uintL(3)) ::
("staminaMax" | uint16L) ::
("stamina" | uint16L) ::
ignore(147) ::
("certs" | listOfN(uint8L, CertificationType.codec)) ::
optional(bool, uint32L) :: //ask about sample CCRIDER
ignore(4) ::
(("implants" | PacketHelpers.listOfNSized(numberOfImplantSlots(bep), implant_entry_codec)) >>:~ { implants =>
ignore(12) ::
(("firstTimeEvent_length" | uint32L) >>:~ { len =>
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned(ftePadding(len, implantFieldPadding(implants, pad_length)))) ::
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
(("tutorial_length" | uint32L) >>:~ { len2 =>
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, implantFieldPadding(implants, pad_length)))) ::
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
ignore(160) ::
(bool >>:~ { br24 => //BR24+
newcodecs.binary_choice(br24, ignore(33), ignore(46)) ::
conditional(br24, Cosmetics.codec)
})
})
})
})
}
).exmap[DetailedCharacterData] (
("unk1" | uint32L) ::
("unk2" | uint32L) ::
("unk3" | uint32L) ::
("healthMax" | uint16L) ::
("health" | uint16L) ::
("unk4" | bool) ::
("armor" | uint16L) ::
("unk5" | uint32) :: //endianness?
("staminaMax" | uint16L) ::
("stamina" | uint16L) ::
conditional(false, uint32L) :: //see ps.c: sub_901150, line#1070692
("unk6" | uint16L) ::
("unk7" | uint(3)) ::
("unk8" | uint32L) ::
("unk9" | PacketHelpers.listOfNSized(6, uint16L)) :: //always length of 6
("certs" | listOfN(uint8L, CertificationType.codec))
).exmap[DetailedCharacterA] (
{
case bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: _ :: _ :: cosmetics :: HNil =>
//prepend the displaced first elements to their lists
val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else { fte1 }
val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else { tut1 }
Attempt.successful(DetailedCharacterData(bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cosmetics)(pad_length))
case bep :: cep :: u1 :: u2 :: u3 :: healthMax :: health :: u4 :: armor :: u5 :: staminaMax :: stamina :: None :: u6 :: u7 :: u8 :: u9 :: certs :: HNil =>
Attempt.successful(DetailedCharacterA(bep, cep, u1, u2, u3, healthMax, health, u4, armor, u5, staminaMax, stamina, u6, u7, u8, u9, certs))
},
{
case DetailedCharacterData(bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, cos) =>
case DetailedCharacterA(bep, cep, u1, u2, u3, healthMax, health, u4, armor, u5, staminaMax, stamina, u6, u7, u8, u9, certs) =>
Attempt.successful(
bep :: cep :: u1 :: u2 :: u3 :: healthMax :: health :: u4 :: armor :: u5 :: staminaMax :: stamina :: None :: u6 :: u7 :: u8 :: u9 :: certs :: HNil
)
}
)
def b_codec(bep : Long, pad_length : Option[Int]) : Codec[DetailedCharacterB] = (
optional(bool, "unk1" | uint32L) ::
(("implants" | PacketHelpers.listOfNSized(numberOfImplantSlots(bep), implant_entry_codec)) >>:~ { implants =>
("unk2" | dcd_list_codec(paddingCalculations(pad_length, implants, Nil))) >>:~ { unk2 =>
("unk3" | dcd_list_codec(paddingCalculations(pad_length, implants, List(unk2)))) >>:~ { unk3 =>
("firstTimeEvents" | eventsListCodec(paddingCalculations(pad_length, implants, List(unk3, unk2)))) >>:~ { fte =>
("tutorials" | eventsListCodec(paddingCalculations(pad_length, implants, List(fte, unk3, unk2)))) >>:~ { tut =>
("unk4" | uint32L) ::
("unk5" | uint32L) ::
("unk6" | uint32L) ::
("unk7" | uint32L) ::
("unk8" | uint32L) ::
(bool >>:~ { br24 => //BR24+
conditional(!br24, "unk9" | dcd_extra2_codec) >>:~ { unk9 =>
("unkA" | listOfN(uint16L, uint32L)) ::
("unkB" | unkBCodec(
paddingCalculations(
displaceByUnk9(pad_length, unk9, 5),
implants,
List(optToList(unk9), tut, fte, unk3, unk2)
)
)) ::
("unkC" | bool) ::
conditional(br24, "cosmetics" | Cosmetics.codec)
}
})
}
}
}
}
})
).exmap[DetailedCharacterB] (
{
case u1 :: implants :: u2 :: u3 :: fte :: tut :: u4 :: u5 :: u6 :: u7 :: u8 :: _ :: u9 :: uA :: uB :: uC :: cosmetics :: HNil =>
Attempt.successful(
DetailedCharacterB(u1, implants, u2, u3, fte, tut, u4, u5, u6, u7, u8, u9, uA, uB, uC, cosmetics)(bep, pad_length)
)
},
{
case DetailedCharacterB(u1, implants, u2, u3, fte, tut, u4, u5, u6, u7, u8, u9, uA, uB, uC, cosmetics) =>
val implantCapacity : Int = numberOfImplantSlots(bep)
val implantList = if(implants.length > implantCapacity) {
implants.slice(0, implantCapacity)
@ -305,20 +606,26 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
else {
recursiveEnsureImplantSlots(implantCapacity, implants)
}
//shift the first elements off their lists
val (firstEvent, fteListCopy) = fteList match {
case (f : String) +: Nil => (Some(f), Nil)
case ((f : String) +: (rest : List[String])) => (Some(f), rest)
case Nil => (None, Nil)
}
val (firstTutorial, tutListCopy) = tutList match {
case (f : String) +: Nil => (Some(f), Nil)
case ((f : String) +: (rest : List[String])) => (Some(f), rest)
case Nil => (None, Nil)
}
val br24 : Boolean = isBR24(bep)
val cosmetics : Option[Cosmetics] = if(br24) { cos } else { None }
Attempt.successful(bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: None :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: br24 :: () :: cosmetics :: HNil)
val cos : Option[Cosmetics] = if(br24) { cosmetics } else { None }
Attempt.successful(
u1 :: implantList :: u2 :: u3 :: fte :: tut :: u4 :: u5 :: u6 :: u7 :: u8 :: br24 :: u9 :: uA :: uB :: uC :: cos :: HNil
)
}
)
def codec(pad_length : Option[Int]) : Codec[DetailedCharacterData] = (
("a" | a_codec) >>:~ { a =>
("b" | b_codec(a.bep, pad_length)).hlist
}
).exmap[DetailedCharacterData] (
{
case a :: b :: HNil =>
Attempt.successful(DetailedCharacterData(a, b)(pad_length))
},
{
case DetailedCharacterData(a, b) =>
Attempt.successful(a :: b :: HNil)
}
)

View file

@ -21,15 +21,16 @@ import shapeless.{::, HNil}
* 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.
* These offsets exist in the form of `String` and `List` padding.
* @see `DetailedCharacterData`<br>
* `InventoryData`<br>
* `DrawnSlot`
* @see `CharacterAppearanceData`
* @see `DetailedCharacterData`
* @see `InventoryData`
* @see `DrawnSlot`
* @param pos the optional position of the character in the world environment
* @param basic_appearance common fields regarding the the character's appearance
* @param character_data the class-specific data that explains about the character
* @param position_defined used by the `Codec` to seed the state of the optional `pos` field
* @param inventory the player's full inventory
* @param drawn_slot the holster that is initially drawn
* @param character_data the class-specific data that discusses the character
* @param position_defined used to seed the state of the optional position fields
* @param inventory the player's full or partial (holsters-only) inventory
* @param drawn_slot the holster that is depicted as exposed, or "drawn"
*/
final case class DetailedPlayerData(pos : Option[PlacementData],
basic_appearance : CharacterAppearanceData,
@ -54,8 +55,8 @@ object DetailedPlayerData extends Marshallable[DetailedPlayerData] {
* 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 inventory the player's full or partial (holsters-only) inventory
* @param drawn_slot the holster that is depicted as exposed, or "drawn";
* technically, always `DrawnSlot.None`, but the field is preserved to maintain similarity
* @return a `DetailedPlayerData` object
*/
@ -70,7 +71,7 @@ object DetailedPlayerData extends Marshallable[DetailedPlayerData] {
* 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 drawn_slot the holster that is depicted as exposed, or "drawn;"
* technically, always `DrawnSlot.None`, but the field is preserved to maintain similarity
* @return a `DetailedPlayerData` object
*/
@ -86,8 +87,8 @@ object DetailedPlayerData extends Marshallable[DetailedPlayerData] {
* @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 inventory the player's inventory
* @param drawn_slot the holster that is initially drawn
* @param inventory the player's full or partial (holsters-only) inventory
* @param drawn_slot the holster that is depicted as exposed, or "drawn"
* @return a `DetailedPlayerData` object
*/
def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Option[Int])=>DetailedCharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedPlayerData = {
@ -102,7 +103,7 @@ object DetailedPlayerData extends Marshallable[DetailedPlayerData] {
* @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
* @param drawn_slot the holster that is depicted as exposed, or "drawn"
* @return a `DetailedPlayerData` object
*/
def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Option[Int])=>DetailedCharacterData, drawn_slot : DrawnSlot.Value) : DetailedPlayerData = {

View file

@ -65,7 +65,7 @@ object PlayerData extends Marshallable[PlayerData] {
*/
def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type) : PlayerData = {
val appearance = basic_appearance(5)
PlayerData(None, appearance, character_data(appearance.backpack, true), Some(inventory), drawn_slot)(false)
PlayerData(None, appearance, character_data(appearance.a.altModel, true), Some(inventory), drawn_slot)(false)
}
/**
* Overloaded constructor that ignores the coordinate information and the inventory.
@ -79,7 +79,7 @@ object PlayerData extends Marshallable[PlayerData] {
*/
def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, drawn_slot : DrawnSlot.Type) : PlayerData = {
val appearance = basic_appearance(5)
PlayerData(None, appearance, character_data(appearance.backpack, true), None, drawn_slot)(false)
PlayerData(None, appearance, character_data(appearance.a.altModel, true), None, drawn_slot)(false)
}
/**
@ -95,7 +95,7 @@ object PlayerData extends Marshallable[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( PaddingOffset(Some(pos)) )
PlayerData(Some(pos), appearance, character_data(appearance.backpack, false), Some(inventory), drawn_slot)(true)
PlayerData(Some(pos), appearance, character_data(appearance.a.altModel, false), Some(inventory), drawn_slot)(true)
}
/**
* Overloaded constructor that includes the coordinate information but ignores the inventory.
@ -109,7 +109,7 @@ object PlayerData extends Marshallable[PlayerData] {
*/
def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, drawn_slot : DrawnSlot.Type) : PlayerData = {
val appearance = basic_appearance( PaddingOffset(Some(pos)) )
PlayerData(Some(pos), appearance, character_data(appearance.backpack, false), None, drawn_slot)(true)
PlayerData(Some(pos), appearance, character_data(appearance.a.altModel, false), None, drawn_slot)(true)
}
/**
@ -166,8 +166,8 @@ object PlayerData extends Marshallable[PlayerData] {
conditional(position_defined, "pos" | PlacementData.codec) >>:~ { pos =>
("basic_appearance" | CharacterAppearanceData.codec(PaddingOffset(pos))) >>:~ { app =>
("character_data" | newcodecs.binary_choice(position_defined,
CharacterData.codec(app.backpack),
CharacterData.codec_seated(app.backpack))) ::
CharacterData.codec(app.b.backpack),
CharacterData.codec_seated(app.b.backpack))) ::
optional(bool, "inventory" | InventoryData.codec) ::
("drawn_slot" | DrawnSlot.codec) ::
bool //usually false
@ -193,7 +193,7 @@ object PlayerData extends Marshallable[PlayerData] {
*/
def codec(offset : Int) : Codec[PlayerData] = (
("basic_appearance" | CharacterAppearanceData.codec(offset)) >>:~ { app =>
("character_data" | CharacterData.codec_seated(app.backpack)) ::
("character_data" | CharacterData.codec_seated(app.b.backpack)) ::
optional(bool, "inventory" | InventoryData.codec) ::
("drawn_slot" | DrawnSlot.codec) ::
bool //usually false

View file

@ -165,7 +165,7 @@ object VehicleData extends Marshallable[VehicleData] {
*/
def PlayerData(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type, accumulative : Long) : Player_Data = {
val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative))
Player_Data(None, appearance, character_data(appearance.backpack, true), Some(inventory), drawn_slot)(false)
Player_Data(None, appearance, character_data(appearance.b.backpack, true), Some(inventory), drawn_slot)(false)
}
/**
* Constructor for `PlayerData` that ignores the coordinate information and the inventory
@ -181,7 +181,7 @@ object VehicleData extends Marshallable[VehicleData] {
*/
def PlayerData(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, drawn_slot : DrawnSlot.Type, accumulative : Long) : Player_Data = {
val appearance = basic_appearance(CumulativeSeatedPlayerNamePadding(accumulative))
Player_Data.apply(None, appearance, character_data(appearance.backpack, true), None, drawn_slot)(false)
Player_Data.apply(None, appearance, character_data(appearance.b.backpack, true), None, drawn_slot)(false)
}
private val driveState8u = PacketHelpers.createEnumerationCodec(DriveState, uint8L)

View file

@ -33,41 +33,67 @@ class CharacterDataTest extends Specification {
pos.vel.isDefined mustEqual true
pos.vel.get mustEqual Vector3(1.4375f, -0.4375f, 0f)
basic.app.name mustEqual "ScrawnyRonnie"
basic.app.faction mustEqual PlanetSideEmpire.TR
basic.app.sex mustEqual CharacterGender.Male
basic.app.head mustEqual 5
basic.app.voice mustEqual CharacterVoice.Voice5
basic.voice2 mustEqual 3
basic.black_ops mustEqual false
basic.jammered mustEqual false
basic.exosuit mustEqual ExoSuitType.Reinforced
basic.outfit_name mustEqual "Black Beret Armoured Corps"
basic.outfit_logo mustEqual 23
basic.facingPitch mustEqual 340.3125f
basic.facingYawUpper mustEqual 0
basic.lfs mustEqual false
basic.grenade_state mustEqual GrenadeState.None
basic.is_cloaking mustEqual false
basic.charging_pose mustEqual false
basic.on_zipline mustEqual false
basic.ribbons.upper mustEqual MeritCommendation.MarkovVeteran
basic.ribbons.middle mustEqual MeritCommendation.HeavyInfantry4
basic.ribbons.lower mustEqual MeritCommendation.TankBuster7
basic.ribbons.tos mustEqual MeritCommendation.SixYearTR
basic match {
case CharacterAppearanceData(a, b, ribbons) =>
a.app.name mustEqual "ScrawnyRonnie"
a.app.faction mustEqual PlanetSideEmpire.TR
a.app.sex mustEqual CharacterGender.Male
a.app.head mustEqual 5
a.app.voice mustEqual CharacterVoice.Voice5
a.black_ops mustEqual false
a.jammered mustEqual false
a.exosuit mustEqual ExoSuitType.Reinforced
a.unk1 mustEqual false
a.unk2 mustEqual None
a.unk3 mustEqual None
a.unk4 mustEqual 0
a.unk5 mustEqual 0
a.unk6 mustEqual 30777081L
a.unk7 mustEqual 1
a.unk8 mustEqual 4
a.unk9 mustEqual 0
a.unkA mustEqual 0
b.outfit_name mustEqual "Black Beret Armoured Corps"
b.outfit_logo mustEqual 23
b.backpack mustEqual false
b.facingPitch mustEqual 320.625f
b.facingYawUpper mustEqual 0
b.lfs mustEqual false
b.grenade_state mustEqual GrenadeState.None
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline mustEqual None
b.unk0 mustEqual 316554L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false
b.unk4 mustEqual false
b.unk5 mustEqual false
b.unk6 mustEqual false
b.unk7 mustEqual false
ribbons.upper mustEqual MeritCommendation.MarkovVeteran
ribbons.middle mustEqual MeritCommendation.HeavyInfantry4
ribbons.lower mustEqual MeritCommendation.TankBuster7
ribbons.tos mustEqual MeritCommendation.SixYearTR
case _ =>
ko
}
char.health mustEqual 255
char.armor mustEqual 253
char.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade
char.command_rank mustEqual 5
char.implant_effects.isDefined mustEqual true
char.implant_effects.get mustEqual ImplantEffects.NoEffects
char.implant_effects.length mustEqual 1
char.implant_effects.head mustEqual ImplantEffects.NoEffects
char.cosmetics.isDefined mustEqual true
char.cosmetics.get.no_helmet mustEqual true
char.cosmetics.get.beret mustEqual true
char.cosmetics.get.sunglasses mustEqual true
char.cosmetics.get.earpiece mustEqual true
char.cosmetics.get.brimmed_cap mustEqual false
char.unk mustEqual 7
//short test of inventory items
inv.isDefined mustEqual true
val contents = inv.get.contents
@ -116,7 +142,7 @@ class CharacterDataTest extends Specification {
ko
}
}
//
"decode (seated)" in {
PacketCoding.DecodePacket(string_seated).require match {
case ObjectCreateMessage(len, cls, guid, parent, data) =>
@ -126,29 +152,54 @@ class CharacterDataTest extends Specification {
parent mustEqual Some(ObjectCreateMessageParent(PlanetSideGUID(1234), 0))
data match {
case Some(PlayerData(None, basic, char, inv, hand)) =>
basic.app.name mustEqual "ScrawnyRonnie"
basic.app.faction mustEqual PlanetSideEmpire.TR
basic.app.sex mustEqual CharacterGender.Male
basic.app.head mustEqual 5
basic.app.voice mustEqual CharacterVoice.Voice5
basic.voice2 mustEqual 3
basic.black_ops mustEqual false
basic.jammered mustEqual false
basic.exosuit mustEqual ExoSuitType.Reinforced
basic.outfit_name mustEqual "Black Beret Armoured Corps"
basic.outfit_logo mustEqual 23
basic.facingPitch mustEqual 340.3125f
basic.facingYawUpper mustEqual 0
basic.lfs mustEqual false
basic.grenade_state mustEqual GrenadeState.None
basic.is_cloaking mustEqual false
basic.charging_pose mustEqual false
basic.on_zipline mustEqual false
basic.ribbons.upper mustEqual MeritCommendation.MarkovVeteran
basic.ribbons.middle mustEqual MeritCommendation.HeavyInfantry4
basic.ribbons.lower mustEqual MeritCommendation.TankBuster7
basic.ribbons.tos mustEqual MeritCommendation.SixYearTR
//etc..
basic match {
case CharacterAppearanceData(a, b, ribbons) =>
a.app.name mustEqual "ScrawnyRonnie"
a.app.faction mustEqual PlanetSideEmpire.TR
a.app.sex mustEqual CharacterGender.Male
a.app.head mustEqual 5
a.app.voice mustEqual CharacterVoice.Voice5
a.black_ops mustEqual false
a.jammered mustEqual false
a.exosuit mustEqual ExoSuitType.Reinforced
a.unk1 mustEqual false
a.unk2 mustEqual None
a.unk3 mustEqual None
a.unk4 mustEqual 0
a.unk5 mustEqual 0
a.unk6 mustEqual 192L
a.unk7 mustEqual 0
a.unk8 mustEqual 0
a.unk9 mustEqual 0
a.unkA mustEqual 0
b.outfit_name mustEqual "Black Beret Armoured Corps"
b.outfit_logo mustEqual 23
b.backpack mustEqual false
b.facingPitch mustEqual 320.625f
b.facingYawUpper mustEqual 0
b.lfs mustEqual false
b.grenade_state mustEqual GrenadeState.None
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline mustEqual None
b.unk0 mustEqual 26L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false
b.unk4 mustEqual false
b.unk5 mustEqual false
b.unk6 mustEqual false
b.unk7 mustEqual false
ribbons.upper mustEqual MeritCommendation.MarkovVeteran
ribbons.middle mustEqual MeritCommendation.HeavyInfantry4
ribbons.lower mustEqual MeritCommendation.TankBuster7
ribbons.tos mustEqual MeritCommendation.SixYearTR
//etc..
case _ =>
ko
}
case _ =>
ko
}
@ -170,40 +221,67 @@ class CharacterDataTest extends Specification {
pos.orient mustEqual Vector3(0, 0, 126.5625f)
pos.vel.isDefined mustEqual false
basic.app.name mustEqual "Angello"
basic.app.faction mustEqual PlanetSideEmpire.VS
basic.app.sex mustEqual CharacterGender.Male
basic.app.head mustEqual 10
basic.app.voice mustEqual CharacterVoice.Voice2
basic.voice2 mustEqual 0
basic.black_ops mustEqual false
basic.jammered mustEqual false
basic.exosuit mustEqual ExoSuitType.MAX
basic.outfit_name mustEqual "Original District"
basic.outfit_logo mustEqual 23
basic.facingPitch mustEqual 0
basic.facingYawUpper mustEqual 180.0f
basic.lfs mustEqual false
basic.grenade_state mustEqual GrenadeState.None
basic.is_cloaking mustEqual false
basic.charging_pose mustEqual false
basic.on_zipline mustEqual false
basic.ribbons.upper mustEqual MeritCommendation.Jacking2
basic.ribbons.middle mustEqual MeritCommendation.ScavengerVS1
basic.ribbons.lower mustEqual MeritCommendation.AMSSupport4
basic.ribbons.tos mustEqual MeritCommendation.SixYearVS
basic match {
case CharacterAppearanceData(a, b, ribbons) =>
a.app.name mustEqual "Angello"
a.app.faction mustEqual PlanetSideEmpire.VS
a.app.sex mustEqual CharacterGender.Male
a.app.head mustEqual 10
a.app.voice mustEqual CharacterVoice.Voice2
a.black_ops mustEqual false
a.jammered mustEqual false
a.exosuit mustEqual ExoSuitType.MAX
a.unk1 mustEqual false
a.unk2 mustEqual None
a.unk3 mustEqual None
a.unk4 mustEqual 0
a.unk5 mustEqual 1
a.unk6 mustEqual 0L
a.unk7 mustEqual 0
a.unk8 mustEqual 0
a.unk9 mustEqual 0
a.unkA mustEqual 0
b.outfit_name mustEqual "Original District"
b.outfit_logo mustEqual 23
b.backpack mustEqual true
b.facingPitch mustEqual 351.5625f
b.facingYawUpper mustEqual 0
b.lfs mustEqual false
b.grenade_state mustEqual GrenadeState.None
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline mustEqual None
b.unk0 mustEqual 529687L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false
b.unk4 mustEqual false
b.unk5 mustEqual false
b.unk6 mustEqual false
b.unk7 mustEqual false
ribbons.upper mustEqual MeritCommendation.Jacking2
ribbons.middle mustEqual MeritCommendation.ScavengerVS1
ribbons.lower mustEqual MeritCommendation.AMSSupport4
ribbons.tos mustEqual MeritCommendation.SixYearVS
//etc..
case _ =>
ko
}
char.health mustEqual 0
char.armor mustEqual 0
char.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade
char.command_rank mustEqual 2
char.implant_effects.isDefined mustEqual false
char.implant_effects.isEmpty mustEqual true
char.cosmetics.isDefined mustEqual true
char.cosmetics.get.no_helmet mustEqual true
char.cosmetics.get.beret mustEqual true
char.cosmetics.get.sunglasses mustEqual true
char.cosmetics.get.earpiece mustEqual true
char.cosmetics.get.brimmed_cap mustEqual false
char.unk mustEqual 1
hand mustEqual DrawnSlot.Pistol1
case _ =>
@ -220,7 +298,7 @@ class CharacterDataTest extends Specification {
Vector3(0f, 0f, 64.6875f),
Some(Vector3(1.4375f, -0.4375f, 0f))
)
val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
val a : Int=>CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(
"ScrawnyRonnie",
PlanetSideEmpire.TR,
@ -228,17 +306,43 @@ class CharacterDataTest extends Specification {
5,
CharacterVoice.Voice5
),
3,
false,
false,
false,
None,
false,
ExoSuitType.Reinforced,
None,
0,
0,
30777081L,
1,
4,
0,
0
)
val b : (Boolean,Int)=>CharacterAppearanceB = CharacterAppearanceB(
316554L,
"Black Beret Armoured Corps",
23,
false,
340.3125f, 0f,
false,
false,
false,
false,
320.625f, 0f,
false,
GrenadeState.None,
false, false, false,
false,
false,
false,
false,
false,
None
)
val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
a, b,
RibbonBars(
MeritCommendation.MarkovVeteran,
MeritCommendation.HeavyInfantry4,
@ -249,8 +353,9 @@ class CharacterDataTest extends Specification {
val char : (Boolean,Boolean)=>CharacterData = CharacterData(
255, 253,
UniformStyle.ThirdUpgrade,
7,
5,
Some(ImplantEffects.NoEffects),
List(ImplantEffects.NoEffects),
Some(Cosmetics(true, true, true, true, false))
)
val inv = InventoryData(
@ -265,18 +370,11 @@ class CharacterDataTest extends Specification {
val msg = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(3902), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
val pkt_bitv = pkt.toBitVector
val ori_bitv = string.toBitVector
pkt_bitv.take(452) mustEqual ori_bitv.take(452) //skip 126
pkt_bitv.drop(578).take(438) mustEqual ori_bitv.drop(578).take(438) //skip 2
pkt_bitv.drop(1018).take(17) mustEqual ori_bitv.drop(1018).take(17) //skip 11
pkt_bitv.drop(1046).take(147) mustEqual ori_bitv.drop(1046).take(147) //skip 3
pkt_bitv.drop(1196) mustEqual ori_bitv.drop(1196)
//TODO work on CharacterData to make this pass as a single stream
pkt mustEqual string
}
"encode (seated)" in {
val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
val a : Int=>CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(
"ScrawnyRonnie",
PlanetSideEmpire.TR,
@ -284,17 +382,43 @@ class CharacterDataTest extends Specification {
5,
CharacterVoice.Voice5
),
3,
false,
false,
false,
None,
false,
ExoSuitType.Reinforced,
None,
0,
0,
192L,
0,
0,
0,
0
)
val b : (Boolean,Int)=>CharacterAppearanceB = CharacterAppearanceB(
26L,
"Black Beret Armoured Corps",
23,
false,
340.3125f, 0f,
false,
false,
false,
false,
320.625f, 0f,
false,
GrenadeState.None,
false, false, false,
false,
false,
false,
false,
false,
None
)
val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
a, b,
RibbonBars(
MeritCommendation.MarkovVeteran,
MeritCommendation.HeavyInfantry4,
@ -306,7 +430,7 @@ class CharacterDataTest extends Specification {
255, 253,
UniformStyle.ThirdUpgrade,
5,
Some(ImplantEffects.NoEffects),
List(ImplantEffects.NoEffects),
Some(Cosmetics(true, true, true, true, false))
)
val inv = InventoryData(
@ -329,7 +453,7 @@ class CharacterDataTest extends Specification {
Vector3(4629.8906f, 6316.4453f, 54.734375f),
Vector3(0, 0, 126.5625f)
)
val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
val a : Int=>CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(
"Angello",
PlanetSideEmpire.VS,
@ -337,17 +461,43 @@ class CharacterDataTest extends Specification {
10,
CharacterVoice.Voice2
),
0,
false,
true,
false,
None,
false,
ExoSuitType.MAX,
None,
0,
1,
0L,
0,
0,
0,
0
)
val b : (Boolean,Int)=>CharacterAppearanceB = CharacterAppearanceB(
529687L,
"Original District",
23,
false, //unk1
true, //backpack
0f, 180.0f,
false,
false, //unk2
false, //unk3
false, //unk4
351.5625f, 0f,
false, //lfs
GrenadeState.None,
false, false, false,
false, //is_cloaking
false, //unk5
false, //unk6
false, //charging_pose
false, //unk7
None
)
val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
a, b,
RibbonBars(
MeritCommendation.Jacking2,
MeritCommendation.ScavengerVS1,
@ -358,22 +508,19 @@ class CharacterDataTest extends Specification {
val char : (Boolean,Boolean)=>CharacterData = CharacterData(
0, 0,
UniformStyle.ThirdUpgrade,
2,
None,
1,
List(),
Some(Cosmetics(true, true, true, true, false))
)
val obj = PlayerData(pos, app, char, DrawnSlot.Pistol1)
val msg = ObjectCreateMessage(ObjectClass.avatar, PlanetSideGUID(3380), obj)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
//granular test
val pkt_bitv = pkt.toBitVector
val ori_bitv = string_backpack.toBitVector
pkt_bitv.take(300) mustEqual ori_bitv.take(300) //skip 2
pkt_bitv.drop(302).take(14) mustEqual ori_bitv.drop(302).take(14) //skip 126
pkt_bitv.drop(442).take(305) mustEqual ori_bitv.drop(442).take(305) //skip 1
pkt_bitv.drop(748).take(9) mustEqual ori_bitv.drop(748).take(9) // skip 2
pkt_bitv.drop(759).take(157) mustEqual ori_bitv.drop(759).take(157) //skip 1
pkt_bitv.drop(917) mustEqual ori_bitv.drop(917)
pkt_bitv.take(916) mustEqual pkt_bitv.take(916) //skip 4
pkt_bitv.drop(920) mustEqual pkt_bitv.drop(920)
//TODO work on CharacterData to make this pass as a single stream
}
}

File diff suppressed because one or more lines are too long

View file

@ -50,34 +50,60 @@ class MountedVehiclesTest extends Specification {
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 CharacterVoice.Voice5
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))
case PlayerData(None, app, char, Some(InventoryData(inv)), DrawnSlot.None) =>
app match {
case CharacterAppearanceData(a, b, ribbons) =>
a.app mustEqual BasicCharacterData("ScrawnyRonnie", PlanetSideEmpire.TR, CharacterGender.Male, 5, CharacterVoice.Voice5)
a.black_ops mustEqual false
a.jammered mustEqual false
a.exosuit mustEqual ExoSuitType.Agile
a.unk1 mustEqual false
a.unk2 mustEqual None
a.unk3 mustEqual None
a.unk4 mustEqual 0
a.unk5 mustEqual 0
a.unk6 mustEqual 30777081L
a.unk7 mustEqual 1
a.unk8 mustEqual 4
a.unk9 mustEqual 0
a.unkA mustEqual 0
b.outfit_name mustEqual "Black Beret Armoured Corps"
b.outfit_logo mustEqual 23
b.backpack mustEqual false
b.facingPitch mustEqual 348.75f
b.facingYawUpper mustEqual 0
b.lfs mustEqual false
b.grenade_state mustEqual GrenadeState.None
b.is_cloaking mustEqual false
b.charging_pose mustEqual false
b.on_zipline mustEqual None
b.unk0 mustEqual 316554L
b.unk1 mustEqual false
b.unk2 mustEqual false
b.unk3 mustEqual false
b.unk4 mustEqual false
b.unk5 mustEqual false
b.unk6 mustEqual false
b.unk7 mustEqual false
case _ =>
ko
}
char match {
case CharacterData(health, armor, uniform, unk, cr, implants, cosmetics) =>
health mustEqual 100
armor mustEqual 0
uniform mustEqual UniformStyle.ThirdUpgrade
unk mustEqual 7
cr mustEqual 5
implants mustEqual Nil
cosmetics mustEqual Some(Cosmetics(true, true, true, true, false))
case _ =>
ko
}
//briefly ...
inv.size mustEqual 4
inv.head.objectClass mustEqual ObjectClass.medicalapplicator
inv.head.parentSlot mustEqual 0
@ -87,10 +113,10 @@ class MountedVehiclesTest extends Specification {
inv(2).parentSlot mustEqual 2
inv(3).objectClass mustEqual ObjectClass.chainblade
inv(3).parentSlot mustEqual 4
hand mustEqual DrawnSlot.None
case _ =>
ko
}
//back to mosquito inventory
list(1).objectClass mustEqual ObjectClass.rotarychaingun_mosquito
list(1).parentSlot mustEqual 1
case None =>
@ -105,17 +131,51 @@ class MountedVehiclesTest extends Specification {
}
"encode (Scrawny Ronnie's mosquito)" in {
val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
BasicCharacterData("ScrawnyRonnie", PlanetSideEmpire.TR, CharacterGender.Male, 5, CharacterVoice.Voice5),
3,
false, false,
val a : Int=>CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(
"ScrawnyRonnie",
PlanetSideEmpire.TR,
CharacterGender.Male,
5,
CharacterVoice.Voice5
),
false,
false,
false,
None,
false,
ExoSuitType.Agile,
None,
0,
0,
30777081L,
1,
4,
0,
0
)
val b : (Boolean,Int)=>CharacterAppearanceB = CharacterAppearanceB(
316554L,
"Black Beret Armoured Corps",
23,
false,
354.375f, 0.0f,
false,
GrenadeState.None, false, false, false,
false,
false,
false,
348.75f, 0,
false,
GrenadeState.None,
false,
false,
false,
false,
false,
None
)
val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
a, b,
RibbonBars(
MeritCommendation.MarkovVeteran,
MeritCommendation.HeavyInfantry4,
@ -126,9 +186,9 @@ class MountedVehiclesTest extends Specification {
val char : (Boolean,Boolean)=>CharacterData = CharacterData(
100, 0,
UniformStyle.ThirdUpgrade,
0,
7,
5,
None,
Nil,
Some(Cosmetics(true, true, true, true, false))
)
val inv : InventoryData = InventoryData(
@ -178,15 +238,7 @@ class MountedVehiclesTest extends Specification {
)(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
pkt mustEqual string_mosquito_seated
}
}

View file

@ -456,7 +456,6 @@ class PacketCodingActorITest extends ActorTest {
val pos : PlacementData = PlacementData(Vector3.Zero, Vector3.Zero)
val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
BasicCharacterData("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1),
3,
false,
false,
ExoSuitType.Standard,
@ -468,7 +467,7 @@ class PacketCodingActorITest extends ActorTest {
GrenadeState.None,
false,
false,
false,
None,
RibbonBars()
)
var char : (Option[Int])=>DetailedCharacterData = DetailedCharacterData(
@ -476,7 +475,6 @@ class PacketCodingActorITest extends ActorTest {
0,
100, 100,
50,
1, 7, 7,
100, 100,
List(CertificationType.StandardAssault, CertificationType.MediumAssault, CertificationType.ATV, CertificationType.Harasser, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit, CertificationType.ReinforcedExoSuit),
List(),
@ -486,7 +484,7 @@ class PacketCodingActorITest extends ActorTest {
)
val obj = DetailedPlayerData(pos, app, char, InventoryData(Nil), DrawnSlot.None)
val pkt = MultiPacketBundle(List(ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj)))
val string_hex = hex"000900001879060000bc84b000000000000000000002040000097049006c006c006c004900490049006c006c006c0049006c0049006c006c0049006c006c006c0049006c006c0049008452700000000000000000000000000000002000000fe6a703fffffffffffffffffffffffffffffffc00000000000000000000000000000000000000019001900064000001007ec800c80000000000000000000000000000000000000001c00042c54686c7000000000000000000000000000000000000000000000000000000000000000000000000200700"
val string_hex = hex"000900001879060000bc84b000000000000000000002040000097049006c006c006c004900490049006c006c006c0049006c0049006c006c0049006c006c006c0049006c006c00490084524000000000000000000000000000000020000007f35703fffffffffffffffffffffffffffffffc000000000000000000000000000000000000000190019000640000000000c800c80000000000000000000000000000000000000001c00042c54686c7000000000000000000000000000000000000000000000000000000000000000000000400e0"
"PacketCodingActor" should {
"bundle an r-originating packet into an l-facing SlottedMetaPacket byte stream data (SlottedMetaPacket)" in {
@ -547,36 +545,92 @@ class PacketCodingActorJTest extends ActorTest {
class PacketCodingActorKTest extends ActorTest {
import net.psforever.packet.game.objectcreate._
val pos : PlacementData = PlacementData(Vector3.Zero, Vector3.Zero)
val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
BasicCharacterData("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, CharacterVoice.Voice1),
3,
val aa : Int=>CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(
"IlllIIIlllIlIllIlllIllI",
PlanetSideEmpire.VS,
CharacterGender.Female,
41,
CharacterVoice.Voice1
),
false,
false,
true,
None,
false,
ExoSuitType.Standard,
None,
0,
0,
41605313L,
0,
0,
0,
65535
)
val ab : (Boolean,Int)=>CharacterAppearanceB = CharacterAppearanceB(
0L,
"",
0,
false,
false,
false,
false,
false,
2.8125f, 210.9375f,
true,
false,
GrenadeState.None,
false,
false,
false,
RibbonBars()
)
var char : (Option[Int])=>DetailedCharacterData = DetailedCharacterData(
0,
0,
100, 100,
50,
1, 7, 7,
100, 100,
List(CertificationType.StandardAssault, CertificationType.MediumAssault, CertificationType.ATV, CertificationType.Harasser, CertificationType.StandardExoSuit, CertificationType.AgileExoSuit, CertificationType.ReinforcedExoSuit),
List(),
List("xpe_sanctuary_help", "xpe_th_firemodes", "used_beamer", "map13"),
List.empty,
false,
false,
None
)
val app : (Int)=>CharacterAppearanceData = CharacterAppearanceData(
aa, ab,
RibbonBars()
)
val ba : DetailedCharacterA = DetailedCharacterA(
0L,
0L,
0L, 0L, 0L,
100, 100,
false,
50,
32831L,
100, 100,
0, 0, 0L,
List(0, 0, 0, 0, 0, 0),
List(
CertificationType.StandardAssault,
CertificationType.MediumAssault,
CertificationType.ATV,
CertificationType.Harasser,
CertificationType.StandardExoSuit,
CertificationType.AgileExoSuit,
CertificationType.ReinforcedExoSuit
)
)
val bb : (Long, Option[Int])=>DetailedCharacterB = DetailedCharacterB(
None,
Nil,
Nil, Nil,
List(
"xpe_sanctuary_help",
"xpe_th_firemodes",
"used_beamer",
"map13"
),
Nil,
0L, 0L, 0L, 0L, 0L,
Some(DCDExtra2(0, 0)),
Nil, Nil, false,
None
)
val char : (Option[Int])=>DetailedCharacterData =
(pad_length : Option[Int]) => DetailedCharacterData(ba, bb(ba.bep, pad_length))(pad_length)
val obj = DetailedPlayerData(pos, app, char, InventoryData(Nil), DrawnSlot.None)
val list = List(
ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj),