diff --git a/common/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala b/common/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala
index bdd77ed8e..a5d1ac358 100644
--- a/common/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala
@@ -6,9 +6,21 @@ import scodec.Codec
import scodec.codecs._
/**
- * Details regarding this shortcut.
+ * Details regarding this shortcut.
+ *
+ * The parameters `purpose` and `tile` are closely related, as is explained below.
+ * These two fields are constant for all shortcuts.
+ * The parameters `effect1` and `effect2` are exclusive to text macro shortcuts and are defaulted to empty `String`s.
+ * `tile` is related to what kind of graphic is displayed in this shortcut's slot on the hotbar.
+ * Implants and the medkit have self-explanatory graphics.
+ * The `shortcut_macro` setting displays a word bubble superimposed by the (first three letters of) `effect1` text.
*
* Purpose:
+ * `0 - Medkit`
+ * `1 - Macro`
+ * `2 - Implant`
+ *
+ * Tile:
* `advanced_regen` (regeneration)
* `audio_amplifier`
* `darklight_vision`
@@ -20,29 +32,41 @@ import scodec.codecs._
* `shortcut_macro`
* `silent_run` (sensor shield)
* `surge`
- * `targeting` (enchanced targetting)
+ * `targeting` (enhanced targetting)
+ *
+ * Exploration:
+ * Does the shortcut do something different when `purpose` is set to 3?
* @param purpose the primary purpose/use of this shortcut
+ * @param tile the visual element of the shortcut
* @param effect1 for macros, a three letter acronym displayed in the hotbar
* @param effect2 for macros, the chat message content
*/
-final case class Shortcut(purpose : String,
+final case class Shortcut(purpose : Int,
+ tile : String,
effect1 : String = "",
effect2 : String = "")
/**
- * Manipulate a quick-use button for the hotbar.
+ * Facilitate a quick-use button for the hotbar.
*
- * na
+ * The hotbar is the eight quick-use slots along the bottom center of the HUD.
+ * Each of these slots is a shortcut to the application of medkits from one's inventory, or use of an implant, or repetition of a text macro.
+ * There are actually sixty-four of these slots, divided into sets of eight, and tentatively bound to the Function keys depending on which set is selected at the time.
+ *
+ * When `addShortcut` is `true`, the provided `shortcut` will be defined and attached to the respective hotbar slot indicated by `slot`.
+ * If it is `false`, the given slot will be unbound and `shortcut` will be undefined.
+ * Nothing happens if the `slot` selection is invalid.
* @param player_guid the player
* @param slot the hotbar slot number (one-indexed)
- * @param unk1 na
- * @param unk2 na
+ * @param unk na; always zero?
+ * @param addShortcut true, if we are adding a shortcut; false, if we are removing any current shortcut
* @param shortcut optional; details about the shortcut to be created
+ * @see ChangeShortcutBankMessage
*/
final case class CreateShortcutMessage(player_guid : PlanetSideGUID,
slot : Int,
- unk1 : Int,
- unk2 : Int,
+ unk : Int,
+ addShortcut : Boolean,
shortcut : Option[Shortcut] = None)
extends PlanetSideGamePacket {
type Packet = CreateShortcutMessage
@@ -51,34 +75,63 @@ final case class CreateShortcutMessage(player_guid : PlanetSideGUID,
}
object Shortcut extends Marshallable[Shortcut] {
- // Needs to be Marshallable[T] for .as[T] to work its magic on the type of the codec Codec[T]
implicit val codec : Codec[Shortcut] = (
- ("purpose" | PacketHelpers.encodedStringAligned(5)) ::
+ ("purpose" | uintL(2)) ::
+ ("tile" | PacketHelpers.encodedStringAligned(5)) ::
("effect1" | PacketHelpers.encodedWideString) ::
("effect2" | PacketHelpers.encodedWideString)
).as[Shortcut]
// Convenient predefined Shortcuts for the Medkit and Implants
- final val AUDIO_AMPLIFIER : Shortcut = Shortcut("audio_amplifier")
- final val DARKLIGHT_VISION : Shortcut = Shortcut("darklight_vision")
- final val ENHANCED_TARGETING = Shortcut("targeting")
- final val MEDKIT : Shortcut = Shortcut("medkit")
- final val MELEE_BOOSTER : Shortcut = Shortcut("melee_booster")
- final val PERSONAL_SHIELD : Shortcut = Shortcut("personal_shield")
- final val RANGE_MAGNIFIER : Shortcut = Shortcut("range_magnifier")
- final val REGENERATION : Shortcut = Shortcut("advanced_regen")
- final val SECOND_WIND : Shortcut = Shortcut("second_wind")
- final val SENSOR_SHIELD : Shortcut = Shortcut("silent_run")
- final val SURGE : Shortcut = Shortcut("surge")
+ /**
+ * Preset for the Audio Amplifier implant. */
+ final val AUDIO_AMPLIFIER : Some[Shortcut] = Some(Shortcut(2, "audio_amplifier"))
+ /**
+ * Preset for the Darklight Vision implant. */
+ final val DARKLIGHT_VISION : Some[Shortcut] = Some(Shortcut(2, "darklight_vision"))
+ /**
+ * Preset for the Enhanced Targeting implant. */
+ final val ENHANCED_TARGETING : Some[Shortcut] = Some(Shortcut(2, "targeting"))
+ /**
+ * Preset for the medkit quick-use option. */
+ final val MEDKIT : Some[Shortcut] = Some(Shortcut(0, "medkit"))
+ /**
+ * Preset for the Melee Booster implant. */
+ final val MELEE_BOOSTER : Some[Shortcut] = Some(Shortcut(2, "melee_booster"))
+ /**
+ * Preset for the Personal Shield implant. */
+ final val PERSONAL_SHIELD : Some[Shortcut] = Some(Shortcut(2, "personal_shield"))
+ /**
+ * Preset for the Range Magnifier implant. */
+ final val RANGE_MAGNIFIER : Some[Shortcut] = Some(Shortcut(2, "range_magnifier"))
+ /**
+ * Preset for the Regeneration implant. */
+ final val REGENERATION : Some[Shortcut] = Some(Shortcut(2, "advanced_regen"))
+ /**
+ * Preset for the Second Wind implant. */
+ final val SECOND_WIND : Some[Shortcut] = Some(Shortcut(2, "second_wind"))
+ /**
+ * Preset for the Sensor Shield implant. */
+ final val SENSOR_SHIELD : Some[Shortcut] = Some(Shortcut(2, "silent_run"))
+ /**
+ * Preset for the Surge implant. */
+ final val SURGE : Some[Shortcut] = Some(Shortcut(2, "surge"))
+ /**
+ * Converter for text macro parameters that acts like a preset.
+ * @param effect1 a three letter acronym displayed in the hotbar
+ * @param effect2 the chat message content
+ * @return `Some` shortcut that represents a voice macro command
+ */
+ def MACRO(effect1 : String, effect2 : String) : Some[Shortcut] = Some(Shortcut(1, "shortcut_macro", effect1, effect2))
}
object CreateShortcutMessage extends Marshallable[CreateShortcutMessage] {
implicit val codec : Codec[CreateShortcutMessage] = (
("player_guid" | PlanetSideGUID.codec) ::
("slot" | uint8L) ::
- ("unk1" | uint8L) ::
- (("unk2" | uintL(3)) >>:~ { value =>
- conditional(value > 0, "shortcut" | Shortcut.codec).hlist
+ ("unk" | uint8L) ::
+ (("addShortcut" | bool) >>:~ { value =>
+ conditional(value, "shortcut" | Shortcut.codec).hlist
})
).as[CreateShortcutMessage]
}
diff --git a/common/src/test/scala/GamePacketTest.scala b/common/src/test/scala/GamePacketTest.scala
index ab89e8789..16053c5e8 100644
--- a/common/src/test/scala/GamePacketTest.scala
+++ b/common/src/test/scala/GamePacketTest.scala
@@ -301,35 +301,19 @@ class GamePacketTest extends Specification {
"CreateShortcutMessage" should {
val stringMedkit = hex"28 7210 01 00 90 C0 6D65646B6974 80 80"
- val stringImplant = hex"28 7210 04 00 D0 A0 7375726765 80 80"
val stringMacro = hex"28 4C05 08 00 B1 C0 73686F72746375745F6D6163726F 83 4E00 5400 5500 9B 2F00 7000 6C00 6100 7400 6F00 6F00 6E00 2000 4900 6E00 6300 6F00 6D00 6900 6E00 6700 2000 4E00 5400 5500 2000 7300 7000 6100 6D00 2100"
val stringRemove = hex"28 4C05 01 00 00"
"decode (medkit)" in {
PacketCoding.DecodePacket(stringMedkit).require match {
- case CreateShortcutMessage(player_guid, slot, unk1, unk2, shortcut) =>
+ case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) =>
player_guid mustEqual PlanetSideGUID(4210)
slot mustEqual 1
- unk1 mustEqual 0
- unk2 mustEqual 4
+ unk mustEqual 0
+ addShortcut mustEqual true
shortcut.isDefined mustEqual true
- shortcut.get.purpose mustEqual "medkit"
- shortcut.get.effect1 mustEqual ""
- shortcut.get.effect2 mustEqual ""
- case default =>
- ko
- }
- }
-
- "decode (implant)" in {
- PacketCoding.DecodePacket(stringImplant).require match {
- case CreateShortcutMessage(player_guid, slot, unk1, unk2, shortcut) =>
- player_guid mustEqual PlanetSideGUID(4210)
- slot mustEqual 4
- unk1 mustEqual 0
- unk2 mustEqual 6
- shortcut.isDefined mustEqual true
- shortcut.get.purpose mustEqual "surge"
+ shortcut.get.purpose mustEqual 0
+ shortcut.get.tile mustEqual "medkit"
shortcut.get.effect1 mustEqual ""
shortcut.get.effect2 mustEqual ""
case default =>
@@ -339,13 +323,14 @@ class GamePacketTest extends Specification {
"decode (macro)" in {
PacketCoding.DecodePacket(stringMacro).require match {
- case CreateShortcutMessage(player_guid, slot, unk1, unk2, shortcut) =>
+ case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) =>
player_guid mustEqual PlanetSideGUID(1356)
slot mustEqual 8
- unk1 mustEqual 0
- unk2 mustEqual 5
+ unk mustEqual 0
+ addShortcut mustEqual true
shortcut.isDefined mustEqual true
- shortcut.get.purpose mustEqual "shortcut_macro"
+ shortcut.get.purpose mustEqual 1
+ shortcut.get.tile mustEqual "shortcut_macro"
shortcut.get.effect1 mustEqual "NTU"
shortcut.get.effect2 mustEqual "/platoon Incoming NTU spam!"
case default =>
@@ -355,11 +340,11 @@ class GamePacketTest extends Specification {
"decode (remove)" in {
PacketCoding.DecodePacket(stringRemove).require match {
- case CreateShortcutMessage(player_guid, slot, unk1, unk2, shortcut) =>
+ case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) =>
player_guid mustEqual PlanetSideGUID(1356)
slot mustEqual 1
- unk1 mustEqual 0
- unk2 mustEqual 0
+ unk mustEqual 0
+ addShortcut mustEqual false
shortcut.isDefined mustEqual false
case default =>
ko
@@ -367,32 +352,58 @@ class GamePacketTest extends Specification {
}
"encode (medkit)" in {
- val msg = CreateShortcutMessage(PlanetSideGUID(4210), 1, 0, 4, Some(Shortcut("medkit")))
+ val msg = CreateShortcutMessage(PlanetSideGUID(4210), 1, 0, true, Some(Shortcut(0, "medkit")))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual stringMedkit
}
- "encode (implant)" in {
- val msg = CreateShortcutMessage(PlanetSideGUID(4210), 4, 0, 6, Some(Shortcut("surge")))
- val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
-
- pkt mustEqual stringImplant
- }
-
"encode (macro)" in {
- val msg = CreateShortcutMessage(PlanetSideGUID(1356), 8, 0, 5, Some(Shortcut("shortcut_macro", "NTU", "/platoon Incoming NTU spam!")))
+ val msg = CreateShortcutMessage(PlanetSideGUID(1356), 8, 0, true, Some(Shortcut(1, "shortcut_macro", "NTU", "/platoon Incoming NTU spam!")))
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual stringMacro
}
"encode (remove)" in {
- val msg = CreateShortcutMessage(PlanetSideGUID(1356), 1, 0, 0)
+ val msg = CreateShortcutMessage(PlanetSideGUID(1356), 1, 0, false)
val pkt = PacketCoding.EncodePacket(msg).require.toByteVector
pkt mustEqual stringRemove
}
+
+ "macro" in {
+ val MACRO : Some[Shortcut] = Shortcut.MACRO("NTU", "/platoon Incoming NTU spam!")
+ MACRO.get.purpose mustEqual 1
+ MACRO.get.tile mustEqual "shortcut_macro"
+ MACRO.get.effect1 mustEqual "NTU"
+ MACRO.get.effect2 mustEqual "/platoon Incoming NTU spam!"
+ }
+
+ "presets" in {
+ Shortcut.AUDIO_AMPLIFIER.get.purpose mustEqual 2
+ Shortcut.AUDIO_AMPLIFIER.get.tile mustEqual "audio_amplifier"
+ Shortcut.DARKLIGHT_VISION.get.purpose mustEqual 2
+ Shortcut.DARKLIGHT_VISION.get.tile mustEqual "darklight_vision"
+ Shortcut.ENHANCED_TARGETING.get.purpose mustEqual 2
+ Shortcut.ENHANCED_TARGETING.get.tile mustEqual "targeting"
+ Shortcut.MEDKIT.get.purpose mustEqual 0
+ Shortcut.MEDKIT.get.tile mustEqual "medkit"
+ Shortcut.MELEE_BOOSTER.get.purpose mustEqual 2
+ Shortcut.MELEE_BOOSTER.get.tile mustEqual "melee_booster"
+ Shortcut.PERSONAL_SHIELD.get.purpose mustEqual 2
+ Shortcut.PERSONAL_SHIELD.get.tile mustEqual "personal_shield"
+ Shortcut.RANGE_MAGNIFIER.get.purpose mustEqual 2
+ Shortcut.RANGE_MAGNIFIER.get.tile mustEqual "range_magnifier"
+ Shortcut.REGENERATION.get.purpose mustEqual 2
+ Shortcut.REGENERATION.get.tile mustEqual "advanced_regen"
+ Shortcut.SECOND_WIND.get.purpose mustEqual 2
+ Shortcut.SECOND_WIND.get.tile mustEqual "second_wind"
+ Shortcut.SENSOR_SHIELD.get.purpose mustEqual 2
+ Shortcut.SENSOR_SHIELD.get.tile mustEqual "silent_run"
+ Shortcut.SURGE.get.purpose mustEqual 2
+ Shortcut.SURGE.get.tile mustEqual "surge"
+ }
}
"DropItemMessage" should {
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 4902709da..e1bfb323a 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -171,7 +171,7 @@ class WorldSessionActor extends Actor with MDCContextAware {
true))) //Boosted generator room pain field
sendResponse(PacketCoding.CreateGamePacket(0, SetCurrentAvatarMessage(PlanetSideGUID(guid),0,0)))
- sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(PlanetSideGUID(guid), 1, 0, 6, Some(Shortcut.MEDKIT))))
+ sendResponse(PacketCoding.CreateGamePacket(0, CreateShortcutMessage(PlanetSideGUID(guid), 1, 0, true, Shortcut.MEDKIT)))
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global