diff --git a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala index cad228880..dcde70310 100644 --- a/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala +++ b/common/src/main/scala/net/psforever/packet/GamePacketOpcode.scala @@ -363,7 +363,7 @@ object GamePacketOpcode extends Enumeration { case 0x23 => noDecoder(ActionCancelAcknowledgeMessage) case 0x24 => game.SetEmpireMessage.decode case 0x25 => game.EmoteMsg.decode - case 0x26 => noDecoder(UnuseItemMessage) + case 0x26 => game.UnuseItemMessage.decode case 0x27 => game.ObjectDetachMessage.decode // 0x28 case 0x28 => game.CreateShortcutMessage.decode @@ -392,7 +392,7 @@ object GamePacketOpcode extends Enumeration { case 0x3c => game.GenericCollisionMsg.decode case 0x3d => game.QuantityUpdateMessage.decode case 0x3e => game.ArmorChangedMessage.decode - case 0x3f => noDecoder(ProjectileStateMessage) + case 0x3f => game.ProjectileStateMessage.decode // OPCODES 0x40-4f case 0x40 => noDecoder(MountVehicleCargoMsg) @@ -415,7 +415,7 @@ object GamePacketOpcode extends Enumeration { // OPCODES 0x50-5f case 0x50 => game.TargetingInfoMessage.decode - case 0x51 => noDecoder(TriggerEffectMessage) + case 0x51 => game.TriggerEffectMessage.decode case 0x52 => game.WeaponDryFireMessage.decode case 0x53 => noDecoder(DroppodLaunchRequestMessage) case 0x54 => noDecoder(HackMessage) @@ -425,7 +425,7 @@ object GamePacketOpcode extends Enumeration { // 0x58 case 0x58 => game.AvatarImplantMessage.decode case 0x59 => noDecoder(UnknownMessage89) - case 0x5a => noDecoder(DelayedPathMountMsg) + case 0x5a => game.DelayedPathMountMsg.decode case 0x5b => noDecoder(OrbitalShuttleTimeMsg) case 0x5c => noDecoder(AIDamage) case 0x5d => game.DeployObjectMessage.decode @@ -461,7 +461,7 @@ object GamePacketOpcode extends Enumeration { case 0x76 => game.DeployableObjectsInfoMessage.decode case 0x77 => noDecoder(SquadState) // 0x78 - case 0x78 => noDecoder(OxygenStateMessage) + case 0x78 => game.OxygenStateMessage.decode case 0x79 => noDecoder(TradeMessage) case 0x7a => noDecoder(UnknownMessage122) case 0x7b => noDecoder(DamageFeedbackMessage) @@ -510,7 +510,7 @@ object GamePacketOpcode extends Enumeration { // OPCODES 0xa0-af case 0xa0 => game.BuildingInfoUpdateMessage.decode - case 0xa1 => noDecoder(FireHintMessage) + case 0xa1 => game.FireHintMessage.decode case 0xa2 => noDecoder(UplinkRequest) case 0xa3 => noDecoder(UplinkResponse) case 0xa4 => game.WarpgateRequest.decode @@ -533,7 +533,7 @@ object GamePacketOpcode extends Enumeration { case 0xb2 => game.VoiceHostInfo.decode case 0xb3 => game.BattleplanMessage.decode case 0xb4 => game.BattleExperienceMessage.decode - case 0xb5 => noDecoder(TargetingImplantRequest) + case 0xb5 => game.TargetingImplantRequest.decode case 0xb6 => game.ZonePopulationUpdateMessage.decode case 0xb7 => game.DisconnectMessage.decode // 0xb8 @@ -567,7 +567,7 @@ object GamePacketOpcode extends Enumeration { // OPCODES 0xd0-df case 0xd0 => noDecoder(UnknownMessage208) - case 0xd1 => noDecoder(DisplayedAwardMessage) + case 0xd1 => game.DisplayedAwardMessage.decode case 0xd2 => noDecoder(RespawnAMSInfoMessage) case 0xd3 => noDecoder(ComponentDamageMessage) case 0xd4 => noDecoder(GenericObjectActionAtPositionMessage) diff --git a/common/src/main/scala/net/psforever/packet/game/DelayedPathMountMsg.scala b/common/src/main/scala/net/psforever/packet/game/DelayedPathMountMsg.scala new file mode 100644 index 000000000..4c713e096 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/DelayedPathMountMsg.scala @@ -0,0 +1,32 @@ +// Copyright (c) 2016 PSForever.net to present +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * na + * @param player_guid na + * @param vehicle_guid vehicle ?, turret ? Found a HART GUID for now. Need more search. + * @param u1 na - maybe a delay ? + * @param u2 na + */ +final case class DelayedPathMountMsg(player_guid : PlanetSideGUID, + vehicle_guid : PlanetSideGUID, + u1 : Int, + u2 : Boolean) + extends PlanetSideGamePacket { + type Packet = DelayedPathMountMsg + def opcode = GamePacketOpcode.DelayedPathMountMsg + def encode = DelayedPathMountMsg.encode(this) +} + +object DelayedPathMountMsg extends Marshallable[DelayedPathMountMsg] { + implicit val codec : Codec[DelayedPathMountMsg] = ( + ("player_guid" | PlanetSideGUID.codec) :: + ("vehicle_guid" | PlanetSideGUID.codec) :: + ("u1" | uint8L) :: + ("u2" | bool) + ).as[DelayedPathMountMsg] +} diff --git a/common/src/main/scala/net/psforever/packet/game/DisplayedAwardMessage.scala b/common/src/main/scala/net/psforever/packet/game/DisplayedAwardMessage.scala new file mode 100644 index 000000000..2f7024adb --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/DisplayedAwardMessage.scala @@ -0,0 +1,60 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.types.MeritCommendation +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * An `Enumeration` of the slots for award ribbons on a player's `RibbonBars`. + */ +object RibbonBarsSlot extends Enumeration { + type Type = Value + + val Top, + Middle, + Bottom, + TermOfService //technically,the slot above "Top" + = Value + + implicit val codec = PacketHelpers.createEnumerationCodec(this, uint4L) +} + +/** + * Dispatched to configure a player's merit commendation ribbons.
+ *
+ * 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. + * It never set any of the other ribbon slot positions during login.
+ *
+ * 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` + * @see `RibbonBars` + * @see `MeritCommendation` + */ +final case class DisplayedAwardMessage(player_guid : PlanetSideGUID, + ribbon : MeritCommendation.Value = MeritCommendation.None, + bar : RibbonBarsSlot.Value = RibbonBarsSlot.TermOfService) + extends PlanetSideGamePacket { + type Packet = DisplayedAwardMessage + def opcode = GamePacketOpcode.DisplayedAwardMessage + def encode = DisplayedAwardMessage.encode(this) +} + +object DisplayedAwardMessage extends Marshallable[DisplayedAwardMessage] { + implicit val codec : Codec[DisplayedAwardMessage] = ( + ("player_guid" | PlanetSideGUID.codec) :: + ("ribbon" | MeritCommendation.codec) :: + ("bar" | RibbonBarsSlot.codec) + ).as[DisplayedAwardMessage] +} diff --git a/common/src/main/scala/net/psforever/packet/game/FireHintMessage.scala b/common/src/main/scala/net/psforever/packet/game/FireHintMessage.scala new file mode 100644 index 000000000..53bbd519c --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/FireHintMessage.scala @@ -0,0 +1,44 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.Codec +import scodec.codecs._ + +/** + * not sure for u1 / u2 / u3, maybe need a real brain ... + * @param weapon_guid na + * @param pos na; pos of what ?! + * @param u1 na + * @param u2 na + * @param u3 na + * @param u4 na + * @param u5 na; vel of what ?! + */ + +final case class FireHintMessage(weapon_guid : PlanetSideGUID, + pos : Vector3, + u1 : Int, + u2 : Int, + u3 : Int, + u4 : Int, + u5 : Option[Vector3] = None) + extends PlanetSideGamePacket { + type Packet = FireHintMessage + def opcode = GamePacketOpcode.FireHintMessage + def encode = FireHintMessage.encode(this) +} + +object FireHintMessage extends Marshallable[FireHintMessage] { + + implicit val codec : Codec[FireHintMessage] = ( + ("weapon_guid" | PlanetSideGUID.codec) :: + ("pos" | Vector3.codec_pos) :: + ("u1" | uint16L) :: + ("u2" | uint16L) :: + ("u3" | uint16L) :: + ("u4" | uintL(3)) :: + optional(bool, "u5" | Vector3.codec_vel) + ).as[FireHintMessage] +} diff --git a/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala b/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala index 2f2ab416e..4040714fe 100644 --- a/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/ObjectCreateDetailedMessage.scala @@ -55,8 +55,10 @@ object ObjectCreateDetailedMessage extends Marshallable[ObjectCreateDetailedMess * @param data the data used to construct this type of object * @return an ObjectCreateMessage */ - def apply(objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateDetailedMessage = - ObjectCreateDetailedMessage(0L, objectClass, guid, Some(parentInfo), Some(data)) + def apply(objectClass : Int, guid : PlanetSideGUID, parentInfo : ObjectCreateMessageParent, data : ConstructorData) : ObjectCreateDetailedMessage = { + val parentInfoOpt : Option[ObjectCreateMessageParent] = Some(parentInfo) + ObjectCreateDetailedMessage(ObjectCreateBase.streamLen(parentInfoOpt, data), objectClass, guid, parentInfoOpt, Some(data)) + } /** * An abbreviated constructor for creating `ObjectCreateMessages`, ignoring `parentInfo`. @@ -65,8 +67,9 @@ object ObjectCreateDetailedMessage extends Marshallable[ObjectCreateDetailedMess * @param data the data used to construct this type of object * @return an ObjectCreateMessage */ - def apply(objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateDetailedMessage = - ObjectCreateDetailedMessage(0L, objectClass, guid, None, Some(data)) + def apply(objectClass : Int, guid : PlanetSideGUID, data : ConstructorData) : ObjectCreateDetailedMessage = { + ObjectCreateDetailedMessage(ObjectCreateBase.streamLen(None, data), objectClass, guid, None, Some(data)) + } /** * Take the important information of a game piece and transform it into bit data. diff --git a/common/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala new file mode 100644 index 000000000..54da0ab08 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/OxygenStateMessage.scala @@ -0,0 +1,111 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.newcodecs.newcodecs +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.{Attempt, Codec} +import scodec.codecs._ +import shapeless.{::, HNil} + +/** + * Alert the condition of a vehicle the player is using when going too far underwater. + * The player must be mounted in/on this vehicle at start time for this countdown to display. + * @param vehicle_guid the player's mounted vehicle + * @param progress the remaining countdown; + * for vehicle waterlog condition, the progress per second rate is very high + * @param active show a new countdown if `true` (resets any active countdown); + * clear any active countdowns if `false`; + * defaults to `true` + */ +final case class WaterloggedVehicleState(vehicle_guid : PlanetSideGUID, + progress : Float, + active : Boolean = true) + +/** + * Dispatched by the server to cause the player to slowly drown. + * If the player is mounted in a vehicle at the time, alert the player that the vehicle may be disabled.
+ *
+ * When a player walks too far underwater, a borderless red progress bar with a countdown from 100 (98) is displayed across the screen. + * The countdown proceeds to zero at a fixed rate and is timed with the depleting progress bar. + * When it reaches zero, the player will be killed. + * If the player is in a vehicle after a certain depth, a blue bar and countdown pair will superimpose the red indicators. + * It depletes much more rapidly than the red indicators. + * When it reaches zero, the vehicle will become disabled. + * All players in the vehicle's seats will be kicked and they will not be allowed back in.
+ *
+ * Normally, the countdowns should be set to begin at 100 (100.0). + * This is the earliest the drowning GUI will appear for either blue or red indicators. + * Passing greater intervals - up to 204.8 - will start the countdown silently but the GUI will be hidden until 100.0. + * (The progress indicators will actually appear to start counting from 98.) + * Managing the secondary vehicle countdown independent of the primary player countdown requires updating with the correct levels. + * The countdown can be cancelled by instructing it to be `active = false`.
+ *
+ * Except for updating the indicators, all other functionality of "drowning" is automated by the server. + * @param player_guid the player + * @param progress the remaining countdown; + * for character oxygen, the progress per second rate is about 1 + * @param active show a new countdown if `true` (resets any active countdown); + * clear any active countdowns if `false` + * @param vehicle_state optional state of the vehicle the player is driving + */ +final case class OxygenStateMessage(player_guid : PlanetSideGUID, + progress : Float, + active : Boolean, + vehicle_state : Option[WaterloggedVehicleState] = None) + extends PlanetSideGamePacket { + type Packet = OxygenStateMessage + def opcode = GamePacketOpcode.OxygenStateMessage + def encode = OxygenStateMessage.encode(this) +} + +object OxygenStateMessage extends Marshallable[OxygenStateMessage] { + /** + * Overloaded constructor that removes the optional state of the `WaterloggedVehicleState` parameter. + * @param player_guid the player + * @param progress the remaining countdown + * @param active show or clear the countdown + * @param vehicle_state state of the vehicle the player is driving + * @return + */ + def apply(player_guid : PlanetSideGUID, progress : Float, active : Boolean, vehicle_state : WaterloggedVehicleState) : OxygenStateMessage = + OxygenStateMessage(player_guid, progress, active, Some(vehicle_state)) + + /** + * A simple pattern that expands the datatypes of the packet's basic `Codec`. + */ + private type basePattern = PlanetSideGUID :: Float :: Boolean :: HNil + + /** + * A `Codec` for the repeated processing of three values. + * This `Codec` is the basis for the packet's data. + */ + private val base_codec : Codec[basePattern] = + PlanetSideGUID.codec :: + newcodecs.q_float(0.0f, 204.8f, 11) :: //hackish: 2^11 == 2047, so it should be 204.7; but, 204.8 allows decode == encode + bool + + implicit val codec : Codec[OxygenStateMessage] = ( + base_codec.exmap[basePattern] ( + { + case guid :: time :: active :: HNil => + Attempt.successful(guid :: time :: active :: HNil) + }, + { + case guid :: time :: active :: HNil => + Attempt.successful(guid :: time :: active :: HNil) + } + ) :+ + optional(bool, + "vehicle_state" | base_codec.exmap[WaterloggedVehicleState] ( + { + case guid :: time :: active :: HNil => + Attempt.successful(WaterloggedVehicleState(guid, time, active)) + }, + { + case WaterloggedVehicleState(guid, time, active) => + Attempt.successful(guid :: time :: active :: HNil) + } + ).as[WaterloggedVehicleState] + ) + ).as[OxygenStateMessage] +} diff --git a/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala new file mode 100644 index 000000000..329d67429 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/ProjectileStateMessage.scala @@ -0,0 +1,65 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.Codec +import scodec.codecs._ + +/** + * Dispatched to deliberately render certain projectiles of a weapon on other players' clients.
+ *
+ * This packet is generated by firing specific weapons in specific fire modes. + * For example, the Phoenix (`hunterseeker`) discharged in its primary fire mode generates this packet; + * but, the Phoenix in secondary fire mode does not. + * The Striker (`striker`) discharged in its primary fire mode generates this packet; + * but, the Striker in secondary fire mode does not. + * The chosen fire mode(s) are not a straight-fire projectile but one that has special control asserted over it. + * For the Phoenix, it is user-operated. + * For the Striker, it tracks towards a target while the weapon's reticle hovers over that target.
+ *
+ * This packet will continue to be dispatched by the client for as long as the projectile being tracked is in the air. + * All projectiles have a maximum lifespan before they will lose control and either despawn and/or explode. + * This number is tracked in the packet for simplicity. + * If the projectile strikes a valid target, the count will jump to a significantly enormous value beyond its normal lifespan. + * This ensures that the projectile - locally and the shared model - will despawn. + * @param projectile_guid the projectile + * @param shot_pos the position of the projectile + * @param shot_vel the velocity of the projectile + * @param unk1 na; + * usually 0 + * @param unk2 na; + * will remain consistent for the lifespan of a given projectile in most cases + * @param unk3 na; + * will remain consistent for the lifespan of a given projectile in most cases + * @param unk4 na; + * usually false + * @param time_alive how long the projectile has been in the air; + * often expressed in multiples of 2 + */ +final case class ProjectileStateMessage(projectile_guid : PlanetSideGUID, + shot_pos : Vector3, + shot_vel : Vector3, + unk1 : Int, + unk2 : Int, + unk3 : Int, + unk4 : Boolean, + time_alive : Int) + extends PlanetSideGamePacket { + type Packet = ProjectileStateMessage + def opcode = GamePacketOpcode.ProjectileStateMessage + def encode = ProjectileStateMessage.encode(this) +} + +object ProjectileStateMessage extends Marshallable[ProjectileStateMessage] { + implicit val codec : Codec[ProjectileStateMessage] = ( + ("projectile_guid" | PlanetSideGUID.codec) :: + ("shot_pos" | Vector3.codec_pos) :: + ("shot_vel" | Vector3.codec_float) :: + ("unk1" | uint8L) :: + ("unk2" | uint8L) :: + ("unk3" | uint8L) :: + ("unk4" | bool) :: + ("time_alive" | uint16L) + ).as[ProjectileStateMessage] +} diff --git a/common/src/main/scala/net/psforever/packet/game/TargetingImplantRequest.scala b/common/src/main/scala/net/psforever/packet/game/TargetingImplantRequest.scala new file mode 100644 index 000000000..63cceeb65 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/TargetingImplantRequest.scala @@ -0,0 +1,36 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * An entry regarding a specific target. + * @param target_guid the target + * @param unk na + */ +final case class TargetRequest(target_guid : PlanetSideGUID, + unk : Boolean) + +/** + * Dispatched by the client when the advanced targeting implant activates to collect status information from the server.
+ *
+ * This packet is answered by a `TargetingInfoMessage` with `List` entries of thed corresponding UIDs. + * @param target_list a `List` of targets + */ +final case class TargetingImplantRequest(target_list : List[TargetRequest]) + extends PlanetSideGamePacket { + type Packet = TargetingImplantRequest + def opcode = GamePacketOpcode.TargetingImplantRequest + def encode = TargetingImplantRequest.encode(this) +} + +object TargetingImplantRequest extends Marshallable[TargetingImplantRequest] { + private val request_codec : Codec[TargetRequest] = ( + ("target_guid" | PlanetSideGUID.codec) :: + ("unk" | bool) + ).as[TargetRequest] + + implicit val codec : Codec[TargetingImplantRequest] = ("target_list" | listOfN(intL(6), request_codec)).as[TargetingImplantRequest] +} diff --git a/common/src/main/scala/net/psforever/packet/game/TriggerEffectMessage.scala b/common/src/main/scala/net/psforever/packet/game/TriggerEffectMessage.scala new file mode 100644 index 000000000..a8c2cc9be --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/TriggerEffectMessage.scala @@ -0,0 +1,83 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} +import net.psforever.types.Vector3 +import scodec.Codec +import scodec.codecs._ + +/** + * na + * @param unk1 na; + * `true` to apply the effect usually + * @param unk2 na + */ +final case class TriggeredEffect(unk1 : Boolean, + unk2 : Long) + +/** + * Activate an effect that is not directly associated with an existing game object. + * Without a game object from which to inherit position and orientation, those explicit parameters must be provided. + * @param pos the position in the game world + * @param roll the amount of roll that affects orientation + * @param pitch the amount of pitch that affects orientation + * @param yaw the amount of yaw that affects orientation + */ +final case class TriggeredEffectLocation(pos : Vector3, + roll : Int, + pitch : Int, + yaw : Int) + +/** + * Dispatched by the server to cause a client to display a special graphical effect.
+ *
+ * The effect being triggered can be based around a specific game object or replayed freely, absent of an anchoring object. + * If object-based then the kinds of effects that can be activated are specific to the object. + * If unbound, then a wider range of effects can be displayed. + * Regardless, one category will rarely ever be activated under the same valid conditions of the other category. + * For example, the effect "on" will only work on objects that accept "on" normally, like a deployed `motionalarmsensor`. + * The effect "spawn_object_effect" can be applied anywhere in the environment; + * but, it can not be activated in conjunction with an existing object. + * @param obj an object that accepts the effect + * @param effect the name of the effect + * @param unk na; + * when activating an effect on an existing object + * @param location an optional position where the effect will be displayed; + * when activating an effect independently + */ +final case class TriggerEffectMessage(obj : PlanetSideGUID, + effect : String, + unk : Option[TriggeredEffect] = None, + location : Option[TriggeredEffectLocation] = None + ) extends PlanetSideGamePacket { + type Packet = TriggerEffectMessage + def opcode = GamePacketOpcode.TriggerEffectMessage + def encode = TriggerEffectMessage.encode(this) +} + +object TriggerEffectMessage extends Marshallable[TriggerEffectMessage] { + /** + * A `Codec` for `TriggeredEffect` data. + */ + private val effect_codec : Codec[TriggeredEffect] = ( + ("unk1" | bool) :: + ("unk2" | uint32L) + ).as[TriggeredEffect] + + /** + * A `Codec` for `TriggeredEffectLocation` data. + */ + private val effect_location_codec : Codec[TriggeredEffectLocation] = ( + ("pos" | Vector3.codec_pos) :: + ("roll" | uint8L) :: + ("pitch" | uint8L) :: + ("yaw" | uint8L) + ).as[TriggeredEffectLocation] + + implicit val codec : Codec[TriggerEffectMessage] = ( + ("obj" | PlanetSideGUID.codec) >>:~ { obj => + ("effect" | PacketHelpers.encodedString) :: + optional(bool, "unk" | effect_codec) :: + conditional(obj.guid == 0, "location" | effect_location_codec) + }).as[TriggerEffectMessage] +} diff --git a/common/src/main/scala/net/psforever/packet/game/UnuseItemMessage.scala b/common/src/main/scala/net/psforever/packet/game/UnuseItemMessage.scala new file mode 100644 index 000000000..63433ded8 --- /dev/null +++ b/common/src/main/scala/net/psforever/packet/game/UnuseItemMessage.scala @@ -0,0 +1,30 @@ +// Copyright (c) 2017 PSForever +package net.psforever.packet.game + +import net.psforever.packet.{GamePacketOpcode, Marshallable, PlanetSideGamePacket} +import scodec.Codec +import scodec.codecs._ + +/** + * Dispatched by the client when its player is done using something.
+ *
+ * The common example is sifting through backpacks, an activity that only one player is allowed to do at a time. + * When a backpack is accessed by one player, other players are blocked. + * When the first player is done accessing the backpack, this packet informs the server so other players may be allowed access. + * @param player_guid the player + * @param item_guid the item + */ +final case class UnuseItemMessage(player_guid : PlanetSideGUID, + item_guid : PlanetSideGUID) + extends PlanetSideGamePacket { + type Packet = UnuseItemMessage + def opcode = GamePacketOpcode.UnuseItemMessage + def encode = UnuseItemMessage.encode(this) +} + +object UnuseItemMessage extends Marshallable[UnuseItemMessage] { + implicit val codec : Codec[UnuseItemMessage] = ( + ("player_guid" | PlanetSideGUID.codec) :: + ("item_guid" | PlanetSideGUID.codec) + ).as[UnuseItemMessage] +} diff --git a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala index 305d9a488..83949ee51 100644 --- a/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala +++ b/common/src/main/scala/net/psforever/packet/game/objectcreate/RibbonBars.scala @@ -2,35 +2,33 @@ package net.psforever.packet.game.objectcreate import net.psforever.packet.Marshallable +import net.psforever.types.MeritCommendation import scodec.Codec import scodec.codecs._ /** * Enumerate the player-displayed merit commendation awards granted for excellence (or tenacity) in combat. - * These are the medals players wish to brandish on their left pauldron.
- *
- * All merit commendation ribbons are represented by a 32-bit signature. - * The default "no-ribbon" value is `0xFFFFFFFF`, although some illegal values will also work. - * The term of service ribbon can not be modified by the user and will apply itself to its slot automatically when valid. + * These are the medals players wish to brandish on their left pauldron. * @param upper the "top" configurable merit ribbon * @param middle the central configurable merit ribbon * @param lower the lower configurable merit ribbon * @param tos the top-most term of service merit ribbon + * @see `MeritCommendation` + * @see `DisplayedAwardMessage` */ -final case class RibbonBars(upper : Long = RibbonBars.noRibbon, - middle : Long = RibbonBars.noRibbon, - lower : Long = RibbonBars.noRibbon, - tos : Long = RibbonBars.noRibbon) extends StreamBitSize { +final case class RibbonBars(upper : MeritCommendation.Value = MeritCommendation.None, + middle : MeritCommendation.Value = MeritCommendation.None, + lower : MeritCommendation.Value = MeritCommendation.None, + tos : MeritCommendation.Value = MeritCommendation.None + ) extends StreamBitSize { override def bitsize : Long = 128L } object RibbonBars extends Marshallable[RibbonBars] { - val noRibbon : Long = 0xFFFFFFFFL - implicit val codec : Codec[RibbonBars] = ( - ("upper" | uint32L) :: - ("middle" | uint32L) :: - ("lower" | uint32L) :: - ("tos" | uint32L) + ("upper" | MeritCommendation.codec) :: + ("middle" | MeritCommendation.codec) :: + ("lower" | MeritCommendation.codec) :: + ("tos" | MeritCommendation.codec) ).as[RibbonBars] } diff --git a/common/src/main/scala/net/psforever/types/MeritCommendation.scala b/common/src/main/scala/net/psforever/types/MeritCommendation.scala new file mode 100644 index 000000000..7dd056de4 --- /dev/null +++ b/common/src/main/scala/net/psforever/types/MeritCommendation.scala @@ -0,0 +1,522 @@ +// Copyright (c) 2017 PSForever +package net.psforever.types + +import scodec.{Attempt, Err} +import scodec.codecs._ + +/** + * An `Enumeration` of all merit commendation award categories organized into associated ribbons. + * By astonishing coincidence, with exception of the first ten special awards, the rest of list is in alphabetical order. + */ +object MeritCommendation extends Enumeration { + type Type = Value + + val //0 + FanFaire2005Commander, + FanFaire2005Soldier, + FanFaire2006Atlanta, + HalloweenMassacre2006NC, + HalloweenMassacre2006TR, + HalloweenMassacre2006VS, + FanFaire2007, + FanFaire2008, + FanFaire2009, + AdvancedMedic1, + //10 + AdvancedMedic2, + AdvancedMedic3, + AdvancedMedic4, + AdvancedMedic5, + AdvancedMedic6, + AdvancedMedic7, + AdvancedMedicAssists1, + AdvancedMedicAssists2, + AdvancedMedicAssists3, + AdvancedMedicAssists4, + //20 + AdvancedMedicAssists5, + AdvancedMedicAssists6, + AdvancedMedicAssists7, + AirDefender1, + AirDefender2, + AirDefender3, + AirDefender4, + AirDefender5, + AirDefender6, + AirDefender7, + //30 + AMSSupport1, + AMSSupport2, + AMSSupport3, + AMSSupport4, + AMSSupport5, + AMSSupport6, + AMSSupport7, + AntiVehicular1, + AntiVehicular2, + AntiVehicular3, + //40 + AntiVehicular4, + AntiVehicular5, + AntiVehicular6, + AntiVehicular7, + Avenger1, + Avenger2, + Avenger3, + Avenger4, + Avenger5, + Avenger6, + //50 + Avenger7, + AwardColors, //what? + BendingMovieActor, + BFRAdvanced, + BFRAdvanced2, + BFRAdvanced3, + BFRAdvanced4, + BFRAdvanced5, + BFRBuster1, + BFRBuster2, + //60 + BFRBuster3, + BFRBuster4, + BFRBuster5, + BFRBuster6, + BFRBuster7, + BlackOpsHunter1, + BlackOpsHunter2, + BlackOpsHunter3, + BlackOpsHunter4, + BlackOpsHunter5, + //70 + BlackOpsParticipant, + BlackOpsVictory, + Bombadier1, + Bombadier2, + Bombadier3, + Bombadier4, + Bombadier5, + Bombadier6, + Bombadier7, + BomberAce1, + //80 + BomberAce2, + BomberAce3, + BomberAce4, + BomberAce5, + BomberAce6, + BomberAce7, + Boomer1, + Boomer2, + Boomer3, + Boomer4, + //90 + Boomer5, + Boomer6, + Boomer7, + CalvaryDriver1, + CalvaryDriver2, + CalvaryDriver3, + CalvaryDriver4, + CalvaryDriver5, + CalvaryDriver6, + CalvaryDriver7, + //100 + CalvaryPilot, + CalvaryPilot2, + CalvaryPilot3, + CalvaryPilot4, + CalvaryPilot5, + CalvaryPilot6, + CalvaryPilot7, + CMTopOutfit, + CombatMedic, + CombatMedic2, + //110 + CombatMedic3, + CombatMedic4, + CombatMedic5, + CombatMedic6, + CombatMedic7, + CombatRepair1, + CombatRepair2, + CombatRepair3, + CombatRepair4, + CombatRepair5, + //120 + CombatRepair6, + CombatRepair7, + ContestFirstBR40, + ContestMovieMaker, + ContestMovieMakerOutfit, + ContestPlayerOfTheMonth, + ContestPlayerOfTheYear, + CSAppreciation, + DefenseNC1, + DefenseNC2, + //130 + DefenseNC3, + DefenseNC4, + DefenseNC5, + DefenseNC6, + DefenseNC7, + DefenseTR1, + DefenseTR2, + DefenseTR3, + DefenseTR4, + DefenseTR5, + //40 + DefenseTR6, + DefenseTR7, + DefenseVS1, + DefenseVS2, + DefenseVS3, + DefenseVS4, + DefenseVS5, + DefenseVS6, + DefenseVS7, + DevilDogsMovie, + //150 + DogFighter1, + DogFighter2, + DogFighter3, + DogFighter4, + DogFighter5, + DogFighter6, + DogFighter7, + DriverGunner1, + DriverGunner2, + DriverGunner3, + //160 + DriverGunner4, + DriverGunner5, + DriverGunner6, + DriverGunner7, + EliteAssault0, + EliteAssault1, + EliteAssault2, + EliteAssault3, + EliteAssault4, + EliteAssault5, + //170 + EliteAssault6, + EliteAssault7, + EmeraldVeteran, + Engineer1, + Engineer2, + Engineer3, + Engineer4, + Engineer5, + Engineer6, + EquipmentSupport1, + //180 + EquipmentSupport2, + EquipmentSupport3, + EquipmentSupport4, + EquipmentSupport5, + EquipmentSupport6, + EquipmentSupport7, + EventNCCommander, + EventNCElite, + EventNCSoldier, + EventTRCommander, + //190 + EventTRElite, + EventTRSoldier, + EventVSCommander, + EventVSElite, + EventVSSoldier, + Explorer1, + FiveYearNC, + FiveYearTR, + FiveYearVS, + FourYearNC, + //200 + FourYearTR, + FourYearVS, + GalaxySupport1, + GalaxySupport2, + GalaxySupport3, + GalaxySupport4, + GalaxySupport5, + GalaxySupport6, + GalaxySupport7, + Grenade1, + //210 + Grenade2, + Grenade3, + Grenade4, + Grenade5, + Grenade6, + Grenade7, + GroundGunner1, + GroundGunner2, + GroundGunner3, + GroundGunner4, + //220 + GroundGunner5, + GroundGunner6, + GroundGunner7, + HackingSupport1, + HackingSupport2, + HackingSupport3, + HackingSupport4, + HackingSupport5, + HackingSupport6, + HackingSupport7, + //230 + HeavyAssault1, + HeavyAssault2, + HeavyAssault3, + HeavyAssault4, + HeavyAssault5, + HeavyAssault6, + HeavyAssault7, + HeavyInfantry, + HeavyInfantry2, + HeavyInfantry3, + //240 + HeavyInfantry4, + InfantryExpert1, + InfantryExpert2, + InfantryExpert3, + Jacking, + Jacking2, + Jacking3, + Jacking4, + Jacking5, + Jacking6, + //250 + Jacking7, + KnifeCombat1, + KnifeCombat2, + KnifeCombat3, + KnifeCombat4, + KnifeCombat5, + KnifeCombat6, + KnifeCombat7, + LightInfantry, + LockerCracker1, + //260 + LockerCracker2, + LockerCracker3, + LockerCracker4, + LockerCracker5, + LockerCracker6, + LockerCracker7, + LodestarSupport1, + LodestarSupport2, + LodestarSupport3, + LodestarSupport4, + //270 + LodestarSupport5, + LodestarSupport6, + LodestarSupport7, + Loser, + Loser2, + Loser3, + Loser4, + MarkovVeteran, + Max1, + Max2, + //280 + Max3, + Max4, + Max5, + Max6, + MaxBuster1, + MaxBuster2, + MaxBuster3, + MaxBuster4, + MaxBuster5, + MaxBuster6, + //290 + MediumAssault1, + MediumAssault2, + MediumAssault3, + MediumAssault4, + MediumAssault5, + MediumAssault6, + MediumAssault7, + OneYearNC, + OneYearTR, + OneYearVS, + //300 + Orion1, + Orion2, + Orion3, + Orion4, + Orion5, + Orion6, + Orion7, + Osprey1, + Osprey2, + Osprey3, + //310 + Osprey4, + Osprey5, + Osprey6, + Osprey7, + Phalanx1, + Phalanx2, + Phalanx3, + Phalanx4, + Phalanx5, + Phalanx6, + //320 + Phalanx7, + PSUMaAttendee, + PSUMbAttendee, + QAAppreciation, + ReinforcementHackSpecialist, + ReinforcementInfantrySpecialist, + ReinforcementSpecialist, + ReinforcementVehicleSpecialist, + RouterSupport1, + RouterSupport2, + //330 + RouterSupport3, + RouterSupport4, + RouterSupport5, + RouterSupport6, + RouterSupport7, + RouterTelepadDeploy1, + RouterTelepadDeploy2, + RouterTelepadDeploy3, + RouterTelepadDeploy4, + RouterTelepadDeploy5, + //340 + RouterTelepadDeploy6, + RouterTelepadDeploy7, + ScavengerNC1, + ScavengerNC2, + ScavengerNC3, + ScavengerNC4, + ScavengerNC5, + ScavengerNC6, + ScavengerTR1, + ScavengerTR2, + //350 + ScavengerTR3, + ScavengerTR4, + ScavengerTR5, + ScavengerTR6, + ScavengerVS1, + ScavengerVS2, + ScavengerVS3, + ScavengerVS4, + ScavengerVS5, + ScavengerVS6, + //360 + SixYearNC, + SixYearTR, + SixYearVS, + Sniper1, + Sniper2, + Sniper3, + Sniper4, + Sniper5, + Sniper6, + Sniper7, + //370 + SpecialAssault1, + SpecialAssault2, + SpecialAssault3, + SpecialAssault4, + SpecialAssault5, + SpecialAssault6, + SpecialAssault7, + StandardAssault1, + StandardAssault2, + StandardAssault3, + //380 + StandardAssault4, + StandardAssault5, + StandardAssault6, + StandardAssault7, + StracticsHistorian, + Supply1, + Supply2, + Supply3, + Supply4, + Supply5, + //390 + Supply6, + Supply7, + TankBuster1, + TankBuster2, + TankBuster3, + TankBuster4, + TankBuster5, + TankBuster6, + TankBuster7, + ThreeYearNC, + //400 + ThreeYearTR, + ThreeYearVS, + TinyRoboticSupport1, + TinyRoboticSupport2, + TinyRoboticSupport3, + TinyRoboticSupport4, + TinyRoboticSupport5, + TinyRoboticSupport6, + TinyRoboticSupport7, + Transport1, + //410 + Transport2, + Transport3, + Transport4, + Transport5, + Transport6, + Transport7, + TransportationCitation1, + TransportationCitation2, + TransportationCitation3, + TransportationCitation4, + //420 + TransportationCitation5, + TwoYearNC, + TwoYearTR, + TwoYearVS, + ValentineFemale, + ValentineMale, + WernerVeteran, + XmasGingerman, + XmasSnowman, + XmasSpirit + = Value + + /* + The value None requires special consideration. + - A Long number is required for this Enumeration codec. + - Enumerations are designed to only handle Int numbers. + - A Codec only handles unsigned numbers. + - The value of MeritCommendation.None is intended to be 0xFFFFFFFF, which (a) is 4294967295 as a Long, but (b) is -1 as an Integer. + - Due to (a), an Enumeration can not be used to represent that number. + - Due to (b), a Codec can not be used to convert to that number. + */ + val None = Value(-1) + + /** + * Carefully and explicitly convert between `Codec[Long] -> Long -> Int -> MeritCommendation.Value`. + */ + implicit val codec = uint32L.exmap[MeritCommendation.Value] ( + { + case 0xFFFFFFFFL => + Attempt.successful(MeritCommendation.None) + case n => + if(n > Int.MaxValue) { + Attempt.failure(Err(s"value $n is too high, above maximum integer value ${Int.MaxValue}")) + } + else { + Attempt.successful(MeritCommendation(n.toInt)) + } + }, + { + case MeritCommendation.None => + Attempt.successful(0xFFFFFFFFL) + case enum => + Attempt.successful(enum.id.toLong) + } + ) +} diff --git a/common/src/test/scala/game/DelayedPathMountMsgTest.scala b/common/src/test/scala/game/DelayedPathMountMsgTest.scala new file mode 100644 index 000000000..14bc82bba --- /dev/null +++ b/common/src/test/scala/game/DelayedPathMountMsgTest.scala @@ -0,0 +1,30 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class DelayedPathMountMsgTest extends Specification { + val string = hex"5a f50583044680" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case DelayedPathMountMsg(player_guid, vehicle_guid,u3,u4) => + player_guid mustEqual PlanetSideGUID(1525) + vehicle_guid mustEqual PlanetSideGUID(1155) + u3 mustEqual 70 + u4 mustEqual true + case _ => + ko + } + } + + "encode" in { + val msg = DelayedPathMountMsg(PlanetSideGUID(1525), PlanetSideGUID(1155),70,true) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} diff --git a/common/src/test/scala/game/DisplayedAwardMessageTest.scala b/common/src/test/scala/game/DisplayedAwardMessageTest.scala new file mode 100644 index 000000000..2c8a58517 --- /dev/null +++ b/common/src/test/scala/game/DisplayedAwardMessageTest.scala @@ -0,0 +1,30 @@ +// Copyright (c) 2017 PSForever +package game + +import net.psforever.types.MeritCommendation +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class DisplayedAwardMessageTest extends Specification { + val string = hex"D1 9F06 A6010000 3 0" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case DisplayedAwardMessage(player_guid, ribbon, bar) => + player_guid mustEqual PlanetSideGUID(1695) + ribbon mustEqual MeritCommendation.TwoYearTR + bar mustEqual RibbonBarsSlot.TermOfService + case _ => + ko + } + } + + "encode" in { + val msg = DisplayedAwardMessage(PlanetSideGUID(1695), MeritCommendation.TwoYearTR, RibbonBarsSlot.TermOfService) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} diff --git a/common/src/test/scala/game/FireHintMessageTest.scala b/common/src/test/scala/game/FireHintMessageTest.scala new file mode 100644 index 000000000..9893694c1 --- /dev/null +++ b/common/src/test/scala/game/FireHintMessageTest.scala @@ -0,0 +1,55 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.Vector3 +import scodec.bits._ + +class FireHintMessageTest extends Specification { + val string = hex"a1 0117 23cd63f1d7480d 000077ff9d1d00" + val string2 = hex"a1 080e 65af5705074411 0000cffee0fc7b08899f5580" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case FireHintMessage(weapon_guid, pos, u1, u2, u3, u4, u5) => + weapon_guid mustEqual PlanetSideGUID(5889) + pos mustEqual Vector3(3482.2734f,3642.4922f,53.125f) + u1 mustEqual 0 + u2 mustEqual 65399 + u3 mustEqual 7581 + u4 mustEqual 0 + u5 mustEqual None + case _ => + ko + } + } + "decode string2" in { + PacketCoding.DecodePacket(string2).require match { + case FireHintMessage(weapon_guid, pos, u1, u2, u3, u4, u5) => + weapon_guid mustEqual PlanetSideGUID(3592) + pos mustEqual Vector3(2910.789f,3744.875f,69.0625f) + u1 mustEqual 0 + u2 mustEqual 65231 + u3 mustEqual 64736 + u4 mustEqual 3 + u5 mustEqual Some(Vector3(21.5f,-6.8125f,2.65625f)) + case _ => + ko + } + } + + "encode" in { + val msg = FireHintMessage(PlanetSideGUID(5889), Vector3(3482.2734f,3642.4922f,53.125f), 0, 65399, 7581, 0) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } + "encode string2" in { + val msg = FireHintMessage(PlanetSideGUID(3592), Vector3(2910.789f,3744.875f,69.0625f), 0, 65231, 64736, 3, Some(Vector3(21.5f,-6.8125f,2.65625f))) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string2 + } +} diff --git a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala index 4bcebf31d..c27f70641 100644 --- a/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala +++ b/common/src/test/scala/game/ObjectCreateDetailedMessageTest.scala @@ -201,10 +201,10 @@ class ObjectCreateDetailedMessageTest extends Specification { char.appearance.is_cloaking mustEqual false char.appearance.charging_pose mustEqual false char.appearance.on_zipline mustEqual false - char.appearance.ribbons.upper mustEqual 0xFFFFFFFFL //none - char.appearance.ribbons.middle mustEqual 0xFFFFFFFFL //none - char.appearance.ribbons.lower mustEqual 0xFFFFFFFFL //none - char.appearance.ribbons.tos mustEqual 0xFFFFFFFFL //none + char.appearance.ribbons.upper mustEqual MeritCommendation.None + char.appearance.ribbons.middle mustEqual MeritCommendation.None + char.appearance.ribbons.lower mustEqual MeritCommendation.None + char.appearance.ribbons.tos mustEqual MeritCommendation.None char.healthMax mustEqual 100 char.health mustEqual 100 char.armor mustEqual 50 //standard exosuit value diff --git a/common/src/test/scala/game/ObjectCreateMessageTest.scala b/common/src/test/scala/game/ObjectCreateMessageTest.scala index b60bb8115..5b6196870 100644 --- a/common/src/test/scala/game/ObjectCreateMessageTest.scala +++ b/common/src/test/scala/game/ObjectCreateMessageTest.scala @@ -4,7 +4,7 @@ package game import net.psforever.packet._ import net.psforever.packet.game._ import net.psforever.packet.game.objectcreate._ -import net.psforever.types.{CharacterGender, ExoSuitType, GrenadeState, PlanetSideEmpire, Vector3} +import net.psforever.types._ import org.specs2.mutable._ import scodec.bits._ @@ -684,10 +684,10 @@ class ObjectCreateMessageTest extends Specification { pc.appearance.is_cloaking mustEqual false pc.appearance.charging_pose mustEqual false pc.appearance.on_zipline mustEqual false - pc.appearance.ribbons.upper mustEqual 276L - pc.appearance.ribbons.middle mustEqual 239L - pc.appearance.ribbons.lower mustEqual 397L - pc.appearance.ribbons.tos mustEqual 360L + pc.appearance.ribbons.upper mustEqual MeritCommendation.Loser4 + pc.appearance.ribbons.middle mustEqual MeritCommendation.HeavyInfantry3 + pc.appearance.ribbons.lower mustEqual MeritCommendation.TankBuster6 + pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearNC pc.health mustEqual 255 pc.armor mustEqual 253 pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade @@ -780,10 +780,10 @@ class ObjectCreateMessageTest extends Specification { pc.appearance.is_cloaking mustEqual false pc.appearance.charging_pose mustEqual false pc.appearance.on_zipline mustEqual false - pc.appearance.ribbons.upper mustEqual 244L - pc.appearance.ribbons.middle mustEqual 353L - pc.appearance.ribbons.lower mustEqual 33L - pc.appearance.ribbons.tos mustEqual 361L + pc.appearance.ribbons.upper mustEqual MeritCommendation.Jacking + pc.appearance.ribbons.middle mustEqual MeritCommendation.ScavengerTR6 + pc.appearance.ribbons.lower mustEqual MeritCommendation.AMSSupport4 + pc.appearance.ribbons.tos mustEqual MeritCommendation.SixYearTR pc.health mustEqual 0 pc.armor mustEqual 0 pc.uniform_upgrade mustEqual UniformStyle.ThirdUpgrade @@ -1107,7 +1107,12 @@ class ObjectCreateMessageTest extends Specification { false, GrenadeState.None, false, false, false, - RibbonBars(276L, 239L, 397L, 360L) + RibbonBars( + MeritCommendation.Loser4, + MeritCommendation.HeavyInfantry3, + MeritCommendation.TankBuster6, + MeritCommendation.SixYearNC + ) ), 255, 253, UniformStyle.ThirdUpgrade, @@ -1159,7 +1164,12 @@ class ObjectCreateMessageTest extends Specification { false, GrenadeState.None, false, false, false, - RibbonBars(244L, 353L, 33L, 361L) + RibbonBars( + MeritCommendation.Jacking, + MeritCommendation.ScavengerTR6, + MeritCommendation.AMSSupport4, + MeritCommendation.SixYearTR + ) ), 0, 0, UniformStyle.ThirdUpgrade, diff --git a/common/src/test/scala/game/OxygenStateMessageTest.scala b/common/src/test/scala/game/OxygenStateMessageTest.scala new file mode 100644 index 000000000..034be18ef --- /dev/null +++ b/common/src/test/scala/game/OxygenStateMessageTest.scala @@ -0,0 +1,53 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class OxygenStateMessageTest extends Specification { + val string_self = hex"78 4b00f430" + val string_vehicle = hex"78 4b00f4385037a180" + + "decode (self)" in { + PacketCoding.DecodePacket(string_self).require match { + case OxygenStateMessage(guid, progress, active, veh_state) => + guid mustEqual PlanetSideGUID(75) + progress mustEqual 50.0 + active mustEqual true + veh_state.isDefined mustEqual false + case _ => + ko + } + } + + "decode (vehicle)" in { + PacketCoding.DecodePacket(string_vehicle).require match { + case OxygenStateMessage(guid, progress, active, veh_state) => + guid mustEqual PlanetSideGUID(75) + progress mustEqual 50.0f + active mustEqual true + veh_state.isDefined mustEqual true + veh_state.get.vehicle_guid mustEqual PlanetSideGUID(1546) + veh_state.get.progress mustEqual 50.0f + veh_state.get.active mustEqual true + case _ => + ko + } + } + + "encode (self)" in { + val msg = OxygenStateMessage(PlanetSideGUID(75), 50.0f, true) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_self + } + + "encode (vehicle)" in { + val msg = OxygenStateMessage(PlanetSideGUID(75), 50.0f, true, WaterloggedVehicleState(PlanetSideGUID(1546), 50.0f, true)) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_vehicle + } +} diff --git a/common/src/test/scala/game/ProjectileStateMessageTest.scala b/common/src/test/scala/game/ProjectileStateMessageTest.scala new file mode 100644 index 000000000..f62e181e1 --- /dev/null +++ b/common/src/test/scala/game/ProjectileStateMessageTest.scala @@ -0,0 +1,44 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.Vector3 +import scodec.bits._ + +class ProjectileStateMessageTest extends Specification { + val string = hex"3f 259d c5019 30e4a 9514 c52c9541 d9ba05c2 c5973941 00 f8 ec 020000" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case ProjectileStateMessage(projectile, pos, vel, unk1, unk2, unk3, unk4, time_alive) => + projectile mustEqual PlanetSideGUID(40229) + pos.x mustEqual 4611.539f + pos.y mustEqual 5576.375f + pos.z mustEqual 82.328125f + vel.x mustEqual 18.64686f + vel.y mustEqual -33.43247f + vel.z mustEqual 11.599553f + unk1 mustEqual 0 + unk2 mustEqual 248 + unk3 mustEqual 236 + unk4 mustEqual false + time_alive mustEqual 4 + case _ => + ko + } + } + + "encode" in { + val msg = ProjectileStateMessage( + PlanetSideGUID(40229), + Vector3(4611.539f, 5576.375f, 82.328125f), + Vector3(18.64686f, -33.43247f, 11.599553f), + 0, 248, 236, false, 4 + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} diff --git a/common/src/test/scala/game/TargetingImplantRequestTest.scala b/common/src/test/scala/game/TargetingImplantRequestTest.scala new file mode 100644 index 000000000..0a2112c15 --- /dev/null +++ b/common/src/test/scala/game/TargetingImplantRequestTest.scala @@ -0,0 +1,116 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class TargetingImplantRequestTest extends Specification { + val string_single = hex"b5 061016" + val string_long = hex"b5 41edeb12d4409f0144053f8010541ba91d03df376831b1e26000611041e1107c0209c0"//0510085013d9ffb6720d5b132900003770? + + "decode (single)" in { + PacketCoding.DecodePacket(string_single).require match { + case TargetingImplantRequest(target_list) => + target_list.length mustEqual 1 + //0 + target_list.head.target_guid mustEqual PlanetSideGUID(1412) + target_list.head.unk mustEqual true + case _ => + ko + } + } + + "decode (long)" in { + PacketCoding.DecodePacket(string_long).require match { + case TargetingImplantRequest(target_list) => + target_list.length mustEqual 16 + //0 + target_list.head.target_guid mustEqual PlanetSideGUID(31355) + target_list.head.unk mustEqual true + //1 + target_list(1).target_guid mustEqual PlanetSideGUID(27273) + target_list(1).unk mustEqual false + //2 + target_list(2).target_guid mustEqual PlanetSideGUID(40768) + target_list(2).unk mustEqual false + //3 + target_list(3).target_guid mustEqual PlanetSideGUID(34818) + target_list(3).unk mustEqual false + //4 + target_list(4).target_guid mustEqual PlanetSideGUID(65044) + target_list(4).unk mustEqual false + //5 + target_list(5).target_guid mustEqual PlanetSideGUID(33280) + target_list(5).unk mustEqual true + //6 + target_list(6).target_guid mustEqual PlanetSideGUID(47681) + target_list(6).unk mustEqual true + //7 + target_list(7).target_guid mustEqual PlanetSideGUID(40995) + target_list(7).unk mustEqual false + //8 + target_list(8).target_guid mustEqual PlanetSideGUID(52727) + target_list(8).unk mustEqual true + //9 + target_list(9).target_guid mustEqual PlanetSideGUID(6324) + target_list(9).unk mustEqual true + //10 + target_list(10).target_guid mustEqual PlanetSideGUID(58033) + target_list(10).unk mustEqual false + //11 + target_list(11).target_guid mustEqual PlanetSideGUID(192) + target_list(11).unk mustEqual true + //12 + target_list(12).target_guid mustEqual PlanetSideGUID(16772) + target_list(12).unk mustEqual false + //13 + target_list(13).target_guid mustEqual PlanetSideGUID(2063) + target_list(13).unk mustEqual true + //14 + target_list(14).target_guid mustEqual PlanetSideGUID(49159) + target_list(14).unk mustEqual false + //15 + target_list(15).target_guid mustEqual PlanetSideGUID(14401) + target_list(15).unk mustEqual false + case _ => + ko + } + } + + "encode (single)" in { + val msg = TargetingImplantRequest( + TargetRequest(PlanetSideGUID(1412), true) :: + Nil + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_single + } + + "encode (long)" in { + val msg = TargetingImplantRequest( + TargetRequest(PlanetSideGUID(31355), true) :: + TargetRequest(PlanetSideGUID(27273), false) :: + TargetRequest(PlanetSideGUID(40768), false) :: + TargetRequest(PlanetSideGUID(34818), false) :: + TargetRequest(PlanetSideGUID(65044), false) :: + TargetRequest(PlanetSideGUID(33280), true) :: + TargetRequest(PlanetSideGUID(47681), true) :: + TargetRequest(PlanetSideGUID(40995), false) :: + TargetRequest(PlanetSideGUID(52727), true) :: + TargetRequest(PlanetSideGUID(6324), true) :: + TargetRequest(PlanetSideGUID(58033), false) :: + TargetRequest(PlanetSideGUID(192), true) :: + TargetRequest(PlanetSideGUID(16772), false) :: + TargetRequest(PlanetSideGUID(2063), true) :: + TargetRequest(PlanetSideGUID(49159), false) :: + TargetRequest(PlanetSideGUID(14401), false) :: + Nil + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_long + } +} diff --git a/common/src/test/scala/game/TriggerEffectMessageTest.scala b/common/src/test/scala/game/TriggerEffectMessageTest.scala new file mode 100644 index 000000000..9e3e6faa5 --- /dev/null +++ b/common/src/test/scala/game/TriggerEffectMessageTest.scala @@ -0,0 +1,73 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import net.psforever.types.Vector3 +import scodec.bits._ + +class TriggerEffectMessageTest extends Specification { + val string_motionalarmsensor = hex"51 970B 82 6F6E FA00C00000" + val string_boomer = hex"51 0000 93 737061776E5F6F626A6563745F656666656374 417BB2CB3B4F8E00000000" + + "decode (motion alarm sensor)" in { + PacketCoding.DecodePacket(string_motionalarmsensor).require match { + case TriggerEffectMessage(guid, effect, unk, location) => + guid mustEqual PlanetSideGUID(2967) + effect mustEqual "on" + unk.isDefined mustEqual true + unk.get.unk1 mustEqual true + unk.get.unk2 mustEqual 1000L + location.isDefined mustEqual false + case _ => + ko + } + } + + "decode (boomer)" in { + PacketCoding.DecodePacket(string_boomer).require match { + case TriggerEffectMessage(guid, effect, unk, location) => + guid mustEqual PlanetSideGUID(0) + effect mustEqual "spawn_object_effect" + unk.isDefined mustEqual false + location.isDefined mustEqual true + location.get.pos.x mustEqual 3567.0156f + location.get.pos.y mustEqual 3278.6953f + location.get.pos.z mustEqual 114.484375f + location.get.roll mustEqual 0 + location.get.pitch mustEqual 0 + location.get.yaw mustEqual 0 + case _ => + ko + } + } + + "encode (motion alarm sensor)" in { + val msg = TriggerEffectMessage( + PlanetSideGUID(2967), + "on", + Some(TriggeredEffect(true, 1000L)), + None + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_motionalarmsensor + } + + "encode (boomer)" in { + val msg = TriggerEffectMessage( + PlanetSideGUID(0), + "spawn_object_effect", + None, + Some(TriggeredEffectLocation( + Vector3(3567.0156f, 3278.6953f, 114.484375f), + 0, 0, 0 + )) + ) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string_boomer + } +} + diff --git a/common/src/test/scala/game/UnuseItemMessageTest.scala b/common/src/test/scala/game/UnuseItemMessageTest.scala new file mode 100644 index 000000000..c4b08213d --- /dev/null +++ b/common/src/test/scala/game/UnuseItemMessageTest.scala @@ -0,0 +1,28 @@ +// Copyright (c) 2017 PSForever +package game + +import org.specs2.mutable._ +import net.psforever.packet._ +import net.psforever.packet.game._ +import scodec.bits._ + +class UnuseItemMessageTest extends Specification { + val string = hex"26 4B00 340D" + + "decode" in { + PacketCoding.DecodePacket(string).require match { + case UnuseItemMessage(player, item) => + player mustEqual PlanetSideGUID(75) + item mustEqual PlanetSideGUID(3380) + case _ => + ko + } + } + + "encode" in { + val msg = UnuseItemMessage(PlanetSideGUID(75), PlanetSideGUID(3380)) + val pkt = PacketCoding.EncodePacket(msg).require.toByteVector + + pkt mustEqual string + } +} diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index 33430253d..3347b1db6 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -255,6 +255,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ ChildObjectStateMessage(object_guid : PlanetSideGUID, pitch : Int, yaw : Int) => //log.info("ChildObjectState: " + msg) + case msg @ ProjectileStateMessage(projectile_guid, shot_pos, shot_vector, unk1, unk2, unk3, unk4, time_alive) => + //log.info("ProjectileState: " + msg) + case msg @ ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) => // TODO: Prevents log spam, but should be handled correctly if (messagetype != ChatMessageType.CMT_TOGGLE_GM) { @@ -363,6 +366,9 @@ class WorldSessionActor extends Actor with MDCContextAware { // TODO: This should only actually be sent to doors upon opening; may break non-door items upon use sendResponse(PacketCoding.CreateGamePacket(0, GenericObjectStateMsg(object_guid, 16))) } + + case msg @ UnuseItemMessage(player, item) => + log.info("UnuseItem: " + msg) case msg @ DeployObjectMessage(guid, unk1, pos, roll, pitch, yaw, unk2) => log.info("DeployObject: " + msg) @@ -440,6 +446,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ WeaponDryFireMessage(weapon) => log.info("WeaponDryFireMessage: "+msg) + case msg @ TargetingImplantRequest(list) => + log.info("TargetingImplantRequest: "+msg) + case default => log.error(s"Unhandled GamePacket ${pkt}") }