Merge branch 'master' into continents

This commit is contained in:
Fate-JH 2017-09-23 21:57:48 -04:00 committed by GitHub
commit bf178a1b34
16 changed files with 842 additions and 398 deletions

View file

@ -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"

View file

@ -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).<br>
* <br>
* 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)
}
}

View file

@ -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.<br>
* A slot "on the player" into which an implant is installed.
* In total, players have three implant slots.<br>
* <br>
* 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.<br>
* <br>
* 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()
}

View file

@ -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

View file

@ -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
}
/**

View file

@ -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

View file

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

View file

@ -1,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.<br>
@ -15,16 +35,16 @@ import shapeless.{::, HNil}
* <br>
* Divisions exist to make the data more manageable.
* The first division of data only manages the general appearance of the player's in-game model.
* The second division (currently, the fields actually in this class) manages the status of the character as an avatar.
* It is shared between `DetailedCharacterData` avatars and `CharacterData` player characters.
* The second division (of fields) manages the status of the character as an avatar.
* In general, it passes more thorough data about the character that the client can display to the owner of the client.
* For example, health is a full number, rather than a percentage.
* Just as prominent is the list of first time events and the list of completed tutorials.
* The third subdivision is also exclusive to avatar-prepared characters and contains (omitted).
* The fourth is the inventory (composed of `Direct`-type objects).<br>
* <br>
* Exploration:<br>
* Lots of analysis needed for the remainder of the byte data.
* The fourth is the inventory (composed of `Direct`-type objects).
* @param appearance data about the avatar's basic aesthetics
* @param bep the avatar's battle experience points, which determines his Battle Rank
* @param cep the avatar's command experience points, which determines his Command Rank
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value;
* range is 0-65535
* @param health for `x / y` of hitpoints, this is the avatar's `x` value;
@ -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`<br>
* `CharacterData`<br>
* `CertificationType`<br>
* `InventoryData`<br>
* `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.<br>
* Get the padding of the first entry in the completed tutorials list.<br>
* <br>
* 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)
}
)
}

View file

@ -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.<br>
* <br>
* 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.<br>
* <br>
* 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)
}

View file

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

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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
}
}
}

View file

@ -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 {

View file

@ -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)