diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
index 8c974d75..766c0d63 100644
--- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala
+++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
@@ -24,7 +24,6 @@ import net.psforever.objects.avatar.{
BattleRank,
Certification,
Cooldowns,
- Cosmetic,
Friend => AvatarFriend,
Ignored => AvatarIgnored,
Implant,
@@ -45,8 +44,9 @@ import net.psforever.objects.vital.HealFromImplant
import net.psforever.packet.game.objectcreate.{ObjectClass, RibbonBars}
import net.psforever.packet.game.{Friend => GameFriend, _}
import net.psforever.types.{
- CharacterVoice,
CharacterSex,
+ CharacterVoice,
+ Cosmetic,
ExoSuitType,
ImplantType,
LoadoutType,
@@ -806,7 +806,13 @@ object AvatarActor {
out.future
}
- def toAvatar(avatar: persistence.Avatar): Avatar =
+ def toAvatar(avatar: persistence.Avatar): Avatar = {
+ val bep = avatar.bep
+ val convertedCosmetics = if (BattleRank.showCosmetics(bep)) {
+ avatar.cosmetics.map(c => Cosmetic.valuesFromObjectCreateValue(c))
+ } else {
+ None
+ }
Avatar(
avatar.id,
BasicCharacterData(
@@ -816,10 +822,11 @@ object AvatarActor {
avatar.headId,
CharacterVoice(avatar.voiceId)
),
- avatar.bep,
+ bep,
avatar.cep,
- decoration = ProgressDecoration(cosmetics = avatar.cosmetics.map(c => Cosmetic.valuesFromObjectCreateValue(c)))
+ decoration = ProgressDecoration(cosmetics = convertedCosmetics)
)
+ }
}
class AvatarActor(
@@ -936,6 +943,7 @@ class AvatarActor(
case DeleteAvatar(id) =>
import ctx._
val performDeletion = for {
+ _ <- ctx.run(query[persistence.Shortcut].filter(_.avatarId == lift(id)).delete)
_ <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(id)).delete)
_ <- ctx.run(query[persistence.Loadout].filter(_.avatarId == lift(id)).delete)
_ <- ctx.run(query[persistence.Locker].filter(_.avatarId == lift(id)).delete)
@@ -1886,9 +1894,10 @@ class AvatarActor(
)
.onComplete {
case Success(_) =>
+ val zone = session.get.zone
avatarCopy(avatar.copy(decoration = avatar.decoration.copy(cosmetics = Some(cosmetics))))
- session.get.zone.AvatarEvents ! AvatarServiceMessage(
- session.get.zone.id,
+ zone.AvatarEvents ! AvatarServiceMessage(
+ zone.id,
AvatarAction.PlanetsideAttributeToAll(
session.get.player.GUID,
106,
@@ -2816,23 +2825,31 @@ class AvatarActor(
def setBep(bep: Long, modifier: ExperienceType): Unit = {
import ctx._
+ val current = BattleRank.withExperience(avatar.bep).value
+ val next = BattleRank.withExperience(bep).value
+ lazy val br24 = BattleRank.BR24.value
val result = for {
- _ <-
- if (BattleRank.withExperience(bep).value < BattleRank.BR24.value) setCosmetics(Set())
- else Future.successful(())
r <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatar.id)).update(_.bep -> lift(bep)))
} yield r
result.onComplete {
case Success(_) =>
val sess = session.get
val zone = sess.zone
- val pguid = sess.player.GUID
+ val zoneId = zone.id
+ val events = zone.AvatarEvents
+ val player = sess.player
+ val pguid = player.GUID
val localModifier = modifier
sessionActor ! SessionActor.SendResponse(BattleExperienceMessage(pguid, bep, localModifier))
- zone.AvatarEvents ! AvatarServiceMessage(
- zone.id,
- AvatarAction.PlanetsideAttributeToAll(pguid, 17, bep)
- )
+ events ! AvatarServiceMessage(zoneId, AvatarAction.PlanetsideAttributeToAll(pguid, 17, bep))
+ if (current < br24 && next >= br24 || current >= br24 && next < br24) {
+ setCosmetics(Set()).onComplete { _ =>
+ val evts = events
+ val name = player.Name
+ val guid = pguid
+ evts ! AvatarServiceMessage(name, AvatarAction.PlanetsideAttributeToAll(guid, 106, 1)) //set to no helmet
+ }
+ }
// when the level is reduced, take away any implants over the implant slot limit
val implants = avatar.implants.zipWithIndex.map {
case (implant, index) =>
@@ -2845,9 +2862,7 @@ class AvatarActor(
)
.onComplete {
case Success(_) =>
- sessionActor ! SessionActor.SendResponse(
- AvatarImplantMessage(pguid, ImplantAction.Remove, index, 0)
- )
+ sessionActor ! SessionActor.SendResponse(AvatarImplantMessage(pguid, ImplantAction.Remove, index, 0))
case Failure(exception) =>
log.error(exception)("db failure")
}
diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala
index d209d398..ce9f46f2 100644
--- a/src/main/scala/net/psforever/actors/session/ChatActor.scala
+++ b/src/main/scala/net/psforever/actors/session/ChatActor.scala
@@ -19,7 +19,7 @@ import scala.concurrent.duration._
import net.psforever.actors.zone.BuildingActor
import net.psforever.login.WorldSession
import net.psforever.objects.{Default, Player, Session}
-import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Cosmetic}
+import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank}
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.{Amenity, Building}
@@ -30,7 +30,7 @@ import net.psforever.services.{CavernRotationService, InterstellarClusterService
import net.psforever.services.chat.ChatService
import net.psforever.services.chat.ChatService.ChatChannel
import net.psforever.types.ChatMessageType.UNK_229
-import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, Vector3}
+import net.psforever.types.{ChatMessageType, Cosmetic, PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.util.{Config, PointOfInterest}
import net.psforever.zones.Zones
diff --git a/src/main/scala/net/psforever/objects/avatar/BattleRank.scala b/src/main/scala/net/psforever/objects/avatar/BattleRank.scala
index 6482dc1d..5564a295 100644
--- a/src/main/scala/net/psforever/objects/avatar/BattleRank.scala
+++ b/src/main/scala/net/psforever/objects/avatar/BattleRank.scala
@@ -1,7 +1,7 @@
package net.psforever.objects.avatar
import enumeratum.values.{IntEnum, IntEnumEntry}
-import net.psforever.packet.game.objectcreate.UniformStyle
+import net.psforever.types.UniformStyle
/** Battle ranks and their starting experience values
* Source: http://wiki.psforever.net/wiki/Battle_Rank
@@ -19,9 +19,11 @@ sealed abstract class BattleRank(val value: Int, val experience: Long) extends I
}
}
- def uniformStyle: UniformStyle.Value = {
+ def uniformStyle: UniformStyle = {
if (this.value >= BattleRank.BR25.value) {
UniformStyle.ThirdUpgrade
+ } else if (this.value == BattleRank.BR24.value) {
+ UniformStyle.SecondUpgradeBR24
} else if (this.value >= BattleRank.BR14.value) {
UniformStyle.SecondUpgrade
} else if (this.value >= BattleRank.BR7.value) {
@@ -30,7 +32,6 @@ sealed abstract class BattleRank(val value: Int, val experience: Long) extends I
UniformStyle.Normal
}
}
-
}
case object BattleRank extends IntEnum[BattleRank] {
@@ -103,4 +104,26 @@ case object BattleRank extends IntEnum[BattleRank] {
}
)
}
+
+ /**
+ * Given a number of battle experience points,
+ * determine if the resulting battle rank is sufficient to display cosmetic details on a player character.
+ * @see `BattleRank.withExperience`
+ * @param bep amount of battle experience
+ * @return `true`, if cosmetic elements will be visible;
+ * `false`, otherwise
+ */
+ def showCosmetics(bep: Long): Boolean = {
+ showCosmetics(BattleRank.withExperience(bep).uniformStyle)
+ }
+ /**
+ * Given a certain level of uniform dress corresponding to battle rank,
+ * determine if the resulting battle rank is sufficient to display cosmetic details on a player character.
+ * @param uniform the style of uniform
+ * @return `true`, if cosmetic elements will be visible;
+ * `false`, otherwise
+ */
+ def showCosmetics(uniform: UniformStyle): Boolean = {
+ uniform.value > UniformStyle.SecondUpgrade.value
+ }
}
diff --git a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
index f2da98a8..62b3eeff 100644
--- a/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
+++ b/src/main/scala/net/psforever/objects/definition/converter/AvatarConverter.scala
@@ -2,6 +2,7 @@
package net.psforever.objects.definition.converter
import net.psforever.objects.Player
+import net.psforever.objects.avatar.BattleRank
import net.psforever.objects.equipment.{Equipment, EquipmentSlot}
import net.psforever.packet.game.objectcreate._
import net.psforever.types.{ExoSuitType, GrenadeState, PlanetSideEmpire, PlanetSideGUID}
@@ -109,25 +110,41 @@ object AvatarConverter {
}
def MakeCharacterData(obj: Player): (Boolean, Boolean) => CharacterData = {
+ val avatar = obj.avatar
+ val uniformStyle = avatar.br.uniformStyle
+ val cosmetics = if (BattleRank.showCosmetics(uniformStyle)) {
+ avatar.decoration.cosmetics
+ } else {
+ None
+ }
val MaxArmor = obj.MaxArmor
+ val armor = if (MaxArmor == 0) {
+ 0
+ } else {
+ StatConverter.Health(obj.Armor, MaxArmor)
+ }
CharacterData(
StatConverter.Health(obj.Health, obj.MaxHealth),
- if (MaxArmor == 0) {
- 0
- } else {
- StatConverter.Health(obj.Armor, MaxArmor)
- },
- obj.avatar.br.uniformStyle,
+ armor,
+ uniformStyle,
0,
- obj.avatar.cr.value,
- obj.avatar.implants.flatten.filter(_.active).flatMap(_.definition.implantType.effect).toList,
- obj.avatar.decoration.cosmetics
+ avatar.cr.value,
+ avatar.implants.flatten.filter(_.active).flatMap(_.definition.implantType.effect).toList,
+ cosmetics
)
}
def MakeDetailedCharacterData(obj: Player): Option[Int] => DetailedCharacterData = {
- val maxOpt: Option[Long] = if (obj.ExoSuit == ExoSuitType.MAX) { Some(0L) }
- else { None }
+ val maxOpt: Option[Long] = if (obj.ExoSuit == ExoSuitType.MAX) {
+ Some(0L)
+ } else {
+ None
+ }
+ val cosmetics = if (BattleRank.BR24.experience >= obj.avatar.bep) {
+ obj.avatar.decoration.cosmetics
+ } else {
+ None
+ }
val ba: DetailedCharacterA = DetailedCharacterA(
obj.avatar.bep,
obj.avatar.cep,
@@ -164,7 +181,7 @@ object AvatarConverter {
Nil,
Nil,
unkC = false,
- obj.avatar.decoration.cosmetics
+ cosmetics
)
pad_length: Option[Int] => DetailedCharacterData(ba, bb(obj.avatar.bep, pad_length))(pad_length)
}
@@ -229,7 +246,7 @@ object AvatarConverter {
equip.GUID,
5,
DetailedLockerContainerData(
- CommonFieldData(PlanetSideEmpire.NEUTRAL, false, false, true, None, false, None, None, PlanetSideGUID(0)),
+ CommonFieldData(PlanetSideEmpire.NEUTRAL, bops=false, alternate=false, v1=true, None, jammered=false, None, None, PlanetSideGUID(0)),
None
)
))
diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
index 72e0b82a..014c46bd 100644
--- a/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
+++ b/src/main/scala/net/psforever/packet/game/objectcreate/CharacterData.scala
@@ -1,8 +1,9 @@
// Copyright (c) 2017 PSForever
package net.psforever.packet.game.objectcreate
-import net.psforever.objects.avatar.Cosmetic
+import net.psforever.objects.avatar.BattleRank
import net.psforever.packet.{Marshallable, PacketHelpers}
+import net.psforever.types.{Cosmetic, UniformStyle}
import scodec.codecs._
import scodec.{Attempt, Codec, Err}
import shapeless.{::, HNil}
@@ -26,24 +27,6 @@ object ImplantEffects extends Enumeration {
implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L)
}
-/**
- * Values for the four different color designs that impact a player's uniform.
- * Exo-suits get minor graphical updates at the following battle rank levels: seven (1), fourteen (2), and twenty-five (4).
- * The values 3 and 5 also exist and are visually descriptive to the third upgrade.
- */
-object UniformStyle extends Enumeration {
- type Type = Value
-
- val Normal = Value(0)
- val FirstUpgrade = Value(1)
- val SecondUpgrade = Value(2)
- val SecondUpgradeEx = Value(3)
- val ThirdUpgrade = Value(4)
- val ThirdUpgradeEx = Value(5)
-
- implicit val codec = PacketHelpers.createEnumerationCodec(this, uint(3))
-}
-
/**
* A representation of a portion of an avatar's `ObjectCreateDetailedMessage` packet data.
*
@@ -70,9 +53,9 @@ object UniformStyle extends Enumeration {
* cosmetic armor associated with the command rank will be applied automatically
* @param implant_effects the effects of implants that can be seen on a player's character;
* the number of entries equates to the number of effects applied;
- * the maximu number of effects is three
+ * the maximum number of effects is three
* @param cosmetics optional decorative features that are added to the player's head model by console/chat commands;
- * they become available at battle rank 24, but here they require the third uniform upgrade (rank 25);
+ * they become available at battle rank 24;
* these flags do not exist if they are not applicable
* @param is_backpack this player character should be depicted as a corpse;
* corpses are either coffins (defunct), backpacks (normal), or a pastry (festive);
@@ -83,7 +66,7 @@ object UniformStyle extends Enumeration {
final case class CharacterData(
health: Int,
armor: Int,
- uniform_upgrade: UniformStyle.Value,
+ uniform_upgrade: UniformStyle,
unk: Int,
command_rank: Int,
implant_effects: List[ImplantEffects.Value],
@@ -124,7 +107,7 @@ object CharacterData extends Marshallable[CharacterData] {
def apply(
health: Int,
armor: Int,
- uniform: UniformStyle.Value,
+ uniform: UniformStyle,
cr: Int,
implant_effects: List[ImplantEffects.Value],
cosmetics: Option[Set[Cosmetic]]
@@ -139,7 +122,7 @@ object CharacterData extends Marshallable[CharacterData] {
uint(3) :: //uniform_upgrade is actually interpreted as a 6u field, but the lower 3u seems to be discarded
("command_rank" | uintL(3)) ::
listOfN(uint2, "implant_effects" | ImplantEffects.codec) ::
- conditional(style.id > UniformStyle.SecondUpgrade.id, "cosmetics" | Cosmetic.codec)
+ ("cosmetics" | conditional(BattleRank.showCosmetics(style), Cosmetic.codec))
})
).exmap[CharacterData](
{
@@ -178,7 +161,7 @@ object CharacterData extends Marshallable[CharacterData] {
uint(3) :: //uniform_upgrade is actually interpreted as a 6u field, but the lower 3u seems to be discarded
("command_rank" | uintL(3)) ::
listOfN(uint2, "implant_effects" | ImplantEffects.codec) ::
- conditional(style.id > UniformStyle.SecondUpgrade.id, "cosmetics" | Cosmetic.codec)
+ ("cosmetics" | conditional(BattleRank.showCosmetics(style), Cosmetic.codec))
}
).exmap[CharacterData](
{
@@ -200,7 +183,12 @@ object CharacterData extends Marshallable[CharacterData] {
},
{
case CharacterData(_, _, uniform, unk, cr, implant_effects, cosmetics) =>
- Attempt.Successful(uniform :: unk :: cr :: implant_effects :: cosmetics :: HNil)
+ val cos = if (BattleRank.showCosmetics(uniform)) {
+ cosmetics.orElse(Some(Set[Cosmetic]()))
+ } else {
+ None
+ }
+ Attempt.Successful(uniform :: unk :: cr :: implant_effects :: cos :: HNil)
case _ =>
Attempt.Failure(Err("invalid character data; can not decode"))
diff --git a/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala b/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala
index a44a64bc..a341e8a7 100644
--- a/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala
+++ b/src/main/scala/net/psforever/packet/game/objectcreate/DetailedCharacterData.scala
@@ -2,9 +2,9 @@
package net.psforever.packet.game.objectcreate
import net.psforever.newcodecs.newcodecs
-import net.psforever.objects.avatar.{BattleRank, Certification, Cosmetic}
+import net.psforever.objects.avatar.{BattleRank, Certification}
import net.psforever.packet.{Marshallable, PacketHelpers}
-import net.psforever.types.{ExoSuitType, ImplantType}
+import net.psforever.types.{Cosmetic, ExoSuitType, ImplantType}
import scodec.{Attempt, Codec}
import scodec.codecs._
import shapeless.{::, HNil}
diff --git a/src/main/scala/net/psforever/objects/avatar/Cosmetic.scala b/src/main/scala/net/psforever/types/Cosmetic.scala
similarity index 95%
rename from src/main/scala/net/psforever/objects/avatar/Cosmetic.scala
rename to src/main/scala/net/psforever/types/Cosmetic.scala
index 8d980fe0..3a93391e 100644
--- a/src/main/scala/net/psforever/objects/avatar/Cosmetic.scala
+++ b/src/main/scala/net/psforever/types/Cosmetic.scala
@@ -1,8 +1,8 @@
-package net.psforever.objects.avatar
+package net.psforever.types
import enumeratum.values.{IntEnum, IntEnumEntry}
-import scodec.{Attempt, Codec}
import scodec.codecs.uint
+import scodec.{Attempt, Codec}
/** Avatar cosmetic options */
sealed abstract class Cosmetic(val value: Int) extends IntEnumEntry
@@ -58,9 +58,8 @@ case object Cosmetic extends IntEnum[Cosmetic] {
}
/** Codec for object create messages */
- implicit val codec: Codec[Set[Cosmetic]] = uint(5).exmap(
+ implicit val codec: Codec[Set[Cosmetic]] = uint(bits = 5).exmap(
value => Attempt.Successful(Cosmetic.valuesFromObjectCreateValue(value)),
cosmetics => Attempt.Successful(Cosmetic.valuesToObjectCreateValue(cosmetics))
)
-
}
diff --git a/src/main/scala/net/psforever/types/UniformStyle.scala b/src/main/scala/net/psforever/types/UniformStyle.scala
new file mode 100644
index 00000000..29334bc4
--- /dev/null
+++ b/src/main/scala/net/psforever/types/UniformStyle.scala
@@ -0,0 +1,28 @@
+// Copyright (c) 2017 PSForever
+package net.psforever.types
+
+import enumeratum.values.{IntEnum, IntEnumEntry}
+import net.psforever.packet.PacketHelpers
+import scodec.codecs._
+import scodec.Codec
+
+sealed abstract class UniformStyle(val value: Int) extends IntEnumEntry
+
+/**
+ * Values for the four different color designs that impact a player's uniform.
+ * Exo-suits get minor graphical updates at the following battle rank levels: seven (1), fourteen (2), and twenty-five (4).
+ * At battle rank twenty-four (3), the style does not update visually but switches to one suitable for display of cosmetics.
+ * The design for value 5 is visually descriptive of the third upgrade.
+ */
+object UniformStyle extends IntEnum[UniformStyle] {
+ val values: IndexedSeq[UniformStyle] = findValues
+
+ case object Normal extends UniformStyle(value = 0)
+ case object FirstUpgrade extends UniformStyle(value = 1)
+ case object SecondUpgrade extends UniformStyle(value = 2)
+ case object SecondUpgradeBR24 extends UniformStyle(value = 3)
+ case object ThirdUpgrade extends UniformStyle(value = 4)
+ case object ThirdUpgradeEx extends UniformStyle(value = 5)
+
+ implicit val codec: Codec[UniformStyle] = PacketHelpers.createIntEnumCodec(this, uint(bits = 3))
+}
diff --git a/src/test/scala/game/objectcreate/CharacterDataTest.scala b/src/test/scala/game/objectcreate/CharacterDataTest.scala
index e0898627..6422a9a9 100644
--- a/src/test/scala/game/objectcreate/CharacterDataTest.scala
+++ b/src/test/scala/game/objectcreate/CharacterDataTest.scala
@@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever
package game.objectcreate
-import net.psforever.objects.avatar.Cosmetic
import net.psforever.packet.PacketCoding
import net.psforever.packet.game.ObjectCreateMessage
import net.psforever.packet.game.objectcreate._
diff --git a/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala b/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala
index 496f6d59..fd92006c 100644
--- a/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala
+++ b/src/test/scala/game/objectcreatedetailed/DetailedCharacterDataTest.scala
@@ -1,7 +1,7 @@
// Copyright (c) 2017 PSForever
package game.objectcreatedetailed
-import net.psforever.objects.avatar.{BattleRank, Certification, Cosmetic}
+import net.psforever.objects.avatar.{BattleRank, Certification}
import org.specs2.mutable._
import net.psforever.packet._
import net.psforever.packet.game.ObjectCreateDetailedMessage
diff --git a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala
index de95c886..f384ea65 100644
--- a/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala
+++ b/src/test/scala/game/objectcreatevehicle/MountedVehiclesTest.scala
@@ -1,7 +1,6 @@
// Copyright (c) 2017 PSForever
package game.objectcreatevehicle
-import net.psforever.objects.avatar.Cosmetic
import net.psforever.packet._
import net.psforever.packet.game.objectcreate._
import net.psforever.packet.game.ObjectCreateMessage