mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
Wearing Your Accomplishments on Your Sleeve (#988)
* ability to swap merit commendation ribbons on shoulder and have other players see it * ability to swap merit commendation ribbons on shoulder and have other players see it * VehicleControlTest from elsewhere * giver all non-Exclusive ribbons that would become available to faction/sex and allow modification of the ribbon bars * awards only need to load during login activities; fixing a few awards that were not being allocated correctly * wrong conditional for sex check
This commit is contained in:
parent
0d8c717b73
commit
e5fe6cf89a
|
|
@ -1,3 +1,4 @@
|
|||
// Copyright (c) 2019 PSForever
|
||||
package net.psforever.actors.session
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
|
@ -16,7 +17,7 @@ import net.psforever.objects._
|
|||
import net.psforever.objects.ballistics.PlayerSource
|
||||
import net.psforever.objects.locker.LockerContainer
|
||||
import net.psforever.objects.vital.HealFromImplant
|
||||
import net.psforever.packet.game.objectcreate.ObjectClass
|
||||
import net.psforever.packet.game.objectcreate.{ObjectClass, RibbonBars}
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types._
|
||||
import net.psforever.util.Database._
|
||||
|
|
@ -178,6 +179,8 @@ object AvatarActor {
|
|||
/** Set cosmetics. Only allowed for BR24 or higher. */
|
||||
final case class SetCosmetics(personalStyles: Set[Cosmetic]) extends Command
|
||||
|
||||
final case class SetRibbon(ribbon: MeritCommendation.Value, bar: RibbonBarSlot.Value) extends Command
|
||||
|
||||
private case class ServiceManagerLookupResult(result: ServiceManager.LookupResult) extends Command
|
||||
|
||||
final case class SetStamina(stamina: Int) extends Command
|
||||
|
|
@ -188,6 +191,14 @@ object AvatarActor {
|
|||
|
||||
final case class AvatarLoginResponse(avatar: Avatar)
|
||||
|
||||
def changeRibbons(ribbons: RibbonBars, ribbon: MeritCommendation.Value, bar: RibbonBarSlot.Value): RibbonBars = {
|
||||
bar match {
|
||||
case RibbonBarSlot.Top => ribbons.copy(upper = ribbon)
|
||||
case RibbonBarSlot.Middle => ribbons.copy(middle = ribbon)
|
||||
case RibbonBarSlot.Bottom => ribbons.copy(lower = ribbon)
|
||||
case RibbonBarSlot.TermOfService => ribbons.copy(tos = ribbon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AvatarActor(
|
||||
|
|
@ -379,7 +390,7 @@ class AvatarActor(
|
|||
))
|
||||
// if we need to start stamina regeneration
|
||||
tryRestoreStaminaForSession(stamina = 1) match {
|
||||
case Some(sess) =>
|
||||
case Some(_) =>
|
||||
defaultStaminaRegen(initialDelay = 0.5f seconds)
|
||||
case _ => ;
|
||||
}
|
||||
|
|
@ -1003,6 +1014,22 @@ class AvatarActor(
|
|||
case SetCosmetics(cosmetics) =>
|
||||
setCosmetics(cosmetics)
|
||||
Behaviors.same
|
||||
|
||||
case SetRibbon(ribbon, bar) =>
|
||||
val previousRibbonBars = avatar.ribbonBars
|
||||
val useRibbonBars = Seq(previousRibbonBars.upper, previousRibbonBars.middle, previousRibbonBars.lower)
|
||||
.indexWhere { _ == ribbon } match {
|
||||
case -1 => previousRibbonBars
|
||||
case n => AvatarActor.changeRibbons(previousRibbonBars, MeritCommendation.None, RibbonBarSlot(n))
|
||||
}
|
||||
replaceAvatar(avatar.copy(ribbonBars = AvatarActor.changeRibbons(useRibbonBars, ribbon, bar)))
|
||||
val player = session.get.player
|
||||
val zone = player.Zone
|
||||
zone.AvatarEvents ! AvatarServiceMessage(
|
||||
zone.id,
|
||||
AvatarAction.SendResponse(Service.defaultPlayerGUID, DisplayedAwardMessage(player.GUID, ribbon, bar))
|
||||
)
|
||||
Behaviors.same
|
||||
}
|
||||
.receiveSignal {
|
||||
case (_, PostStop) =>
|
||||
|
|
|
|||
|
|
@ -159,6 +159,8 @@ object SessionActor {
|
|||
tool: ConstructionItem,
|
||||
index: Int
|
||||
)
|
||||
|
||||
private final case class AvatarAwardMessageBundle(bundle: Iterable[Iterable[PlanetSidePacket]], delay: Long)
|
||||
}
|
||||
|
||||
class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], connectionId: String, sessionId: Long)
|
||||
|
|
@ -279,6 +281,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
var heightTrend: Boolean = false //up = true, down = false
|
||||
var heightHistory: Float = 0f
|
||||
val collisionHistory: mutable.HashMap[ActorRef, Long] = mutable.HashMap()
|
||||
var populateAvatarAwardRibbonsFunc: (Int, Long) => Unit = setupAvatarAwardMessageDelivery
|
||||
|
||||
var clientKeepAlive: Cancellable = Default.Cancellable
|
||||
var progressBarUpdate: Cancellable = Default.Cancellable
|
||||
|
|
@ -1347,6 +1350,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
}
|
||||
}
|
||||
|
||||
case SessionActor.AvatarAwardMessageBundle(pkts, delay) =>
|
||||
performAvatarAwardMessageDelivery(pkts, delay)
|
||||
|
||||
case ResponseToSelf(pkt) =>
|
||||
sendResponse(pkt)
|
||||
|
||||
|
|
@ -3157,8 +3163,9 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
if (tplayer.ExoSuit == ExoSuitType.MAX) {
|
||||
sendResponse(PlanetsideAttributeMessage(guid, 7, tplayer.Capacitor.toLong))
|
||||
}
|
||||
//AvatarAwardMessage
|
||||
//DisplayAwardMessage
|
||||
// AvatarAwardMessage
|
||||
populateAvatarAwardRibbonsFunc(1, 20L)
|
||||
|
||||
sendResponse(PlanetsideStringAttributeMessage(guid, 0, "Outfit Name"))
|
||||
//squad stuff (loadouts, assignment)
|
||||
squadSetup()
|
||||
|
|
@ -3251,6 +3258,83 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
HandleSetCurrentAvatar(tplayer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Don't extract the award advancement information from a player character upon respawning or zoning.
|
||||
* You only need to perform that population once at login.
|
||||
* @param bundleSize it doesn't matter
|
||||
* @param delay it doesn't matter
|
||||
*/
|
||||
def skipAvatarAwardMessageDelivery(bundleSize: Int, delay: Long): Unit = { }
|
||||
|
||||
/**
|
||||
* Extract the award advancement information from a player character, and
|
||||
* coordinate timed dispatches of groups of packets.
|
||||
* @param bundleSize divide packets into groups of this size
|
||||
* @param delay dispatch packet divisions in intervals
|
||||
*/
|
||||
def setupAvatarAwardMessageDelivery(bundleSize: Int, delay: Long): Unit = {
|
||||
setupAvatarAwardMessageDelivery(player, bundleSize, delay)
|
||||
populateAvatarAwardRibbonsFunc = skipAvatarAwardMessageDelivery
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the merit commendation advancement information from a player character,
|
||||
* filter unnecessary or not applicable statistics,
|
||||
* translate the information into packet data, and
|
||||
* coordinate timed dispatches of groups of packets.
|
||||
* @param tplayer the player character
|
||||
* @param bundleSize divide packets into groups of this size
|
||||
* @param delay dispatch packet divisions in intervals
|
||||
*/
|
||||
def setupAvatarAwardMessageDelivery(tplayer: Player, bundleSize: Int, delay: Long): Unit = {
|
||||
val date: Int = (System.currentTimeMillis() / 1000L).toInt - 604800 //last week, in seconds
|
||||
performAvatarAwardMessageDelivery(
|
||||
Award
|
||||
.values
|
||||
.filter { merit =>
|
||||
val label = merit.value
|
||||
val alignment = merit.alignment
|
||||
if (merit.category == AwardCategory.Exclusive) false
|
||||
else if (alignment != PlanetSideEmpire.NEUTRAL && alignment != player.Faction) false
|
||||
else if (label.contains("Male") && player.Sex != CharacterSex.Male) false
|
||||
else if (label.contains("Female") && player.Sex != CharacterSex.Female) false
|
||||
else true
|
||||
}
|
||||
.flatMap { merit =>
|
||||
merit.progression.map { level =>
|
||||
AvatarAwardMessage(level.commendation, AwardCompletion(date))
|
||||
}
|
||||
}
|
||||
.grouped(bundleSize)
|
||||
.iterator
|
||||
.to(Iterable),
|
||||
delay
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinate timed dispatches of groups of packets.
|
||||
* @param messageBundles groups of packets to be dispatched
|
||||
* @param delay dispatch packet divisions in intervals
|
||||
*/
|
||||
def performAvatarAwardMessageDelivery(
|
||||
messageBundles: Iterable[Iterable[PlanetSidePacket]],
|
||||
delay: Long
|
||||
): Unit = {
|
||||
messageBundles match {
|
||||
case Nil => ;
|
||||
case x :: Nil =>
|
||||
x.foreach { sendResponse }
|
||||
case x :: xs =>
|
||||
x.foreach { sendResponse }
|
||||
context.system.scheduler.scheduleOnce(
|
||||
delay.milliseconds,
|
||||
self,
|
||||
SessionActor.AvatarAwardMessageBundle(xs, delay)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* These messages are dispatched when first starting up the client and connecting to the server for the first time.
|
||||
* While many of these messages will be reused for other situations, they appear in this order only during startup.
|
||||
|
|
@ -5966,12 +6050,16 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
|
|||
log.error(s"InvalidTerrain: ${player.Name} is complaining about a thing@$vehicle_guid that can not be found")
|
||||
}
|
||||
|
||||
case msg @ ActionCancelMessage(u1, u2, u3) =>
|
||||
case ActionCancelMessage(_, _, _) =>
|
||||
progressBarUpdate.cancel()
|
||||
progressBarValue = None
|
||||
|
||||
case TradeMessage(trade) =>
|
||||
log.info(s"${player.Name} wants to trade, for some reason - $trade")
|
||||
log.trace(s"${player.Name} wants to trade for some reason - $trade")
|
||||
|
||||
case DisplayedAwardMessage(_, ribbon, bar) =>
|
||||
log.trace(s"${player.Name} changed the $bar displayed award ribbon to $ribbon")
|
||||
avatarActor ! AvatarActor.SetRibbon(ribbon, bar)
|
||||
|
||||
case _ =>
|
||||
log.warn(s"Unhandled GamePacket $pkt")
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// Copyright (c) 2017 PSForever
|
||||
package net.psforever.objects.avatar
|
||||
|
||||
import net.psforever.objects.definition.{AvatarDefinition, BasicDefinition}
|
||||
|
|
@ -6,6 +7,7 @@ import net.psforever.objects.inventory.LocallyRegisteredInventory
|
|||
import net.psforever.objects.loadouts.{Loadout, SquadLoadout}
|
||||
import net.psforever.objects.locker.{LockerContainer, LockerEquipment}
|
||||
import net.psforever.objects.{GlobalDefinitions, OffhandEquipmentSlot}
|
||||
import net.psforever.packet.game.objectcreate.RibbonBars
|
||||
import net.psforever.types._
|
||||
import org.joda.time.{Duration, LocalDateTime, Seconds}
|
||||
|
||||
|
|
@ -94,6 +96,7 @@ case class Avatar(
|
|||
stamina: Int = 100,
|
||||
fatigued: Boolean = false,
|
||||
cosmetics: Option[Set[Cosmetic]] = None,
|
||||
ribbonBars: RibbonBars = RibbonBars(),
|
||||
certifications: Set[Certification] = Set(),
|
||||
loadouts: Seq[Option[Loadout]] = Seq.fill(20)(None),
|
||||
squadLoadouts: Seq[Option[SquadLoadout]] = Seq.fill(10)(None),
|
||||
|
|
|
|||
1201
src/main/scala/net/psforever/objects/avatar/Award.scala
Normal file
1201
src/main/scala/net/psforever/objects/avatar/Award.scala
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -105,7 +105,7 @@ object AvatarConverter {
|
|||
unk7 = false,
|
||||
on_zipline = None
|
||||
)
|
||||
CharacterAppearanceData(aa, ab, RibbonBars())
|
||||
CharacterAppearanceData(aa, ab, obj.avatar.ribbonBars)
|
||||
}
|
||||
|
||||
def MakeCharacterData(obj: Player): (Boolean, Boolean) => CharacterData = {
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ class CharacterSelectConverter extends AvatarConverter {
|
|||
unk7 = false,
|
||||
on_zipline = None
|
||||
)
|
||||
CharacterAppearanceData(aa, ab, RibbonBars())
|
||||
CharacterAppearanceData(aa, ab, obj.avatar.ribbonBars)
|
||||
}
|
||||
|
||||
private def MakeDetailedCharacterData(obj: Player): Option[Int] => DetailedCharacterData = {
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class CorpseConverter extends AvatarConverter {
|
|||
unk7 = false,
|
||||
on_zipline = None
|
||||
)
|
||||
CharacterAppearanceData(aa, ab, RibbonBars())
|
||||
CharacterAppearanceData(aa, ab, obj.avatar.ribbonBars)
|
||||
}
|
||||
|
||||
private def MakeDetailedCharacterData(obj: Player): Option[Int] => DetailedCharacterData = {
|
||||
|
|
|
|||
|
|
@ -2,37 +2,73 @@
|
|||
package net.psforever.packet.game
|
||||
|
||||
import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket}
|
||||
import net.psforever.types.MeritCommendation
|
||||
import scodec.Codec
|
||||
import scodec.codecs._
|
||||
import shapeless.{::, HNil}
|
||||
|
||||
import scala.annotation.switch
|
||||
|
||||
abstract class AwardOption(val code: Int) {
|
||||
def unk1: Long
|
||||
def unk2: Long
|
||||
/**
|
||||
* Base class for all merit commendation advancement stages.
|
||||
*/
|
||||
sealed trait AwardOption {
|
||||
def value: Long
|
||||
def completion: Long
|
||||
}
|
||||
|
||||
final case class AwardOptionZero(unk1: Long, unk2: Long) extends AwardOption(code = 0)
|
||||
|
||||
final case class AwardOptionOne(unk1: Long) extends AwardOption(code = 1) {
|
||||
def unk2: Long = 0L
|
||||
/**
|
||||
* Display this award's development progress.
|
||||
* @param value the current count towards this award
|
||||
* @param completion the target (maximum) count
|
||||
*/
|
||||
final case class AwardProgress(value: Long, completion: Long) extends AwardOption
|
||||
/**
|
||||
* Display this award's qualification progress.
|
||||
* The process is the penultimate conditions necessary for award completion,
|
||||
* separate from the aforementioned progress.
|
||||
* This is almost always a kill streak,
|
||||
* where the user must terminate a certain numbers of enemies without dying.
|
||||
* @param value the current count towards this award
|
||||
*/
|
||||
final case class AwardQualificationProgress(value: Long) extends AwardOption {
|
||||
/** zero'd as the value is not reported here */
|
||||
def completion: Long = 0L
|
||||
}
|
||||
|
||||
final case class AwardOptionTwo(unk1: Long) extends AwardOption(code = 3) {
|
||||
def unk2: Long = 0L
|
||||
/**
|
||||
* Display this award as completed.
|
||||
* @param value the date (mm/dd/yyyy) that the award was achieved in POSIX seconds;
|
||||
* that's `System.currentTimeMillis() / 1000`
|
||||
*/
|
||||
final case class AwardCompletion(value: Long) extends AwardOption {
|
||||
/** same as the parameter value */
|
||||
def completion: Long = value
|
||||
}
|
||||
|
||||
/**
|
||||
* na
|
||||
* @param unk1 na
|
||||
* @param unk2 na
|
||||
* @param unk3 na
|
||||
* Dispatched from the server to load information about a character's merit commendation awards progress.<br>
|
||||
* <br>
|
||||
* The three stages of a merit commendation award are: progress, qualification, and completion.
|
||||
* The progress stage and the qualification stage have their own development conditions.
|
||||
* Ocassionally, the development is nonexistent and the award is merely an on/off switch.
|
||||
* Occasionally, there is no qualification requirement and the award merely advances in the progress stage
|
||||
* then transitions directly from progress to completion.
|
||||
* Completion information is available from the character info / achievements tab
|
||||
* and takes the form of ribbons associated with the merit commendation at a given rank
|
||||
* and the date that rank was attained.
|
||||
* Progress and qualification information are visible from the character info / achievements / award progress window
|
||||
* and take the form of the name and rank of the merit commendation
|
||||
* and two numbers that indicate the current and the goal towards the next stage.
|
||||
* The completion stage is also visible from this window
|
||||
* and will take the form of the same name and rank of the merit commendation indicated as "Completed" as of a date.
|
||||
* @see `MeritCommendation.Value`
|
||||
* @param merit_commendation the award and rank
|
||||
* @param state the current state of the award advancement
|
||||
* @param unk na;
|
||||
* 0 and 1 are the possible values;
|
||||
* 0 is the common value
|
||||
*/
|
||||
final case class AvatarAwardMessage(
|
||||
unk1: Long,
|
||||
unk2: AwardOption,
|
||||
unk3: Int
|
||||
merit_commendation: MeritCommendation.Value,
|
||||
state: AwardOption,
|
||||
unk: Int
|
||||
)
|
||||
extends PlanetSideGamePacket {
|
||||
type Packet = AvatarAwardMessage
|
||||
|
|
@ -41,61 +77,67 @@ final case class AvatarAwardMessage(
|
|||
}
|
||||
|
||||
object AvatarAwardMessage extends Marshallable[AvatarAwardMessage] {
|
||||
private val codec_one: Codec[AwardOptionOne] = {
|
||||
def apply(meritCommendation: MeritCommendation.Value, state: AwardOption):AvatarAwardMessage =
|
||||
AvatarAwardMessage(meritCommendation, state, unk = 0)
|
||||
|
||||
private val qualification_codec: Codec[AwardOption] = {
|
||||
uint32L.hlist
|
||||
}.xmap[AwardOptionOne](
|
||||
}.xmap[AwardOption](
|
||||
{
|
||||
case a :: HNil => AwardOptionOne(a)
|
||||
case a :: HNil => AwardQualificationProgress(a)
|
||||
},
|
||||
{
|
||||
case AwardOptionOne(a) => a :: HNil
|
||||
case AwardQualificationProgress(a) => a :: HNil
|
||||
}
|
||||
)
|
||||
|
||||
private val codec_two: Codec[AwardOptionTwo] = {
|
||||
private val completion_codec: Codec[AwardOption] = {
|
||||
uint32L.hlist
|
||||
}.xmap[AwardOptionTwo](
|
||||
}.xmap[AwardOption](
|
||||
{
|
||||
case a :: HNil => AwardOptionTwo(a)
|
||||
case a :: HNil => AwardCompletion(a)
|
||||
},
|
||||
{
|
||||
case AwardOptionTwo(a) => a :: HNil
|
||||
case AwardCompletion(a) => a :: HNil
|
||||
}
|
||||
)
|
||||
|
||||
private val codec_zero: Codec[AwardOptionZero] = {
|
||||
private val progress_codec: Codec[AwardOption] = {
|
||||
uint32L :: uint32L
|
||||
}.xmap[AwardOptionZero](
|
||||
}.xmap[AwardOption](
|
||||
{
|
||||
case a :: b :: HNil => AwardOptionZero(a, b)
|
||||
case a :: b :: HNil => AwardProgress(a, b)
|
||||
},
|
||||
{
|
||||
case AwardOptionZero(a, b) => a :: b :: HNil
|
||||
case AwardProgress(a, b) => a :: b :: HNil
|
||||
}
|
||||
)
|
||||
|
||||
private def selectAwardOption(code: Int): Codec[AwardOption] = {
|
||||
((code: @switch) match {
|
||||
case 2 | 3 => codec_two
|
||||
case 1 => codec_one
|
||||
case 0 => codec_zero
|
||||
}).asInstanceOf[Codec[AwardOption]]
|
||||
}
|
||||
|
||||
implicit val codec: Codec[AvatarAwardMessage] = (
|
||||
("unk1" | uint32L) ::
|
||||
(uint2 >>:~ { code =>
|
||||
("unk2" | selectAwardOption(code)) ::
|
||||
("unk3" | uint8L)
|
||||
})
|
||||
).xmap[AvatarAwardMessage](
|
||||
{
|
||||
case unk1 :: _ :: unk2 :: unk3 :: HNil =>
|
||||
AvatarAwardMessage(unk1, unk2, unk3)
|
||||
},
|
||||
{
|
||||
case AvatarAwardMessage(unk1, unk2, unk3) =>
|
||||
unk1 :: unk2.code :: unk2 :: unk3 :: HNil
|
||||
}
|
||||
)
|
||||
("merit_commendation" | MeritCommendation.codec) ::
|
||||
("state" | either(bool,
|
||||
either(bool, progress_codec, qualification_codec).xmap[AwardOption](
|
||||
{
|
||||
case Left(d) => d
|
||||
case Right(d) => d
|
||||
},
|
||||
{
|
||||
case d: AwardProgress => Left(d)
|
||||
case d: AwardQualificationProgress => Right(d)
|
||||
}
|
||||
),
|
||||
completion_codec
|
||||
).xmap[AwardOption](
|
||||
{
|
||||
case Left(d) => d
|
||||
case Right(d) => d
|
||||
},
|
||||
{
|
||||
case d: AwardProgress => Left(d)
|
||||
case d: AwardQualificationProgress => Left(d)
|
||||
case d: AwardCompletion => Right(d)
|
||||
}
|
||||
)) ::
|
||||
("unk" | uint8L)
|
||||
).as[AvatarAwardMessage]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import scodec.codecs._
|
|||
/**
|
||||
* An `Enumeration` of the slots for award ribbons on a player's `RibbonBars`.
|
||||
*/
|
||||
object RibbonBarsSlot extends Enumeration {
|
||||
object RibbonBarSlot extends Enumeration {
|
||||
type Type = Value
|
||||
|
||||
val Top, Middle, Bottom, TermOfService //technically,the slot above "Top"
|
||||
|
|
@ -21,29 +21,28 @@ object RibbonBarsSlot extends Enumeration {
|
|||
/**
|
||||
* Dispatched to configure a player's merit commendation ribbons.<br>
|
||||
* <br>
|
||||
* Normally, this packet is dispatched by the client when managing merit commendations through the "Character Info/Achievements" tab.
|
||||
* Normally, this packet is dispatched by the client when managing merit commendations
|
||||
* through the "Character Info/Achievements" tab.
|
||||
* On Gemini Live, this packet was also always dispatched once by the server during character login.
|
||||
* It set the term of service ribbon explicitly.
|
||||
* Generally, this was unnecessary, as the encoded character data maintains information about displayed ribbons.
|
||||
* This behavior was probably a routine that ensured that correct yearly progression was tracked if the player earned it while offline.
|
||||
* This behavior was probably a routine that ensured that correct yearly progression was tracked
|
||||
* if the player earned it while offline.
|
||||
* It never set any of the other ribbon slot positions during login.<br>
|
||||
* <br>
|
||||
* A specific ribbon may only be set once to one slot.
|
||||
* The last set slot is considered the valid position to which that ribbon will be placed/moved.
|
||||
* @param player_guid the player
|
||||
* @param ribbon the award to be displayed;
|
||||
* defaults to `MeritCommendation.None`;
|
||||
* use `MeritCommendation.None` when indicating "no ribbon"
|
||||
* @param bar any of the four positions where the award ribbon is to be displayed;
|
||||
* defaults to `TermOfService`
|
||||
* @param ribbon the award to be displayed
|
||||
* @param bar any of the four positions where the award ribbon is to be displayed
|
||||
* @see `RibbonBars`
|
||||
* @see `MeritCommendation`
|
||||
*/
|
||||
final case class DisplayedAwardMessage(
|
||||
player_guid: PlanetSideGUID,
|
||||
ribbon: MeritCommendation.Value = MeritCommendation.None,
|
||||
bar: RibbonBarsSlot.Value = RibbonBarsSlot.TermOfService
|
||||
) extends PlanetSideGamePacket {
|
||||
player_guid: PlanetSideGUID,
|
||||
ribbon: MeritCommendation.Value,
|
||||
bar: RibbonBarSlot.Value
|
||||
) extends PlanetSideGamePacket {
|
||||
type Packet = DisplayedAwardMessage
|
||||
def opcode = GamePacketOpcode.DisplayedAwardMessage
|
||||
def encode = DisplayedAwardMessage.encode(this)
|
||||
|
|
@ -52,7 +51,7 @@ final case class DisplayedAwardMessage(
|
|||
object DisplayedAwardMessage extends Marshallable[DisplayedAwardMessage] {
|
||||
implicit val codec: Codec[DisplayedAwardMessage] = (
|
||||
("player_guid" | PlanetSideGUID.codec) ::
|
||||
("ribbon" | MeritCommendation.codec) ::
|
||||
("bar" | RibbonBarsSlot.codec)
|
||||
("ribbon" | MeritCommendation.codec) ::
|
||||
("bar" | RibbonBarSlot.codec)
|
||||
).as[DisplayedAwardMessage]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,20 @@ package game
|
|||
import org.specs2.mutable._
|
||||
import net.psforever.packet._
|
||||
import net.psforever.packet.game._
|
||||
import net.psforever.types.MeritCommendation
|
||||
import scodec.bits._
|
||||
|
||||
class AvatarAwardMessageTest extends Specification {
|
||||
val string0 = hex"cf 15010000014000003d0040000000"
|
||||
val string1 = hex"cf 2a010000c717b12a0000"
|
||||
val string2 = hex"cf a6010000e9058cab0080"
|
||||
val string3 = hex"cf 7a010000400000000000"
|
||||
|
||||
"decode (0)" in {
|
||||
PacketCoding.decodePacket(string0).require match {
|
||||
case AvatarAwardMessage(unk1, unk2, unk3) =>
|
||||
unk1 mustEqual 277
|
||||
unk2 mustEqual AwardOptionZero(5, 500)
|
||||
unk1 mustEqual MeritCommendation.Max1
|
||||
unk2 mustEqual AwardProgress(5, 500)
|
||||
unk3 mustEqual 0
|
||||
case _ =>
|
||||
ko
|
||||
|
|
@ -25,8 +27,8 @@ class AvatarAwardMessageTest extends Specification {
|
|||
"decode (1)" in {
|
||||
PacketCoding.decodePacket(string1).require match {
|
||||
case AvatarAwardMessage(unk1, unk2, unk3) =>
|
||||
unk1 mustEqual 298
|
||||
unk2 mustEqual AwardOptionTwo(2831441436L)
|
||||
unk1 mustEqual MeritCommendation.OneYearVS
|
||||
unk2 mustEqual AwardCompletion(1415720846L)
|
||||
unk3 mustEqual 0
|
||||
case _ =>
|
||||
ko
|
||||
|
|
@ -36,32 +38,50 @@ class AvatarAwardMessageTest extends Specification {
|
|||
"decode (2)" in {
|
||||
PacketCoding.decodePacket(string2).require match {
|
||||
case AvatarAwardMessage(unk1, unk2, unk3) =>
|
||||
unk1 mustEqual 422
|
||||
unk2 mustEqual AwardOptionTwo(2888963748L)
|
||||
unk3 mustEqual 2
|
||||
unk1 mustEqual MeritCommendation.TwoYearVS
|
||||
unk2 mustEqual AwardCompletion(1444482002L)
|
||||
unk3 mustEqual 1
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"decode (3)" in {
|
||||
PacketCoding.decodePacket(string3).require match {
|
||||
case AvatarAwardMessage(unk1, unk2, unk3) =>
|
||||
unk1 mustEqual MeritCommendation.StandardAssault3
|
||||
unk2 mustEqual AwardQualificationProgress(0)
|
||||
unk3 mustEqual 0
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode (0)" in {
|
||||
val msg = AvatarAwardMessage(277, AwardOptionZero(5, 500), 0)
|
||||
val msg = AvatarAwardMessage(MeritCommendation.Max1, AwardProgress(5, 500))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string0
|
||||
}
|
||||
|
||||
"encode (1)" in {
|
||||
val msg = AvatarAwardMessage(298, AwardOptionTwo(2831441436L), 0)
|
||||
val msg = AvatarAwardMessage(MeritCommendation.OneYearVS, AwardCompletion(1415720846L))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string1
|
||||
}
|
||||
|
||||
"encode (2)" in {
|
||||
val msg = AvatarAwardMessage(422, AwardOptionTwo(2888963748L), 2)
|
||||
val msg = AvatarAwardMessage(MeritCommendation.TwoYearVS, AwardCompletion(1444482002L), 1)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string2
|
||||
}
|
||||
|
||||
"encode (3)" in {
|
||||
val msg = AvatarAwardMessage(MeritCommendation.StandardAssault3, AwardQualificationProgress(0))
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string3
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,14 +15,14 @@ class DisplayedAwardMessageTest extends Specification {
|
|||
case DisplayedAwardMessage(player_guid, ribbon, bar) =>
|
||||
player_guid mustEqual PlanetSideGUID(1695)
|
||||
ribbon mustEqual MeritCommendation.TwoYearVS
|
||||
bar mustEqual RibbonBarsSlot.TermOfService
|
||||
bar mustEqual RibbonBarSlot.TermOfService
|
||||
case _ =>
|
||||
ko
|
||||
}
|
||||
}
|
||||
|
||||
"encode" in {
|
||||
val msg = DisplayedAwardMessage(PlanetSideGUID(1695), MeritCommendation.TwoYearVS, RibbonBarsSlot.TermOfService)
|
||||
val msg = DisplayedAwardMessage(PlanetSideGUID(1695), MeritCommendation.TwoYearVS, RibbonBarSlot.TermOfService)
|
||||
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
|
||||
|
||||
pkt mustEqual string
|
||||
|
|
|
|||
Loading…
Reference in a new issue