diff --git a/server/src/main/resources/db/migration/V007__Shortcuts.sql b/server/src/main/resources/db/migration/V007__Shortcuts.sql
new file mode 100644
index 00000000..3ec11d39
--- /dev/null
+++ b/server/src/main/resources/db/migration/V007__Shortcuts.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS "shortcut" (
+ "avatar_id" INT NOT NULL REFERENCES avatar (id),
+ "slot" SMALLINT NOT NULL,
+ "purpose" SMALLINT NOT NULL,
+ "tile" VARCHAR(20) NOT NULL,
+ "effect1" VARCHAR(3),
+ "effect2" TEXT,
+ UNIQUE(avatar_id, slot)
+);
diff --git a/server/src/test/scala/actor/service/AvatarServiceTest.scala b/server/src/test/scala/actor/service/AvatarServiceTest.scala
index 448d4a27..665c7ec7 100644
--- a/server/src/test/scala/actor/service/AvatarServiceTest.scala
+++ b/server/src/test/scala/actor/service/AvatarServiceTest.scala
@@ -237,8 +237,8 @@ class ObjectHeldTest extends ActorTest {
ServiceManager.boot(system)
val service = system.actorOf(Props(classOf[AvatarService], Zone.Nowhere), AvatarServiceTest.TestName)
service ! Service.Join("test")
- service ! AvatarServiceMessage("test", AvatarAction.ObjectHeld(PlanetSideGUID(10), 1))
- expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectHeld(1)))
+ service ! AvatarServiceMessage("test", AvatarAction.ObjectHeld(PlanetSideGUID(10), 1, 2))
+ expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectHeld(1, 2)))
}
}
}
diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
index f4a1ac1c..33883ed9 100644
--- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala
+++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
@@ -13,7 +13,7 @@ import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
import scala.util.{Failure, Success}
import scala.concurrent.duration._
//
-import net.psforever.objects.avatar.{Friend => AvatarFriend, Ignored => AvatarIgnored, _}
+import net.psforever.objects.avatar.{Friend => AvatarFriend, Ignored => AvatarIgnored, Shortcut => AvatarShortcut, _}
import net.psforever.objects.definition.converter.CharacterSelectConverter
import net.psforever.objects.definition._
import net.psforever.objects.inventory.Container
@@ -189,6 +189,10 @@ object AvatarActor {
final case class MemberListRequest(action: MemberAction.Value, name: String) extends Command
+ final case class AddShortcut(slot: Int, shortcut: Shortcut) extends Command
+
+ final case class RemoveShortcut(slot: Int) extends Command
+
final case class AvatarResponse(avatar: Avatar)
final case class AvatarLoginResponse(avatar: Avatar)
@@ -826,7 +830,7 @@ class AvatarActor(
characters.headOption match {
case None =>
val result = for {
- id <- ctx.run(
+ _ <- ctx.run(
query[persistence.Avatar]
.insert(
_.name -> lift(name),
@@ -838,17 +842,6 @@ class AvatarActor(
_.bep -> lift(Config.app.game.newAvatar.br.experience),
_.cep -> lift(Config.app.game.newAvatar.cr.experience)
)
- .returningGenerated(_.id)
- )
- _ <- ctx.run(
- liftQuery(
- List(
- persistence.Certification(Certification.MediumAssault.value, id),
- persistence.Certification(Certification.ReinforcedExoSuit.value, id),
- persistence.Certification(Certification.ATV.value, id),
- persistence.Certification(Certification.Harasser.value, id)
- )
- ).foreach(c => query[persistence.Certification].insert(c))
)
} yield ()
@@ -921,53 +914,46 @@ class AvatarActor(
case LoginAvatar(replyTo) =>
import ctx._
val avatarId = avatar.id
- val result = for {
- //log this login
- _ <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId))
- .update(_.lastLogin -> lift(LocalDateTime.now()))
- )
- //log this choice of faction (no empire switching)
- _ <- ctx.run(query[persistence.Account].filter(_.id == lift(account.id))
- .update(_.lastFactionId -> lift(avatar.faction.id))
- )
- //retrieve avatar data
- loadouts <- initializeAllLoadouts()
- implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatarId)))
- certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatarId)))
- locker <- loadLocker(avatarId)
- friends <- loadFriendList(avatarId)
- ignored <- loadIgnoredList(avatarId)
- saved <- AvatarActor.loadSavedAvatarData(avatarId)
- } yield (loadouts, implants, certs, locker, friends, ignored, saved)
- result.onComplete {
- case Success((_loadouts, implants, certs, locker, friendsList, ignoredList, saved)) =>
- avatarCopy(
- avatar.copy(
- loadouts = avatar.loadouts.copy(suit = _loadouts),
- certifications =
- certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications,
- implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None),
- locker = locker,
- people = MemberLists(
- friend = friendsList,
- ignored = ignoredList
- ),
- cooldowns = Cooldowns(
- purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log),
- use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log)
- )
- )
- )
- // if we need to start stamina regeneration
- tryRestoreStaminaForSession(stamina = 1) match {
- case Some(_) =>
- defaultStaminaRegen(initialDelay = 0.5f seconds)
- case _ => ;
- }
- replyTo ! AvatarLoginResponse(avatar)
- case Failure(e) =>
- log.error(e)("db failure")
- }
+ ctx.run(
+ query[persistence.Avatar]
+ .filter(_.id == lift(avatarId))
+ .map { c => (c.created, c.lastLogin) }
+ )
+ .onComplete {
+ case Success(value) if value.nonEmpty =>
+ val (created, lastLogin) = value.head
+ if (created.equals(lastLogin)) {
+ //first login
+ //initialize default values that would be compromised during login if blank
+ val inits = for {
+ _ <- ctx.run(
+ liftQuery(
+ List(
+ persistence.Certification(Certification.StandardExoSuit.value, avatarId),
+ persistence.Certification(Certification.AgileExoSuit.value, avatarId),
+ persistence.Certification(Certification.ReinforcedExoSuit.value, avatarId),
+ persistence.Certification(Certification.StandardAssault.value, avatarId),
+ persistence.Certification(Certification.MediumAssault.value, avatarId),
+ persistence.Certification(Certification.ATV.value, avatarId),
+ persistence.Certification(Certification.Harasser.value, avatarId)
+ )
+ ).foreach(c => query[persistence.Certification].insert(c))
+ )
+ _ <- ctx.run(
+ liftQuery(
+ List(persistence.Shortcut(avatarId, 0, 0, "medkit"))
+ ).foreach(c => query[persistence.Shortcut].insert(c))
+ )
+ } yield true
+ inits.onComplete {
+ case Success(_) => performAvatarLogin(avatarId, account.id, replyTo)
+ case Failure(e) => log.error(e)("db failure")
+ }
+ } else {
+ performAvatarLogin(avatarId, account.id, replyTo)
+ }
+ case Failure(e) => log.error(e)("db failure")
+ }
Behaviors.same
case ReplaceAvatar(newAvatar) =>
@@ -1353,7 +1339,7 @@ class AvatarActor(
updatePurchaseTimer(
name,
cooldown.toSeconds,
- DefinitionUtil.fromString(name).isInstanceOf[VehicleDefinition]
+ item.isInstanceOf[VehicleDefinition]
)
case _ => ;
}
@@ -1617,6 +1603,91 @@ class AvatarActor(
case MemberListRequest(action, name) =>
memberListAction(action, name)
Behaviors.same
+
+ case AddShortcut(slot, shortcut) =>
+ import ctx._
+ if (slot > -1) {
+ val targetShortcut = avatar.shortcuts.lift(slot).flatten
+ //short-circuit if the shortcut already exists at the given location
+ val isMacroShortcut = shortcut.isInstanceOf[Shortcut.Macro]
+ val isDifferentShortcut = !(targetShortcut match {
+ case Some(target) => AvatarShortcut.equals(shortcut, target)
+ case _ => false
+ })
+ if (isDifferentShortcut) {
+ if (!isMacroShortcut && avatar.shortcuts.flatten.exists {
+ a => AvatarShortcut.equals(shortcut, a)
+ }) {
+ //duplicate implant or medkit found
+ if (shortcut.isInstanceOf[Shortcut.Implant]) {
+ //duplicate implant
+ targetShortcut match {
+ case Some(existingShortcut) =>
+ //redraw redundant shortcut slot with existing shortcut
+ sessionActor ! SessionActor.SendResponse(
+ CreateShortcutMessage(session.get.player.GUID, slot + 1, Some(AvatarShortcut.convert(existingShortcut)))
+ )
+ case _ =>
+ //blank shortcut slot
+ sessionActor ! SessionActor.SendResponse(CreateShortcutMessage(session.get.player.GUID, slot + 1, None))
+ }
+ }
+ } else {
+ //macro, or implant or medkit
+ val (optEffect1, optEffect2, optShortcut) = shortcut match {
+ case Shortcut.Macro(acro, msg) =>
+ (
+ acro,
+ msg,
+ Some(AvatarShortcut(shortcut.code, shortcut.tile, acro, msg))
+ )
+ case _ =>
+ (null, null, Some(AvatarShortcut(shortcut.code, shortcut.tile)))
+ }
+ targetShortcut match {
+ case Some(_) =>
+ ctx.run(
+ query[persistence.Shortcut]
+ .filter(_.avatarId == lift(avatar.id.toLong))
+ .filter(_.slot == lift(slot))
+ .update(
+ _.purpose -> lift(shortcut.code),
+ _.tile -> lift(shortcut.tile),
+ _.effect1 -> Option(lift(optEffect1)),
+ _.effect2 -> Option(lift(optEffect2))
+ )
+ )
+ case None =>
+ ctx.run(
+ query[persistence.Shortcut].insert(
+ _.avatarId -> lift(avatar.id.toLong),
+ _.slot -> lift(slot),
+ _.purpose -> lift(shortcut.code),
+ _.tile -> lift(shortcut.tile),
+ _.effect1 -> Option(lift(optEffect1)),
+ _.effect2 -> Option(lift(optEffect2))
+ )
+ )
+ }
+ avatar.shortcuts.update(slot, optShortcut)
+ }
+ }
+ }
+ Behaviors.same
+
+ case RemoveShortcut(slot) =>
+ import ctx._
+ avatar.shortcuts.lift(slot).flatten match {
+ case None => ;
+ case Some(_) =>
+ ctx.run(query[persistence.Shortcut]
+ .filter(_.avatarId == lift(avatar.id.toLong))
+ .filter(_.slot == lift(slot))
+ .delete
+ )
+ avatar.shortcuts.update(slot, None)
+ }
+ Behaviors.same
}
.receiveSignal {
case (_, PostStop) =>
@@ -1636,6 +1707,78 @@ class AvatarActor(
Future.failed(ex).asInstanceOf[Future[Loadout]]
}
+ def performAvatarLogin(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
+ import ctx._
+
+ val result = for {
+ //log this login
+ _ <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId))
+ .update(_.lastLogin -> lift(LocalDateTime.now()))
+ )
+ //log this choice of faction (no empire switching)
+ _ <- ctx.run(query[persistence.Account].filter(_.id == lift(accountId))
+ .update(_.lastFactionId -> lift(avatar.faction.id))
+ )
+ //retrieve avatar data
+ loadouts <- initializeAllLoadouts()
+ implants <- ctx.run(query[persistence.Implant].filter(_.avatarId == lift(avatarId)))
+ certs <- ctx.run(query[persistence.Certification].filter(_.avatarId == lift(avatarId)))
+ locker <- loadLocker(avatarId)
+ friends <- loadFriendList(avatarId)
+ ignored <- loadIgnoredList(avatarId)
+ shortcuts <- loadShortcuts(avatarId)
+ saved <- AvatarActor.loadSavedAvatarData(avatarId)
+ } yield (loadouts, implants, certs, locker, friends, ignored, shortcuts, saved)
+ result.onComplete {
+ case Success((_loadouts, implants, certs, locker, friendsList, ignoredList, shortcutList, saved)) =>
+ //shortcuts must have a hotbar option for each implant
+// val implantShortcuts = shortcutList.filter {
+// case Some(e) => e.purpose == 0
+// case None => false
+// }
+// implants.filterNot { implant =>
+// implantShortcuts.exists {
+// case Some(a) => a.tile.equals(implant.name)
+// case None => false
+// }
+// }.foreach { c =>
+// shortcutList.indexWhere { _.isEmpty } match {
+// case -1 => ;
+// case index =>
+// shortcutList.update(index, Some(AvatarShortcut(2, c.name)))
+// }
+// }
+ //
+ avatarCopy(
+ avatar.copy(
+ loadouts = avatar.loadouts.copy(suit = _loadouts),
+ certifications =
+ certs.map(cert => Certification.withValue(cert.id)).toSet ++ Config.app.game.baseCertifications,
+ implants = implants.map(implant => Some(Implant(implant.toImplantDefinition))).padTo(3, None),
+ shortcuts = shortcutList,
+ locker = locker,
+ people = MemberLists(
+ friend = friendsList,
+ ignored = ignoredList
+ ),
+ cooldowns = Cooldowns(
+ purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log),
+ use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log)
+ )
+ )
+ )
+ // if we need to start stamina regeneration
+ tryRestoreStaminaForSession(stamina = 1) match {
+ case Some(_) =>
+ defaultStaminaRegen(initialDelay = 0.5f seconds)
+ case _ => ;
+ }
+ replyTo ! AvatarLoginResponse(avatar)
+ case Failure(e) =>
+ log.error(e)("db failure")
+ }
+ }
+
/**
* na
* @see `avatarCopy(Avatar)`
@@ -1781,8 +1924,6 @@ class AvatarActor(
CreateShortcutMessage(
session.get.player.GUID,
slot + 2,
- 0,
- addShortcut = true,
Some(implant.definition.implantType.shortcut)
)
)
@@ -2303,6 +2444,30 @@ class AvatarActor(
out.future
}
+ def loadShortcuts(avatarId: Long): Future[Array[Option[AvatarShortcut]]] = {
+ import ctx._
+ val out: Promise[Array[Option[AvatarShortcut]]] = Promise()
+
+ val queryResult = ctx.run(
+ query[persistence.Shortcut].filter { _.avatarId == lift(avatarId) }
+ .map { shortcut => (shortcut.slot, shortcut.purpose, shortcut.tile, shortcut.effect1, shortcut.effect2) }
+ )
+ val output = Array.fill[Option[AvatarShortcut]](64)(None)
+ queryResult.onComplete {
+ case Success(list) =>
+ list.foreach { case (slot, purpose, tile, effect1, effect2) =>
+ output.update(slot, Some(AvatarShortcut(purpose, tile, effect1.getOrElse(""), effect2.getOrElse(""))))
+ }
+ out.completeWith(Future(output))
+ case Failure(e) =>
+ //something went wrong, but we can recover
+ log.warn(e)("db failure")
+ //output.update(0, Some(AvatarShortcut(0, "medkit")))
+ out.completeWith(Future(output))
+ }
+ out.future
+ }
+
def startIfStoppedStaminaRegen(initialDelay: FiniteDuration): Unit = {
if (staminaRegenTimer.isCancelled) {
defaultStaminaRegen(initialDelay)
diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala
index e83a2c22..e031b452 100644
--- a/src/main/scala/net/psforever/actors/session/ChatActor.scala
+++ b/src/main/scala/net/psforever/actors/session/ChatActor.scala
@@ -1,31 +1,37 @@
package net.psforever.actors.session
import akka.actor.Cancellable
-import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy}
+import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer}
+import akka.actor.typed.scaladsl.adapter._
+import net.psforever.objects.avatar.{Shortcut => AvatarShortcut}
+import net.psforever.objects.definition.ImplantDefinition
+import net.psforever.packet.game.{CreateShortcutMessage, Shortcut}
+import net.psforever.packet.game.objectcreate.DrawnSlot
+import net.psforever.types.ImplantType
+
+import scala.collection.mutable
+import scala.concurrent.ExecutionContextExecutor
+import scala.concurrent.duration._
+//
import net.psforever.actors.zone.BuildingActor
+import net.psforever.login.WorldSession
+import net.psforever.objects.{Default, Player, Session}
import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Cosmetic}
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad}
-import net.psforever.objects.{Default, Player, Session}
import net.psforever.objects.serverobject.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets}
import net.psforever.objects.zones.Zoning
import net.psforever.packet.game.{ChatMsg, DeadState, RequestDestroyMessage, ZonePopulationUpdateMessage}
+import net.psforever.services.{CavernRotationService, InterstellarClusterService}
+import net.psforever.services.chat.ChatService
+import net.psforever.services.chat.ChatService.ChatChannel
+import net.psforever.types.ChatMessageType.UNK_229
import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.util.{Config, PointOfInterest}
import net.psforever.zones.Zones
-import net.psforever.services.chat.ChatService
-import net.psforever.services.chat.ChatService.ChatChannel
-
-import scala.concurrent.ExecutionContextExecutor
-import scala.concurrent.duration._
-import akka.actor.typed.scaladsl.adapter._
-import net.psforever.services.{CavernRotationService, InterstellarClusterService}
-import net.psforever.types.ChatMessageType.UNK_229
-
-import scala.collection.mutable
object ChatActor {
def apply(
@@ -430,7 +436,7 @@ class ChatActor(
case (_, _, "!loc") =>
val continent = session.zone
- val player = session.player
+ val player = session.player
val loc =
s"zone=${continent.id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}"
log.info(loc)
@@ -487,8 +493,8 @@ class ChatActor(
val buffer = content.toLowerCase.split("\\s+")
val (facility, customNtuValue) = (buffer.lift(1), buffer.lift(2)) match {
case (Some(x), Some(y)) if y.toIntOption.nonEmpty => (Some(x), Some(y.toInt))
- case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
- case _ => (None, None)
+ case (Some(x), None) if x.toIntOption.nonEmpty => (None, Some(x.toInt))
+ case _ => (None, None)
}
val silos = (facility match {
case Some(cur) if cur.toLowerCase().startsWith("curr") =>
@@ -501,12 +507,17 @@ class ChatActor(
case Some(all) if all.toLowerCase.startsWith("all") =>
session.zone.Buildings.values
case Some(x) =>
- session.zone.Buildings.values.find { _.Name.equalsIgnoreCase(x) }.toList
+ session.zone.Buildings.values.find {
+ _.Name.equalsIgnoreCase(x)
+ }.toList
case _ =>
session.zone.Buildings.values
})
- .flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } }
- ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent=s"$facility")
+ .flatMap { building => building.Amenities.filter {
+ _.isInstanceOf[ResourceSilo]
+ }
+ }
+ ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent = s"$facility")
case (_, _, content) if content.startsWith("!zonerotate") && gmCommandAllowed =>
val buffer = contents.toLowerCase.split("\\s+")
@@ -523,8 +534,53 @@ class ChatActor(
tplayer.Revive
tplayer.Actor ! Player.Die()
- case _ =>
- // unknown ! commands are ignored
+ case (_, _, content) if content.startsWith("!grenade") =>
+ WorldSession.QuickSwapToAGrenade(session.player, DrawnSlot.Pistol1.id, log)
+
+ case (_, _, content) if content.startsWith("!macro") =>
+ val avatar = session.avatar
+ val args = contents.split(" ").filter(_ != "")
+ (args.lift(1), args.lift(2)) match {
+ case (Some(cmd), other) =>
+ cmd.toLowerCase() match {
+ case "medkit" =>
+ medkitSanityTest(session.player.GUID, avatar.shortcuts)
+
+ case "implants" =>
+ //implant shortcut sanity test
+ implantSanityTest(
+ session.player.GUID,
+ avatar.implants.collect {
+ case Some(implant) if implant.definition.implantType != ImplantType.None => implant.definition
+ },
+ avatar.shortcuts
+ )
+
+ case name
+ if ImplantType.values.exists { a => a.shortcut.tile.equals(name) } =>
+ avatar.implants.find {
+ case Some(implant) => implant.definition.Name.equalsIgnoreCase(name)
+ case None => false
+ } match {
+ case Some(Some(implant)) =>
+ //specific implant shortcut sanity test
+ implantSanityTest(session.player.GUID, Seq(implant.definition), avatar.shortcuts)
+ case _ if other.nonEmpty =>
+ //add macro?
+ macroSanityTest(session.player.GUID, name, args.drop(2).mkString(" "), avatar.shortcuts)
+ case _ => ;
+ }
+
+ case name
+ if name.nonEmpty && other.nonEmpty =>
+ //add macro
+ macroSanityTest(session.player.GUID, name, args.drop(2).mkString(" "), avatar.shortcuts)
+
+ case _ => ;
+ }
+ case _ => ;
+ // unknown ! commands are ignored
+ }
}
case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed =>
@@ -1168,7 +1224,107 @@ class ChatActor(
case _ =>
Behaviors.same
}
-
}
+ /**
+ * Create a medkit shortcut if there is no medkit shortcut on the hotbar.
+ * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
+ * or cancel / invalidate the shortcut creation.
+ * @see `Array::indexWhere`
+ * @see `CreateShortcutMessage`
+ * @see `net.psforever.objects.avatar.Shortcut`
+ * @see `net.psforever.packet.game.Shortcut.Medkit`
+ * @see `SessionActor.SendResponse`
+ * @param guid current player unique identifier for the target client
+ * @param shortcuts list of all existing shortcuts, used for early validation
+ */
+ def medkitSanityTest(
+ guid: PlanetSideGUID,
+ shortcuts: Array[Option[AvatarShortcut]]
+ ): Unit = {
+ if (!shortcuts.exists {
+ case Some(a) => a.purpose == 0
+ case None => false
+ }) {
+ shortcuts.indexWhere(_.isEmpty) match {
+ case -1 => ;
+ case index =>
+ //new shortcut
+ sessionActor ! SessionActor.SendResponse(CreateShortcutMessage(
+ guid,
+ index + 1,
+ Some(Shortcut.Medkit())
+ ))
+ }
+ }
+ }
+
+ /**
+ * Create all implant macro shortcuts for all implants whose shortcuts have been removed from the hotbar.
+ * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
+ * or cancel / invalidate the shortcut creation.
+ * @see `CreateShortcutMessage`
+ * @see `ImplantDefinition`
+ * @see `net.psforever.objects.avatar.Shortcut`
+ * @see `SessionActor.SendResponse`
+ * @param guid current player unique identifier for the target client
+ * @param haveImplants list of implants the player possesses
+ * @param shortcuts list of all existing shortcuts, used for early validation
+ */
+ def implantSanityTest(
+ guid: PlanetSideGUID,
+ haveImplants: Iterable[ImplantDefinition],
+ shortcuts: Array[Option[AvatarShortcut]]
+ ): Unit = {
+ val haveImplantShortcuts = shortcuts.collect {
+ case Some(shortcut) if shortcut.purpose == 2 => shortcut.tile
+ }
+ var start: Int = 0
+ haveImplants.filterNot { imp => haveImplantShortcuts.contains(imp.Name) }
+ .foreach { implant =>
+ shortcuts.indexWhere(_.isEmpty, start) match {
+ case -1 => ;
+ case index => ;
+ //new shortcut
+ start = index + 1
+ sessionActor ! SessionActor.SendResponse(CreateShortcutMessage(
+ guid,
+ start,
+ Some(implant.implantType.shortcut)
+ ))
+ }
+ }
+ }
+
+ /**
+ * Create a text chat macro shortcut if it doesn't already exist.
+ * Bounce the packet to the client and the client will bounce it back to the server to continue the setup,
+ * or cancel / invalidate the shortcut creation.
+ * @see `Array::indexWhere`
+ * @see `CreateShortcutMessage`
+ * @see `net.psforever.objects.avatar.Shortcut`
+ * @see `net.psforever.packet.game.Shortcut.Macro`
+ * @see `SessionActor.SendResponse`
+ * @param guid current player unique identifier for the target client
+ * @param acronym three letters emblazoned on the shortcut icon
+ * @param msg the message published to text chat
+ * @param shortcuts a list of all existing shortcuts, used for early validation
+ */
+ def macroSanityTest(
+ guid: PlanetSideGUID,
+ acronym: String,
+ msg: String,
+ shortcuts: Array[Option[AvatarShortcut]]
+ ): Unit = {
+ shortcuts.indexWhere(_.isEmpty) match {
+ case -1 => ;
+ case index => ;
+ //new shortcut
+ sessionActor ! SessionActor.SendResponse(CreateShortcutMessage(
+ guid,
+ index + 1,
+ Some(Shortcut.Macro(acronym, msg))
+ ))
+ }
+ }
}
diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala
index f8fa791f..a2b66d94 100644
--- a/src/main/scala/net/psforever/actors/session/SessionActor.scala
+++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala
@@ -9,7 +9,7 @@ import net.psforever.actors.net.MiddlewareActor
import net.psforever.actors.zone.ZoneActor
import net.psforever.login.WorldSession._
import net.psforever.objects._
-import net.psforever.objects.avatar._
+import net.psforever.objects.avatar.{Shortcut => AvatarShortcut, _}
import net.psforever.objects.ballistics._
import net.psforever.objects.ce._
import net.psforever.objects.definition._
@@ -1649,6 +1649,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
player.Armor = results.armor
player.ExoSuit = ExoSuitType(results.exosuitNum)
AvatarActor.buildContainedEquipmentFromClob(player, results.loadout, log)
+ if (player.ExoSuit == ExoSuitType.MAX) {
+ player.DrawnSlot = 0
+ player.ResistArmMotion(PlayerControl.maxRestriction)
+ }
} else {
player.ExoSuit = ExoSuitType.Standard
DefinitionUtil.applyDefaultLoadout(player)
@@ -2311,9 +2315,21 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(ObjectDeleteMessage(item_guid, unk))
}
- case AvatarResponse.ObjectHeld(slot) =>
- if (tplayer_guid != guid) {
- sendResponse(ObjectHeldMessage(guid, slot, false))
+ case AvatarResponse.ObjectHeld(slot, previousSLot) =>
+ if (tplayer_guid == guid) {
+ if (slot > -1) {
+ sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
+ //Stop using proximity terminals if player unholsters a weapon
+ if (player.VisibleSlots.contains(slot)) {
+ continent.GUID(usingMedicalTerminal) match {
+ case Some(term: Terminal with ProximityUnit) =>
+ StopUsingProximityUnit(term)
+ case _ => ;
+ }
+ }
+ }
+ } else {
+ sendResponse(ObjectHeldMessage(guid, previousSLot, unk1=false))
}
case AvatarResponse.OxygenState(player, vehicle) =>
@@ -2565,7 +2581,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(PlanetsideAttributeMessage(target, 4, armor))
if (tplayer_guid == target) {
//happening to this player
- sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, false))
+ sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, true))
//cleanup
(old_holsters ++ old_inventory).foreach {
case (obj, objGuid) =>
@@ -3382,10 +3398,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
CancelAllProximityUnits()
if (player.VisibleSlots.contains(player.DrawnSlot)) {
player.DrawnSlot = Player.HandsDownSlot
- sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, true))
+ sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, unk1=true))
continent.AvatarEvents ! AvatarServiceMessage(
continent.id,
- AvatarAction.ObjectHeld(player.GUID, player.LastDrawnSlot)
+ AvatarAction.SendResponse(player.GUID, ObjectHeldMessage(player.GUID, player.LastDrawnSlot, unk1=false))
)
}
sendResponse(PlanetsideAttributeMessage(vehicle_guid, 22, 1L)) //mount points off
@@ -3608,15 +3624,23 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
//TODO if Medkit does not have shortcut, add to a free slot or write over slot 64
- sendResponse(CreateShortcutMessage(guid, 1, 0, true, Shortcut.Medkit))
+ avatar.shortcuts
+ .zipWithIndex
+ .collect { case (Some(shortcut), index) =>
+ sendResponse(CreateShortcutMessage(
+ guid,
+ index + 1,
+ Some(AvatarShortcut.convert(shortcut))
+ ))
+ }
sendResponse(ChangeShortcutBankMessage(guid, 0))
//Favorites lists
avatarActor ! AvatarActor.InitialRefreshLoadouts()
sendResponse(
- SetChatFilterMessage(ChatChannel.Platoon, false, ChatChannel.values.toList)
+ SetChatFilterMessage(ChatChannel.Platoon, origin=false, ChatChannel.values.toList)
) //TODO will not always be "on" like this
- sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, true))
+ sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0, 0, tplayer.Position, player.Faction, unk5=true))
//looking for squad (members)
if (tplayer.avatar.lookingForSquad || lfsm) {
sendResponse(PlanetsideAttributeMessage(guid, 53, 1))
@@ -5033,54 +5057,8 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
log.warn(s"ReloadMessage: either can not find $item_guid or the object found was not a Tool")
}
- case msg @ ObjectHeldMessage(avatar_guid, held_holsters, unk1) =>
- val before = player.DrawnSlot
- if (before != held_holsters) {
- if (player.ExoSuit == ExoSuitType.MAX && held_holsters != 0) {
- log.warn(s"ObjectHeld: ${player.Name} is denied changing hands to $held_holsters as a MAX")
- player.DrawnSlot = 0
- sendResponse(ObjectHeldMessage(avatar_guid, 0, true))
- } else if ((player.DrawnSlot = held_holsters) != before) {
- continent.AvatarEvents ! AvatarServiceMessage(
- player.Continent,
- AvatarAction.ObjectHeld(player.GUID, player.LastDrawnSlot)
- )
- // Ignore non-equipment holsters
- //todo: check current suit holster slots?
- val isHolsters = held_holsters >= 0 && held_holsters < 5
- val equipment = player.Slot(held_holsters).Equipment.orElse { player.Slot(before).Equipment }
- if (isHolsters) {
- equipment match {
- case Some(unholsteredItem: Equipment) =>
- log.info(s"${player.Name} has drawn a $unholsteredItem from its holster")
- if (unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) {
- //rek beam/icon colour must match the player's correct hack level
- continent.AvatarEvents ! AvatarServiceMessage(
- player.Continent,
- AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, player.avatar.hackingSkillLevel())
- )
- }
- case None => ;
- }
- } else {
- equipment match {
- case Some(holsteredEquipment) =>
- log.info(s"${player.Name} has put ${player.Sex.possessive} ${holsteredEquipment.Definition.Name} down")
- case None =>
- log.info(s"${player.Name} lowers ${player.Sex.possessive} hand")
- }
- }
-
- // Stop using proximity terminals if player unholsters a weapon (which should re-trigger the proximity effect and re-holster the weapon)
- if (player.VisibleSlots.contains(held_holsters)) {
- continent.GUID(usingMedicalTerminal) match {
- case Some(term: Terminal with ProximityUnit) =>
- StopUsingProximityUnit(term)
- case _ => ;
- }
- }
- }
- }
+ case ObjectHeldMessage(_, held_holsters, _) =>
+ player.Actor ! PlayerControl.ObjectHeld(held_holsters)
case msg @ AvatarJumpMessage(state) =>
avatarActor ! AvatarActor.ConsumeStamina(10)
@@ -6490,7 +6468,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
log.info(lament)
log.debug(s"Battleplan: $lament - $msg")
- case msg @ CreateShortcutMessage(player_guid, slot, unk, add, shortcut) => ;
+ case CreateShortcutMessage(_, slot, Some(shortcut)) =>
+ avatarActor ! AvatarActor.AddShortcut(slot - 1, shortcut)
+
+ case CreateShortcutMessage(_, slot, None) =>
+ avatarActor ! AvatarActor.RemoveShortcut(slot - 1)
+
+ case ChangeShortcutBankMessage(_, bank) =>
+ log.info(s"${player.Name} referencing hotbar bank $bank")
case FriendsRequest(action, name) =>
avatarActor ! AvatarActor.MemberListRequest(action, name)
diff --git a/src/main/scala/net/psforever/login/WorldSession.scala b/src/main/scala/net/psforever/login/WorldSession.scala
index 901bc9a7..6c768d74 100644
--- a/src/main/scala/net/psforever/login/WorldSession.scala
+++ b/src/main/scala/net/psforever/login/WorldSession.scala
@@ -3,6 +3,7 @@ package net.psforever.login
import akka.actor.ActorRef
import akka.pattern.{AskTimeoutException, ask}
import akka.util.Timeout
+import net.psforever.objects._
import net.psforever.objects.equipment.{Ammo, Equipment, EquipmentSize}
import net.psforever.objects.guid._
import net.psforever.objects.inventory.{Container, InventoryItem}
@@ -10,9 +11,7 @@ import net.psforever.objects.locker.LockerContainer
import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.containable.Containable
import net.psforever.objects.zones.Zone
-import net.psforever.objects._
-import net.psforever.packet.game.ObjectHeldMessage
-import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3}
+import net.psforever.types.{ExoSuitType, PlanetSideGUID, TransactionType, Vector3}
import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@@ -35,7 +34,7 @@ object WorldSession {
private implicit val timeout = new Timeout(5000 milliseconds)
/**
- * Use this for placing equipment that has yet to be registered into a container,
+ * Use this for placing equipment that has already been registered into a container,
* such as in support of changing ammunition types in `Tool` objects (weapons).
* If the object can not be placed into the container, it will be dropped onto the ground.
* It will also be dropped if it takes too long to be placed.
@@ -299,41 +298,7 @@ object WorldSession {
if (player.VisibleSlots.contains(slot)) {
val localZone = player.Zone
TaskBundle(
- new StraightforwardTask() {
- private val localPlayer = player
- private val localGUID = player.GUID
- private val localItem = item
- private val localSlot = slot
-
- def action(): Future[Any] = {
- ask(localPlayer.Actor, Containable.PutItemInSlotOnly(localItem, localSlot))
- .onComplete {
- case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
- TaskWorkflow.execute(GUIDTask.unregisterEquipment(localZone.GUID, localItem))
- case _ =>
- if (localPlayer.DrawnSlot != Player.HandsDownSlot) {
- localPlayer.DrawnSlot = Player.HandsDownSlot
- localZone.AvatarEvents ! AvatarServiceMessage(
- localPlayer.Name,
- AvatarAction.SendResponse(
- Service.defaultPlayerGUID,
- ObjectHeldMessage(localGUID, Player.HandsDownSlot, false)
- )
- )
- localZone.AvatarEvents ! AvatarServiceMessage(
- localZone.id,
- AvatarAction.ObjectHeld(localGUID, localPlayer.LastDrawnSlot)
- )
- }
- localPlayer.DrawnSlot = localSlot
- localZone.AvatarEvents ! AvatarServiceMessage(
- localZone.id,
- AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectHeldMessage(localGUID, localSlot, false))
- )
- }
- Future(this)
- }
- },
+ TaskToHoldEquipmentUp(player)(item, slot),
GUIDTask.registerEquipment(localZone.GUID, item)
)
} else {
@@ -342,6 +307,32 @@ object WorldSession {
}
}
+ def TaskToHoldEquipmentUp(player: Player)(item: Equipment, slot: Int): Task = {
+ new StraightforwardTask() {
+ private val localPlayer = player
+ private val localGUID = player.GUID
+ private val localItem = item
+ private val localSlot = slot
+ private val localZone = player.Zone
+
+ def action(): Future[Any] = {
+ ask(localPlayer.Actor, Containable.PutItemInSlotOnly(localItem, localSlot))
+ .onComplete {
+ case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
+ TaskWorkflow.execute(GUIDTask.unregisterEquipment(localZone.GUID, localItem))
+ case _ =>
+ forcedTolowerRaisedArm(localPlayer, localPlayer.GUID, localZone)
+ localPlayer.DrawnSlot = localSlot
+ localZone.AvatarEvents ! AvatarServiceMessage(
+ localZone.id,
+ AvatarAction.ObjectHeld(localGUID, localSlot, localSlot)
+ )
+ }
+ Future(this)
+ }
+ }
+ }
+
/**
* Get an item from the ground and put it into the given container.
* The zone in which the item is found is expected to be the same in which the container object is located.
@@ -726,6 +717,194 @@ object WorldSession {
}
}
+ /**
+ * Quickly draw a grenade from anywhere on the player's person and place it into a certain hand
+ * at the ready to be used as a weapon.
+ * Soldiers in mechanized assault exo-suits can not perform this action.
+ *
+ * This is not vanilla behavior.
+ *
+ * Search for a grenade of either fragmentation- or plasma-type in the hands (holsters) or backpack (inventory)
+ * and bring it to hand and draw that grenade as a weapon as quickly as possible.
+ * If the player has a weapon already drawn, remove it from his active hand quickly.
+ * It may be placed back into the slot once the hand is / will be occupied by a grenade.
+ * For anything in the first sidearm weapon slot, where the grenade will be placed,
+ * either find room in the backpack for it or drop it on the ground.
+ * If the player's already-drawn hand is the same as the one that will hold the grenade (first sidearm holster),
+ * treat it like the sidearm occupier rather than the already-drawn weapon -
+ * the old weapon goes into the backpack or onto the ground.
+ * @see `AvatarAction.ObjectHeld`
+ * @see `AvatarServiceMessage`
+ * @see `Containable.RemoveItemFromSlot`
+ * @see `countRestrictAttempts`
+ * @see `forcedTolowerRaisedArm`
+ * @see `GlobalDefinitions.isGrenade`
+ * @see `InventoryItem`
+ * @see `Player.DrawnSlot`
+ * @see `Player.HandsDownSlot`
+ * @see `Player.Holsters`
+ * @see `Player.ResistArmMotion`
+ * @see `Player.Slot`
+ * @see `PutEquipmentInInventoryOrDrop`
+ * @see `PutEquipmentInInventorySlot`
+ * @see `TaskBundle`
+ * @see `TaskToHoldEquipmentUp`
+ * @see `TaskWorkflow.execute`
+ * @param tplayer player who wants to draw a grenade
+ * @param equipSlot slot being used as the final destination for any discovered grenade
+ * @param log reference to the messaging protocol
+ * @return if there was a discovered grenade
+ */
+ def QuickSwapToAGrenade(
+ tplayer: Player,
+ equipSlot: Int,
+ log: org.log4s.Logger): Boolean = {
+ if (tplayer.ExoSuit != ExoSuitType.MAX) {
+ val previouslyDrawnSlot = tplayer.DrawnSlot
+ val optGrenadeInSlot = {
+ tplayer.Holsters().zipWithIndex.find { case (slot, _) =>
+ slot.Equipment match {
+ case Some(equipment) =>
+ val definition = equipment.Definition
+ val name = definition.Name
+ GlobalDefinitions.isGrenade(definition) && (name.contains("frag") || name.contains("plasma"))
+ case _ =>
+ false
+ }
+ } match {
+ case Some((_, slotNum)) if slotNum == previouslyDrawnSlot =>
+ //grenade already in hand; do nothing
+ None
+ case Some((grenadeSlot, slotNum)) =>
+ //grenade is holstered in some other slot; just extend it (or swap hands)
+ val guid = tplayer.GUID
+ val zone = tplayer.Zone
+ val grenade = grenadeSlot.Equipment.get
+ val drawnSlotItem = tplayer.Slot(tplayer.DrawnSlot).Equipment
+ if (forcedTolowerRaisedArm(tplayer, guid, zone)) {
+ log.info(s"${tplayer.Name} has dropped ${tplayer.Sex.possessive} ${drawnSlotItem.get.Definition.Name}")
+ }
+ //put up hand with grenade in it
+ tplayer.DrawnSlot = slotNum
+ zone.AvatarEvents ! AvatarServiceMessage(
+ zone.id,
+ AvatarAction.ObjectHeld(guid, slotNum, slotNum)
+ )
+ log.info(s"${tplayer.Name} has quickly drawn a ${grenade.Definition.Name}")
+ None
+ case None =>
+ //check inventory for a grenade
+ tplayer.Inventory.Items.find { case InventoryItem(equipment, _) =>
+ val definition = equipment.Definition
+ val name = definition.Name
+ GlobalDefinitions.isGrenade(definition) && (name.contains("frag") || name.contains("plasma"))
+ } match {
+ case Some(InventoryItem(equipment, slotNum)) =>Some(equipment.asInstanceOf[Tool], slotNum)
+ case None => None
+ }
+ }
+ }
+ optGrenadeInSlot match {
+ case Some((grenade, slotNum)) =>
+ tplayer.ResistArmMotion(countRestrictAttempts(count=1))
+ val itemInPreviouslyDrawnSlotToDrop = if (equipSlot != previouslyDrawnSlot) {
+ forcedTolowerRaisedArm(tplayer, tplayer.GUID, tplayer.Zone)
+ tplayer.Slot(previouslyDrawnSlot).Equipment match {
+ case out @ Some(_) => out
+ case _ => None
+ }
+ } else {
+ None
+ }
+ val itemPreviouslyInPistolSlot = tplayer.Slot(equipSlot).Equipment
+ val result = for {
+ //remove grenade from inventory
+ a <- ask(tplayer.Actor, Containable.RemoveItemFromSlot(slotNum))
+ //remove equipment from pistol slot, where grenade will go
+ b <- itemPreviouslyInPistolSlot match {
+ case Some(_) => ask(tplayer.Actor, Containable.RemoveItemFromSlot(equipSlot))
+ case _ => Future(true)
+ }
+ //remove held equipment (if any)
+ c <- itemInPreviouslyDrawnSlotToDrop match {
+ case Some(_) => ask(tplayer.Actor, Containable.RemoveItemFromSlot(previouslyDrawnSlot))
+ case _ => Future(false)
+ }
+ } yield (a, b, c)
+ result.onComplete {
+ case Success((_, _, _)) =>
+ //put equipment in hand and hold grenade up
+ TaskWorkflow.execute(TaskBundle(TaskToHoldEquipmentUp(tplayer)(grenade, equipSlot)))
+ //what to do with the equipment that was removed for the grenade
+ itemPreviouslyInPistolSlot match {
+ case Some(e) =>
+ log.info(s"${tplayer.Name} has dropped ${tplayer.Sex.possessive} ${e.Definition.Name}")
+ PutEquipmentInInventoryOrDrop(tplayer)(e)
+ case _ => ;
+ }
+ //restore previously-held-up equipment
+ itemInPreviouslyDrawnSlotToDrop match {
+ case Some(e) => PutEquipmentInInventorySlot(tplayer)(e, previouslyDrawnSlot)
+ case _ => ;
+ }
+ log.info(s"${tplayer.Name} has quickly drawn a ${grenade.Definition.Name}")
+ case _ => ;
+ }
+ case None => ;
+ }
+ optGrenadeInSlot.nonEmpty
+ } else {
+ false
+ }
+ }
+
+ /**
+ * If the player has a raised arm, lower it.
+ * Do it manually, bypassing the checks in the normal procedure.
+ * @see `AvatarAction.ObjectHeld`
+ * @see `AvatarServiceMessage`
+ * @see `Player.DrawnSlot`
+ * @see `Player.HandsDownSlot`
+ * @param tplayer the player
+ * @param guid target guid (usually the player)
+ * @param zone the zone of reporting
+ * @return if the hand has a drawn equipment in it and tries to lower
+ */
+ private def forcedTolowerRaisedArm(tplayer: Player, guid:PlanetSideGUID, zone: Zone): Boolean = {
+ val slot = tplayer.DrawnSlot
+ if (slot != Player.HandsDownSlot) {
+ tplayer.DrawnSlot = Player.HandsDownSlot
+ zone.AvatarEvents ! AvatarServiceMessage(
+ zone.id,
+ AvatarAction.ObjectHeld(guid, Player.HandsDownSlot, slot)
+ )
+ true
+ } else {
+ false
+ }
+ }
+
+ /**
+ * Restriction logic that stops the player
+ * from lowering or raising any drawn equipment a certain number of times.
+ * Reset to default restriction behavior when no longer valid.
+ * @see `Player.neverRestrict`
+ * @see `Player.ResistArmMotion`
+ * @param count number of times to stop the player from adjusting their arm
+ * @param player target player
+ * @param slot slot being switched to (unused here)
+ * @return if the motion is restricted
+ */
+ def countRestrictAttempts(count: Int)(player: Player, slot: Int): Boolean = {
+ if (count > 0) {
+ player.ResistArmMotion(countRestrictAttempts(count - 1))
+ true
+ } else {
+ player.ResistArmMotion(Player.neverRestrict) //reset
+ false
+ }
+ }
+
/**
* If a timeout occurs on the manipulation, declare a terminal transaction failure.
* @see `AskTimeoutException`
diff --git a/src/main/scala/net/psforever/objects/Player.scala b/src/main/scala/net/psforever/objects/Player.scala
index 8e5e4242..43b35ba0 100644
--- a/src/main/scala/net/psforever/objects/Player.scala
+++ b/src/main/scala/net/psforever/objects/Player.scala
@@ -14,6 +14,7 @@ import net.psforever.objects.serverobject.environment.InteractWithEnvironment
import net.psforever.objects.serverobject.mount.MountableEntity
import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.Vitality
+import net.psforever.objects.vital.damage.DamageProfile
import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation}
@@ -79,6 +80,8 @@ class Player(var avatar: Avatar)
val squadLoadouts = new LoadoutManager(10)
+ var resistArmMotion: (Player,Int)=>Boolean = Player.neverRestrict
+
//init
Health = 0 //player health is artificially managed as a part of their lifecycle; start entity as dead
Destroyed = true //see isAlive
@@ -176,8 +179,8 @@ class Player(var avatar: Avatar)
capacitorState
}
- def CapacitorLastUsedMillis = capacitorLastUsedMillis
- def CapacitorLastChargedMillis = capacitorLastChargedMillis
+ def CapacitorLastUsedMillis: Long = capacitorLastUsedMillis
+ def CapacitorLastChargedMillis: Long = capacitorLastChargedMillis
def VisibleSlots: Set[Int] =
if (exosuit.SuitType == ExoSuitType.MAX) {
@@ -253,7 +256,7 @@ class Player(var avatar: Avatar)
}
}
- def FreeHand = freeHand
+ def FreeHand: EquipmentSlot = freeHand
def FreeHand_=(item: Option[Equipment]): Option[Equipment] = {
if (freeHand.Equipment.isEmpty || item.isEmpty) {
@@ -313,6 +316,18 @@ class Player(var avatar: Avatar)
}
}
+ def ResistArmMotion(func: (Player,Int)=>Boolean): Unit = {
+ resistArmMotion = func
+ }
+
+ def TestArmMotion(): Boolean = {
+ resistArmMotion(this, drawnSlot)
+ }
+
+ def TestArmMotion(slot: Int): Boolean = {
+ resistArmMotion(this, slot)
+ }
+
def DrawnSlot: Int = drawnSlot
def DrawnSlot_=(slot: Int): Int = {
@@ -339,15 +354,15 @@ class Player(var avatar: Avatar)
ChangeSpecialAbility()
}
- def Subtract = exosuit.Subtract
+ def Subtract: DamageProfile = exosuit.Subtract
- def ResistanceDirectHit = exosuit.ResistanceDirectHit
+ def ResistanceDirectHit: Int = exosuit.ResistanceDirectHit
- def ResistanceSplash = exosuit.ResistanceSplash
+ def ResistanceSplash: Int = exosuit.ResistanceSplash
- def ResistanceAggravated = exosuit.ResistanceAggravated
+ def ResistanceAggravated: Int = exosuit.ResistanceAggravated
- def RadiationShielding = exosuit.RadiationShielding
+ def RadiationShielding: Float = exosuit.RadiationShielding
def FacingYawUpper: Float = facingYawUpper
@@ -533,7 +548,7 @@ class Player(var avatar: Avatar)
ZoningRequest
}
- def DamageModel = exosuit.asInstanceOf[DamageResistanceModel]
+ def DamageModel: DamageResistanceModel = exosuit.asInstanceOf[DamageResistanceModel]
def canEqual(other: Any): Boolean = other.isInstanceOf[Player]
@@ -601,6 +616,10 @@ object Player {
player
}
}
+
+ def neverRestrict(player: Player, slot: Int): Boolean = {
+ false
+ }
}
private class InteractWithMinesUnlessSpectating(
diff --git a/src/main/scala/net/psforever/objects/Players.scala b/src/main/scala/net/psforever/objects/Players.scala
index c577822e..cc5bdc40 100644
--- a/src/main/scala/net/psforever/objects/Players.scala
+++ b/src/main/scala/net/psforever/objects/Players.scala
@@ -439,13 +439,9 @@ object Players {
}
if (player.DrawnSlot == Player.HandsDownSlot) {
player.DrawnSlot = index
- events ! AvatarServiceMessage(
- name,
- AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectHeldMessage(pguid, index, true))
- )
events ! AvatarServiceMessage(
zone.id,
- AvatarAction.ObjectHeld(pguid, index)
+ AvatarAction.ObjectHeld(pguid, index, index)
)
}
}
diff --git a/src/main/scala/net/psforever/objects/avatar/Avatar.scala b/src/main/scala/net/psforever/objects/avatar/Avatar.scala
index 4b3e47d5..b9d35746 100644
--- a/src/main/scala/net/psforever/objects/avatar/Avatar.scala
+++ b/src/main/scala/net/psforever/objects/avatar/Avatar.scala
@@ -123,6 +123,7 @@ case class Avatar(
fatigued: Boolean = false,
certifications: Set[Certification] = Set(),
implants: Seq[Option[Implant]] = Seq(None, None, None),
+ shortcuts: Array[Option[Shortcut]] = Array.fill[Option[Shortcut]](64)(None),
locker: LockerContainer = Avatar.makeLocker(),
deployables: DeployableToolbox = new DeployableToolbox(),
lookingForSquad: Boolean = false,
diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
index 7a9c9ff9..46e90a21 100644
--- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
+++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
@@ -306,10 +306,53 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case PlayerControl.SetExoSuit(exosuit: ExoSuitType.Value, subtype: Int) =>
setExoSuit(exosuit, subtype)
+ case PlayerControl.ObjectHeld(slot, updateMyHolsterArm) =>
+ val before = player.DrawnSlot
+ val events = player.Zone.AvatarEvents
+ val resistance = player.TestArmMotion(slot)
+ if (resistance && !updateMyHolsterArm) {
+ events ! AvatarServiceMessage(
+ player.Name,
+ AvatarAction.ObjectHeld(player.GUID, before, -1)
+ )
+ } else if (!resistance && before != slot && (player.DrawnSlot = slot) != before) {
+ val mySlot = if (updateMyHolsterArm) slot else -1 //use as a short-circuit
+ events ! AvatarServiceMessage(
+ player.Continent,
+ AvatarAction.ObjectHeld(player.GUID, mySlot, player.LastDrawnSlot)
+ )
+ val isHolsters = player.VisibleSlots.contains(slot)
+ val equipment = player.Slot(slot).Equipment.orElse { player.Slot(before).Equipment }
+ if (isHolsters) {
+ equipment match {
+ case Some(unholsteredItem: Equipment) =>
+ log.info(s"${player.Name} has drawn a ${unholsteredItem.Definition.Name} from its holster")
+ if (unholsteredItem.Definition == GlobalDefinitions.remote_electronics_kit) {
+ //rek beam/icon colour must match the player's correct hack level
+ events ! AvatarServiceMessage(
+ player.Continent,
+ AvatarAction.PlanetsideAttribute(unholsteredItem.GUID, 116, player.avatar.hackingSkillLevel())
+ )
+ }
+ case None => ;
+ }
+ } else {
+ equipment match {
+ case Some(holsteredEquipment) =>
+ log.info(s"${player.Name} has put ${player.Sex.possessive} ${holsteredEquipment.Definition.Name} down")
+ case None =>
+ log.info(s"${player.Name} lowers ${player.Sex.possessive} hand")
+ }
+ }
+ }
+
case Terminal.TerminalMessage(_, msg, order) =>
order match {
case Terminal.BuyExosuit(exosuit, subtype) =>
val result = setExoSuit(exosuit, subtype)
+ if (exosuit == ExoSuitType.MAX) {
+ player.ResistArmMotion(PlayerControl.maxRestriction)
+ }
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result)
@@ -345,6 +388,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
if (
Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) {
+ player.ResistArmMotion(PlayerControl.maxRestriction)
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
player.avatar.purchaseCooldown(weapon) match {
case Some(_) => false
@@ -353,6 +397,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
true
}
} else {
+ player.ResistArmMotion(Player.neverRestrict)
true
})
) {
@@ -1329,6 +1374,12 @@ object PlayerControl {
/** na */
final case class SetExoSuit(exosuit: ExoSuitType.Value, subtype: Int)
+ /** na */
+ final case class ObjectHeld(slot: Int, updateMyHolsterArm: Boolean)
+ object ObjectHeld {
+ def apply(slot: Int): ObjectHeld = ObjectHeld(slot, updateMyHolsterArm=false)
+ }
+
/**
* Transform an applicable Aura effect into its `PlanetsideAttributeMessage` value.
* @see `Aura`
@@ -1347,4 +1398,13 @@ object PlayerControl {
def sendResponse(zone: Zone, channel: String, msg: PlanetSideGamePacket): Unit = {
zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.SendResponse(Service.defaultPlayerGUID, msg))
}
+
+ def maxRestriction(player: Player, slot: Int): Boolean = {
+ if (player.ExoSuit == ExoSuitType.MAX) {
+ slot != 0
+ } else {
+ player.ResistArmMotion(Player.neverRestrict) //reset
+ false
+ }
+ }
}
diff --git a/src/main/scala/net/psforever/objects/avatar/Shortcut.scala b/src/main/scala/net/psforever/objects/avatar/Shortcut.scala
new file mode 100644
index 00000000..0251ad7a
--- /dev/null
+++ b/src/main/scala/net/psforever/objects/avatar/Shortcut.scala
@@ -0,0 +1,76 @@
+// Copyright (c) 2022 PSForever
+package net.psforever.objects.avatar
+
+import net.psforever.packet.game.{Shortcut => GameShortcut}
+
+/**
+ * The internal respresentation of a shortcut on the hotbar.
+ * @param purpose integer value related to the type of shortcut
+ * @param tile details how the shortcut is to be used (net.psforever.packet.game.Schortcut)
+ * @param effect1 three letters emblazoned on the shortcut icon;
+ * defaults to empty string
+ * @param effect2 the message published to text chat;
+ * defaults to empty string
+ */
+case class Shortcut(
+ purpose: Int,
+ tile: String,
+ effect1: String = "",
+ effect2: String = ""
+ )
+
+object Shortcut {
+ /**
+ * Transform the internal form of the `Shortcut`
+ * into the packet form of the `Shortcut`.
+ * @see `net.psforever.packet.game.Shortcut`
+ * @param shortcut internal form of the `Shortcut`
+ * @return equivalent packet form of the `Shortcut`
+ * @throws `AssertionError` if an implant is not named
+ */
+ def convert(shortcut: Shortcut): GameShortcut = {
+ shortcut.tile match {
+ case "medkit" => GameShortcut.Medkit()
+ case "shortcut_macro" => GameShortcut.Macro(shortcut.effect1, shortcut.effect2)
+ case _ => GameShortcut.Implant(shortcut.tile)
+ }
+ }
+
+ /**
+ * Is an internal form of the `Shortcut` equivalent to a packet form of the `Shortcut`?
+ * @param a internal form of the `Shortcut`
+ * @param b packet form of the `Shortcut`
+ * @return `true`, if the forms of `Shortcut` are equivalent;
+ * `false`, otherwise
+ */
+ def equals(a: Shortcut, b: GameShortcut): Boolean = {
+ a.purpose == b.code && typeEquals(a, b)
+ }
+ /**
+ * Is an internal form of the `Shortcut` equivalent to a packet form of the `Shortcut`?
+ * @param a internal form of the `Shortcut`
+ * @param b packet form of the `Shortcut`
+ * @return `true`, if the forms of `Shortcut` are equivalent;
+ * `false`, otherwise
+ */
+ def equals(b: GameShortcut, a: Shortcut): Boolean = {
+ a.purpose == b.code && typeEquals(a, b)
+ }
+
+ /**
+ * Is an internal form of the `Shortcut` equivalent to a packet form of the `Shortcut`?
+ * Test against individual types of packet forms and then the fields associated with that form.
+ * @param a internal form of the `Shortcut`
+ * @param b packet form of the `Shortcut`
+ * @return `true`, if the forms of `Shortcut` are equivalent;
+ * `false`, otherwise
+ */
+ private def typeEquals(a: Shortcut, b: GameShortcut): Boolean = {
+ b match {
+ case GameShortcut.Medkit() => true
+ case GameShortcut.Macro(x, y) => x.equals(a.effect1) && y.equals(a.effect2)
+ case GameShortcut.Implant(tile) => tile.equals(a.tile)
+ case _ => true
+ }
+ }
+}
diff --git a/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala b/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala
index ec4b1476..ba518fc2 100644
--- a/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala
+++ b/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala
@@ -2,9 +2,10 @@
package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
-import net.psforever.types.PlanetSideGUID
-import scodec.Codec
+import net.psforever.types.{ImplantType, PlanetSideGUID}
+import scodec.{Codec, TransformSyntax}
import scodec.codecs._
+import shapeless.{::, HNil}
/**
* Details regarding this shortcut.
@@ -18,33 +19,24 @@ import scodec.codecs._
* The `shortcut_macro` setting displays a word bubble superimposed by the (first three letters of) `effect1` text.
* Implants and the medkit should have self-explanatory graphics.
*
- * Purpose:
- * `0 - Medkit`
- * `1 - Macro`
- * `2 - Implant`
- *
- * Tile:
- * `advanced_regen` (regeneration)
- * `audio_amplifier`
- * `darklight_vision`
- * `medkit`
- * `melee_booster`
- * `personal_shield`
- * `range_magnifier`
- * `second_wind`
- * `shortcut_macro`
- * `silent_run` (sensor shield)
- * `surge`
- * `targeting` (enhanced targetting)
- *
- * Exploration:
- * What is `purpose` when 3?
- * @param purpose the primary 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
+ * Tile - Code
+ * `advanced_regen` (regeneration) - 2
+ * `audio_amplifier` - 2
+ * `darklight_vision` - 2
+ * `medkit` - 0
+ * `melee_booster` - 2
+ * `personal_shield` - 2
+ * `range_magnifier` - 2
+ * `second_wind` - 2
+ * `shortcut_macro` - 1
+ * `silent_run` (sensor shield) - 2
+ * `surge` - 2
+ * `targeting` (enhanced targeting) - 2
+ * @param code the primary use of this shortcut
*/
-final case class Shortcut(purpose: Int, tile: String, effect1: String = "", effect2: String = "")
+abstract class Shortcut(val code: Int) {
+ def tile: String
+}
/**
* Facilitate a quick-use button for the hotbar.
@@ -64,17 +56,13 @@ final case class Shortcut(purpose: Int, tile: String, effect1: String = "", effe
* The prior functionality will rarely be appreciated, however, as players rarely never have their medkit shortcut unbound.
* @param player_guid the player
* @param slot the hotbar slot number (one-indexed)
- * @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
+ * @see `ChangeShortcutBankMessage`
*/
final case class CreateShortcutMessage(
player_guid: PlanetSideGUID,
slot: Int,
- unk: Int,
- addShortcut: Boolean,
- shortcut: Option[Shortcut] = None
+ shortcut: Option[Shortcut]
) extends PlanetSideGamePacket {
type Packet = CreateShortcutMessage
def opcode = GamePacketOpcode.CreateShortcutMessage
@@ -82,33 +70,110 @@ final case class CreateShortcutMessage(
}
object Shortcut extends Marshallable[Shortcut] {
-
/** Preset for the medkit quick-use option. */
- final val Medkit: Some[Shortcut] = Some(Shortcut(0, "medkit"))
+ final case class Medkit() extends Shortcut(code=0) {
+ def tile = "medkit"
+ }
/**
- * 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))
+ * Converter for text macro parameters that acts like a preset.
+ * @param acronym a three letter acronym displayed in the hotbar
+ * @param msg the chat message content
+ * @return `Some` shortcut that represents a voice macro command
+ */
+ final case class Macro(acronym: String, msg: String) extends Shortcut(code=1) {
+ override val tile: String = "shortcut_macro"
+ }
- implicit val codec: Codec[Shortcut] = (
- ("purpose" | uint2L) ::
- ("tile" | PacketHelpers.encodedStringAligned(5)) ::
+ /**
+ * Converter for an implant name token that acts like a preset.
+ * @param tile implant name
+ * @return `Some` shortcut that represents an implant
+ * @throws `AssertionError` if an implant is not named
+ */
+ final case class Implant(tile: String) extends Shortcut(code=2) {
+ assert(ImplantType.names.exists(_.equals(tile)), s"not an implant - $tile")
+ }
+
+ /**
+ * Main transcoder for medkit shortcuts.
+ */
+ val medkitCodec: Codec[Medkit] = (
+ ("tile" | PacketHelpers.encodedStringAligned(adjustment=5)) ::
("effect1" | PacketHelpers.encodedWideString) ::
("effect2" | PacketHelpers.encodedWideString)
- ).as[Shortcut]
+ ).xmap[Medkit](
+ _ => Medkit(),
+ {
+ case Medkit() => "medkit" :: "" :: "" :: HNil
+ }
+ )
+
+ /**
+ * Main transcoder for text chat macro shortcuts.
+ * All strings transcoders are utilized.
+ */
+ val macroCodec: Codec[Macro] = (
+ ("tile" | PacketHelpers.encodedStringAligned(adjustment=5)) ::
+ ("effect1" | PacketHelpers.encodedWideString) ::
+ ("effect2" | PacketHelpers.encodedWideString)
+ ).xmap[Macro](
+ {
+ case _ :: acronym :: msg :: HNil => Macro(acronym, msg)
+ },
+ {
+ case Macro(acronym, msg) => "shortcut_macro" :: acronym :: msg :: HNil
+ }
+ )
+
+ /**
+ * Main transcoder for implant quick-use shortcuts.
+ */
+ val implantCodec: Codec[Implant] = (
+ ("tile" | PacketHelpers.encodedStringAligned(adjustment=5)) ::
+ ("effect1" | PacketHelpers.encodedWideString) ::
+ ("effect2" | PacketHelpers.encodedWideString)
+ ).xmap[Implant](
+ {
+ case implant :: _ :: _ :: HNil => Implant(implant)
+ },
+ {
+ case Implant(implant) => implant :: "" :: "" :: HNil
+ }
+ )
+
+ /**
+ * Convert the numeric flag for a specific kind of shortcut into the transcoder for that kind of shortcut.
+ * @param code numeric code for shortcut type
+ * @return transcoder for that shortcut type
+ * @throws IllegalArgumentException if the numeric code does not map to any valid transcoders
+ */
+ def shortcutSwitch(code: Int): Codec[Shortcut] = {
+ (code match {
+ case 0 => medkitCodec
+ case 1 => macroCodec
+ case 2 => implantCodec
+ case _ => throw new IllegalArgumentException(s"code not associated with shortcut type - $code")
+ }).asInstanceOf[Codec[Shortcut]]
+ }
+
+ implicit val codec: Codec[Shortcut] = (
+ uint(bits=2) >>:~ { code =>
+ shortcutSwitch(code).hlist
+ }).xmap[Shortcut](
+ {
+ case _ :: shortcut :: HNil => shortcut
+ },
+ {
+ shortcut => shortcut.code :: shortcut :: HNil
+ }
+ )
}
object CreateShortcutMessage extends Marshallable[CreateShortcutMessage] {
implicit val codec: Codec[CreateShortcutMessage] = (
("player_guid" | PlanetSideGUID.codec) ::
- ("slot" | uint8L) ::
- ("unk" | uint8L) ::
- (("addShortcut" | bool) >>:~ { value =>
- conditional(value, "shortcut" | Shortcut.codec).hlist
- })
+ ("slot" | uint16L) ::
+ ("shortcut" | optional(bool, Shortcut.codec))
).as[CreateShortcutMessage]
}
diff --git a/src/main/scala/net/psforever/persistence/Shortcut.scala b/src/main/scala/net/psforever/persistence/Shortcut.scala
new file mode 100644
index 00000000..5d33ebde
--- /dev/null
+++ b/src/main/scala/net/psforever/persistence/Shortcut.scala
@@ -0,0 +1,11 @@
+// Copyright (c) 2022 PSForever
+package net.psforever.persistence
+
+case class Shortcut(
+ avatarId: Long,
+ slot: Int,
+ purpose: Int,
+ tile: String,
+ effect1: Option[String] = None,
+ effect2: Option[String] = None
+ )
diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala
index 4d1abe34..c17c9cc1 100644
--- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala
+++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala
@@ -173,9 +173,9 @@ class AvatarService(zone: Zone) extends Actor {
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ObjectDelete(item_guid, unk))
)
- case AvatarAction.ObjectHeld(player_guid, slot) =>
+ case AvatarAction.ObjectHeld(player_guid, slot, previousSlot) =>
AvatarEvents.publish(
- AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ObjectHeld(slot))
+ AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ObjectHeld(slot, previousSlot))
)
case AvatarAction.OxygenState(player, vehicle) =>
AvatarEvents.publish(
diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala
index c36d7f56..d5335f2a 100644
--- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala
+++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala
@@ -67,7 +67,7 @@ object AvatarAction {
cdata: ConstructorData
) extends Action
final case class ObjectDelete(player_guid: PlanetSideGUID, item_guid: PlanetSideGUID, unk: Int = 0) extends Action
- final case class ObjectHeld(player_guid: PlanetSideGUID, slot: Int) extends Action
+ final case class ObjectHeld(player_guid: PlanetSideGUID, slot: Int, previousSLot: Int) extends Action
final case class OxygenState(player: OxygenStateTarget, vehicle: Option[OxygenStateTarget]) extends Action
final case class PlanetsideAttribute(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long)
extends Action
diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala
index fb06956a..440d4bc2 100644
--- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala
+++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala
@@ -47,7 +47,7 @@ object AvatarResponse {
final case class LoadPlayer(pkt: ObjectCreateMessage) extends Response
final case class LoadProjectile(pkt: ObjectCreateMessage) extends Response
final case class ObjectDelete(item_guid: PlanetSideGUID, unk: Int) extends Response
- final case class ObjectHeld(slot: Int) extends Response
+ final case class ObjectHeld(slot: Int, previousSLot: Int) extends Response
final case class OxygenState(player: OxygenStateTarget, vehicle: Option[OxygenStateTarget]) extends Response
final case class PlanetsideAttribute(attribute_type: Int, attribute_value: Long) extends Response
final case class PlanetsideAttributeToAll(attribute_type: Int, attribute_value: Long) extends Response
diff --git a/src/main/scala/net/psforever/types/ImplantType.scala b/src/main/scala/net/psforever/types/ImplantType.scala
index 10cd0fc2..845e2ac4 100644
--- a/src/main/scala/net/psforever/types/ImplantType.scala
+++ b/src/main/scala/net/psforever/types/ImplantType.scala
@@ -5,6 +5,7 @@ import enumeratum.values.{IntEnum, IntEnumEntry}
import net.psforever.packet.PacketHelpers
import net.psforever.packet.game.Shortcut
import net.psforever.packet.game.objectcreate.ImplantEffects
+import scodec.Codec
import scodec.codecs._
sealed abstract class ImplantType(
@@ -16,52 +17,60 @@ sealed abstract class ImplantType(
) extends IntEnumEntry
case object ImplantType extends IntEnum[ImplantType] {
-
case object AdvancedRegen
extends ImplantType(
value = 0,
- shortcut = Shortcut(2, "advanced_regen"),
+ shortcut = Shortcut.Implant("advanced_regen"),
effect = Some(ImplantEffects.RegenEffects)
)
- case object Targeting extends ImplantType(value = 1, shortcut = Shortcut(2, "targeting"))
+ case object Targeting extends ImplantType(value = 1, shortcut = Shortcut.Implant("targeting"))
- case object AudioAmplifier extends ImplantType(value = 2, shortcut = Shortcut(2, "audio_amplifier"))
+ case object AudioAmplifier extends ImplantType(value = 2, shortcut = Shortcut.Implant("audio_amplifier"))
case object DarklightVision
extends ImplantType(
value = 3,
- shortcut = Shortcut(2, "darklight_vision"),
+ shortcut = Shortcut.Implant("darklight_vision"),
effect = Some(ImplantEffects.DarklightEffects)
)
- case object MeleeBooster extends ImplantType(value = 4, shortcut = Shortcut(2, "melee_booster"))
+ case object MeleeBooster extends ImplantType(value = 4, shortcut = Shortcut.Implant("melee_booster"))
case object PersonalShield
extends ImplantType(
value = 5,
- shortcut = Shortcut(2, "personal_shield"),
+ shortcut = Shortcut.Implant("personal_shield"),
disabledFor = Set(ExoSuitType.Infiltration),
effect = Some(ImplantEffects.PersonalShieldEffects)
)
- case object RangeMagnifier extends ImplantType(value = 6, shortcut = Shortcut(2, "range_magnifier"))
+ case object RangeMagnifier extends ImplantType(value = 6, shortcut = Shortcut.Implant("range_magnifier"))
- case object SecondWind extends ImplantType(value = 7, shortcut = Shortcut(2, "second_wind"))
+ case object SecondWind extends ImplantType(value = 7, shortcut = Shortcut.Implant("second_wind"))
- case object SilentRun extends ImplantType(value = 8, shortcut = Shortcut(2, "silent_run"))
+ case object SilentRun extends ImplantType(value = 8, shortcut = Shortcut.Implant("silent_run"))
- case object Surge
- extends ImplantType(
- value = 9,
- shortcut = Shortcut(2, "surge"),
- disabledFor = Set(ExoSuitType.MAX),
- effect = Some(ImplantEffects.SurgeEffects)
- )
+ case object Surge extends ImplantType(
+ value = 9,
+ shortcut = Shortcut.Implant("surge"),
+ disabledFor = Set(ExoSuitType.MAX),
+ effect = Some(ImplantEffects.SurgeEffects)
+ )
- case object None extends ImplantType(value = 15, shortcut = Shortcut(2, ""))
+ case object None extends ImplantType(
+ value = 15,
+ shortcut = Shortcut.Macro(acronym="ERR", msg=""),
+ disabledFor = ExoSuitType.values
+ )
def values: IndexedSeq[ImplantType] = findValues
- implicit val codec = PacketHelpers.createIntEnumCodec(this, uint4L)
+ final val names: Seq[String] = Seq(
+ "advanced_regen", "targeting", "audio_amplifier",
+ "darklight_vision", "melee_booster", "personal_shield", "range_magnifier",
+ "second_wind", "silent_run", "surge"
+ )
+
+ implicit val codec: Codec[ImplantType] = PacketHelpers.createIntEnumCodec(this, uint4L)
}
diff --git a/src/test/scala/game/CreateShortcutMessageTest.scala b/src/test/scala/game/CreateShortcutMessageTest.scala
index 5a8ad92d..2ac619e7 100644
--- a/src/test/scala/game/CreateShortcutMessageTest.scala
+++ b/src/test/scala/game/CreateShortcutMessageTest.scala
@@ -15,16 +15,13 @@ class CreateShortcutMessageTest extends Specification {
"decode (medkit)" in {
PacketCoding.decodePacket(stringMedkit).require match {
- case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) =>
+ case CreateShortcutMessage(player_guid, slot, shortcut) =>
player_guid mustEqual PlanetSideGUID(4210)
slot mustEqual 1
- unk mustEqual 0
- addShortcut mustEqual true
- shortcut.isDefined mustEqual true
- shortcut.get.purpose mustEqual 0
- shortcut.get.tile mustEqual "medkit"
- shortcut.get.effect1 mustEqual ""
- shortcut.get.effect2 mustEqual ""
+ shortcut match {
+ case Some(Shortcut.Medkit()) => ok
+ case _ => ko
+ }
case _ =>
ko
}
@@ -32,16 +29,13 @@ class CreateShortcutMessageTest extends Specification {
"decode (macro)" in {
PacketCoding.decodePacket(stringMacro).require match {
- case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) =>
+ case CreateShortcutMessage(player_guid, slot, shortcut) =>
player_guid mustEqual PlanetSideGUID(1356)
slot mustEqual 8
- unk mustEqual 0
- addShortcut mustEqual true
- shortcut.isDefined mustEqual true
- shortcut.get.purpose mustEqual 1
- shortcut.get.tile mustEqual "shortcut_macro"
- shortcut.get.effect1 mustEqual "NTU"
- shortcut.get.effect2 mustEqual "/platoon Incoming NTU spam!"
+ shortcut match {
+ case Some(Shortcut.Macro("NTU", "/platoon Incoming NTU spam!")) => ok
+ case _ => ko
+ }
case _ =>
ko
}
@@ -49,11 +43,9 @@ class CreateShortcutMessageTest extends Specification {
"decode (remove)" in {
PacketCoding.decodePacket(stringRemove).require match {
- case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) =>
+ case CreateShortcutMessage(player_guid, slot, shortcut) =>
player_guid mustEqual PlanetSideGUID(1356)
slot mustEqual 1
- unk mustEqual 0
- addShortcut mustEqual false
shortcut.isDefined mustEqual false
case _ =>
ko
@@ -61,7 +53,7 @@ class CreateShortcutMessageTest extends Specification {
}
"encode (medkit)" in {
- val msg = CreateShortcutMessage(PlanetSideGUID(4210), 1, 0, true, Some(Shortcut(0, "medkit")))
+ val msg = CreateShortcutMessage(PlanetSideGUID(4210), 1, Some(Shortcut.Medkit()))
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual stringMedkit
@@ -71,9 +63,7 @@ class CreateShortcutMessageTest extends Specification {
val msg = CreateShortcutMessage(
PlanetSideGUID(1356),
8,
- 0,
- true,
- Some(Shortcut(1, "shortcut_macro", "NTU", "/platoon Incoming NTU spam!"))
+ Some(Shortcut.Macro("NTU", "/platoon Incoming NTU spam!"))
)
val pkt = PacketCoding.encodePacket(msg).require.toByteVector
@@ -81,42 +71,40 @@ class CreateShortcutMessageTest extends Specification {
}
"encode (remove)" in {
- val msg = CreateShortcutMessage(PlanetSideGUID(1356), 1, 0, false)
+ val msg = CreateShortcutMessage(PlanetSideGUID(1356), 1, None)
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!"
+ val MACRO: Shortcut.Macro = Shortcut.Macro("NTU", "/platoon Incoming NTU spam!")
+ MACRO.acronym mustEqual "NTU"
+ MACRO.msg mustEqual "/platoon Incoming NTU spam!"
}
"presets" in {
- ImplantType.AudioAmplifier.shortcut.purpose mustEqual 2
+ ImplantType.AudioAmplifier.shortcut.code mustEqual 2
ImplantType.AudioAmplifier.shortcut.tile mustEqual "audio_amplifier"
- ImplantType.DarklightVision.shortcut.purpose mustEqual 2
+ ImplantType.DarklightVision.shortcut.code mustEqual 2
ImplantType.DarklightVision.shortcut.tile mustEqual "darklight_vision"
- ImplantType.Targeting.shortcut.purpose mustEqual 2
+ ImplantType.Targeting.shortcut.code mustEqual 2
ImplantType.Targeting.shortcut.tile mustEqual "targeting"
- Shortcut.Medkit.get.purpose mustEqual 0
- Shortcut.Medkit.get.tile mustEqual "medkit"
- ImplantType.MeleeBooster.shortcut.purpose mustEqual 2
+ Shortcut.Medkit().code mustEqual 0
+ Shortcut.Medkit().tile mustEqual "medkit"
+ ImplantType.MeleeBooster.shortcut.code mustEqual 2
ImplantType.MeleeBooster.shortcut.tile mustEqual "melee_booster"
- ImplantType.PersonalShield.shortcut.purpose mustEqual 2
+ ImplantType.PersonalShield.shortcut.code mustEqual 2
ImplantType.PersonalShield.shortcut.tile mustEqual "personal_shield"
- ImplantType.RangeMagnifier.shortcut.purpose mustEqual 2
+ ImplantType.RangeMagnifier.shortcut.code mustEqual 2
ImplantType.RangeMagnifier.shortcut.tile mustEqual "range_magnifier"
- ImplantType.AdvancedRegen.shortcut.purpose mustEqual 2
+ ImplantType.AdvancedRegen.shortcut.code mustEqual 2
ImplantType.AdvancedRegen.shortcut.tile mustEqual "advanced_regen"
- ImplantType.SecondWind.shortcut.purpose mustEqual 2
+ ImplantType.SecondWind.shortcut.code mustEqual 2
ImplantType.SecondWind.shortcut.tile mustEqual "second_wind"
- ImplantType.SilentRun.shortcut.purpose mustEqual 2
+ ImplantType.SilentRun.shortcut.code mustEqual 2
ImplantType.SilentRun.shortcut.tile mustEqual "silent_run"
- ImplantType.Surge.shortcut.purpose mustEqual 2
+ ImplantType.Surge.shortcut.code mustEqual 2
ImplantType.Surge.shortcut.tile mustEqual "surge"
}
}