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.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 scala.annotation.tailrec
@ -31,15 +31,16 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
Success(
DetailedCharacterData(
MakeAppearanceData(obj),
0,
0L,
0L,
obj.MaxHealth,
obj.Health,
obj.Armor,
1, 7, 7,
obj.MaxStamina,
obj.Stamina,
28, 4,
BattleRankFieldData(44, 84, 104, 108, 112, 0, 0),
List(0, 1, 11, 21, 26, 27, 28), //TODO certification list
List(), //TODO implant list
List.empty[String], //TODO fte list
List.empty[String], //TODO tutorial list
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 nameStringSize : Long = StreamBitSize.stringBitSize(basic_appearance.name, 16) + CharacterAppearanceData.namePadding(pos.vel)
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
}
}
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.
* The padding will always be a number 0-7.
@ -151,7 +166,7 @@ object CharacterAppearanceData extends Marshallable[CharacterAppearanceData] {
("faction" | PlanetSideEmpire.codec) ::
("black_ops" | bool) ::
(("alt_model" | bool) >>:~ { alt_model => //modifies stream format (to display alternate player models)
ignore(1) :: //unknown
ignore(1) :: //unknown
("jammered" | bool) ::
bool :: //crashes client
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"))
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_extrabit : Option[Boolean] = None
if(zipline || bpack) {

View file

@ -1,42 +1,27 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
import net.psforever.newcodecs.newcodecs
import net.psforever.packet.{Marshallable, PacketHelpers}
import scodec.{Attempt, Codec, Err}
import net.psforever.types.ImplantType
import scodec.{Attempt, Codec}
import scodec.codecs._
import shapeless.{::, HNil}
final case class BattleRankFieldData(field00 : Int,
field01 : Int,
field02 : Int,
field03 : Int,
field04 : Int,
field05 : Int,
field06 : Int,
field07 : Option[Int] = None,
field08 : Option[Int] = None,
field09 : Option[Int] = None,
field0A : Option[Int] = None,
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 {
import scala.annotation.tailrec
/**
* An entry in the `List` of valid implant slots in `DetailedCharacterData`.
* "`activation`" is not necessarily the best word for it ...
* @param implant the type of implant
* @param activation na
* @see `ImplantType`
*/
final case class ImplantEntry(implant : ImplantType.Value,
activation : Option[Int]) extends StreamBitSize {
override def bitsize : Long = {
val extraFieldSize : Long = if(field10.isDefined) {
70L
}
else if(field0E.isDefined) {
50L
}
else if(field09.isDefined) {
10L
}
else {
0L
}
55L + extraFieldSize
val activationSize = if(activation.isDefined) { 12L } else { 5L }
5L + activationSize
}
}
@ -49,16 +34,16 @@ final case class BattleRankFieldData(field00 : Int,
* <br>
* 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 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.
* 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.
* The third subdivision is also exclusive to avatar-prepared characters and contains (omitted).
* The fourth is the inventory (composed of `Direct`-type objects).<br>
* <br>
* Exploration:<br>
* Lots of analysis needed for the remainder of the byte data.
* The fourth is the inventory (composed of `Direct`-type objects).
* @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;
* range is 0-65535
* @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
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value;
* range is 0-65535
* @param unk4 na;
* defaults to 28
* @param unk5 na;
* defaults to 4
* @param brFields na
* @param certs the `List` of active certifications
* @param implants the `List` of implant slots currently possessed by this avatar
* @param firstTimeEvents the list of first time events performed by this avatar;
* the size field is a 32-bit number;
* 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 first entry may be padded
* @param inventory the avatar's inventory
@ -95,7 +77,8 @@ final case class BattleRankFieldData(field00 : Int,
* @see `DrawnSlot`
*/
final case class DetailedCharacterData(appearance : CharacterAppearanceData,
bep : Int,
bep : Long,
cep : Long,
healthMax : Int,
health : Int,
armor : Int,
@ -104,9 +87,8 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData,
unk3 : Int, //7
staminaMax : Int,
stamina : Int,
unk4 : Int, //28
unk5 : Int, //4
brFields : BattleRankFieldData,
certs : List[Int],
implants : List[ImplantEntry],
firstTimeEvents : List[String],
tutorials : List[String],
inventory : Option[InventoryData],
@ -114,46 +96,36 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData,
) extends ConstructorData {
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 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
var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen, bep)
var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen, implantPadding)
for(str <- firstTimeEvents) {
eventListSize += StreamBitSize.stringBitSize(str)
}
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) {
tutorialListSize += StreamBitSize.stringBitSize(str)
}
var inventorySize : Long = 0L //inventory
if(inventory.isDefined) {
inventorySize = inventory.get.bitsize
val inventorySize : Long = if(inventory.isDefined) { //inventory
inventory.get.bitsize
}
658L + appearanceSize + brFieldSize + eventListSize + tutorialListSize + inventorySize
else {
0L
}
649L + appearanceSize + certSize + implantSize + eventListSize + tutorialListSize + inventorySize
}
}
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.
* @param appearance data about the avatar's basic aesthetics
@ -165,171 +137,108 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
* @param unk3 na
* @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 unk4 na
* @param unk5 na
* @param unk6 na
* @param certs na
* @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, 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 =
new DetailedCharacterData(appearance, bep, healthMax, health, armor, unk1, unk2, unk3, staminaMax, stamina, unk4, unk5, unk6, firstTimeEvents, tutorials, Some(inventory), drawn_slot)
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, 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) ::
("f2" | uint8L) ::
("f3" | uint8L) ::
("f4" | uint8L) ::
("f5" | uint8L) ::
("f6" | uint8L) ::
("f7" | uintL(7))
).exmap[BattleRankFieldData] (
/**
* `Codec` for entires in the list of implants.
*/
private val implant_entry_codec : Codec[ImplantEntry] = (
("implant" | ImplantType.codec) ::
(bool >>:~ { guard =>
newcodecs.binary_choice(guard, uintL(5), uintL(12)).hlist
})
).xmap[ImplantEntry] (
{
case f1 :: f2 :: f3 :: f4 :: f5 :: f6 :: f7 :: HNil =>
Attempt.successful(BattleRankFieldData(f1, f2, f3, f4, f5, f6, f7))
case implant :: true :: _ :: HNil =>
ImplantEntry(implant, None)
case implant :: false :: extra :: HNil =>
ImplantEntry(implant, Some(extra))
},
{
case BattleRankFieldData(f1, f2, f3, f4, f5, f6, f7, _, _, _, _, _, _, _, _, _, _) =>
Attempt.successful(f1 :: f2 :: f3 :: f4 :: f5 :: f6 :: f7 :: HNil)
}
)
case ImplantEntry(implant, None) =>
implant :: true :: 0 :: HNil
private val br6FieldCodec : Codec[BattleRankFieldData] = ( //+10u
("f1" | uint8L) ::
("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"))
case ImplantEntry(implant, Some(extra)) =>
implant :: false :: extra :: HNil
}
)
/**
* na
* @param bep the battle experience points
* @return the appropriate `Codec` for the fields representing a player with the implied battle rank
* A player's battle rank, determined by their battle experience points, determines how many implants to which they have access.
* Starting with "no implants" at BR1, a player earns one at each of the three ranks: BR6, BR12, and BR18.
* @param bep battle experience points
* @return the number of accessible implant slots
*/
private def selectBattleRankFieldCodec(bep : Int) : Codec[BattleRankFieldData] = {
if(bep > 754370) {
br18FieldCodec
private def numberOfImplantSlots(bep : Long) : Int = {
if(bep > 754370) { //BR18+
3
}
else if(bep > 197753) {
br12FieldCodec
else if(bep > 197753) { //BR12+
2
}
else if(bep > 29999) {
br6FieldCodec
else if(bep > 29999) { //BR6+
1
}
else {
br1FieldCodec
else { //BR1+
0
}
}
/**
* The padding value of the first entry in either of two byte-aligned `List` structures.
* @param bep the battle experience points
* @return the pad length in bits `n < 8`
* @param implants implant entries
* @return the pad length in bits `0 <= n < 8`
*/
private def bepFieldPadding(bep : Int) : Int = {
if(bep > 754370) { //BR18+
7
private def implantFieldPadding(implants : List[ImplantEntry], varBit : Option[Int] = None) : Int = {
val base : Int = 5 //the offset with no implant entries
val baseOffset : Int = base - varBit.getOrElse(0)
val resultA = if(baseOffset < 0) { 8 - baseOffset } else { baseOffset % 8 }
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+
1
}
else if(bep > 29999) { //BR6+
3
}
else { //BR1+
5
else {
recursiveEnsureImplantSlots(size, list :+ ImplantEntry(ImplantType.None, None))
}
}
/**
* Get the padding of the first entry in the first time events list.
* @param len the length of the list
* @param bep the battle experience points
* @return the pad length in bits `n < 8`
* @param len the length of the first time events list
* @param implantPadding the padding that resulted from implant entries
* @return the pad length in bits `0 <= n < 8`
*/
private def ftePadding(len : Long, bep : Int) : Int = {
//TODO the parameters for this function are not correct
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) {
bepFieldPadding(bep)
implantPadding
}
else {
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.
* 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 list
* @param bep the battle experience points
* @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, bep : Int) : Int = {
private def tutPadding(len : Long, len2 : Long, implantPadding : Int) : Int = {
if(len > 0) {
//automatic alignment from previous List
0
0 //automatic alignment from previous List
}
else if(len2 > 0) {
//need to align for elements
bepFieldPadding(bep)
implantPadding //need to align for elements
}
else {
//both lists are empty
0
0 //both lists are empty
}
}
implicit val codec : Codec[DetailedCharacterData] = (
("appearance" | CharacterAppearanceData.codec) ::
(("bep" | uint24L) >>:~ { bep =>
ignore(136) ::
("appearance" | CharacterAppearanceData.codec) >>:~ { app =>
("bep" | uint32L) >>:~ { bep =>
("cep" | uint32L) ::
ignore(96) ::
("healthMax" | uint16L) ::
("health" | uint16L) ::
ignore(1) ::
@ -376,47 +284,57 @@ object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
("unk3" | uintL(3)) ::
("staminaMax" | uint16L) ::
("stamina" | uint16L) ::
ignore(149) ::
("unk4" | uint16L) ::
("unk5" | uint8L) ::
("brFields" | selectBattleRankFieldCodec(bep)) :: //TODO do this for all these fields until their bits are better defined
(("firstTimeEvent_length" | uint32L) >>:~ { len =>
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned(ftePadding(len, bep))) ::
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
(("tutorial_length" | uint32L) >>:~ { len2 =>
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, bep))) ::
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
ignore(207) ::
optional(bool, "inventory" | InventoryData.codec_detailed) ::
("drawn_slot" | DrawnSlot.codec) ::
bool //usually false
ignore(147) ::
("certs" | listOfN(uint8L, uint8L)) ::
ignore(5) ::
(("implants" | PacketHelpers.listOfNSized(numberOfImplantSlots(bep), implant_entry_codec)) >>:~ { implants =>
ignore(12) ::
(("firstTimeEvent_length" | uint32L) >>:~ { len =>
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned(ftePadding(len, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) ::
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
(("tutorial_length" | uint32L) >>:~ { len2 =>
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) ::
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
ignore(207) ::
optional(bool, "inventory" | InventoryData.codec_detailed) ::
("drawn_slot" | DrawnSlot.codec) ::
bool //usually false
})
})
})
})
}
}
).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
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(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
var fteListCopy = fteList
var firstEvent : Option[String] = None
if(fteList.nonEmpty) {
firstEvent = Some(fteList.head)
fteListCopy = fteList.drop(1)
fteListCopy = fteList.tail
}
var tutListCopy = tutList
var firstTutorial : Option[String] = None
if(tutList.nonEmpty) {
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>
* Implant:<br>
* `
* 00 - Regeneration (advanced_regen)<br>
* 01 - Enhanced Targeting (targeting)<br>
* 02 - Audio Amplifier (audio_amplifier)<br>
* 03 - Darklight Vision (darklight_vision)<br>
* 04 - Melee Booster (melee_booster)<br>
* 05 - Personal Shield (personal_shield)<br>
* 06 - Range Magnifier (range_magnifier)<br>
* 07 - Second Wind `(na)`<br>
* 08 - Sensor Shield (silent_run)<br>
* 09 - Surge (surge)<br>
* 0 - Regeneration (advanced_regen)<br>
* 1 - Enhanced Targeting (targeting)<br>
* 2 - Audio Amplifier (audio_amplifier)<br>
* 3 - Darklight Vision (darklight_vision)<br>
* 4 - Melee Booster (melee_booster)<br>
* 5 - Personal Shield (personal_shield)<br>
* 6 - Range Magnifier (range_magnifier)<br>
* 7 - Second Wind `(na)`<br>
* 8 - Sensor Shield (silent_run)<br>
* 9 - Surge (surge)
* `
*/
object ImplantType extends Enumeration {
@ -34,5 +34,7 @@ object ImplantType extends Enumeration {
SilentRun,
Surge = Value
val None = Value(15) //TODO unconfirmed
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
}

View file

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

File diff suppressed because one or more lines are too long