diff --git a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index e6dcec51..21bfbd07 100644 --- a/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/common/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -162,6 +162,36 @@ object GlobalDefinitions { } } + /** + * Using the definition for a piece of `Equipment` determine if it is a grenade-type weapon. + * Only the normal grenades count; the grenade packs are excluded. + * @param edef the `EquipmentDefinition` of the item + * @return `true`, if it is a grenade-type weapon; `false`, otherwise + */ + def isGrenade(edef : EquipmentDefinition) : Boolean = { + edef match { + case `frag_grenade` | `jammer_grenade` | `plasma_grenade` => + true + case _ => + false + } + } + + /** + * Using the definition for a piece of `Equipment` determine if it is a grenade-type weapon. + * Only the grenade packs count; the normal grenades are excluded. + * @param edef the `EquipmentDefinition` of the item + * @return `true`, if it is a grenade-type weapon; `false`, otherwise + */ + def isGrenadePack(edef : EquipmentDefinition) : Boolean = { + edef match { + case `frag_cartridge` | `jammer_cartridge` | `plasma_cartridge` => + true + case _ => + false + } + } + /** * Using the definition for a piece of `Equipment` determine with which faction it aligns if it is a weapon. * Only checks `Tool` objects. @@ -242,6 +272,43 @@ object GlobalDefinitions { } } + /* + Implants + */ + val + advanced_regen = ImplantDefinition(0) + + val + targeting = ImplantDefinition(1) + + val + audio_amplifier = ImplantDefinition(2) + + val + darklight_vision = ImplantDefinition(3) + + val + melee_booster = ImplantDefinition(4) + + val + personal_shield = ImplantDefinition(5) + + val + range_magnifier = ImplantDefinition(6) + + val + second_wind = ImplantDefinition(7) + + val + silent_run = ImplantDefinition(8) + + val + surge = ImplantDefinition(9) + + /* + Equipment (locker_container, kits, ammunition, weapons) + */ + import net.psforever.packet.game.objectcreate.ObjectClass val locker_container = new EquipmentDefinition(456) { Name = "locker container" diff --git a/common/src/main/scala/net/psforever/objects/Implant.scala b/common/src/main/scala/net/psforever/objects/Implant.scala deleted file mode 100644 index 2d7483f7..00000000 --- a/common/src/main/scala/net/psforever/objects/Implant.scala +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2017 PSForever -package net.psforever.objects - -import net.psforever.objects.definition.{ImplantDefinition, Stance} -import net.psforever.types.{ExoSuitType, ImplantType} - -/** - * A type of installable player utility that grants a perk, usually in exchange for stamina (energy).
- *
- * An implant starts with a never-to-initialized timer value of -1 and will not report as `Ready` until the timer is 0. - * The `Timer`, however, will report to the user a time of 0 since negative time does not make sense. - * Although the `Timer` can be manually set, using `Reset` is the better way to default the initialization timer to the correct amount. - * An external script will be necessary to operate the actual initialization countdown. - * An implant must be `Ready` before it can be `Active`. - * The `Timer` must be set (or reset) (or countdown) to 0 to be `Ready` and then it must be activated. - * @param implantDef the `ObjectDefinition` that constructs this item and maintains some of its immutable fields - */ -class Implant(implantDef : ImplantDefinition) { - private var active : Boolean = false - private var initTimer : Long = -1L - - def Name : String = implantDef.Name - - def Ready : Boolean = initTimer == 0L - - def Active : Boolean = active - - def Active_=(isActive : Boolean) : Boolean = { - active = Ready && isActive - Active - } - - def Timer : Long = math.max(0, initTimer) - - def Timer_=(time : Long) : Long = { - initTimer = math.max(0, time) - Timer - } - - def MaxTimer : Long = implantDef.Initialization - - def ActivationCharge : Int = Definition.ActivationCharge - - /** - * Calculate the stamina consumption of the implant for any given moment of being active after its activation. - * As implant energy use can be influenced by both exo-suit worn and general stance held, both are considered. - * @param suit the exo-suit being worn - * @param stance the player's stance - * @return the amount of stamina (energy) that is consumed - */ - def Charge(suit : ExoSuitType.Value, stance : Stance.Value) : Int = { - if(active) { - implantDef.DurationChargeBase + implantDef.DurationChargeByExoSuit(suit) + implantDef.DurationChargeByStance(stance) - } - else { - 0 - } - } - - /** - * Place an implant back in its initializing state. - */ - def Reset() : Unit = { - Active = false - Timer = MaxTimer - } - - /** - * Place an implant back in its pre-initialization state. - * The implant is inactive and can not proceed to a `Ready` condition naturally from this state. - */ - def Jammed() : Unit = { - Active = false - Timer = -1 - } - - def Definition : ImplantDefinition = implantDef -} - -object Implant { - def default : Implant = new Implant(ImplantDefinition(ImplantType.RangeMagnifier)) - - def apply(implantDef : ImplantDefinition) : Implant = { - new Implant(implantDef) - } -} diff --git a/common/src/main/scala/net/psforever/objects/ImplantSlot.scala b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala index 00951f50..736b03f1 100644 --- a/common/src/main/scala/net/psforever/objects/ImplantSlot.scala +++ b/common/src/main/scala/net/psforever/objects/ImplantSlot.scala @@ -1,61 +1,117 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.definition.ImplantDefinition -import net.psforever.types.ImplantType +import net.psforever.objects.definition.{ImplantDefinition, Stance} +import net.psforever.types.{ExoSuitType, ImplantType} /** - * A slot "on the player" into which an implant is installed.
+ * A slot "on the player" into which an implant is installed. + * In total, players have three implant slots.
*
- * In total, players have three implant slots. - * At battle rank one (BR1), however, all of those slots are locked. - * The player earns implants at BR16, BR12, and BR18. - * A locked implant slot can not be used. - * (The code uses "not yet unlocked" logic.) - * When unlocked, an implant may be installed into that slot.
- *
- * The default implant that the underlying slot utilizes is the "Range Magnifier." - * Until the `Installed` condition is some value other than `None`, however, the implant in the slot will not work. + * All implants slots start as "locked" and must be "unlocked" through battle rank advancement. + * Only after it is "unlocked" may an implant be "installed" into the slot. + * Upon installation, it undergoes an initialization period and then, after which, it is ready for user activation. + * Being jammed de-activates the implant, put it into a state of "not being ready," and causes the initialization to repeat. */ class ImplantSlot { /** is this slot available for holding an implant */ private var unlocked : Boolean = false + /** whether this implant is ready for use */ + private var initialized : Boolean = false + /** is this implant active */ + private var active : Boolean = false /** what implant is currently installed in this slot; None if there is no implant currently installed */ - private var installed : Option[ImplantType.Value] = None - /** the entry for that specific implant used by the a player; always occupied by some type of implant */ - private var implant : Implant = ImplantSlot.default + private var implant : Option[ImplantDefinition] = None def Unlocked : Boolean = unlocked def Unlocked_=(lock : Boolean) : Boolean = { - unlocked = lock + unlocked = lock || unlocked Unlocked } - def Installed : Option[ImplantType.Value] = installed + def Initialized : Boolean = initialized - def Implant : Option[Implant] = if(Installed.isDefined) { Some(implant) } else { None } + def Initialized_=(init : Boolean) : Boolean = { + initialized = Installed.isDefined && init + Active = Active && initialized //can not be active just yet + Initialized + } - def Implant_=(anImplant : Option[Implant]) : Option[Implant] = { - anImplant match { - case Some(module) => - Implant = module - case None => - installed = None + def Active : Boolean = active + + def Active_=(state : Boolean) : Boolean = { + active = Initialized && state + Active + } + + def Implant : ImplantType.Value = if(Installed.isDefined) { + implant.get.Type + } + else { + Active = false + Initialized = false + ImplantType.None + } + + def Implant_=(anImplant : ImplantDefinition) : ImplantType.Value = { + Implant_=(Some(anImplant)) + } + + def Implant_=(anImplant : Option[ImplantDefinition]) : ImplantType.Value = { + if(Unlocked) { + anImplant match { + case Some(_) => + implant = anImplant + case None => + implant = None + } } Implant } - def Implant_=(anImplant : Implant) : Option[Implant] = { - implant = anImplant - installed = Some(anImplant.Definition.Type) - Implant + def Installed : Option[ImplantDefinition] = implant + + def MaxTimer : Long = Implant match { + case ImplantType.None => + -1L + case _ => + Installed.get.Initialization + } + + def ActivationCharge : Int = { + if(Active) { + Installed.get.ActivationCharge + } + else { + 0 + } + } + + /** + * Calculate the stamina consumption of the implant for any given moment of being active after its activation. + * As implant energy use can be influenced by both exo-suit worn and general stance held, both are considered. + * @param suit the exo-suit being worn + * @param stance the player's stance + * @return the amount of stamina (energy) that is consumed + */ + def Charge(suit : ExoSuitType.Value, stance : Stance.Value) : Int = { + if(Active) { + val inst = Installed.get + inst.DurationChargeBase + inst.DurationChargeByExoSuit(suit) + inst.DurationChargeByStance(stance) + } + else { + 0 + } + } + + def Jammed() : Unit = { + Active = false + Initialized = false } } object ImplantSlot { - private val default = new Implant(ImplantDefinition(ImplantType.RangeMagnifier)) - def apply() : ImplantSlot = { new ImplantSlot() } diff --git a/common/src/main/scala/net/psforever/objects/Player.scala b/common/src/main/scala/net/psforever/objects/Player.scala index c2300965..9d59f733 100644 --- a/common/src/main/scala/net/psforever/objects/Player.scala +++ b/common/src/main/scala/net/psforever/objects/Player.scala @@ -1,13 +1,14 @@ // Copyright (c) 2017 PSForever package net.psforever.objects -import net.psforever.objects.definition.AvatarDefinition +import net.psforever.objects.definition.{AvatarDefinition, ImplantDefinition} import net.psforever.objects.equipment.{Equipment, EquipmentSize} import net.psforever.objects.inventory.{GridInventory, InventoryItem} import net.psforever.packet.game.PlanetSideGUID import net.psforever.types._ import scala.annotation.tailrec +import scala.collection.mutable class Player(private val name : String, private val faction : PlanetSideEmpire.Value, @@ -33,6 +34,9 @@ class Player(private val name : String, private val loadouts : Array[Option[InfantryLoadout]] = Array.fill[Option[InfantryLoadout]](10)(None) + private var bep : Long = 0 + private var cep : Long = 0 + private val certifications : mutable.Set[CertificationType.Value] = mutable.Set[CertificationType.Value]() private val implants : Array[ImplantSlot] = Array.fill[ImplantSlot](3)(new ImplantSlot) // private var tosRibbon : MeritCommendation.Value = MeritCommendation.None @@ -312,28 +316,40 @@ class Player(private val name : String, exosuit = suit } + def BEP : Long = bep + + def BEP_=(battleExperiencePoints : Long) : Long = { + bep = math.max(0L, math.min(battleExperiencePoints, 4294967295L)) + BEP + } + + def CEP : Long = cep + + def CEP_=(commandExperiencePoints : Long) : Long = { + cep = math.max(0L, math.min(commandExperiencePoints, 4294967295L)) + CEP + } + + def Certifications : mutable.Set[CertificationType.Value] = certifications + def Implants : Array[ImplantSlot] = implants - def Implant(slot : Int) : Option[ImplantType.Value] = { - if(-1 < slot && slot < implants.length) { implants(slot).Installed } else { None } + def Implant(slot : Int) : ImplantType.Value = { + if(-1 < slot && slot < implants.length) { implants(slot).Implant } else { ImplantType.None } } - def Implant(implantType : ImplantType.Value) : Option[Implant] = { - implants.find(_.Installed.contains(implantType)) match { - case Some(slot) => - slot.Implant - case None => - None - } - } - - def InstallImplant(implant : Implant) : Boolean = { - getAvailableImplantSlot(implants.iterator, implant.Definition.Type) match { - case Some(slot) => - slot.Implant = implant - slot.Implant.get.Reset() - true + def InstallImplant(implant : ImplantDefinition) : Boolean = { + implants.find({p => p.Installed.contains(implant)}) match { //try to find the installed implant case None => + //install in a free slot + getAvailableImplantSlot(implants.iterator, implant.Type) match { + case Some(slot) => + slot.Implant = implant + true + case None => + false + } + case Some(_) => false } } @@ -344,7 +360,7 @@ class Player(private val name : String, } else { val slot = iter.next - if(!slot.Unlocked || slot.Installed.contains(implantType)) { + if(!slot.Unlocked || slot.Implant == implantType) { None } else if(slot.Installed.isEmpty) { @@ -357,7 +373,7 @@ class Player(private val name : String, } def UninstallImplant(implantType : ImplantType.Value) : Boolean = { - implants.find({slot => slot.Installed.contains(implantType)}) match { + implants.find({slot => slot.Implant == implantType}) match { case Some(slot) => slot.Implant = None true @@ -368,9 +384,9 @@ class Player(private val name : String, def ResetAllImplants() : Unit = { implants.foreach(slot => { - slot.Implant match { - case Some(implant) => - implant.Reset() + slot.Installed match { + case Some(_) => + slot.Initialized = false case None => ; } }) @@ -570,7 +586,7 @@ object Player { //hand over implants (0 until 3).foreach(index => { if(obj.Implants(index).Unlocked = player.Implants(index).Unlocked) { - obj.Implants(index).Implant = player.Implants(index).Implant + obj.Implants(index).Implant = player.Implants(index).Installed } }) //hand over knife diff --git a/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala b/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala index 1825b58e..7984b310 100644 --- a/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala +++ b/common/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala @@ -9,10 +9,13 @@ import scala.collection.mutable * An `Enumeration` of a variety of poses or generalized movement. */ object Stance extends Enumeration { - val Crouching, - Standing, - Walking, //not used, but should still be defined - Running = Value + val + Crouching, + CrouchWalking, //not used, but should still be defined + Standing, + Walking, //not used, but should still be defined + Running + = Value } /** diff --git a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala index e94547b1..b20812fd 100644 --- a/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala +++ b/common/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala @@ -1,11 +1,10 @@ // Copyright (c) 2017 PSForever package net.psforever.objects.definition.converter -import net.psforever.objects.{EquipmentSlot, Player} +import net.psforever.objects.{EquipmentSlot, GlobalDefinitions, ImplantSlot, Player} import net.psforever.objects.equipment.Equipment -import net.psforever.packet.game.PlanetSideGUID -import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle} -import net.psforever.types.GrenadeState +import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle} +import net.psforever.types.{GrenadeState, ImplantType} import scala.annotation.tailrec import scala.util.{Success, Try} @@ -17,11 +16,11 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { MakeAppearanceData(obj), obj.Health / obj.MaxHealth * 255, //TODO not precise obj.Armor / obj.MaxArmor * 255, //TODO not precise - UniformStyle.Normal, - 0, + DressBattleRank(obj), + DressCommandRank(obj), + recursiveMakeImplantEffects(obj.Implants.iterator), None, //TODO cosmetics - None, //TODO implant effects - InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)), + InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)), //TODO is sorting necessary? GetDrawnSlot(obj) ) ) @@ -32,20 +31,21 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { Success( DetailedCharacterData( MakeAppearanceData(obj), + obj.BEP, + obj.CEP, obj.MaxHealth, obj.Health, obj.Armor, - 1, 7, 7, obj.MaxStamina, obj.Stamina, - 28, 4, 44, 84, 104, 1900, + obj.Certifications.toList.sortBy(_.id), //TODO is sorting necessary? + MakeImplantEntries(obj), List.empty[String], //TODO fte list List.empty[String], //TODO tutorial list InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)), GetDrawnSlot(obj) ) ) - //TODO tidy this mess up } /** @@ -64,8 +64,8 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { "", 0, obj.isBackpack, - obj.Orientation.y.toInt, - obj.FacingYawUpper.toInt, + obj.Orientation.y, + obj.FacingYawUpper, true, GrenadeState.None, false, @@ -75,6 +75,107 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { ) } + /** + * Select the appropriate `UniformStyle` design for a player's accumulated battle experience points. + * At certain battle ranks, all exo-suits undergo some form of coloration change. + * @param obj the `Player` game object + * @return the resulting uniform upgrade level + */ + private def DressBattleRank(obj : Player) : UniformStyle.Value = { + val bep : Long = obj.BEP + if(bep > 2583440) { //BR25+ + UniformStyle.ThirdUpgrade + } + else if(bep > 308989) { //BR14+ + UniformStyle.SecondUpgrade + } + else if(bep > 44999) { //BR7+ + UniformStyle.FirstUpgrade + } + else { //BR1+ + UniformStyle.Normal + } + } + + /** + * Select the appropriate design for a player's accumulated command experience points. + * Visual cues for command rank include armlets, anklets, and, finally, a backpack, awarded at different ranks. + * @param obj the `Player` game object + * @return the resulting uniform upgrade level + */ + private def DressCommandRank(obj : Player) : Int = { + val cep = obj.CEP + if(cep > 599999) { + 5 + } + else if(cep > 299999) { + 4 + } + else if(cep > 149999) { + 3 + } + else if(cep > 49999) { + 2 + } + else if(cep > 9999) { + 1 + } + else { + 0 + } + } + + /** + * Transform an `Array` of `Implant` objects into a `List` of `ImplantEntry` objects suitable as packet data. + * @param obj the `Player` game object + * @return the resulting implant `List` + * @see `ImplantEntry` in `DetailedCharacterData` + */ + private def MakeImplantEntries(obj : Player) : List[ImplantEntry] = { + obj.Implants.map(slot => { + slot.Installed match { + case Some(_) => + if(slot.Initialized) { + ImplantEntry(slot.Implant, None) + } + else { + ImplantEntry(slot.Implant, Some(slot.Installed.get.Initialization.toInt)) + } + case None => + ImplantEntry(ImplantType.None, None) + } + }).toList + } + + /** + * Find an active implant whose effect will be displayed on this player. + * @param iter an `Iterator` of `ImplantSlot` objects + * @return the effect of an active implant + */ + @tailrec private def recursiveMakeImplantEffects(iter : Iterator[ImplantSlot]) : Option[ImplantEffects.Value] = { + if(!iter.hasNext) { + None + } + else { + val slot = iter.next + if(slot.Active) { + import GlobalDefinitions._ + slot.Installed match { + case Some(`advanced_regen`) => + Some(ImplantEffects.RegenEffects) + case Some(`darklight_vision`) => + Some(ImplantEffects.DarklightEffects) + case Some(`personal_shield`) => + Some(ImplantEffects.PersonalShieldEffects) + case Some(`surge`) => + Some(ImplantEffects.SurgeEffects) + case _ => ; + } + } + recursiveMakeImplantEffects(iter) + } + } + /** * Given a player with an inventory, convert the contents of that inventory into converted-decoded packet data. * The inventory is not represented in a `0x17` `Player`, so the conversion is only valid for `0x18` avatars. @@ -139,6 +240,14 @@ class AvatarConverter extends ObjectCreateConverter[Player]() { InternalSlot(equip.Definition.ObjectId, equip.GUID, index, equip.Definition.Packet.DetailedConstructorData(equip).get) } + /** + * Given some equipment holsters, convert the contents of those holsters into converted-decoded packet data. + * @param iter an `Iterator` of `EquipmentSlot` objects that are a part of the player's holsters + * @param builder the function used to transform to the decoded packet form + * @param list the current `List` of transformed data + * @param index which holster is currently being explored + * @return the `List` of inventory data created from the holsters + */ @tailrec private def recursiveMakeHolsters(iter : Iterator[EquipmentSlot], builder : ((Int, Equipment) => InternalSlot), list : List[InternalSlot] = Nil, index : Int = 0) : List[InternalSlot] = { if(!iter.hasNext) { list diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala index 66380b62..92dd6e6b 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/CharacterAppearanceData.scala @@ -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) { diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala index 34e6a6db..13ccf6ee 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala @@ -1,11 +1,31 @@ // Copyright (c) 2017 PSForever package net.psforever.packet.game.objectcreate +import net.psforever.newcodecs.newcodecs import net.psforever.packet.{Marshallable, PacketHelpers} +import net.psforever.types.{CertificationType, ImplantType} import scodec.{Attempt, Codec} import scodec.codecs._ import shapeless.{::, HNil} +import scala.annotation.tailrec + +/** + * An entry in the `List` of valid implant slots in `DetailedCharacterData`. + * `activation`, if defined, indicates the time remaining (in seconds?) before an implant becomes usable. + * @param implant the type of implant + * @param activation the activation timer; + * technically, this is "unconfirmed" + * @see `ImplantType` + */ +final case class ImplantEntry(implant : ImplantType.Value, + activation : Option[Int]) extends StreamBitSize { + override def bitsize : Long = { + val activationSize = if(activation.isDefined) { 12L } else { 5L } + 5L + activationSize + } +} + /** * A representation of the avatar portion of `ObjectCreateDetailedMessage` packet data. * This densely-packed information outlines most of the specifics required to depict a character as an avatar.
@@ -15,16 +35,16 @@ import shapeless.{::, HNil} *
* 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).
- *
- * Exploration:
- * 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; @@ -42,32 +62,25 @@ import shapeless.{::, HNil} * 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 unk6 na; - * defaults to 44 - * @param unk7 na; - * defaults to 84 - * @param unk8 na; - * defaults to 104 - * @param unk9 na; - * defaults to 1900 + * @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 * @param drawn_slot the holster that is initially drawn - * @see `CharacterAppearanceData` - * @see `CharacterData` - * @see `InventoryData` - * @see `DrawnSlot` + * @see `CharacterAppearanceData`
+ * `CharacterData`
+ * `CertificationType`
+ * `InventoryData`
+ * `DrawnSlot` */ final case class DetailedCharacterData(appearance : CharacterAppearanceData, + bep : Long, + cep : Long, healthMax : Int, health : Int, armor : Int, @@ -76,177 +89,252 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData, unk3 : Int, //7 staminaMax : Int, stamina : Int, - unk4 : Int, //28 - unk5 : Int, //4 - unk6 : Int, //44 - unk7 : Int, //84 - unk8 : Int, //104 - unk9 : Int, //1900 + certs : List[CertificationType.Value], + implants : List[ImplantEntry], firstTimeEvents : List[String], tutorials : List[String], inventory : Option[InventoryData], drawn_slot : DrawnSlot.Value = DrawnSlot.None - ) extends ConstructorData { + ) 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 certSize = (certs.length + 1) * 8 //cert list + var implantSize : Long = 0L //implant list + for(entry <- implants) { + implantSize += entry.bitsize + } + val implantPadding = DetailedCharacterData.implantFieldPadding(implants, CharacterAppearanceData.altModelBit(appearance)) val fteLen = firstTimeEvents.size //fte list - var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen) + 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) + 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 } - 713L + appearanceSize + 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. + * Overloaded constructor for `DetailedCharacterData` that requires an inventory and drops unknown values. * @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 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, 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, 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 - * @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 unk1 na - * @param unk2 na - * @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 unk7 na - * @param unk8 na - * @param unk9 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 * @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, healthMax : Int, health : Int, armor : Int, unk1 : Int, unk2 : Int, unk3 : Int, staminaMax : Int, stamina : Int, unk4 : Int, unk5 : Int, unk6 : Int, unk7 : Int, unk8 : Int, unk9 : Int, firstTimeEvents : List[String], tutorials : List[String], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData = - new DetailedCharacterData(appearance, healthMax, health, armor, unk1, unk2, unk3, staminaMax, stamina, unk4, unk5, unk6, unk7, unk8, unk9, firstTimeEvents, tutorials, Some(inventory), drawn_slot) + def apply(appearance : CharacterAppearanceData, 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], inventory : InventoryData, drawn_slot : DrawnSlot.Value) : DetailedCharacterData = + new DetailedCharacterData(appearance, bep, cep, healthMax, health, armor, 1, 7, 7, staminaMax, stamina, certs, implants, firstTimeEvents, tutorials, Some(inventory), drawn_slot) + + /** + * `Codec` for entries 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 implant :: true :: _ :: HNil => + ImplantEntry(implant, None) + + case implant :: false :: extra :: HNil => + ImplantEntry(implant, Some(extra)) + }, + { + case ImplantEntry(implant, None) => + implant :: true :: 0 :: HNil + + case ImplantEntry(implant, Some(extra)) => + implant :: false :: extra :: HNil + } + ) + + /** + * 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 numberOfImplantSlots(bep : Long) : Int = { + if(bep > 754370) { //BR18+ + 3 + } + else if(bep > 197753) { //BR12+ + 2 + } + else if(bep > 29999) { //BR6+ + 1 + } + else { //BR1+ + 0 + } + } + + /** + * The padding value of the first entry in either of two byte-aligned `List` structures. + * @param implants implant entries + * @return the pad length in bits `0 <= n < 8` + */ + 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 { + recursiveEnsureImplantSlots(size, list :+ ImplantEntry(ImplantType.None, None)) + } + } /** * Get the padding of the first entry in the first time events list. - * The padding will always be a number 0-7. - * @param len the length of the list - * @return the pad length in bits + * @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) : 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) { - 5 + implantPadding } - else + else { 0 + } } /** - * Get the padding of the first entry in the completed tutorials list. - * The padding will always be a number 0-7.
+ * Get the padding of the first entry in the completed tutorials list.
*
- * The tutorials list follows the first time event list and that contains byte-aligned strings too. - * While there will be more to the padding, this other list is important. - * Any elements in that list causes the automatic byte-alignment of this list's first entry. - * @param len the length of the list - * @return the pad length in bits + * 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) : Int = { - if(len > 0) //automatic alignment from previous List - 0 - else if(len2 > 0) //need to align for elements - 5 - else //both lists are empty - 0 + 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 + } } implicit val codec : Codec[DetailedCharacterData] = ( - ("appearance" | CharacterAppearanceData.codec) :: - ignore(160) :: - ("healthMax" | uint16L) :: - ("health" | uint16L) :: - ignore(1) :: - ("armor" | uint16L) :: - ignore(9) :: - ("unk1" | uint8L) :: - ignore(8) :: - ("unk2" | uint4L) :: - ("unk3" | uintL(3)) :: - ("staminaMax" | uint16L) :: - ("stamina" | uint16L) :: - ignore(149) :: - ("unk4" | uint16L) :: - ("unk5" | uint8L) :: - ("unk6" | uint8L) :: - ("unk7" | uint8L) :: - ("unk8" | uint8L) :: - ("unk9" | uintL(12)) :: - ignore(19) :: - (("firstTimeEvent_length" | uint32L) >>:~ { len => - conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) :: - ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: - (("tutorial_length" | uint32L) >>:~ { len2 => - conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutPadding(len, len2) )) :: - ("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) :: - ignore(207) :: - optional(bool, "inventory" | InventoryData.codec_detailed) :: - ("drawn_slot" | DrawnSlot.codec) :: - bool //usually false - }) - }) + ("appearance" | CharacterAppearanceData.codec) >>:~ { app => + ("bep" | uint32L) >>:~ { bep => + ("cep" | uint32L) :: + ignore(96) :: + ("healthMax" | uint16L) :: + ("health" | uint16L) :: + ignore(1) :: + ("armor" | uint16L) :: + ignore(9) :: + ("unk1" | uint8L) :: + ignore(8) :: + ("unk2" | uint4L) :: + ("unk3" | uintL(3)) :: + ("staminaMax" | uint16L) :: + ("stamina" | uint16L) :: + ignore(147) :: + ("certs" | listOfN(uint8L, CertificationType.codec)) :: + optional(bool, uint32L) :: //ask about sample CCRIDER + ignore(4) :: + (("implants" | PacketHelpers.listOfNSized(numberOfImplantSlots(bep), implant_entry_codec)) >>:~ { implants => + ignore(12) :: + (("firstTimeEvent_length" | uint32L) >>:~ { len => + conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned(ftePadding(len, implantFieldPadding(implants, 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 :: _ :: b :: c :: _ :: d :: _ :: e :: _ :: f :: g :: h :: i :: _ :: j :: k :: l :: m :: n :: o :: _ :: _ :: q :: r :: _ :: t :: u :: _ :: v :: w :: 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(q.isDefined) { q.get :: r } else r - val tutList : List[String] = if(t.isDefined) { t.get :: u } else u - Attempt.successful(DetailedCharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, v, w)) + 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, cep, hpmax, hp, armor, u1, u2, u3, stamax, stam, certs, implants, fteList, tutList, inv, drawn)) }, { - case DetailedCharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, p, q) => + 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) + 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) } - var tutListCopy = tutList - var firstTutorial : Option[String] = None - if(tutList.nonEmpty) { - firstTutorial = Some(tutList.head) - tutListCopy = tutList.drop(1) + 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) } - Attempt.successful(app :: () :: b :: c :: () :: d :: () :: e :: () :: f :: g :: h :: i :: () :: j :: k :: l :: m :: n :: o :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: p :: q :: false :: HNil) + Attempt.successful(app :: bep :: cep :: () :: hpmax :: hp :: () :: armor :: () :: u1 :: () :: u2 :: u3 :: stamax :: stam :: () :: certs :: None :: () :: implantList :: () :: fteList.size.toLong :: firstEvent :: fteListCopy :: tutList.size.toLong :: firstTutorial :: tutListCopy :: () :: inv :: drawn :: false :: HNil) } ) } diff --git a/common/src/main/scala/net/psforever/types/CertificationType.scala b/common/src/main/scala/net/psforever/types/CertificationType.scala new file mode 100644 index 00000000..d06e2d19 --- /dev/null +++ b/common/src/main/scala/net/psforever/types/CertificationType.scala @@ -0,0 +1,79 @@ +// Copyright (c) 2017 PSForever +package net.psforever.types + +import net.psforever.packet.PacketHelpers +import scodec.codecs._ +/** + * An `Enumeration` of the available certifications.
+ *
+ * As indicated, the following certifications are always enqueued on an avatar's permissions: + * `StandardAssault`, `StandardExoSuit`, `AgileExoSuit`. + * They must still be included in any formal lists of permitted equipment for a user. + * The other noted certifications require all prerequisite certifications listed or they themselves will not be listed: + * `ElectronicsExpert` and `AdvancedEngineering`. + * No other certification requires its prerequisites explicitly listed to be listed itself. + * Any certification that contains multiple other certifications overrides those individual certifications in the list. + * There is no certification for the Advanced Nanite Transport.
+ *
+ * In terms of pricing, `StandardAssault`, `StandardExoSuit`, and `AgileExoSuit` are costless. + * A certification that contains multiple other certifications acts as the overriding cost. + * (Taking `UniMAX` while owning `AAMAX` will refund the `AAMAX` cost and replace it with the `UniMAX` cost.) + */ +object CertificationType extends Enumeration { + type Type = Value + val + //0 + StandardAssault, //always listed + MediumAssault, + HeavyAssault, + SpecialAssault, + AntiVehicular, + Sniping, + EliteAssault, + AirCalvaryScout, + AirCalvaryInterceptor, + AirCalvaryAssault, + //10 + AirSupport, + ATV, + LightScout, + AssaultBuggy, + ArmoredAssault1, + ArmoredAssault2, + GroundTransport, + GroundSupport, + BattleFrameRobotics, + Flail, + //20 + Switchblade, + Harasser, + Phantasm, + GalaxyGunship, + BFRAntiAircraft, + BFRAntiInfantry, + StandardExoSuit, //always listed + AgileExoSuit, //always listed + ReinforcedExoSuit, + InfiltrationSuit, + //30 + AAMAX, + AIMAX, + AVMAX, + UniMAX, + Medical, + AdvancedMedical, + Hacking, + AdvancedHacking, + ExpertHacking, + DataCorruption, + //40 + ElectronicsExpert, //requires Hacking and AdvancedHacking + Engineering, + CombatEngineering, + FortificationEngineering, + AssaultEngineering, + AdvancedEngineering //requires Engineering and CombatEngineering + = Value + + implicit val codec = PacketHelpers.createEnumerationCodec(this, uint8L) +} diff --git a/common/src/main/scala/net/psforever/types/ImplantType.scala b/common/src/main/scala/net/psforever/types/ImplantType.scala index 3449c0e4..acd47b5b 100644 --- a/common/src/main/scala/net/psforever/types/ImplantType.scala +++ b/common/src/main/scala/net/psforever/types/ImplantType.scala @@ -9,16 +9,16 @@ import scodec.codecs._ *
* Implant:
* ` - * 00 - Regeneration (advanced_regen)
- * 01 - Enhanced Targeting (targeting)
- * 02 - Audio Amplifier (audio_amplifier)
- * 03 - Darklight Vision (darklight_vision)
- * 04 - Melee Booster (melee_booster)
- * 05 - Personal Shield (personal_shield)
- * 06 - Range Magnifier (range_magnifier)
- * 07 - Second Wind `(na)`
- * 08 - Sensor Shield (silent_run)
- * 09 - Surge (surge)
+ * 0 - Regeneration (advanced_regen)
+ * 1 - Enhanced Targeting (targeting)
+ * 2 - Audio Amplifier (audio_amplifier)
+ * 3 - Darklight Vision (darklight_vision)
+ * 4 - Melee Booster (melee_booster)
+ * 5 - Personal Shield (personal_shield)
+ * 6 - Range Magnifier (range_magnifier)
+ * 7 - Second Wind `(na)`
+ * 8 - Sensor Shield (silent_run)
+ * 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) } diff --git a/common/src/test/scala/game/DisplayedAwardMessageTest.scala b/common/src/test/scala/game/DisplayedAwardMessageTest.scala index 2c8a5851..cbe6d565 100644 --- a/common/src/test/scala/game/DisplayedAwardMessageTest.scala +++ b/common/src/test/scala/game/DisplayedAwardMessageTest.scala @@ -14,7 +14,7 @@ class DisplayedAwardMessageTest extends Specification { PacketCoding.DecodePacket(string).require match { case DisplayedAwardMessage(player_guid, ribbon, bar) => player_guid mustEqual PlanetSideGUID(1695) - ribbon mustEqual MeritCommendation.TwoYearTR + ribbon mustEqual MeritCommendation.TwoYearVS bar mustEqual RibbonBarsSlot.TermOfService case _ => ko @@ -22,7 +22,7 @@ class DisplayedAwardMessageTest extends Specification { } "encode" in { - val msg = DisplayedAwardMessage(PlanetSideGUID(1695), MeritCommendation.TwoYearTR, RibbonBarsSlot.TermOfService) + val msg = DisplayedAwardMessage(PlanetSideGUID(1695), MeritCommendation.TwoYearVS, RibbonBarsSlot.TermOfService) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string diff --git a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala index dfccf584..f6163520 100644 --- a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala +++ b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala @@ -206,6 +206,8 @@ class ObjectCreateDetailedMessageTest extends Specification { char.appearance.ribbons.middle mustEqual MeritCommendation.None char.appearance.ribbons.lower mustEqual MeritCommendation.None char.appearance.ribbons.tos mustEqual MeritCommendation.None + char.bep mustEqual 0 + char.cep mustEqual 0 char.healthMax mustEqual 100 char.health mustEqual 100 char.armor mustEqual 50 //standard exosuit value @@ -214,12 +216,15 @@ 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.unk6 mustEqual 44 - char.unk7 mustEqual 84 - char.unk8 mustEqual 104 - char.unk9 mustEqual 1900 + char.certs.length mustEqual 7 + char.certs.head mustEqual CertificationType.StandardAssault + char.certs(1) mustEqual CertificationType.MediumAssault + char.certs(2) mustEqual CertificationType.ATV + char.certs(3) mustEqual CertificationType.Harasser + char.certs(4) mustEqual CertificationType.StandardExoSuit + char.certs(5) mustEqual CertificationType.AgileExoSuit + char.certs(6) mustEqual CertificationType.ReinforcedExoSuit + char.implants.length mustEqual 0 char.firstTimeEvents.size mustEqual 4 char.firstTimeEvents.head mustEqual "xpe_sanctuary_help" char.firstTimeEvents(1) mustEqual "xpe_th_firemodes" @@ -405,14 +410,25 @@ class ObjectCreateDetailedMessageTest extends Specification { Nil val obj = DetailedCharacterData( app, + 0, + 0, 100, 100, 50, 1, 7, 7, 100, 100, - 28, 4, 44, 84, 104, 1900, + List( + CertificationType.StandardAssault, + CertificationType.MediumAssault, + CertificationType.ATV, + CertificationType.Harasser, + CertificationType.StandardExoSuit, + CertificationType.AgileExoSuit, + CertificationType.ReinforcedExoSuit + ), + List(), "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil, List.empty, - InventoryData(inv), + Some(InventoryData(inv)), DrawnSlot.Pistol1 ) val msg = ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj) diff --git a/common/src/test/scala/game/ObjectCreateMessageTest.scala b/common/src/test/scala/game/ObjectCreateMessageTest.scala index af4d8df2..76b8669e 100644 --- a/common/src/test/scala/game/ObjectCreateMessageTest.scala +++ b/common/src/test/scala/game/ObjectCreateMessageTest.scala @@ -691,10 +691,10 @@ class ObjectCreateMessageTest extends Specification { pc.appearance.is_cloaking mustEqual false pc.appearance.charging_pose mustEqual false pc.appearance.on_zipline mustEqual false - pc.appearance.ribbons.upper mustEqual MeritCommendation.Loser4 - pc.appearance.ribbons.middle mustEqual MeritCommendation.HeavyInfantry3 - pc.appearance.ribbons.lower mustEqual MeritCommendation.TankBuster6 - pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearNC + pc.appearance.ribbons.upper mustEqual MeritCommendation.MarkovVeteran + pc.appearance.ribbons.middle mustEqual MeritCommendation.HeavyInfantry4 + pc.appearance.ribbons.lower mustEqual MeritCommendation.TankBuster7 + pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearTR pc.health mustEqual 255 pc.armor mustEqual 253 pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade @@ -787,10 +787,10 @@ class ObjectCreateMessageTest extends Specification { pc.appearance.is_cloaking mustEqual false pc.appearance.charging_pose mustEqual false pc.appearance.on_zipline mustEqual false - pc.appearance.ribbons.upper mustEqual MeritCommendation.Jacking - pc.appearance.ribbons.middle mustEqual MeritCommendation.ScavengerTR6 + pc.appearance.ribbons.upper mustEqual MeritCommendation.Jacking2 + pc.appearance.ribbons.middle mustEqual MeritCommendation.ScavengerVS1 pc.appearance.ribbons.lower mustEqual MeritCommendation.AMSSupport4 - pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearTR + pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearVS pc.health mustEqual 0 pc.armor mustEqual 0 pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade @@ -1115,10 +1115,10 @@ class ObjectCreateMessageTest extends Specification { GrenadeState.None, false, false, false, RibbonBars( - MeritCommendation.Loser4, - MeritCommendation.HeavyInfantry3, - MeritCommendation.TankBuster6, - MeritCommendation.SixYearNC + MeritCommendation.MarkovVeteran, + MeritCommendation.HeavyInfantry4, + MeritCommendation.TankBuster7, + MeritCommendation.SixYearTR ) ), 255, 253, @@ -1172,10 +1172,10 @@ class ObjectCreateMessageTest extends Specification { GrenadeState.None, false, false, false, RibbonBars( - MeritCommendation.Jacking, - MeritCommendation.ScavengerTR6, + MeritCommendation.Jacking2, + MeritCommendation.ScavengerVS1, MeritCommendation.AMSSupport4, - MeritCommendation.SixYearTR + MeritCommendation.SixYearVS ) ), 0, 0, diff --git a/common/src/test/scala/objects/ImplantTest.scala b/common/src/test/scala/objects/ImplantTest.scala index e1f47936..e7bc2390 100644 --- a/common/src/test/scala/objects/ImplantTest.scala +++ b/common/src/test/scala/objects/ImplantTest.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package objects -import net.psforever.objects.Implant +import net.psforever.objects.ImplantSlot import net.psforever.objects.definition.{ImplantDefinition, Stance} import net.psforever.types.{ExoSuitType, ImplantType} import org.specs2.mutable._ @@ -16,61 +16,122 @@ class ImplantTest extends Specification { sample.DurationChargeByExoSuit += ExoSuitType.Standard -> 1 sample.DurationChargeByStance += Stance.Running -> 1 - "define" in { - sample.Initialization mustEqual 90000 - sample.ActivationCharge mustEqual 3 - sample.DurationChargeBase mustEqual 1 - sample.DurationChargeByExoSuit(ExoSuitType.Agile) mustEqual 2 - sample.DurationChargeByExoSuit(ExoSuitType.Reinforced) mustEqual 2 - sample.DurationChargeByExoSuit(ExoSuitType.Standard) mustEqual 1 - sample.DurationChargeByExoSuit(ExoSuitType.Infiltration) mustEqual 0 //default value - sample.DurationChargeByStance(Stance.Running) mustEqual 1 - sample.DurationChargeByStance(Stance.Crouching) mustEqual 0 //default value - sample.Type mustEqual ImplantType.SilentRun + "ImplantDefinition" should { + "define" in { + sample.Initialization mustEqual 90000 + sample.ActivationCharge mustEqual 3 + sample.DurationChargeBase mustEqual 1 + sample.DurationChargeByExoSuit(ExoSuitType.Agile) mustEqual 2 + sample.DurationChargeByExoSuit(ExoSuitType.Reinforced) mustEqual 2 + sample.DurationChargeByExoSuit(ExoSuitType.Standard) mustEqual 1 + sample.DurationChargeByExoSuit(ExoSuitType.Infiltration) mustEqual 0 //default value + sample.DurationChargeByStance(Stance.Running) mustEqual 1 + sample.DurationChargeByStance(Stance.Crouching) mustEqual 0 //default value + sample.Type mustEqual ImplantType.SilentRun + } } - "construct" in { - val obj = new Implant(sample) - obj.Definition.Type mustEqual sample.Type - obj.Active mustEqual false - obj.Ready mustEqual false - obj.Timer mustEqual 0 - } + "ImplantSlot" should { + "construct" in { + val obj = new ImplantSlot + obj.Unlocked mustEqual false + obj.Initialized mustEqual false + obj.Active mustEqual false + obj.Implant mustEqual ImplantType.None + obj.Installed mustEqual None + } - "reset/init their timer" in { - val obj = new Implant(sample) - obj.Timer mustEqual 0 - obj.Reset() - obj.Timer mustEqual 90000 - } + "load an implant when locked" in { + val obj = new ImplantSlot + obj.Unlocked mustEqual false + obj.Implant mustEqual ImplantType.None - "reset/init their readiness condition" in { - val obj = new Implant(sample) - obj.Ready mustEqual false - obj.Timer = 0 - obj.Ready mustEqual true - obj.Reset() - obj.Ready mustEqual false - } + obj.Implant = sample + obj.Implant mustEqual ImplantType.None + } - "not activate until they are ready" in { - val obj = new Implant(sample) - obj.Active = true - obj.Active mustEqual false - obj.Timer = 0 - obj.Active = true - obj.Active mustEqual true - } + "load an implant when unlocked" in { + val obj = new ImplantSlot + obj.Unlocked mustEqual false + obj.Implant mustEqual ImplantType.None + sample.Type mustEqual ImplantType.SilentRun - "not cost energy while not active" in { - val obj = new Implant(sample) - obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 0 - } + obj.Unlocked = true + obj.Implant = sample + obj.Implant mustEqual ImplantType.SilentRun + } - "cost energy while active" in { - val obj = new Implant(sample) - obj.Timer = 0 - obj.Active = true - obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 4 + "can not re-lock an unlocked implant slot" in { + val obj = new ImplantSlot + obj.Unlocked mustEqual false + + obj.Unlocked = false + obj.Unlocked mustEqual false + obj.Unlocked = true + obj.Unlocked mustEqual true + obj.Unlocked = false + obj.Unlocked mustEqual true + } + + "initialize without an implant" in { + val obj = new ImplantSlot + obj.Initialized mustEqual false + obj.Initialized = true + obj.Initialized mustEqual false + } + + "initialize an implant" in { + val obj = new ImplantSlot + obj.Initialized mustEqual false + + obj.Unlocked = true + obj.Implant = sample + obj.Initialized = true + obj.Initialized mustEqual true + } + + "activate an uninitialized implant" in { + val obj = new ImplantSlot + obj.Unlocked = true + obj.Implant = sample + obj.Initialized mustEqual false + obj.Active mustEqual false + + obj.Active = true + obj.Active mustEqual false + } + + "activate an initialized implant" in { + val obj = new ImplantSlot + obj.Unlocked = true + obj.Implant = sample + obj.Initialized mustEqual false + obj.Active mustEqual false + + obj.Initialized = true + obj.Active = true + obj.Active mustEqual true + } + + "not cost energy while not active" in { + val obj = new ImplantSlot + obj.Unlocked = true + obj.Implant = sample + obj.Initialized = true + obj.Active mustEqual false + obj.ActivationCharge mustEqual 0 + obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 0 + } + + "cost energy while active" in { + val obj = new ImplantSlot + obj.Unlocked = true + obj.Implant = sample + obj.Initialized = true + obj.Active = true + obj.Active mustEqual true + obj.ActivationCharge mustEqual 3 + obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 4 + } } } diff --git a/common/src/test/scala/objects/PlayerTest.scala b/common/src/test/scala/objects/PlayerTest.scala index fd2d3fc5..2a95f598 100644 --- a/common/src/test/scala/objects/PlayerTest.scala +++ b/common/src/test/scala/objects/PlayerTest.scala @@ -1,7 +1,7 @@ // Copyright (c) 2017 PSForever package objects -import net.psforever.objects.{Implant, Player, SimpleItem} +import net.psforever.objects.{Player, SimpleItem} import net.psforever.objects.definition.{ImplantDefinition, SimpleItemDefinition} import net.psforever.objects.equipment.EquipmentSize import net.psforever.types.{CharacterGender, ExoSuitType, ImplantType, PlanetSideEmpire} @@ -106,31 +106,39 @@ class PlayerTest extends Specification { obj.LastDrawnSlot mustEqual 1 } - "install no implants until a slot is unlocked" in { - val testplant : Implant = Implant(ImplantDefinition(1)) + "install an implant" in { + val testplant : ImplantDefinition = ImplantDefinition(1) val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) - obj.Implants(0).Unlocked mustEqual false - obj.Implant(0) mustEqual None - obj.InstallImplant(testplant) - obj.Implant(0) mustEqual None - obj.Implant(ImplantType(1)) mustEqual None - obj.Implants(0).Unlocked = true - obj.InstallImplant(testplant) - obj.Implant(0) mustEqual Some(testplant.Definition.Type) - obj.Implant(ImplantType(1)) mustEqual Some(testplant) + obj.InstallImplant(testplant) mustEqual true + obj.Implants.find({p => p.Implant == ImplantType(1)}) match { //find the installed implant + case Some(slot) => + slot.Installed mustEqual Some(testplant) + case _ => + ko + } + ok + } + + "can not install the same type of implant twice" in { + val testplant1 : ImplantDefinition = ImplantDefinition(1) + val testplant2 : ImplantDefinition = ImplantDefinition(1) + val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) + obj.Implants(0).Unlocked = true + obj.Implants(1).Unlocked = true + obj.InstallImplant(testplant1) mustEqual true + obj.InstallImplant(testplant2) mustEqual false } "uninstall implants" in { - val testplant : Implant = Implant(ImplantDefinition(1)) + val testplant : ImplantDefinition = ImplantDefinition(1) val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) obj.Implants(0).Unlocked = true - obj.InstallImplant(testplant) - obj.Implant(ImplantType(1)) mustEqual Some(testplant) + obj.InstallImplant(testplant) mustEqual true + obj.Implants(0).Installed mustEqual Some(testplant) - obj.UninstallImplant(ImplantType(1)) - obj.Implant(0) mustEqual None - obj.Implant(ImplantType(1)) mustEqual None + obj.UninstallImplant(testplant.Type) + obj.Implants(0).Installed mustEqual None } "administrate" in { diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index f11ab343..87bbdde9 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -601,6 +601,13 @@ class WorldSessionActor extends Actor with MDCContextAware { player = Player("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) player.Position = Vector3(3674.8438f, 2726.789f, 91.15625f) player.Orientation = Vector3(0f, 0f, 90f) + player.Certifications += CertificationType.StandardAssault + player.Certifications += CertificationType.MediumAssault + player.Certifications += CertificationType.StandardExoSuit + player.Certifications += CertificationType.AgileExoSuit + player.Certifications += CertificationType.ReinforcedExoSuit + player.Certifications += CertificationType.ATV + player.Certifications += CertificationType.Harasser player.Slot(0).Equipment = beamer1 player.Slot(2).Equipment = suppressor1 player.Slot(4).Equipment = forceblade1 @@ -902,6 +909,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ ChangeAmmoMessage(item_guid, unk1) => log.info("ChangeAmmo: " + msg) + case msg @ AvatarImplantMessage(_, _, _, _) => //(player_guid, unk1, unk2, implant) => + log.info("AvatarImplantMessage: " + msg) + case msg @ UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType) => log.info("UseItem: " + msg) // TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok)