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. * Using the definition for a piece of `Equipment` determine with which faction it aligns if it is a weapon.
* Only checks `Tool` objects. * 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 val
locker_container = new EquipmentDefinition(456) { locker_container = new EquipmentDefinition(456) {
Name = "locker container" 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 // Copyright (c) 2017 PSForever
package net.psforever.objects package net.psforever.objects
import net.psforever.objects.definition.ImplantDefinition import net.psforever.objects.definition.{ImplantDefinition, Stance}
import net.psforever.types.ImplantType 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> * <br>
* In total, players have three implant slots. * All implants slots start as "locked" and must be "unlocked" through battle rank advancement.
* At battle rank one (BR1), however, all of those slots are locked. * Only after it is "unlocked" may an implant be "installed" into the slot.
* The player earns implants at BR16, BR12, and BR18. * Upon installation, it undergoes an initialization period and then, after which, it is ready for user activation.
* A locked implant slot can not be used. * Being jammed de-activates the implant, put it into a state of "not being ready," and causes the initialization to repeat.
* (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.
*/ */
class ImplantSlot { class ImplantSlot {
/** is this slot available for holding an implant */ /** is this slot available for holding an implant */
private var unlocked : Boolean = false 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 */ /** what implant is currently installed in this slot; None if there is no implant currently installed */
private var installed : Option[ImplantType.Value] = None private var implant : Option[ImplantDefinition] = 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
def Unlocked : Boolean = unlocked def Unlocked : Boolean = unlocked
def Unlocked_=(lock : Boolean) : Boolean = { def Unlocked_=(lock : Boolean) : Boolean = {
unlocked = lock unlocked = lock || unlocked
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] = { def Active : Boolean = active
anImplant match {
case Some(module) => def Active_=(state : Boolean) : Boolean = {
Implant = module active = Initialized && state
case None => Active
installed = None }
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 Implant
} }
def Implant_=(anImplant : Implant) : Option[Implant] = { def Installed : Option[ImplantDefinition] = implant
implant = anImplant
installed = Some(anImplant.Definition.Type) def MaxTimer : Long = Implant match {
Implant 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 { object ImplantSlot {
private val default = new Implant(ImplantDefinition(ImplantType.RangeMagnifier))
def apply() : ImplantSlot = { def apply() : ImplantSlot = {
new ImplantSlot() new ImplantSlot()
} }

View file

@ -1,13 +1,14 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects 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.equipment.{Equipment, EquipmentSize}
import net.psforever.objects.inventory.{GridInventory, InventoryItem} import net.psforever.objects.inventory.{GridInventory, InventoryItem}
import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.game.PlanetSideGUID
import net.psforever.types._ import net.psforever.types._
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.collection.mutable
class Player(private val name : String, class Player(private val name : String,
private val faction : PlanetSideEmpire.Value, 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 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 val implants : Array[ImplantSlot] = Array.fill[ImplantSlot](3)(new ImplantSlot)
// private var tosRibbon : MeritCommendation.Value = MeritCommendation.None // private var tosRibbon : MeritCommendation.Value = MeritCommendation.None
@ -312,28 +316,40 @@ class Player(private val name : String,
exosuit = suit 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 Implants : Array[ImplantSlot] = implants
def Implant(slot : Int) : Option[ImplantType.Value] = { def Implant(slot : Int) : ImplantType.Value = {
if(-1 < slot && slot < implants.length) { implants(slot).Installed } else { None } if(-1 < slot && slot < implants.length) { implants(slot).Implant } else { ImplantType.None }
} }
def Implant(implantType : ImplantType.Value) : Option[Implant] = { def InstallImplant(implant : ImplantDefinition) : Boolean = {
implants.find(_.Installed.contains(implantType)) match { implants.find({p => p.Installed.contains(implant)}) match { //try to find the installed implant
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
case None => 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 false
} }
} }
@ -344,7 +360,7 @@ class Player(private val name : String,
} }
else { else {
val slot = iter.next val slot = iter.next
if(!slot.Unlocked || slot.Installed.contains(implantType)) { if(!slot.Unlocked || slot.Implant == implantType) {
None None
} }
else if(slot.Installed.isEmpty) { else if(slot.Installed.isEmpty) {
@ -357,7 +373,7 @@ class Player(private val name : String,
} }
def UninstallImplant(implantType : ImplantType.Value) : Boolean = { def UninstallImplant(implantType : ImplantType.Value) : Boolean = {
implants.find({slot => slot.Installed.contains(implantType)}) match { implants.find({slot => slot.Implant == implantType}) match {
case Some(slot) => case Some(slot) =>
slot.Implant = None slot.Implant = None
true true
@ -368,9 +384,9 @@ class Player(private val name : String,
def ResetAllImplants() : Unit = { def ResetAllImplants() : Unit = {
implants.foreach(slot => { implants.foreach(slot => {
slot.Implant match { slot.Installed match {
case Some(implant) => case Some(_) =>
implant.Reset() slot.Initialized = false
case None => ; case None => ;
} }
}) })
@ -570,7 +586,7 @@ object Player {
//hand over implants //hand over implants
(0 until 3).foreach(index => { (0 until 3).foreach(index => {
if(obj.Implants(index).Unlocked = player.Implants(index).Unlocked) { 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 //hand over knife

View file

@ -9,10 +9,13 @@ import scala.collection.mutable
* An `Enumeration` of a variety of poses or generalized movement. * An `Enumeration` of a variety of poses or generalized movement.
*/ */
object Stance extends Enumeration { object Stance extends Enumeration {
val Crouching, val
Standing, Crouching,
Walking, //not used, but should still be defined CrouchWalking, //not used, but should still be defined
Running = Value Standing,
Walking, //not used, but should still be defined
Running
= Value
} }
/** /**

View file

@ -1,11 +1,10 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.objects.definition.converter 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.objects.equipment.Equipment
import net.psforever.packet.game.PlanetSideGUID import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, ImplantEffects, ImplantEntry, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle}
import net.psforever.packet.game.objectcreate.{BasicCharacterData, CharacterAppearanceData, CharacterData, DetailedCharacterData, DrawnSlot, InternalSlot, InventoryData, PlacementData, RibbonBars, UniformStyle} import net.psforever.types.{GrenadeState, ImplantType}
import net.psforever.types.GrenadeState
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.util.{Success, Try} import scala.util.{Success, Try}
@ -17,11 +16,11 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
MakeAppearanceData(obj), MakeAppearanceData(obj),
obj.Health / obj.MaxHealth * 255, //TODO not precise obj.Health / obj.MaxHealth * 255, //TODO not precise
obj.Armor / obj.MaxArmor * 255, //TODO not precise obj.Armor / obj.MaxArmor * 255, //TODO not precise
UniformStyle.Normal, DressBattleRank(obj),
0, DressCommandRank(obj),
recursiveMakeImplantEffects(obj.Implants.iterator),
None, //TODO cosmetics None, //TODO cosmetics
None, //TODO implant effects InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)), //TODO is sorting necessary?
InventoryData(MakeHolsters(obj, BuildEquipment).sortBy(_.parentSlot)),
GetDrawnSlot(obj) GetDrawnSlot(obj)
) )
) )
@ -32,20 +31,21 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
Success( Success(
DetailedCharacterData( DetailedCharacterData(
MakeAppearanceData(obj), MakeAppearanceData(obj),
obj.BEP,
obj.CEP,
obj.MaxHealth, obj.MaxHealth,
obj.Health, obj.Health,
obj.Armor, obj.Armor,
1, 7, 7,
obj.MaxStamina, obj.MaxStamina,
obj.Stamina, 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 fte list
List.empty[String], //TODO tutorial list List.empty[String], //TODO tutorial list
InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)), InventoryData((MakeHolsters(obj, BuildDetailedEquipment) ++ MakeFifthSlot(obj) ++ MakeInventory(obj)).sortBy(_.parentSlot)),
GetDrawnSlot(obj) GetDrawnSlot(obj)
) )
) )
//TODO tidy this mess up
} }
/** /**
@ -64,8 +64,8 @@ class AvatarConverter extends ObjectCreateConverter[Player]() {
"", "",
0, 0,
obj.isBackpack, obj.isBackpack,
obj.Orientation.y.toInt, obj.Orientation.y,
obj.FacingYawUpper.toInt, obj.FacingYawUpper,
true, true,
GrenadeState.None, GrenadeState.None,
false, 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. * 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. * 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) 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] = { @tailrec private def recursiveMakeHolsters(iter : Iterator[EquipmentSlot], builder : ((Int, Equipment) => InternalSlot), list : List[InternalSlot] = Nil, index : Int = 0) : List[InternalSlot] = {
if(!iter.hasNext) { if(!iter.hasNext) {
list list

View file

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

View file

@ -1,11 +1,31 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate package net.psforever.packet.game.objectcreate
import net.psforever.newcodecs.newcodecs
import net.psforever.packet.{Marshallable, PacketHelpers} import net.psforever.packet.{Marshallable, PacketHelpers}
import net.psforever.types.{CertificationType, ImplantType}
import scodec.{Attempt, Codec} import scodec.{Attempt, Codec}
import scodec.codecs._ import scodec.codecs._
import shapeless.{::, HNil} 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. * 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> * 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> * <br>
* Divisions exist to make the data more manageable. * Divisions exist to make the data more manageable.
* The first division of data only manages the general appearance of the player's in-game model. * The first division of data only manages the general appearance of the player's in-game model.
* The second division (currently, the fields actually in this class) manages the status of the character as an avatar. * It is shared between `DetailedCharacterData` avatars and `CharacterData` player characters.
* The second division (of fields) manages the status of the character as an avatar.
* In general, it passes more thorough data about the character that the client can display to the owner of the client. * In general, it passes more thorough data about the character that the client can display to the owner of the client.
* For example, health is a full number, rather than a percentage. * For example, health is a full number, rather than a percentage.
* Just as prominent is the list of first time events and the list of completed tutorials. * Just as prominent is the list of first time events and the list of completed tutorials.
* The third subdivision is also exclusive to avatar-prepared characters and contains (omitted). * The third subdivision is also exclusive to avatar-prepared characters and contains (omitted).
* The fourth is the inventory (composed of `Direct`-type objects).<br> * The fourth is the inventory (composed of `Direct`-type objects).
* <br>
* Exploration:<br>
* Lots of analysis needed for the remainder of the byte data.
* @param appearance data about the avatar's basic aesthetics * @param appearance data about the avatar's basic aesthetics
* @param bep the avatar's battle experience points, which determines his Battle Rank
* @param cep the avatar's command experience points, which determines his Command Rank
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value; * @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value;
* range is 0-65535 * range is 0-65535
* @param health for `x / y` of hitpoints, this is the avatar's `x` value; * @param health for `x / y` of hitpoints, this is the avatar's `x` value;
@ -42,32 +62,25 @@ import shapeless.{::, HNil}
* range is 0-65535 * range is 0-65535
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value; * @param stamina for `x / y` of stamina points, this is the avatar's `x` value;
* range is 0-65535 * range is 0-65535
* @param unk4 na; * @param certs the `List` of active certifications
* defaults to 28 * @param implants the `List` of implant slots currently possessed by this avatar
* @param unk5 na;
* defaults to 4
* @param unk6 na;
* defaults to 44
* @param unk7 na;
* defaults to 84
* @param unk8 na;
* defaults to 104
* @param unk9 na;
* defaults to 1900
* @param firstTimeEvents the list of first time events performed by this avatar; * @param firstTimeEvents the list of first time events performed by this avatar;
* the size field is a 32-bit number; * the size field is a 32-bit number;
* the first entry may be padded * the first entry may be padded
* @param tutorials the list of tutorials completed by this avatar; * @param tutorials the `List` of tutorials completed by this avatar;
* the size field is a 32-bit number; * the size field is a 32-bit number;
* the first entry may be padded * the first entry may be padded
* @param inventory the avatar's inventory * @param inventory the avatar's inventory
* @param drawn_slot the holster that is initially drawn * @param drawn_slot the holster that is initially drawn
* @see `CharacterAppearanceData` * @see `CharacterAppearanceData`<br>
* @see `CharacterData` * `CharacterData`<br>
* @see `InventoryData` * `CertificationType`<br>
* @see `DrawnSlot` * `InventoryData`<br>
* `DrawnSlot`
*/ */
final case class DetailedCharacterData(appearance : CharacterAppearanceData, final case class DetailedCharacterData(appearance : CharacterAppearanceData,
bep : Long,
cep : Long,
healthMax : Int, healthMax : Int,
health : Int, health : Int,
armor : Int, armor : Int,
@ -76,177 +89,252 @@ final case class DetailedCharacterData(appearance : CharacterAppearanceData,
unk3 : Int, //7 unk3 : Int, //7
staminaMax : Int, staminaMax : Int,
stamina : Int, stamina : Int,
unk4 : Int, //28 certs : List[CertificationType.Value],
unk5 : Int, //4 implants : List[ImplantEntry],
unk6 : Int, //44
unk7 : Int, //84
unk8 : Int, //104
unk9 : Int, //1900
firstTimeEvents : List[String], firstTimeEvents : List[String],
tutorials : List[String], tutorials : List[String],
inventory : Option[InventoryData], inventory : Option[InventoryData],
drawn_slot : DrawnSlot.Value = DrawnSlot.None drawn_slot : DrawnSlot.Value = DrawnSlot.None
) extends ConstructorData { ) extends ConstructorData {
override def bitsize : Long = { override def bitsize : Long = {
//factor guard bool values into the base size, not its corresponding optional field //factor guard bool values into the base size, not corresponding optional fields, unless contained or enumerated
val appearanceSize = appearance.bitsize val appearanceSize = appearance.bitsize
val 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 val fteLen = firstTimeEvents.size //fte list
var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen) var eventListSize : Long = 32L + DetailedCharacterData.ftePadding(fteLen, implantPadding)
for(str <- firstTimeEvents) { for(str <- firstTimeEvents) {
eventListSize += StreamBitSize.stringBitSize(str) eventListSize += StreamBitSize.stringBitSize(str)
} }
val tutLen = tutorials.size //tutorial list val tutLen = tutorials.size //tutorial list
var tutorialListSize : Long = 32L + DetailedCharacterData.tutPadding(fteLen, tutLen) var tutorialListSize : Long = 32L + DetailedCharacterData.tutPadding(fteLen, tutLen, implantPadding)
for(str <- tutorials) { for(str <- tutorials) {
tutorialListSize += StreamBitSize.stringBitSize(str) tutorialListSize += StreamBitSize.stringBitSize(str)
} }
var inventorySize : Long = 0L //inventory val inventorySize : Long = if(inventory.isDefined) { //inventory
if(inventory.isDefined) { inventory.get.bitsize
inventorySize = inventory.get.bitsize
} }
713L + appearanceSize + eventListSize + tutorialListSize + inventorySize else {
0L
}
649L + appearanceSize + certSize + implantSize + eventListSize + tutorialListSize + inventorySize
} }
} }
object DetailedCharacterData extends Marshallable[DetailedCharacterData] { object DetailedCharacterData extends Marshallable[DetailedCharacterData] {
/** /**
* Overloaded constructor for `DetailedCharacterData` that skips all the unknowns by assigning defaulted values. * Overloaded constructor for `DetailedCharacterData` that requires an inventory and drops unknown values.
* It also allows for a not-optional inventory.
* @param appearance data about the avatar's basic aesthetics * @param appearance data about the avatar's basic aesthetics
* @param bep the avatar's battle experience points, which determines his Battle Rank
* @param cep the avatar's command experience points, which determines his Command Rank
* @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value * @param healthMax for `x / y` of hitpoints, this is the avatar's `y` value
* @param health for `x / y` of hitpoints, this is the avatar's `x` value * @param health for `x / y` of hitpoints, this is the avatar's `x` value
* @param armor for `x / y` of armor points, 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 staminaMax for `x / y` of stamina points, this is the avatar's `y` value
* @param stamina for `x / y` of stamina points, this is the avatar's `x` value * @param stamina for `x / y` of stamina points, this is the avatar's `x` value
* @param firstTimeEvents the list of first time events performed by this avatar * @param certs the `List` of active certifications
* @param tutorials the list of tutorials completed by this avatar * @param implants the `List` of implant slots currently possessed 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 firstTimeEvents the list of first time events performed by this avatar * @param firstTimeEvents the list of first time events performed by this avatar
* @param tutorials the list of tutorials completed by this avatar * @param tutorials the list of tutorials completed by this avatar
* @param inventory the avatar's inventory * @param inventory the avatar's inventory
* @param drawn_slot the holster that is initially drawn * @param drawn_slot the holster that is initially drawn
* @return a `DetailedCharacterData` object * @return a `DetailedCharacterData` object
*/ */
def apply(appearance : CharacterAppearanceData, 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 = 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, healthMax, health, armor, unk1, unk2, unk3, staminaMax, stamina, unk4, unk5, unk6, unk7, unk8, unk9, firstTimeEvents, tutorials, Some(inventory), drawn_slot) 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. * 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 first time events list
* @param len the length of the list * @param implantPadding the padding that resulted from implant entries
* @return the pad length in bits * @return the pad length in bits `0 <= n < 8`
*/ */
private def ftePadding(len : Long) : Int = { private def ftePadding(len : Long, implantPadding : Int) : Int = {
//TODO the parameters for this function are not correct
//TODO the proper padding length should reflect all variability in the stream prior to this point //TODO the proper padding length should reflect all variability in the stream prior to this point
if(len > 0) { if(len > 0) {
5 implantPadding
} }
else else {
0 0
}
} }
/** /**
* Get the padding of the first entry in the completed tutorials list. * Get the padding of the first entry in the completed tutorials list.<br>
* The padding will always be a number 0-7.<br>
* <br> * <br>
* The tutorials list follows the first time event list and that contains byte-aligned strings too. * The tutorials list follows the first time event list and also contains byte-aligned strings.
* While there will be more to the padding, this other list is important. * If the both lists are populated or empty at the same time, the first entry will not need padding.
* Any elements in that list causes the automatic byte-alignment of this list's first entry. * If the first time events list is unpopulated, but this list is populated, the first entry will need padding bits.
* @param len the length of the list * @param len the length of the first time events list
* @return the pad length in bits * @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 = { private def tutPadding(len : Long, len2 : Long, implantPadding : Int) : Int = {
if(len > 0) //automatic alignment from previous List if(len > 0) {
0 0 //automatic alignment from previous List
else if(len2 > 0) //need to align for elements }
5 else if(len2 > 0) {
else //both lists are empty implantPadding //need to align for elements
0 }
else {
0 //both lists are empty
}
} }
implicit val codec : Codec[DetailedCharacterData] = ( implicit val codec : Codec[DetailedCharacterData] = (
("appearance" | CharacterAppearanceData.codec) :: ("appearance" | CharacterAppearanceData.codec) >>:~ { app =>
ignore(160) :: ("bep" | uint32L) >>:~ { bep =>
("healthMax" | uint16L) :: ("cep" | uint32L) ::
("health" | uint16L) :: ignore(96) ::
ignore(1) :: ("healthMax" | uint16L) ::
("armor" | uint16L) :: ("health" | uint16L) ::
ignore(9) :: ignore(1) ::
("unk1" | uint8L) :: ("armor" | uint16L) ::
ignore(8) :: ignore(9) ::
("unk2" | uint4L) :: ("unk1" | uint8L) ::
("unk3" | uintL(3)) :: ignore(8) ::
("staminaMax" | uint16L) :: ("unk2" | uint4L) ::
("stamina" | uint16L) :: ("unk3" | uintL(3)) ::
ignore(149) :: ("staminaMax" | uint16L) ::
("unk4" | uint16L) :: ("stamina" | uint16L) ::
("unk5" | uint8L) :: ignore(147) ::
("unk6" | uint8L) :: ("certs" | listOfN(uint8L, CertificationType.codec)) ::
("unk7" | uint8L) :: optional(bool, uint32L) :: //ask about sample CCRIDER
("unk8" | uint8L) :: ignore(4) ::
("unk9" | uintL(12)) :: (("implants" | PacketHelpers.listOfNSized(numberOfImplantSlots(bep), implant_entry_codec)) >>:~ { implants =>
ignore(19) :: ignore(12) ::
(("firstTimeEvent_length" | uint32L) >>:~ { len => (("firstTimeEvent_length" | uint32L) >>:~ { len =>
conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned( ftePadding(len) )) :: conditional(len > 0, "firstTimeEvent_firstEntry" | PacketHelpers.encodedStringAligned(ftePadding(len, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) ::
("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) :: ("firstTimeEvent_list" | PacketHelpers.listOfNSized(len - 1, PacketHelpers.encodedString)) ::
(("tutorial_length" | uint32L) >>:~ { len2 => (("tutorial_length" | uint32L) >>:~ { len2 =>
conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned( tutPadding(len, len2) )) :: conditional(len2 > 0, "tutorial_firstEntry" | PacketHelpers.encodedStringAligned(tutPadding(len, len2, implantFieldPadding(implants, CharacterAppearanceData.altModelBit(app))))) ::
("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) :: ("tutorial_list" | PacketHelpers.listOfNSized(len2 - 1, PacketHelpers.encodedString)) ::
ignore(207) :: ignore(207) ::
optional(bool, "inventory" | InventoryData.codec_detailed) :: optional(bool, "inventory" | InventoryData.codec_detailed) ::
("drawn_slot" | DrawnSlot.codec) :: ("drawn_slot" | DrawnSlot.codec) ::
bool //usually false bool //usually false
}) })
}) })
})
}
}
).exmap[DetailedCharacterData] ( ).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 //prepend the displaced first elements to their lists
val fteList : List[String] = if(q.isDefined) { q.get :: r } else r val fteList : List[String] = if(fte0.isDefined) { fte0.get +: fte1 } else fte1
val tutList : List[String] = if(t.isDefined) { t.get :: u } else u val tutList : List[String] = if(tut0.isDefined) { tut0.get +: tut1 } else tut1
Attempt.successful(DetailedCharacterData(app, b, c, d, e, f, g, h, i, j, k, l, m, n, o, fteList, tutList, v, w)) 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 //shift the first elements off their lists
var fteListCopy = fteList val (firstEvent, fteListCopy) = fteList match {
var firstEvent : Option[String] = None case (f : String) +: Nil => (Some(f), Nil)
if(fteList.nonEmpty) { case ((f : String) +: (rest : List[String])) => (Some(f), rest)
firstEvent = Some(fteList.head) case Nil => (None, Nil)
fteListCopy = fteList.drop(1)
} }
var tutListCopy = tutList val (firstTutorial, tutListCopy) = tutList match {
var firstTutorial : Option[String] = None case (f : String) +: Nil => (Some(f), Nil)
if(tutList.nonEmpty) { case ((f : String) +: (rest : List[String])) => (Some(f), rest)
firstTutorial = Some(tutList.head) case Nil => (None, Nil)
tutListCopy = tutList.drop(1)
} }
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> * <br>
* Implant:<br> * Implant:<br>
* ` * `
* 00 - Regeneration (advanced_regen)<br> * 0 - Regeneration (advanced_regen)<br>
* 01 - Enhanced Targeting (targeting)<br> * 1 - Enhanced Targeting (targeting)<br>
* 02 - Audio Amplifier (audio_amplifier)<br> * 2 - Audio Amplifier (audio_amplifier)<br>
* 03 - Darklight Vision (darklight_vision)<br> * 3 - Darklight Vision (darklight_vision)<br>
* 04 - Melee Booster (melee_booster)<br> * 4 - Melee Booster (melee_booster)<br>
* 05 - Personal Shield (personal_shield)<br> * 5 - Personal Shield (personal_shield)<br>
* 06 - Range Magnifier (range_magnifier)<br> * 6 - Range Magnifier (range_magnifier)<br>
* 07 - Second Wind `(na)`<br> * 7 - Second Wind `(na)`<br>
* 08 - Sensor Shield (silent_run)<br> * 8 - Sensor Shield (silent_run)<br>
* 09 - Surge (surge)<br> * 9 - Surge (surge)
* ` * `
*/ */
object ImplantType extends Enumeration { object ImplantType extends Enumeration {
@ -34,5 +34,7 @@ object ImplantType extends Enumeration {
SilentRun, SilentRun,
Surge = Value Surge = Value
val None = Value(15) //TODO unconfirmed
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L) implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
} }

View file

@ -14,7 +14,7 @@ class DisplayedAwardMessageTest extends Specification {
PacketCoding.DecodePacket(string).require match { PacketCoding.DecodePacket(string).require match {
case DisplayedAwardMessage(player_guid, ribbon, bar) => case DisplayedAwardMessage(player_guid, ribbon, bar) =>
player_guid mustEqual PlanetSideGUID(1695) player_guid mustEqual PlanetSideGUID(1695)
ribbon mustEqual MeritCommendation.TwoYearTR ribbon mustEqual MeritCommendation.TwoYearVS
bar mustEqual RibbonBarsSlot.TermOfService bar mustEqual RibbonBarsSlot.TermOfService
case _ => case _ =>
ko ko
@ -22,7 +22,7 @@ class DisplayedAwardMessageTest extends Specification {
} }
"encode" in { "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 val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual string pkt mustEqual string

View file

@ -206,6 +206,8 @@ class ObjectCreateDetailedMessageTest extends Specification {
char.appearance.ribbons.middle mustEqual MeritCommendation.None char.appearance.ribbons.middle mustEqual MeritCommendation.None
char.appearance.ribbons.lower mustEqual MeritCommendation.None char.appearance.ribbons.lower mustEqual MeritCommendation.None
char.appearance.ribbons.tos mustEqual MeritCommendation.None char.appearance.ribbons.tos mustEqual MeritCommendation.None
char.bep mustEqual 0
char.cep mustEqual 0
char.healthMax mustEqual 100 char.healthMax mustEqual 100
char.health mustEqual 100 char.health mustEqual 100
char.armor mustEqual 50 //standard exosuit value char.armor mustEqual 50 //standard exosuit value
@ -214,12 +216,15 @@ class ObjectCreateDetailedMessageTest extends Specification {
char.unk3 mustEqual 7 char.unk3 mustEqual 7
char.staminaMax mustEqual 100 char.staminaMax mustEqual 100
char.stamina mustEqual 100 char.stamina mustEqual 100
char.unk4 mustEqual 28 char.certs.length mustEqual 7
char.unk5 mustEqual 4 char.certs.head mustEqual CertificationType.StandardAssault
char.unk6 mustEqual 44 char.certs(1) mustEqual CertificationType.MediumAssault
char.unk7 mustEqual 84 char.certs(2) mustEqual CertificationType.ATV
char.unk8 mustEqual 104 char.certs(3) mustEqual CertificationType.Harasser
char.unk9 mustEqual 1900 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.size mustEqual 4
char.firstTimeEvents.head mustEqual "xpe_sanctuary_help" char.firstTimeEvents.head mustEqual "xpe_sanctuary_help"
char.firstTimeEvents(1) mustEqual "xpe_th_firemodes" char.firstTimeEvents(1) mustEqual "xpe_th_firemodes"
@ -405,14 +410,25 @@ class ObjectCreateDetailedMessageTest extends Specification {
Nil Nil
val obj = DetailedCharacterData( val obj = DetailedCharacterData(
app, app,
0,
0,
100, 100, 100, 100,
50, 50,
1, 7, 7, 1, 7, 7,
100, 100, 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, "xpe_sanctuary_help" :: "xpe_th_firemodes" :: "used_beamer" :: "map13" :: Nil,
List.empty, List.empty,
InventoryData(inv), Some(InventoryData(inv)),
DrawnSlot.Pistol1 DrawnSlot.Pistol1
) )
val msg = ObjectCreateDetailedMessage(0x79, PlanetSideGUID(75), obj) 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.is_cloaking mustEqual false
pc.appearance.charging_pose mustEqual false pc.appearance.charging_pose mustEqual false
pc.appearance.on_zipline mustEqual false pc.appearance.on_zipline mustEqual false
pc.appearance.ribbons.upper mustEqual MeritCommendation.Loser4 pc.appearance.ribbons.upper mustEqual MeritCommendation.MarkovVeteran
pc.appearance.ribbons.middle mustEqual MeritCommendation.HeavyInfantry3 pc.appearance.ribbons.middle mustEqual MeritCommendation.HeavyInfantry4
pc.appearance.ribbons.lower mustEqual MeritCommendation.TankBuster6 pc.appearance.ribbons.lower mustEqual MeritCommendation.TankBuster7
pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearNC pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearTR
pc.health mustEqual 255 pc.health mustEqual 255
pc.armor mustEqual 253 pc.armor mustEqual 253
pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade
@ -787,10 +787,10 @@ class ObjectCreateMessageTest extends Specification {
pc.appearance.is_cloaking mustEqual false pc.appearance.is_cloaking mustEqual false
pc.appearance.charging_pose mustEqual false pc.appearance.charging_pose mustEqual false
pc.appearance.on_zipline mustEqual false pc.appearance.on_zipline mustEqual false
pc.appearance.ribbons.upper mustEqual MeritCommendation.Jacking pc.appearance.ribbons.upper mustEqual MeritCommendation.Jacking2
pc.appearance.ribbons.middle mustEqual MeritCommendation.ScavengerTR6 pc.appearance.ribbons.middle mustEqual MeritCommendation.ScavengerVS1
pc.appearance.ribbons.lower mustEqual MeritCommendation.AMSSupport4 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.health mustEqual 0
pc.armor mustEqual 0 pc.armor mustEqual 0
pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade
@ -1115,10 +1115,10 @@ class ObjectCreateMessageTest extends Specification {
GrenadeState.None, GrenadeState.None,
false, false, false, false, false, false,
RibbonBars( RibbonBars(
MeritCommendation.Loser4, MeritCommendation.MarkovVeteran,
MeritCommendation.HeavyInfantry3, MeritCommendation.HeavyInfantry4,
MeritCommendation.TankBuster6, MeritCommendation.TankBuster7,
MeritCommendation.SixYearNC MeritCommendation.SixYearTR
) )
), ),
255, 253, 255, 253,
@ -1172,10 +1172,10 @@ class ObjectCreateMessageTest extends Specification {
GrenadeState.None, GrenadeState.None,
false, false, false, false, false, false,
RibbonBars( RibbonBars(
MeritCommendation.Jacking, MeritCommendation.Jacking2,
MeritCommendation.ScavengerTR6, MeritCommendation.ScavengerVS1,
MeritCommendation.AMSSupport4, MeritCommendation.AMSSupport4,
MeritCommendation.SixYearTR MeritCommendation.SixYearVS
) )
), ),
0, 0, 0, 0,

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever // Copyright (c) 2017 PSForever
package objects package objects
import net.psforever.objects.Implant import net.psforever.objects.ImplantSlot
import net.psforever.objects.definition.{ImplantDefinition, Stance} import net.psforever.objects.definition.{ImplantDefinition, Stance}
import net.psforever.types.{ExoSuitType, ImplantType} import net.psforever.types.{ExoSuitType, ImplantType}
import org.specs2.mutable._ import org.specs2.mutable._
@ -16,61 +16,122 @@ class ImplantTest extends Specification {
sample.DurationChargeByExoSuit += ExoSuitType.Standard -> 1 sample.DurationChargeByExoSuit += ExoSuitType.Standard -> 1
sample.DurationChargeByStance += Stance.Running -> 1 sample.DurationChargeByStance += Stance.Running -> 1
"define" in { "ImplantDefinition" should {
sample.Initialization mustEqual 90000 "define" in {
sample.ActivationCharge mustEqual 3 sample.Initialization mustEqual 90000
sample.DurationChargeBase mustEqual 1 sample.ActivationCharge mustEqual 3
sample.DurationChargeByExoSuit(ExoSuitType.Agile) mustEqual 2 sample.DurationChargeBase mustEqual 1
sample.DurationChargeByExoSuit(ExoSuitType.Reinforced) mustEqual 2 sample.DurationChargeByExoSuit(ExoSuitType.Agile) mustEqual 2
sample.DurationChargeByExoSuit(ExoSuitType.Standard) mustEqual 1 sample.DurationChargeByExoSuit(ExoSuitType.Reinforced) mustEqual 2
sample.DurationChargeByExoSuit(ExoSuitType.Infiltration) mustEqual 0 //default value sample.DurationChargeByExoSuit(ExoSuitType.Standard) mustEqual 1
sample.DurationChargeByStance(Stance.Running) mustEqual 1 sample.DurationChargeByExoSuit(ExoSuitType.Infiltration) mustEqual 0 //default value
sample.DurationChargeByStance(Stance.Crouching) mustEqual 0 //default value sample.DurationChargeByStance(Stance.Running) mustEqual 1
sample.Type mustEqual ImplantType.SilentRun sample.DurationChargeByStance(Stance.Crouching) mustEqual 0 //default value
sample.Type mustEqual ImplantType.SilentRun
}
} }
"construct" in { "ImplantSlot" should {
val obj = new Implant(sample) "construct" in {
obj.Definition.Type mustEqual sample.Type val obj = new ImplantSlot
obj.Active mustEqual false obj.Unlocked mustEqual false
obj.Ready mustEqual false obj.Initialized mustEqual false
obj.Timer mustEqual 0 obj.Active mustEqual false
} obj.Implant mustEqual ImplantType.None
obj.Installed mustEqual None
}
"reset/init their timer" in { "load an implant when locked" in {
val obj = new Implant(sample) val obj = new ImplantSlot
obj.Timer mustEqual 0 obj.Unlocked mustEqual false
obj.Reset() obj.Implant mustEqual ImplantType.None
obj.Timer mustEqual 90000
}
"reset/init their readiness condition" in { obj.Implant = sample
val obj = new Implant(sample) obj.Implant mustEqual ImplantType.None
obj.Ready mustEqual false }
obj.Timer = 0
obj.Ready mustEqual true
obj.Reset()
obj.Ready mustEqual false
}
"not activate until they are ready" in { "load an implant when unlocked" in {
val obj = new Implant(sample) val obj = new ImplantSlot
obj.Active = true obj.Unlocked mustEqual false
obj.Active mustEqual false obj.Implant mustEqual ImplantType.None
obj.Timer = 0 sample.Type mustEqual ImplantType.SilentRun
obj.Active = true
obj.Active mustEqual true
}
"not cost energy while not active" in { obj.Unlocked = true
val obj = new Implant(sample) obj.Implant = sample
obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 0 obj.Implant mustEqual ImplantType.SilentRun
} }
"cost energy while active" in { "can not re-lock an unlocked implant slot" in {
val obj = new Implant(sample) val obj = new ImplantSlot
obj.Timer = 0 obj.Unlocked mustEqual false
obj.Active = true
obj.Charge(ExoSuitType.Reinforced, Stance.Running) mustEqual 4 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 // Copyright (c) 2017 PSForever
package objects 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.definition.{ImplantDefinition, SimpleItemDefinition}
import net.psforever.objects.equipment.EquipmentSize import net.psforever.objects.equipment.EquipmentSize
import net.psforever.types.{CharacterGender, ExoSuitType, ImplantType, PlanetSideEmpire} import net.psforever.types.{CharacterGender, ExoSuitType, ImplantType, PlanetSideEmpire}
@ -106,31 +106,39 @@ class PlayerTest extends Specification {
obj.LastDrawnSlot mustEqual 1 obj.LastDrawnSlot mustEqual 1
} }
"install no implants until a slot is unlocked" in { "install an implant" in {
val testplant : Implant = Implant(ImplantDefinition(1)) val testplant : ImplantDefinition = ImplantDefinition(1)
val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5) 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.Implants(0).Unlocked = true
obj.InstallImplant(testplant) obj.InstallImplant(testplant) mustEqual true
obj.Implant(0) mustEqual Some(testplant.Definition.Type) obj.Implants.find({p => p.Implant == ImplantType(1)}) match { //find the installed implant
obj.Implant(ImplantType(1)) mustEqual Some(testplant) 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 { "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) val obj = new Player("Chord", PlanetSideEmpire.TR, CharacterGender.Male, 0, 5)
obj.Implants(0).Unlocked = true obj.Implants(0).Unlocked = true
obj.InstallImplant(testplant) obj.InstallImplant(testplant) mustEqual true
obj.Implant(ImplantType(1)) mustEqual Some(testplant) obj.Implants(0).Installed mustEqual Some(testplant)
obj.UninstallImplant(ImplantType(1)) obj.UninstallImplant(testplant.Type)
obj.Implant(0) mustEqual None obj.Implants(0).Installed mustEqual None
obj.Implant(ImplantType(1)) mustEqual None
} }
"administrate" in { "administrate" in {

View file

@ -601,6 +601,13 @@ class WorldSessionActor extends Actor with MDCContextAware {
player = Player("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1) player = Player("IlllIIIlllIlIllIlllIllI", PlanetSideEmpire.VS, CharacterGender.Female, 41, 1)
player.Position = Vector3(3674.8438f, 2726.789f, 91.15625f) player.Position = Vector3(3674.8438f, 2726.789f, 91.15625f)
player.Orientation = Vector3(0f, 0f, 90f) 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(0).Equipment = beamer1
player.Slot(2).Equipment = suppressor1 player.Slot(2).Equipment = suppressor1
player.Slot(4).Equipment = forceblade1 player.Slot(4).Equipment = forceblade1
@ -902,6 +909,9 @@ class WorldSessionActor extends Actor with MDCContextAware {
case msg @ ChangeAmmoMessage(item_guid, unk1) => case msg @ ChangeAmmoMessage(item_guid, unk1) =>
log.info("ChangeAmmo: " + msg) 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) => case msg @ UseItemMessage(avatar_guid, unk1, object_guid, unk2, unk3, unk4, unk5, unk6, unk7, unk8, itemType) =>
log.info("UseItem: " + msg) log.info("UseItem: " + msg)
// TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok) // TODO: Not all fields in the response are identical to source in real packet logs (but seems to be ok)