more accomodations for bep field decoding/encoding

This commit is contained in:
FateJH 2017-09-05 19:22:48 -04:00
parent 2b70b98e35
commit 4c5e67ca89
6 changed files with 261 additions and 334 deletions

View file

@ -3,7 +3,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.{BasicCharacterData, BattleRankFieldData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle} import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle}
import net.psforever.types.GrenadeState import net.psforever.types.GrenadeState
import scala.annotation.tailrec import scala.annotation.tailrec
@ -31,15 +31,16 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
Success( Success(
DetailedCharacterData( DetailedCharacterData(
MakeAppearanceData(obj), MakeAppearanceData(obj),
0, 0L,
0L,
obj.MaxHealth, obj.MaxHealth,
obj.Health, obj.Health,
obj.Armor, obj.Armor,
1, 7, 7, 1, 7, 7,
obj.MaxStamina, obj.MaxStamina,
obj.Stamina, obj.Stamina,
28, 4, List(0, 1, 11, 21, 26, 27, 28), //TODO certification list
BattleRankFieldData(44, 84, 104, 108, 112, 0, 0), List(), //TODO implant list
List.empty[String], //TODO fte list List.empty[String], //TODO fte list
List.empty[String], //TODO tutorial list List.empty[String], //TODO tutorial list
InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)), InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)),

View file

@ -117,12 +117,27 @@ final case class CharacterAppearanceData(pos : PlacementData,
val placementSize : Long = pos.bitsize val placementSize : Long = pos.bitsize
val nameStringSize : Long = StreamBitSize.stringBitSize(basic_appearance.name, 16) + CharacterAppearanceData.namePadding(pos.vel) val nameStringSize : Long = StreamBitSize.stringBitSize(basic_appearance.name, 16) + CharacterAppearanceData.namePadding(pos.vel)
val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) + CharacterAppearanceData.outfitNamePadding val outfitStringSize : Long = StreamBitSize.stringBitSize(outfit_name, 16) + CharacterAppearanceData.outfitNamePadding
val altModelSize = if(on_zipline || backpack) { 1L } else { 0L } val altModelSize = CharacterAppearanceData.altModelBit(this).getOrElse(0)
335L + placementSize + nameStringSize + outfitStringSize + altModelSize 335L + placementSize + nameStringSize + outfitStringSize + altModelSize
} }
} }
object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] { object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
/**
* When a player is released-dead or attached to a zipline, their basic infantry model is replaced with a different one.
* In the former casde, a backpack.
* In the latter case, a ball of colored energy.
* In this state, the length of the stream of data is modified.
* @param app the appearance
* @return the length of the variable field that exists when using alternate models
*/
def altModelBit(app : CharacterAppearanceData) : Option[Int] = if(app.backpack || app.on_zipline) {
Some(1)
}
else {
None
}
/** /**
* Get the padding of the player's name. * Get the padding of the player's name.
* The padding will always be a number 0-7. * The padding will always be a number 0-7.
@ -151,7 +166,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
("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 ignore(1) :: //unknown
("jammered" | bool) :: ("jammered" | bool) ::
bool :: //crashes client bool :: //crashes client
uint(16) :: //unknown, but usually 0 uint(16) :: //unknown, but usually 0
@ -204,7 +219,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
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(pos, BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons) => case CharacterAppearanceData(pos, BasicCharacterData(name, faction, sex, head, v1), v2, bops, jamd, suit, outfit, logo, bpack, facingPitch, facingYawUpper, lfs, gstate, cloaking, charging, zipline, ribbons) =>
val has_outfit_name : Long = outfit.length.toLong //todo this is a kludge val has_outfit_name : Long = outfit.length.toLong //TODO this is a kludge
var alt_model : Boolean = false var alt_model : Boolean = false
var alt_model_extrabit : Option[Boolean] = None var alt_model_extrabit : Option[Boolean] = None
if(zipline || bpack) { if(zipline || bpack) {

View file

@ -1,42 +1,27 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate package net.psforever.packet.game.objectcreate
import net.psforever.newcodecs.newcodecs
import net.psforever.packet.{Marshallable, PacketHelpers} import net.psforever.packet.{Marshallable, PacketHelpers}
import scodec.{Attempt, Codec, Err} import net.psforever.types.ImplantType
import scodec.{Attempt, Codec}
import scodec.codecs._ import scodec.codecs._
import shapeless.{::, HNil} import shapeless.{::, HNil}
final case class BattleRankFieldData(field00 : Int, import scala.annotation.tailrec
field01 : Int,
field02 : Int, /**
field03 : Int, * An entry in the `List` of valid implant slots in `DetailedCharacterData`.
field04 : Int, * "`activation`" is not necessarily the best word for it ...
field05 : Int, * @param implant the type of implant
field06 : Int, * @param activation na
field07 : Option[Int] = None, * @see `ImplantType`
field08 : Option[Int] = None, */
field09 : Option[Int] = None, final case class ImplantEntry(implant : ImplantType.Value,
field0A : Option[Int] = None, activation : Option[Int]) extends StreamBitSize {
field0B : Option[Int] = None,
field0C : Option[Int] = None,
field0D : Option[Int] = None,
field0E : Option[Int] = None,
field0F : Option[Int] = None,
field10 : Option[Int] = None) extends StreamBitSize {
override def bitsize : Long = { override def bitsize : Long = {
val extraFieldSize : Long = if(field10.isDefined) { val activationSize = if(activation.isDefined) { 12L } else { 5L }
70L 5L + activationSize
}
else if(field0E.isDefined) {
50L
}
else if(field09.isDefined) {
10L
}
else {
0L
}
55L + extraFieldSize
} }
} }
@ -49,16 +34,16 @@ final case class BattleRankFieldData(field00 : Int,
* <br> * <br>
* Divisions exist to make the data more manageable. * Divisions exist to make the data more manageable.
* The first division of data only manages the general appearance of the player's in-game model. * The first division of data only manages the general appearance of the player's in-game model.
* The second division (currently, the fields actually in this class) manages the status of the character as an avatar. * It is shared between `DetailedCharacterData` avatars and `CharacterData` player characters.
* The second division (of fields) manages the status of the character as an avatar.
* In general, it passes more thorough data about the character that the client can display to the owner of the client. * In general, 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. * For example, health is a full number, rather than a percentage.
* Just as prominent is the list of first time events and the list of completed tutorials. * Just as prominent is the list of first time events and the list of completed tutorials.
* The third subdivision is also exclusive to avatar-prepared characters and contains (omitted). * The third subdivision is also exclusive to avatar-prepared characters and contains (omitted).
* The fourth is the inventory (composed of `Direct`-type objects).<br> * The fourth is the inventory (composed of `Direct`-type objects).
* <br>
* Exploration:<br>
* Lots of analysis needed for the remainder of the byte data.
* @param appearance data about the avatar's basic aesthetics * @param appearance data about the avatar's basic aesthetics
* @param bep the avatar's battle experience points, which determines his Battle Rank
* @param cep the avatar's command experience points, which determines his Command Rank
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value; * @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value;
* range is 0-65535 * range is 0-65535
* @param health for `x / y` of hitpoints, this is the avatar's `x` value; * @param health for `x / y` of hitpoints, this is the avatar's `x` value;
@ -76,15 +61,12 @@ final case class BattleRankFieldData(field00 : Int,
* 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 unk4 na; * @param certs the `List` of active certifications
* defaults to 28 * @param implants the `List` of implant slots currently possessed by this avatar
* @param unk5 na;
* defaults to 4
* @param brFields na
* @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;
* the first entry may be padded * the first entry may be padded
* @param tutorials the list of tutorials completed by this avatar; * @param tutorials the `List` of tutorials completed by this avatar;
* the size field is a 32-bit number; * the size field is a 32-bit number;
* the first entry may be padded * the first entry may be padded
* @param inventory the avatar's inventory * @param inventory the avatar's inventory
@ -95,7 +77,8 @@ final case class BattleRankFieldData(field00 : Int,
* @see `DrawnSlot` * @see `DrawnSlot`
*/ */
final case class DetailedCharacterData(appearance : CharacterAppearanceData, final case class DetailedCharacterData(appearance : CharacterAppearanceData,
bep : Int, bep : Long,
cep : Long,
healthMax : Int, healthMax : Int,
health : Int, health : Int,
armor : Int, armor : Int,
@ -104,9 +87,8 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData,
unk3 : Int, //7 unk3 : Int, //7
staminaMax : Int, staminaMax : Int,
stamina : Int, stamina : Int,
unk4 : Int, //28 certs : List[Int],
unk5 : Int, //4 implants : List[ImplantEntry],
brFields : BattleRankFieldData,
firstTimeEvents : List[String], firstTimeEvents : List[String],
tutorials : List[String], tutorials : List[String],
inventory : Option[InventoryData], inventory : Option[InventoryData],
@ -114,46 +96,36 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData,
) extends ConstructorData { ) extends ConstructorData {
override def bitsize : Long = { override def bitsize : Long = {
//factor guard bool values into the base size, not its corresponding optional field //factor guard bool values into the base size, not corresponding optional fields, unless contained or enumerated
val appearanceSize = appearance.bitsize val appearanceSize = appearance.bitsize
val brFieldSize = brFields.bitsize val varBit : Option[Int] = CharacterAppearanceData.altModelBit(appearance)
val certSize = (certs.length + 1) * 8 //cert list
var implantSize : Long = 0L //implant list
for(entry <- implants) {
implantSize += entry.bitsize
}
val implantPadding = DetailedCharacterData.implantFieldPadding(implants, varBit)
val fteLen = firstTimeEvents.size //fte list val fteLen = firstTimeEvents.size //fte list
var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen, bep) var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen, implantPadding)
for(str <- firstTimeEvents) { for(str <- firstTimeEvents) {
eventListSize += StreamBitSize.stringBitSize(str) eventListSize += StreamBitSize.stringBitSize(str)
} }
val tutLen = tutorials.size //tutorial list val tutLen = tutorials.size //tutorial list
var tutorialListSize : Long = 32L + DetailedCharacterData.tutPadding(fteLen, tutLen, bep) var tutorialListSize : Long = 32L + DetailedCharacterData.tutPadding(fteLen, tutLen, implantPadding)
for(str <- tutorials) { for(str <- tutorials) {
tutorialListSize += StreamBitSize.stringBitSize(str) tutorialListSize += StreamBitSize.stringBitSize(str)
} }
var inventorySize : Long = 0L //inventory val inventorySize : Long = if(inventory.isDefined) { //inventory
if(inventory.isDefined) { inventory.get.bitsize
inventorySize = inventory.get.bitsize
} }
658L + appearanceSize + brFieldSize + eventListSize + tutorialListSize + inventorySize else {
0L
}
649L + appearanceSize + certSize + implantSize + eventListSize + tutorialListSize + inventorySize
} }
} }
object DetailedCharacterData extends Marshallable[DetailedCharacterData] { object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
// /**
// * Overloaded constructor for `DetailedCharacterData` that skips all the unknowns by assigning defaulted values.
// * It also allows for a not-optional inventory.
// * @param appearance data about the avatar's basic aesthetics
// * @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value
// * @param health for `x / y` of hitpoints, this is the avatar's `x` value
// * @param armor for `x / y` of armor points, this is the avatar's `x` value
// * @param staminaMax for `x / y` of stamina points, this is the avatar's `y` value
// * @param stamina for `x / y` of stamina points, this is the avatar's `x` value
// * @param firstTimeEvents the list of first time events performed by this avatar
// * @param tutorials the list of tutorials completed by this avatar
// * @param inventory the avatar's inventory
// * @param drawn_slot the holster that is initially drawn
// * @return a `DetailedCharacterData` object
// */
// def apply(appearance : CharacterAppearanceData, bep : Int, healthMax : Int, health : Int, armor : Int, staminaMax : Int, stamina : Int, firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
// new DetailedCharacterData(appearance, bep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, 28, 4, 44, 84, 104, 1900, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
/** /**
* Overloaded constructor for `DetailedCharacterData` that allows for a not-optional inventory. * Overloaded constructor for `DetailedCharacterData` that allows for a not-optional inventory.
* @param appearance data about the avatar's basic aesthetics * @param appearance data about the avatar's basic aesthetics
@ -165,171 +137,108 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
* @param unk3 na * @param unk3 na
* @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
* @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
* @param unk4 na * @param certs na
* @param unk5 na
* @param unk6 na
* @param firstTimeEvents the list of first time events performed by this avatar * @param firstTimeEvents the list of first time events performed by this avatar
* @param tutorials the list of tutorials completed by this avatar * @param tutorials the list of tutorials completed by this avatar
* @param inventory the avatar's inventory * @param inventory the avatar's inventory
* @param drawn_slot the holster that is initially drawn * @param drawn_slot the holster that is initially drawn
* @return a `DetailedCharacterData` object * @return a `DetailedCharacterData` object
*/ */
def apply(appearance : CharacterAppearanceData, bep : Int, healthMax : Int, health : Int, armor : Int, unk1 : Int, unk2 : Int, unk3 : Int, staminaMax : Int, stamina : Int, unk4 : Int, unk5 : Int, unk6 : BattleRankFieldData, firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData = def apply(appearance : CharacterAppearanceData, bep : Long, cep : Long, healthMax : Int, health : Int, armor : Int, unk1 : Int, unk2 : Int, unk3 : Int, staminaMax : Int, stamina : Int, certs : List[Int], implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData =
new DetailedCharacterData(appearance, bep, healthMax, health, armor, unk1, unk2, unk3, staminaMax, stamina, unk4, unk5, unk6, firstTimeEvents, tutorials, Some(inventory), drawn_slot) new DetailedCharacterData(appearance, bep, cep, healthMax, health, armor, unk1, unk2, unk3, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
private val br1FieldCodec : Codec[BattleRankFieldData] = ( // +0u /**
("f1" | uint8L) :: * `Codec` for entires in the list of implants.
("f2" | uint8L) :: */
("f3" | uint8L) :: private val implant_entry_codec : Codec[ImplantEntry] = (
("f4" | uint8L) :: ("implant" | ImplantType.codec) ::
("f5" | uint8L) :: (bool >>:~ { guard =>
("f6" | uint8L) :: newcodecs.binary_choice(guard, uintL(5), uintL(12)).hlist
("f7" | uintL(7)) })
).exmap[BattleRankFieldData] ( ).xmap[ImplantEntry] (
{ {
case f1 :: f2 :: f3 :: f4 :: f5 :: f6 :: f7 :: HNil => case implant :: true :: _ :: HNil =>
Attempt.successful(BattleRankFieldData(f1, f2, f3, f4, f5, f6, f7)) ImplantEntry(implant, None)
case implant :: false :: extra :: HNil =>
ImplantEntry(implant, Some(extra))
}, },
{ {
case BattleRankFieldData(f1, f2, f3, f4, f5, f6, f7, _, _, _, _, _, _, _, _, _, _) => case ImplantEntry(implant, None) =>
Attempt.successful(f1 :: f2 :: f3 :: f4 :: f5 :: f6 :: f7 :: HNil) implant :: true :: 0 :: HNil
}
)
private val br6FieldCodec : Codec[BattleRankFieldData] = ( //+10u case ImplantEntry(implant, Some(extra)) =>
("f1" | uint8L) :: implant :: false :: extra :: HNil
("f2" | uint8L) ::
("f3" | uint8L) ::
("f4" | uint8L) ::
("f5" | uint8L) ::
("f6" | uint8L) ::
("f7" | uint8L) ::
("f8" | uint8L) ::
("f9" | bool)
).exmap[BattleRankFieldData] (
{
case f1 :: f2 :: f3 :: f4 :: f5 :: f6 :: f7 :: f8 :: f9 :: HNil =>
val f9Int : Int = if(f9) { 1 } else { 0 }
Attempt.successful(BattleRankFieldData(f1, f2, f3, f4, f5, f6, f7, Some(f8), Some(f9Int)))
},
{
case BattleRankFieldData(f1, f2, f3, f4, f5, f6, f7, Some(f8), Some(f9), _, _, _, _, _, _, _, _) =>
val f9Bool : Boolean = if(f9 == 0) { false } else { true }
Attempt.successful(f1 :: f2 :: f3 :: f4 :: f5 :: f6 :: f7 :: f8 :: f9Bool :: HNil)
case _ =>
Attempt.failure(Err("expected battle rank 6 field data"))
}
)
private val br12FieldCodec : Codec[BattleRankFieldData] = ( //+52u
("f1" | uint8L) ::
("f2" | uint8L) ::
("f3" | uint8L) ::
("f4" | uint8L) ::
("f5" | uint8L) ::
("f6" | uint8L) ::
("f7" | uint8L) ::
("f8" | uint8L) ::
("f9" | uint8L) ::
("fA" | uint8L) ::
("fB" | uint8L) ::
("fC" | uint8L) ::
("fD" | uint8L) ::
("fE" | uintL(3))
).exmap[BattleRankFieldData] (
{
case f1 :: f2 :: f3 :: f4 :: f5 :: f6 :: f7 :: f8 :: f9 :: fa :: fb :: fc :: fd :: fe :: HNil =>
Attempt.successful(BattleRankFieldData(f1, f2, f3, f4, f5, f6, f7, Some(f8), Some(f9), Some(fa), Some(fb), Some(fc), Some(fd), Some(fe)))
},
{
case BattleRankFieldData(f1, f2, f3, f4, f5, f6, f7, Some(f8), Some(f9), Some(fa), Some(fb), Some(fc), Some(fd), Some(fe), _, _, _) =>
Attempt.successful(f1 :: f2 :: f3 :: f4 :: f5 :: f6 :: f7 :: f8 :: f9 :: fa :: fb :: fc :: fd :: fe :: HNil)
case _ =>
Attempt.failure(Err("expected battle rank 12 field data"))
}
)
private val br18FieldCodec : Codec[BattleRankFieldData] = ( //+70u
("f01" | uint8L) ::
("f02" | uint8L) ::
("f03" | uint8L) ::
("f04" | uint8L) ::
("f05" | uint8L) ::
("f06" | uint8L) ::
("f07" | uint8L) ::
("f08" | uint8L) ::
("f09" | uint8L) ::
("f0A" | uint8L) ::
("f0B" | uint8L) ::
("f0C" | uint8L) ::
("f0D" | uint8L) ::
("f0E" | uint8L) ::
("f0F" | uint8L) ::
("f10" | uintL(5))
).exmap[BattleRankFieldData] (
{
case f01 :: f02 :: f03 :: f04 :: f05 :: f06 :: f07 :: f08 :: f09 :: f0a :: f0b :: f0c :: f0d :: f0e :: f0f :: f10 :: HNil =>
Attempt.successful(BattleRankFieldData(f01, f02, f03, f04, f05, f06, f07, Some(f08), Some(f09), Some(f0a), Some(f0b), Some(f0c), Some(f0d), Some(f0e), Some(f0f), Some(f10)))
},
{
case BattleRankFieldData(f01, f02, f03, f04, f05, f06, f07, Some(f08), Some(f09), Some(f0a), Some(f0b), Some(f0c), Some(f0d), Some(f0e), Some(f0f), Some(f10), _) =>
Attempt.successful(f01 :: f02 :: f03 :: f04 :: f05 :: f06 :: f07 :: f08 :: f09 :: f0a :: f0b :: f0c :: f0d :: f0e :: f0f :: f10 :: HNil)
case _ =>
Attempt.failure(Err("expected battle rank 18 field data"))
} }
) )
/** /**
* na * A player's battle rank, determined by their battle experience points, determines how many implants to which they have access.
* @param bep the battle experience points * Starting with "no implants" at BR1, a player earns one at each of the three ranks: BR6, BR12, and BR18.
* @return the appropriate `Codec` for the fields representing a player with the implied battle rank * @param bep battle experience points
* @return the number of accessible implant slots
*/ */
private def selectBattleRankFieldCodec(bep : Int) : Codec[BattleRankFieldData] = { private def numberOfImplantSlots(bep : Long) : Int = {
if(bep > 754370) { if(bep > 754370) { //BR18+
br18FieldCodec 3
} }
else if(bep > 197753) { else if(bep > 197753) { //BR12+
br12FieldCodec 2
} }
else if(bep > 29999) { else if(bep > 29999) { //BR6+
br6FieldCodec 1
} }
else { else { //BR1+
br1FieldCodec 0
} }
} }
/** /**
* The padding value of the first entry in either of two byte-aligned `List` structures. * The padding value of the first entry in either of two byte-aligned `List` structures.
* @param bep the battle experience points * @param implants implant entries
* @return the pad length in bits `n < 8` * @return the pad length in bits `0 <= n < 8`
*/ */
private def bepFieldPadding(bep : Int) : Int = { private def implantFieldPadding(implants : List[ImplantEntry], varBit : Option[Int] = None) : Int = {
if(bep > 754370) { //BR18+ val base : Int = 5 //the offset with no implant entries
7 val baseOffset : Int = base - varBit.getOrElse(0)
val resultA = if(baseOffset < 0) { 8 - baseOffset } else { baseOffset % 8 }
var implantOffset : Int = 0
implants.foreach({entry =>
implantOffset += entry.bitsize.toInt
})
val resultB : Int = resultA - (implantOffset % 8)
if(resultB < 0) { 8 - resultB } else { resultB }
}
/**
* Players with certain battle rank will always have a certain number of implant slots.
* The encoding requires it.
* Pad empty slots onto the end of a list of
* @param size the required number of implant slots
* @param list the `List` of implant slots
* @return a fully-populated (or over-populated) `List` of implant slots
* @see `ImplantEntry`
*/
@tailrec private def recursiveEnsureImplantSlots(size : Int, list : List[ImplantEntry] = Nil) : List[ImplantEntry] = {
if(list.length >= size) {
list
} }
else if(bep > 197753) { //BR12+ else {
1 recursiveEnsureImplantSlots(size, list :+ ImplantEntry(ImplantType.None, None))
}
else if(bep > 29999) { //BR6+
3
}
else { //BR1+
5
} }
} }
/** /**
* Get the padding of the first entry in the first time events list. * Get the padding of the first entry in the first time events list.
* @param len the length of the list * @param len the length of the first time events list
* @param bep the battle experience points * @param implantPadding the padding that resulted from implant entries
* @return the pad length in bits `n < 8` * @return the pad length in bits `0 <= n < 8`
*/ */
private def ftePadding(len : Long, bep : Int) : Int = { private def ftePadding(len : Long, implantPadding : Int) : Int = {
//TODO the parameters for this function are not correct
//TODO the proper padding length should reflect all variability in the stream prior to this point //TODO the proper padding length should reflect all variability in the stream prior to this point
if(len > 0) { if(len > 0) {
bepFieldPadding(bep) implantPadding
} }
else { else {
0 0
@ -342,29 +251,28 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
* The tutorials list follows the first time event list and also contains byte-aligned strings. * 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 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. * 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 list * @param len the length of the first time events list
* @param bep the battle experience points * @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` * @return the pad length in bits `n < 8`
*/ */
private def tutPadding(len : Long, len2 : Long, bep : Int) : Int = { private def tutPadding(len : Long, len2 : Long, implantPadding : Int) : Int = {
if(len > 0) { if(len > 0) {
//automatic alignment from previous List 0 //automatic alignment from previous List
0
} }
else if(len2 > 0) { else if(len2 > 0) {
//need to align for elements implantPadding //need to align for elements
bepFieldPadding(bep)
} }
else { else {
//both lists are empty 0 //both lists are empty
0
} }
} }
implicit val codec : Codec[DetailedCharacterData] = ( implicit val codec : Codec[DetailedCharacterData] = (
("appearance" | CharacterAppearanceData.codec) :: ("appearance" | CharacterAppearanceData.codec) >>:~ { app =>
(("bep" | uint24L) >>:~ { bep => ("bep" | uint32L) >>:~ { bep =>
ignore(136) :: ("cep" | uint32L) ::
ignore(96) ::
("healthMax" | uint16L) :: ("healthMax" | uint16L) ::
("health" | uint16L) :: ("health" | uint16L) ::
ignore(1) :: ignore(1) ::
@ -376,47 +284,57 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
("unk3" | uintL(3)) :: ("unk3" | uintL(3)) ::
("staminaMax" | uint16L) :: ("staminaMax" | uint16L) ::
("stamina" | uint16L) :: ("stamina" | uint16L) ::
ignore(149) :: ignore(147) ::
("unk4" | uint16L) :: ("certs" | listOfN(uint8L, uint8L)) ::
("unk5" | uint8L) :: ignore(5) ::
("brFields" | selectBattleRankFieldCodec(bep)) :: //TODO do this for all these fields until their bits are better defined (("implants" | PacketHelpers.listOfNSized(numberOfImplantSlots(bep), implant_entry_codec)) >>:~ { implants =>
(("firstTimeEvent_length" | uint32L) >>:~ { len => ignore(12) ::
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned(ftePadding(len, bep))) :: (("firstTimeEvent_length" | uint32L) >>:~ { len =>
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned(ftePadding(len, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) ::
(("tutorial_length" | uint32L) >>:~ { len2 => ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, bep))) :: (("tutorial_length" | uint32L) >>:~ { len2 =>
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) :: conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) ::
ignore(207) :: ("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
optional(bool, "inventory" | InventoryData.codec_detailed) :: ignore(207) ::
("drawn_slot" | DrawnSlot.codec) :: optional(bool, "inventory" | InventoryData.codec_detailed) ::
bool //usually false ("drawn_slot" | DrawnSlot.codec) ::
bool //usually false
})
}) })
}) })
}) }
}
).exmap[DetailedCharacterData] ( ).exmap[DetailedCharacterData] (
{ {
case app :: bep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: u4 :: u5 :: brFields :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: inv :: drawn :: false :: HNil => case app :: bep :: cep :: _ :: hpmax :: hp :: _ :: armor :: _ :: u1 :: _ :: u2 :: u3 :: stamax :: stam :: _ :: certs :: _ :: implants :: _ :: _ :: fte0 :: fte1 :: _ :: tut0 :: tut1 :: _ :: inv :: drawn :: false :: HNil =>
//prepend the displaced first elements to their lists //prepend the displaced first elements to their lists
val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else fte1 val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else fte1
val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else tut1 val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else tut1
Attempt.successful(DetailedCharacterData(app, bep, hpmax, hp, armor, u1, u2, u3, stamax, stam, u4, u5, brFields, fteList, tutList, inv, drawn)) Attempt.successful(DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, inv, drawn))
}, },
{ {
case DetailedCharacterData(app, bep, hpmax, hp, armor, u1, u2, u3, stamax, stam, u4, u5, brFields, fteList, tutList, inv, drawn) => case DetailedCharacterData(app, bep, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, inv, drawn) =>
val implantCapacity : Int = numberOfImplantSlots(bep)
val implantList = if(implants.length > implantCapacity) {
implants.slice(0, implantCapacity)
}
else {
recursiveEnsureImplantSlots(implantCapacity, implants)
}
//shift the first elements off their lists //shift the first elements off their lists
var fteListCopy = fteList var fteListCopy = fteList
var firstEvent : Option[String] = None var firstEvent : Option[String] = None
if(fteList.nonEmpty) { if(fteList.nonEmpty) {
firstEvent = Some(fteList.head) firstEvent = Some(fteList.head)
fteListCopy = fteList.drop(1) fteListCopy = fteList.tail
} }
var tutListCopy = tutList var tutListCopy = tutList
var firstTutorial : Option[String] = None var firstTutorial : Option[String] = None
if(tutList.nonEmpty) { if(tutList.nonEmpty) {
firstTutorial = Some(tutList.head) firstTutorial = Some(tutList.head)
tutListCopy = tutList.drop(1) tutListCopy = tutList.tail
} }
Attempt.successful(app :: bep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: u4 :: u5 :: brFields :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: inv :: drawn :: false :: HNil) Attempt.successful(app :: bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: inv :: drawn :: false :: HNil)
} }
) )
} }

View file

@ -9,16 +9,16 @@ import scodec.codecs._
* <br> * <br>
* Implant:<br> * Implant:<br>
* ` * `
* 00 - Regeneration (advanced_regen)<br> * 0 - Regeneration (advanced_regen)<br>
* 01 - Enhanced Targeting (targeting)<br> * 1 - Enhanced Targeting (targeting)<br>
* 02 - Audio Amplifier (audio_amplifier)<br> * 2 - Audio Amplifier (audio_amplifier)<br>
* 03 - Darklight Vision (darklight_vision)<br> * 3 - Darklight Vision (darklight_vision)<br>
* 04 - Melee Booster (melee_booster)<br> * 4 - Melee Booster (melee_booster)<br>
* 05 - Personal Shield (personal_shield)<br> * 5 - Personal Shield (personal_shield)<br>
* 06 - Range Magnifier (range_magnifier)<br> * 6 - Range Magnifier (range_magnifier)<br>
* 07 - Second Wind `(na)`<br> * 7 - Second Wind `(na)`<br>
* 08 - Sensor Shield (silent_run)<br> * 8 - Sensor Shield (silent_run)<br>
* 09 - Surge (surge)<br> * 9 - Surge (surge)
* ` * `
*/ */
object ImplantType extends Enumeration { object ImplantType extends Enumeration {
@ -34,5 +34,7 @@ object ImplantType extends Enumeration {
SilentRun, SilentRun,
Surge = Value Surge = Value
val None = Value(15) //TODO unconfirmed
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L) implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
} }

View file

@ -214,15 +214,7 @@ class ObjectCreateDetailedMessageTest extends Specification {
char.unk3 mustEqual 7 char.unk3 mustEqual 7
char.staminaMax mustEqual 100 char.staminaMax mustEqual 100
char.stamina mustEqual 100 char.stamina mustEqual 100
char.unk4 mustEqual 28 char.certs mustEqual List(0, 1, 11, 21, 26, 27, 28)
char.unk5 mustEqual 4
char.brFields.field00 mustEqual 44
char.brFields.field01 mustEqual 84
char.brFields.field02 mustEqual 104
char.brFields.field03 mustEqual 108
char.brFields.field04 mustEqual 112
char.brFields.field05 mustEqual 0
char.brFields.field06 mustEqual 0
char.firstTimeEvents.size mustEqual 4 char.firstTimeEvents.size mustEqual 4
char.firstTimeEvents.head mustEqual "xpe_sanctuary_help" char.firstTimeEvents.head mustEqual "xpe_sanctuary_help"
char.firstTimeEvents(1) mustEqual "xpe_th_firemodes" char.firstTimeEvents(1) mustEqual "xpe_th_firemodes"
@ -409,12 +401,13 @@ class ObjectCreateDetailedMessageTest extends Specification {
val obj = DetailedCharacterData( val obj = DetailedCharacterData(
app, app,
0, 0,
0,
100, 100, 100, 100,
50, 50,
1, 7, 7, 1, 7, 7,
100, 100, 100, 100,
28, 4, List(0, 1, 11, 21, 26, 27, 28),
BattleRankFieldData(44, 84, 104, 108, 112, 0, 0), List(),
"xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil,
List.empty, List.empty,
InventoryData(inv), InventoryData(inv),

File diff suppressed because one or more lines are too long