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)