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

View file

@ -4,7 +4,7 @@ package net.psforever.objects.definition.converter
import net.psforever.objects.{EquipmentSlot, Player} import net.psforever.objects.{EquipmentSlot, Player}
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
import net.psforever.packet.game.objectcreate._ 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.annotation.tailrec
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
@ -22,20 +22,7 @@ class CharacterSelectConverter extends AvatarConverter {
DetailedPlayerData.apply( DetailedPlayerData.apply(
PlacementData(0, 0, 0), PlacementData(0, 0, 0),
MakeAppearanceData(obj), MakeAppearanceData(obj),
DetailedCharacterData( MakeDetailedCharacterData(obj),
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)
),
InventoryData(recursiveMakeHolsters(obj.Holsters().iterator)), InventoryData(recursiveMakeHolsters(obj.Holsters().iterator)),
AvatarConverter.GetDrawnSlot(obj) AvatarConverter.GetDrawnSlot(obj)
) )
@ -49,24 +36,73 @@ class CharacterSelectConverter extends AvatarConverter {
* @return the resulting `CharacterAppearanceData` * @return the resulting `CharacterAppearanceData`
*/ */
private def MakeAppearanceData(obj : Player) : (Int)=>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), BasicCharacterData(obj.Name, obj.Faction, obj.Sex, obj.Head, CharacterVoice.Mute),
voice2 = 0,
black_ops = false, black_ops = false,
false,
false,
None,
jammered = false, jammered = false,
obj.ExoSuit, obj.ExoSuit,
None,
0,
0,
0L,
0,
0,
0,
0
)
val ab : (Boolean,Int)=>CharacterAppearanceB = CharacterAppearanceB(
0L,
outfit_name = "", outfit_name = "",
outfit_logo = 0, outfit_logo = 0,
false,
backpack = false, backpack = false,
false,
false,
false,
facingPitch = 0, facingPitch = 0,
facingYawUpper = 0, facingYawUpper = 0,
lfs = true, lfs = false,
GrenadeState.None, GrenadeState.None,
is_cloaking = false, obj.Cloaked,
false,
false,
charging_pose = false, charging_pose = false,
on_zipline = false, false,
RibbonBars() 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.{EquipmentSlot, Player}
import net.psforever.objects.equipment.Equipment import net.psforever.objects.equipment.Equipment
import net.psforever.packet.game.objectcreate._ import net.psforever.packet.game.objectcreate._
import net.psforever.types.{CharacterGender, CharacterVoice, GrenadeState, Vector3} import net.psforever.types._
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.util.{Failure, Success, Try} import scala.util.{Failure, Success, Try}
@ -18,20 +18,7 @@ class CorpseConverter extends AvatarConverter {
DetailedPlayerData.apply( DetailedPlayerData.apply(
PlacementData(obj.Position, Vector3(0,0, obj.Orientation.z)), PlacementData(obj.Position, Vector3(0,0, obj.Orientation.z)),
MakeAppearanceData(obj), MakeAppearanceData(obj),
DetailedCharacterData( MakeDetailedCharacterData(obj),
bep = 0,
cep = 0,
healthMax = 0,
health = 0,
armor = 0,
staminaMax = 0,
stamina = 0,
certs = Nil,
implants = Nil,
firstTimeEvents = Nil,
tutorials = Nil,
cosmetics = None
),
InventoryData((MakeHolsters(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)), InventoryData((MakeHolsters(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)),
DrawnSlot.None DrawnSlot.None
) )
@ -44,24 +31,72 @@ class CorpseConverter extends AvatarConverter {
* @return the resulting `CharacterAppearanceData` * @return the resulting `CharacterAppearanceData`
*/ */
private def MakeAppearanceData(obj : Player) : (Int)=>CharacterAppearanceData = { private def MakeAppearanceData(obj : Player) : (Int)=>CharacterAppearanceData = {
CharacterAppearanceData( val aa : Int=>CharacterAppearanceA = CharacterAppearanceA(
BasicCharacterData(obj.Name, obj.Faction, CharacterGender.Male, 0, CharacterVoice.Mute), BasicCharacterData(obj.Name, obj.Faction, CharacterGender.Male, 0, CharacterVoice.Mute),
voice2 = 0,
black_ops = false, black_ops = false,
altModel = true,
false,
None,
jammered = false, jammered = false,
obj.ExoSuit, obj.ExoSuit,
None,
0,
0,
0L,
0,
0,
0,
0
)
val ab : (Boolean,Int)=>CharacterAppearanceB = CharacterAppearanceB(
0L,
outfit_name = "", outfit_name = "",
outfit_logo = 0, outfit_logo = 0,
false,
backpack = true, backpack = true,
facingPitch = obj.Orientation.y, //TODO is this important? false,
false,
false,
facingPitch = 0,
facingYawUpper = 0, facingYawUpper = 0,
lfs = true, lfs = false,
GrenadeState.None, GrenadeState.None,
is_cloaking = false, is_cloaking = false,
false,
false,
charging_pose = false, charging_pose = false,
on_zipline = false, false,
RibbonBars() 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 val container : GUIDContainable = GUIDContainer()
private var current : GUIDContainable = IdentifiableEntity.noGUIDContainer private var current : GUIDContainable = IdentifiableEntity.noGUIDContainer
def HasGUID : Boolean = { def HasGUID : Boolean = current ne IdentifiableEntity.noGUIDContainer
try {
GUID
true
}
catch {
case _ : NoGUIDException =>
false
}
}
def GUID : PlanetSideGUID = current.GUID def GUID : PlanetSideGUID = current.GUID

View file

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

View file

@ -7,6 +7,96 @@ import scodec.{Attempt, Codec, Err}
import scodec.codecs._ import scodec.codecs._
import shapeless.{::, HNil} 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> * A part of a representation of the avatar portion of `ObjectCreateDetailedMessage` packet data.<br>
* <br> * <br>
@ -28,64 +118,16 @@ import shapeless.{::, HNil}
* <br> * <br>
* Exploration:<br> * Exploration:<br>
* How do I crouch? * How do I crouch?
* @see `CharacterData`<br> * @see `CharacterData`
* `DetailedCharacterData`<br> * @see `DetailedCharacterData`
* `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
* @param ribbons the four merit commendation ribbon medals * @param ribbons the four merit commendation ribbon medals
*/ */
final case class CharacterAppearanceData(app : BasicCharacterData, final case class CharacterAppearanceData(a : CharacterAppearanceA,
voice2 : Int, b : CharacterAppearanceB,
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,
ribbons : RibbonBars) ribbons : RibbonBars)
(name_padding : Int) extends StreamBitSize { (name_padding : Int) extends StreamBitSize {
override def bitsize : Long = { override def bitsize : Long = 128L + a.bitsize + b.bitsize
//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
}
/** /**
* External access to the value padding on the name field. * 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] { 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. * 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. * In the former case, a backpack.
@ -110,13 +237,26 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
* @param app the appearance * @param app the appearance
* @return the length of the variable field that exists when using alternate models * @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) Some(1)
} }
else { else {
None 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. * Get the padding of the outfit's name.
* The padding will always be a number 0-7. * The padding will always be a number 0-7.
@ -126,72 +266,60 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
6 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) :: ("faction" | PlanetSideEmpire.codec) ::
("black_ops" | bool) :: ("black_ops" | bool) ::
(("alt_model" | bool) >>:~ { alt_model => //modifies stream format (to display alternate player models) (("alt_model" | bool) >>:~ { alt_model => //modifies stream format (to display alternate player models)
ignore(1) :: //unknown ("unk1" | bool) :: //serves a different internal purpose depending on the state of alt_model
("jammered" | bool) :: (conditional(false, "unk2" | extra_codec) >>:~ { extra => //TODO not sure what causes this branch
bool :: //crashes client ("jammered" | bool) ::
uint(16) :: //unknown, but usually 0 optional(bool, "unk3" | uint16L) ::
("name" | PacketHelpers.encodedWideStringAligned(name_padding)) :: ("unk4" | uint16L) ::
("exosuit" | ExoSuitType.codec) :: ("name" | PacketHelpers.encodedWideStringAligned(namePadding(name_padding, extra))) ::
ignore(2) :: //unknown ("exosuit" | ExoSuitType.codec) ::
("sex" | CharacterGender.codec) :: ("unk5" | uint2) :: //unknown
("head" | uint8L) :: ("sex" | CharacterGender.codec) ::
("voice" | CharacterVoice.codec) :: ("head" | uint8L) ::
("voice2" | uint2L) :: ("voice" | CharacterVoice.codec) ::
ignore(78) :: //unknown ("unk6" | uint32L) ::
uint16L :: //usually either 0 or 65535 ("unk7" | uint16L) ::
uint32L :: //for outfit_name (below) to be visible in-game, this value should be non-zero ("unk8" | uint16L) ::
("outfit_name" | PacketHelpers.encodedWideStringAligned( outfitNamePadding )) :: ("unk9" | uint16L) ::
("outfit_logo" | uint8L) :: ("unkA" | uint16L) //usually either 0 or 65535
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)
}) })
).exmap[CharacterAppearanceData] ( ).exmap[CharacterAppearanceA] (
{ {
case _ :: _ :: false :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: true :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: _ :: HNil | case faction :: bops :: alt :: u1 :: u2 :: jamd :: u3 :: u4 :: name :: suit :: u5 :: sex :: head :: v1 :: u6 :: u7 :: u8 :: u9 :: uA :: 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 =>
Attempt.successful( 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 _ => case _ =>
Attempt.Failure(Err("invalid character appearance data; can not encode")) 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")) 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) => case CharacterAppearanceA(BasicCharacterData(name, faction, sex, head, v1), bops, alt, u1, u2, jamd, suit, u3, u4, u5, u6, u7, u8, u9, uA) =>
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)
}
Attempt.successful( 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 _ => 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) 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. * Values for the implant effects on a character model.
* The effects can not be activated simultaneously. * The effects are not additive and this value is not a bitmask.<br>
* In at least one case, attempting to activate multiple effects will cause the PlanetSide client to crash.<br>
* <br> * <br>
* `RegenEffects` is a reverse-flagged item - inactive when the corresponding bit is set. * `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 { object ImplantEffects extends Enumeration {
type Type = Value type Type = Value
@ -44,19 +43,20 @@ object UniformStyle extends Enumeration {
/** /**
* A representation of a portion of an avatar's `ObjectCreateDetailedMessage` packet data.<br> * A representation of a portion of an avatar's `ObjectCreateDetailedMessage` packet data.<br>
* <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. * 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. * 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> * Of the inventory for this character, only the initial five weapon slots are defined.
* <br>
* In the "backend of the client," the character produced by this data is no different * 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. * 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, * In terms of equipment and complicated features common to an avatar character, however,
* any user would find this character ill-equipped. * 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; * @param health the amount of health the player has, as a percentage of a filled bar;
* the bar has 85 states, with 3 points for each state; * the bar has 85 states, with 3 points for each state;
* when 0% (less than 3 of 255), the player will collapse into a death pose on the ground; * when 0% (less than 3 of 255), the player will collapse into a death pose on the ground;
* while `is_corpse == true`, `health` will always report as 0; * while `is_backpack == true`, `health` will always report as 0;
* while `is_seated == true`, `health` will (try to) report as 100 * while `is_seated == true`, `health` will (try to) report as 100
* @param armor the amount of armor the player has, as a percentage of a filled bar; * @param armor the amount of armor the player has, as a percentage of a filled bar;
* the bar has 85 states, with 3 points for each state; * the bar has 85 states, with 3 points for each state;
@ -65,7 +65,8 @@ object UniformStyle extends Enumeration {
* @param command_rank the player's command rank as a number from 0 to 5; * @param command_rank the player's command rank as a number from 0 to 5;
* cosmetic armor associated with the command rank will be applied automatically * cosmetic armor associated with the command rank will be applied automatically
* @param implant_effects the effects of implants that can be seen on a player's character; * @param implant_effects the effects of implants that can be seen on a player's character;
* 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; * @param cosmetics optional decorative features that are added to the player's head model by console/chat commands;
* they become available at battle rank 24, but here they require the third uniform upgrade (rank 25); * they become available at battle rank 24, but here they require the third uniform upgrade (rank 25);
* these flags do not exist if they are not applicable * these flags do not exist if they are not applicable
@ -74,23 +75,20 @@ object UniformStyle extends Enumeration {
* the alternate model bit should be flipped * the alternate model bit should be flipped
* @param is_seated this player character is seated in a vehicle or mounted to some other object; * @param is_seated this player character is seated in a vehicle or mounted to some other object;
* alternate format for data parsing applies * alternate format for data parsing applies
* @see `DetailedCharacterData`<br>
* `CharacterAppearanceData`
*/ */
final case class CharacterData(health : Int, final case class CharacterData(health : Int,
armor : Int, armor : Int,
uniform_upgrade : UniformStyle.Value, uniform_upgrade : UniformStyle.Value,
unk : Int, unk : Int,
command_rank : Int, command_rank : Int,
implant_effects : Option[ImplantEffects.Value], implant_effects : List[ImplantEffects.Value],
cosmetics : Option[Cosmetics]) cosmetics : Option[Cosmetics])
(is_backpack : Boolean, (is_backpack : Boolean,
is_seated : Boolean) extends ConstructorData { is_seated : Boolean) extends ConstructorData {
override def bitsize : Long = { override def bitsize : Long = {
//factor guard bool values into the base size, not its corresponding optional field
val seatedSize = if(is_seated) { 0 } else { 16 } 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 } val cosmeticsSize : Long = if(cosmetics.isDefined) { cosmetics.get.bitsize } else { 0L }
11L + seatedSize + effectsSize + cosmeticsSize 11L + seatedSize + effectsSize + cosmeticsSize
} }
@ -98,7 +96,7 @@ final case class CharacterData(health : Int,
object CharacterData extends Marshallable[CharacterData] { 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 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 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 * @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 * @param cosmetics optional decorative features that are added to the player's head model by console/chat commands
* @return a `CharacterData` object * @return a `CharacterData` object
*/ */
def apply(health : Int, armor : Int, uniform : UniformStyle.Value, cr : Int, implant_effects : Option[ImplantEffects.Value], cosmetics : Option[Cosmetics]) : (Boolean,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) CharacterData(health, armor, uniform, 0, cr, implant_effects, cosmetics)
def codec(is_backpack : Boolean) : Codec[CharacterData] = ( def codec(is_backpack : Boolean) : Codec[CharacterData] = (
("health" | uint8L) :: //dead state when health == 0 ("health" | uint8L) :: //dead state when health == 0
("armor" | uint8L) :: ("armor" | uint8L) ::
(("uniform_upgrade" | UniformStyle.codec) >>:~ { style => (("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)) :: ("command_rank" | uintL(3)) ::
bool :: //misalignment when == 1 listOfN(uint2, "implant_effects" | ImplantEffects.codec) ::
optional(bool, "implant_effects" | ImplantEffects.codec) ::
conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec) conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec)
}) })
).exmap[CharacterData] ( ).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 } 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 _ => case _ =>
Attempt.Failure(Err("invalid character data; can not encode")) 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 } 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 _ => case _ =>
Attempt.Failure(Err("invalid character data; can not decode")) 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] = ( def codec_seated(is_backpack : Boolean) : Codec[CharacterData] = (
("uniform_upgrade" | UniformStyle.codec) >>:~ { style => ("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)) :: ("command_rank" | uintL(3)) ::
bool :: //stream misalignment when != 1 listOfN(uint2, "implant_effects" | ImplantEffects.codec) ::
optional(bool, "implant_effects" | ImplantEffects.codec) ::
conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec) conditional(style == UniformStyle.ThirdUpgrade, "cosmetics" | Cosmetics.codec)
} }
).exmap[CharacterData] ( ).exmap[CharacterData] (
{ {
case uniform :: _ :: cr :: false :: implant_effects :: cosmetics :: HNil => case uniform :: unk :: cr :: implant_effects :: cosmetics :: HNil =>
Attempt.Successful(new CharacterData(100, 0, uniform, 0, cr, implant_effects, cosmetics)(is_backpack, true)) Attempt.Successful(new CharacterData(100, 0, uniform, unk, cr, implant_effects, cosmetics)(is_backpack, true))
case _ => case _ =>
Attempt.Failure(Err("invalid character data; can not encode")) Attempt.Failure(Err("invalid character data; can not encode"))
}, },
{ {
case obj @ CharacterData(_, _, uniform, _, cr, implant_effects, cosmetics) => case CharacterData(_, _, uniform, unk, cr, implant_effects, cosmetics) =>
Attempt.Successful(uniform :: () :: cr :: false :: implant_effects :: cosmetics :: HNil) Attempt.Successful(uniform :: unk :: cr :: implant_effects :: cosmetics :: HNil)
case _ => case _ =>
Attempt.Failure(Err("invalid character data; can not decode")) 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`. * 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 implant the type of implant
* @param activation the activation timer; * @param initialization the amount of time necessary until this implant is ready to be activated;
* technically, this is "unconfirmed" * technically, this is unconfirmed
* @param active whether this implant is turned on;
* technically, this is unconfirmed
* @see `ImplantType` * @see `ImplantType`
*/ */
final case class ImplantEntry(implant : ImplantType.Value, final case class ImplantEntry(implant : ImplantType.Value,
activation : Option[Int]) extends StreamBitSize { initialization : Option[Int],
active : Boolean) extends StreamBitSize {
override def bitsize : Long = { override def bitsize : Long = {
val activationSize = if(activation.isDefined) { 12L } else { 5L } val timerSize = initialization match { case Some(_) => 8L ; case None => 1L }
5L + activationSize 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> * na
* <br> * @param unk1 na
* This densely-packed information outlines most of the specifics required to depict a character as an avatar. * @param unk2 na
* 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. final case class DCDExtra1(unk1 : String,
* For example, health is a full number, rather than a percentage. unk2 : Int) extends StreamBitSize {
* Just as prominent is the list of first time events and the list of completed tutorials. override def bitsize : Long = 16L + StreamBitSize.stringBitSize(unk1)
* Additionally, a full inventory, as opposed to the initial five weapon slots. }
/**
* 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 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 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 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; * @param armor for `x / y` of armor points, this is the avatar's `x` value;
* range is 0-65535; * range is 0-65535;
* the avatar's `y` armor points is tied to their exo-suit type * 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; * @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value;
* range is 0-65535 * range is 0-65535
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value; * @param stamina for `x / y` of stamina points, this is the avatar's `x` value;
* range is 0-65535 * 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 implants the `List` of implant slots currently possessed by this avatar
* @param firstTimeEvents the list of first time events performed by this avatar; * @param firstTimeEvents the list of first time events performed by this avatar;
* the size field is a 32-bit number; * 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; * @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; * they become available at battle rank 24;
* these flags do not exist if they are not applicable * these flags do not exist if they are not applicable
* @see `CharacterData`<br>
* `CertificationType`
*/ */
final case class DetailedCharacterData(bep : Long, final case class DetailedCharacterB(unk1 : Option[Long],
cep : Long, implants : List[ImplantEntry],
healthMax : Int, unk2 : List[DCDExtra1],
health : Int, unk3 : List[DCDExtra1],
armor : Int, firstTimeEvents : List[String],
unk1 : Int, //1 tutorials : List[String],
unk2 : Int, //7 unk4 : Long,
unk3 : Int, //7 unk5 : Long,
staminaMax : Int, unk6 : Long,
stamina : Int, unk7 : Long,
certs : List[CertificationType.Value], unk8 : Long,
implants : List[ImplantEntry], unk9 : Option[DCDExtra2],
firstTimeEvents : List[String], unkA : List[Long],
tutorials : List[String], unkB : List[String],
cosmetics : Option[Cosmetics]) unkC : Boolean,
(pad_length : Option[Int]) extends ConstructorData { cosmetics : Option[Cosmetics])
(bep : Long,
pad_length : Option[Int]) extends StreamBitSize {
override def bitsize : Long = { override def bitsize : Long = {
//factor guard bool values into the base size, not corresponding optional fields, unless contained or enumerated //unk1
val certSize = (certs.length + 1) * 8 //cert list val unk1Size = unk1 match { case Some(_) => 32L ; case None => 0L }
var implantSize : Long = 0L //implant list //implant list
for(entry <- implants) { val implantSize : Long = implants.foldLeft(0L)(_ + _.bitsize)
implantSize += entry.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) else if(unk3Len > 0) {
val fteLen = firstTimeEvents.size //fte list unkAllLen * unk3.head.bitsize
var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen, implantPadding)
for(str <- firstTimeEvents) {
eventListSize += StreamBitSize.stringBitSize(str)
} }
val tutLen = tutorials.size //tutorial list else {
var tutorialListSize : Long = 32L + DetailedCharacterData.tutPadding(fteLen, tutLen, implantPadding) 0L
for(str <- tutorials) {
tutorialListSize += StreamBitSize.stringBitSize(str)
} }
val br24 = DetailedCharacterData.isBR24(bep) //character is at least BR24 //character is at least BR24
val extraBitSize : Long = if(br24) { 33L } else { 46L } 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 } 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] { object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
/** def apply(bep : Long,
* Overloaded constructor for `DetailedCharacterData` that requires an inventory and drops unknown values. cep : Long,
* @param bep the avatar's battle experience points, which determines his Battle Rank healthMax : Int,
* @param cep the avatar's command experience points, which determines his Command Rank health : Int,
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value armor : Int,
* @param health for `x / y` of hitpoints, this is the avatar's `x` value staminaMax : Int,
* @param armor for `x / y` of armor points, this is the avatar's `x` value stamina : Int,
* @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value certs : List[CertificationType.Value],
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value implants : List[ImplantEntry],
* @param certs the `List` of active certifications firstTimeEvents : List[String],
* @param implants the `List` of implant slots currently possessed by this avatar tutorials : List[String],
* @param firstTimeEvents the list of first time events performed by this avatar cosmetics : Option[Cosmetics]) : Option[Int]=>DetailedCharacterData = {
* @param tutorials the list of tutorials completed by this avatar val a = DetailedCharacterA(
* @return a `DetailedCharacterData` object bep,
*/ cep,
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 = 0L,
DetailedCharacterData(bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, cosmetics) 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. * `Codec` for entries in the `List` of implants.
*/ */
private val implant_entry_codec : Codec[ImplantEntry] = ( private val implant_entry_codec : Codec[ImplantEntry] = (
("implant" | ImplantType.codec) :: ("implant" | uint8L) ::
(bool >>:~ { guard => (bool >>:~ { guard =>
newcodecs.binary_choice(guard, uintL(5), uintL(12)).hlist newcodecs.binary_choice(guard, uint(1), uint8L).hlist
}) })
).xmap[ImplantEntry] ( ).xmap[ImplantEntry] (
{ {
case implant :: true :: _ :: HNil => case implant :: true :: n :: HNil => //initialized (no timer), active/inactive?
ImplantEntry(implant, None) val activeBool : Boolean = n != 0
ImplantEntry(ImplantType(implant), None, activeBool) //TODO catch potential NoSuchElementException?
case implant :: false :: extra :: HNil => case implant :: false :: extra :: HNil => //uninitialized (timer), inactive
ImplantEntry(implant, Some(extra)) ImplantEntry(ImplantType(implant), Some(extra), false) //TODO catch potential NoSuchElementException?
}, },
{ {
case ImplantEntry(implant, None) => case ImplantEntry(implant, None, n) => //initialized (no timer), active/inactive?
implant :: true :: 0 :: HNil val activeInt : Int = if(n) { 1 } else { 0 }
implant.id :: true :: activeInt :: HNil
case ImplantEntry(implant, Some(extra)) => case ImplantEntry(implant, Some(extra), _) => //uninitialized (timer), inactive
implant :: false :: extra :: HNil 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. * `Codec` for a `List` of `DCDExtra1` objects.
* @param implants implant entries * The first entry contains a padded `String` so it must be processed different from the remainder.
* @return the pad length in bits `0 <= n < 8`
*/ */
private def implantFieldPadding(implants : List[ImplantEntry], varBit : Option[Int] = None) : Int = { private def dcd_list_codec(padFunc : (Long)=>Int) : Codec[List[DCDExtra1]] = (
val base : Int = 5 //the offset with no implant entries uint8 >>:~ { size =>
val baseOffset : Int = base - varBit.getOrElse(0) conditional(size > 0, dcd_extra1_codec(padFunc(size))) ::
val resultA = if(baseOffset < 0) { 8 - baseOffset } else { baseOffset % 8 } 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 => * `Codec` for entries in the `List` of `DCDExtra1` objects.
implantOffset += entry.bitsize.toInt * The first entry's size of 80 characters is hard-set by the client.
}) */
val resultB : Int = resultA - (implantOffset % 8) private def dcd_extra1_codec(pad : Int) : Codec[DCDExtra1] = (
if(resultB < 0) { 8 + resultB } else { resultB } ("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. * By comparing the battle experience points to a fixed number of points,
* @param len the length of the first time events list * determine if the player is at least battle rank 24.
* @param implantPadding the padding that resulted from implant entries * Important things happen if the player is at least battle rank 24 ...
* @return the pad length in bits `0 <= n < 8` * @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 isBR24(bep : Long) : Boolean = bep > 2286230
def codec(pad_length : Option[Int]) : Codec[DetailedCharacterData] = ( val a_codec : Codec[DetailedCharacterA] = (
("bep" | uint32L) >>:~ { bep => ("bep" | uint32L) ::
("cep" | uint32L) :: ("cep" | uint32L) ::
ignore(96) :: ("unk1" | uint32L) ::
("healthMax" | uint16L) :: ("unk2" | uint32L) ::
("health" | uint16L) :: ("unk3" | uint32L) ::
ignore(1) :: ("healthMax" | uint16L) ::
("armor" | uint16L) :: ("health" | uint16L) ::
ignore(9) :: ("unk4" | bool) ::
("unk1" | uint8L) :: ("armor" | uint16L) ::
ignore(8) :: ("unk5" | uint32) :: //endianness?
("unk2" | uint4L) :: ("staminaMax" | uint16L) ::
("unk3" | uintL(3)) :: ("stamina" | uint16L) ::
("staminaMax" | uint16L) :: conditional(false, uint32L) :: //see ps.c: sub_901150, line#1070692
("stamina" | uint16L) :: ("unk6" | uint16L) ::
ignore(147) :: ("unk7" | uint(3)) ::
("certs" | listOfN(uint8L, CertificationType.codec)) :: ("unk8" | uint32L) ::
optional(bool, uint32L) :: //ask about sample CCRIDER ("unk9" | PacketHelpers.listOfNSized(6, uint16L)) :: //always length of 6
ignore(4) :: ("certs" | listOfN(uint8L, CertificationType.codec))
(("implants" | PacketHelpers.listOfNSized(numberOfImplantSlots(bep), implant_entry_codec)) >>:~ { implants => ).exmap[DetailedCharacterA] (
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] (
{ {
case bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: _ :: _ :: cosmetics :: HNil => case bep :: cep :: u1 :: u2 :: u3 :: healthMax :: health :: u4 :: armor :: u5 :: staminaMax :: stamina :: None :: u6 :: u7 :: u8 :: u9 :: certs :: HNil =>
//prepend the displaced first elements to their lists Attempt.successful(DetailedCharacterA(bep, cep, u1, u2, u3, healthMax, health, u4, armor, u5, staminaMax, stamina, u6, u7, u8, u9, certs))
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 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 implantCapacity : Int = numberOfImplantSlots(bep)
val implantList = if(implants.length > implantCapacity) { val implantList = if(implants.length > implantCapacity) {
implants.slice(0, implantCapacity) implants.slice(0, implantCapacity)
@ -305,20 +606,26 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
else { else {
recursiveEnsureImplantSlots(implantCapacity, implants) 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 br24 : Boolean = isBR24(bep)
val cosmetics : Option[Cosmetics] = if(br24) { cos } else { None } val cos : Option[Cosmetics] = if(br24) { cosmetics } 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) 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 * The presence or absence of position data as the first division creates a cascading effect
* causing all of fields in the other two divisions to gain offsets. * causing all of fields in the other two divisions to gain offsets.
* These offsets exist in the form of `String` and `List` padding. * These offsets exist in the form of `String` and `List` padding.
* @see `DetailedCharacterData`<br> * @see `CharacterAppearanceData`
* `InventoryData`<br> * @see `DetailedCharacterData`
* `DrawnSlot` * @see `InventoryData`
* @see `DrawnSlot`
* @param pos the optional position of the character in the world environment * @param pos the optional position of the character in the world environment
* @param basic_appearance common fields regarding the the character's appearance * @param basic_appearance common fields regarding the the character's appearance
* @param character_data the class-specific data that explains about the character * @param character_data the class-specific data that discusses the character
* @param position_defined used by the `Codec` to seed the state of the optional `pos` field * @param position_defined used to seed the state of the optional position fields
* @param inventory the player's full inventory * @param inventory the player's full or partial (holsters-only) inventory
* @param drawn_slot the holster that is initially drawn * @param drawn_slot the holster that is depicted as exposed, or "drawn"
*/ */
final case class DetailedPlayerData(pos : Option[PlacementData], final case class DetailedPlayerData(pos : Option[PlacementData],
basic_appearance : CharacterAppearanceData, basic_appearance : CharacterAppearanceData,
@ -54,8 +55,8 @@ object DetailedPlayerData extends Marshallable[DetailedPlayerData] {
* This constructor should be used for players that are mounted. * This constructor should be used for players that are mounted.
* @param basic_appearance a curried function for the common fields regarding the the character's appearance * @param basic_appearance a curried function for the common fields regarding the the character's appearance
* @param character_data a curried function for the class-specific data that explains about the character * @param character_data a curried function for the class-specific data that explains about the character
* @param inventory the player's inventory * @param inventory the player's full or partial (holsters-only) inventory
* @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 * technically, always `DrawnSlot.None`, but the field is preserved to maintain similarity
* @return a `DetailedPlayerData` object * @return a `DetailedPlayerData` object
*/ */
@ -70,7 +71,7 @@ object DetailedPlayerData extends Marshallable[DetailedPlayerData] {
* This constructor should be used for players that are mounted. * This constructor should be used for players that are mounted.
* @param basic_appearance a curried function for the common fields regarding the the character's appearance * @param basic_appearance a curried function for the common fields regarding the the character's appearance
* @param character_data a curried function for the class-specific data that explains about the character * @param character_data a curried function for the class-specific data that explains about the character
* @param 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 * technically, always `DrawnSlot.None`, but the field is preserved to maintain similarity
* @return a `DetailedPlayerData` object * @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 pos the optional position of the character in the world environment
* @param basic_appearance a curried function for the common fields regarding the the character's appearance * @param basic_appearance a curried function for the common fields regarding the the character's appearance
* @param character_data a curried function for the class-specific data that explains about the character * @param character_data a curried function for the class-specific data that explains about the character
* @param inventory the player's inventory * @param inventory the player's full or partial (holsters-only) inventory
* @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 * @return a `DetailedPlayerData` object
*/ */
def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Option[Int])=>DetailedCharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedPlayerData = { 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 pos the optional position of the character in the world environment
* @param basic_appearance a curried function for the common fields regarding the the character's appearance * @param basic_appearance a curried function for the common fields regarding the the character's appearance
* @param character_data a curried function for the class-specific data that explains about the character * @param character_data a curried function for the class-specific data that explains about the character
* @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 * @return a `DetailedPlayerData` object
*/ */
def apply(pos : PlacementData, basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Option[Int])=>DetailedCharacterData, drawn_slot : DrawnSlot.Value) : DetailedPlayerData = { 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 = { def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, inventory : InventoryData, drawn_slot : DrawnSlot.Type) : PlayerData = {
val appearance = basic_appearance(5) 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. * 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 = { def apply(basic_appearance : (Int)=>CharacterAppearanceData, character_data : (Boolean,Boolean)=>CharacterData, drawn_slot : DrawnSlot.Type) : PlayerData = {
val appearance = basic_appearance(5) 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 = { 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)) ) 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. * 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 = { 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)) ) 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 => conditional(position_defined, "pos" | PlacementData.codec) >>:~ { pos =>
("basic_appearance" | CharacterAppearanceData.codec(PaddingOffset(pos))) >>:~ { app => ("basic_appearance" | CharacterAppearanceData.codec(PaddingOffset(pos))) >>:~ { app =>
("character_data" | newcodecs.binary_choice(position_defined, ("character_data" | newcodecs.binary_choice(position_defined,
CharacterData.codec(app.backpack), CharacterData.codec(app.b.backpack),
CharacterData.codec_seated(app.backpack))) :: CharacterData.codec_seated(app.b.backpack))) ::
optional(bool, "inventory" | InventoryData.codec) :: optional(bool, "inventory" | InventoryData.codec) ::
("drawn_slot" | DrawnSlot.codec) :: ("drawn_slot" | DrawnSlot.codec) ::
bool //usually false bool //usually false
@ -193,7 +193,7 @@ object PlayerData extends Marshallable[PlayerData] {
*/ */
def codec(offset : Int) : Codec[PlayerData] = ( def codec(offset : Int) : Codec[PlayerData] = (
("basic_appearance" | CharacterAppearanceData.codec(offset)) >>:~ { app => ("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) :: optional(bool, "inventory" | InventoryData.codec) ::
("drawn_slot" | DrawnSlot.codec) :: ("drawn_slot" | DrawnSlot.codec) ::
bool //usually false 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 = { 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)) 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 * 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 = { 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)) 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) private val driveState8u = PacketHelpers.createEnumerationCodec(DriveState, uint8L)

View file

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

View file

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