Shortcut to Grenade (#1010)

* routine that finds, equips, and draws a grenade if the user has it; moved handling of ObjectHeldMessage from SessionActor to PlayerControl; placed a arm movement restriction condition

* loading of, and adding and removing of shortcuts to/from both the database and the client hotbar

* player-driven sanity tests to reload otherwise unavailable hotbar shortcuts; revamp to CreateShortcutMessage
This commit is contained in:
Fate-JH 2022-10-24 18:16:08 -04:00 committed by GitHub
parent 1369da22f0
commit 630c2809cb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1032 additions and 313 deletions

View file

@ -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)
);

View file

@ -237,8 +237,8 @@ class ObjectHeldTest extends ActorTest {
ServiceManager.boot(system) ServiceManager.boot(system)
val service = system.actorOf(Props(classOf[AvatarService], Zone.Nowhere), AvatarServiceTest.TestName) val service = system.actorOf(Props(classOf[AvatarService], Zone.Nowhere), AvatarServiceTest.TestName)
service ! Service.Join("test") service ! Service.Join("test")
service ! AvatarServiceMessage("test", AvatarAction.ObjectHeld(PlanetSideGUID(10), 1)) service ! AvatarServiceMessage("test", AvatarAction.ObjectHeld(PlanetSideGUID(10), 1, 2))
expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectHeld(1))) expectMsg(AvatarServiceResponse("/test/Avatar", PlanetSideGUID(10), AvatarResponse.ObjectHeld(1, 2)))
} }
} }
} }

View file

@ -13,7 +13,7 @@ import scala.concurrent.{ExecutionContextExecutor, Future, Promise}
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
import scala.concurrent.duration._ 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.converter.CharacterSelectConverter
import net.psforever.objects.definition._ import net.psforever.objects.definition._
import net.psforever.objects.inventory.Container 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 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 AvatarResponse(avatar: Avatar)
final case class AvatarLoginResponse(avatar: Avatar) final case class AvatarLoginResponse(avatar: Avatar)
@ -826,7 +830,7 @@ class AvatarActor(
characters.headOption match { characters.headOption match {
case None => case None =>
val result = for { val result = for {
id <- ctx.run( _ <- ctx.run(
query[persistence.Avatar] query[persistence.Avatar]
.insert( .insert(
_.name -> lift(name), _.name -> lift(name),
@ -838,17 +842,6 @@ class AvatarActor(
_.bep -> lift(Config.app.game.newAvatar.br.experience), _.bep -> lift(Config.app.game.newAvatar.br.experience),
_.cep -> lift(Config.app.game.newAvatar.cr.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 () } yield ()
@ -921,52 +914,45 @@ class AvatarActor(
case LoginAvatar(replyTo) => case LoginAvatar(replyTo) =>
import ctx._ import ctx._
val avatarId = avatar.id val avatarId = avatar.id
val result = for { ctx.run(
//log this login query[persistence.Avatar]
_ <- ctx.run(query[persistence.Avatar].filter(_.id == lift(avatarId)) .filter(_.id == lift(avatarId))
.update(_.lastLogin -> lift(LocalDateTime.now())) .map { c => (c.created, c.lastLogin) }
) )
//log this choice of faction (no empire switching) .onComplete {
_ <- ctx.run(query[persistence.Account].filter(_.id == lift(account.id)) case Success(value) if value.nonEmpty =>
.update(_.lastFactionId -> lift(avatar.faction.id)) 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)
) )
//retrieve avatar data ).foreach(c => query[persistence.Certification].insert(c))
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)
) )
_ <- ctx.run(
liftQuery(
List(persistence.Shortcut(avatarId, 0, 0, "medkit"))
).foreach(c => query[persistence.Shortcut].insert(c))
) )
) } yield true
// if we need to start stamina regeneration inits.onComplete {
tryRestoreStaminaForSession(stamina = 1) match { case Success(_) => performAvatarLogin(avatarId, account.id, replyTo)
case Some(_) => case Failure(e) => log.error(e)("db failure")
defaultStaminaRegen(initialDelay = 0.5f seconds)
case _ => ;
} }
replyTo ! AvatarLoginResponse(avatar) } else {
case Failure(e) => performAvatarLogin(avatarId, account.id, replyTo)
log.error(e)("db failure") }
case Failure(e) => log.error(e)("db failure")
} }
Behaviors.same Behaviors.same
@ -1353,7 +1339,7 @@ class AvatarActor(
updatePurchaseTimer( updatePurchaseTimer(
name, name,
cooldown.toSeconds, cooldown.toSeconds,
DefinitionUtil.fromString(name).isInstanceOf[VehicleDefinition] item.isInstanceOf[VehicleDefinition]
) )
case _ => ; case _ => ;
} }
@ -1617,6 +1603,91 @@ class AvatarActor(
case MemberListRequest(action, name) => case MemberListRequest(action, name) =>
memberListAction(action, name) memberListAction(action, name)
Behaviors.same 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 { .receiveSignal {
case (_, PostStop) => case (_, PostStop) =>
@ -1636,6 +1707,78 @@ class AvatarActor(
Future.failed(ex).asInstanceOf[Future[Loadout]] 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 * na
* @see `avatarCopy(Avatar)` * @see `avatarCopy(Avatar)`
@ -1781,8 +1924,6 @@ class AvatarActor(
CreateShortcutMessage( CreateShortcutMessage(
session.get.player.GUID, session.get.player.GUID,
slot + 2, slot + 2,
0,
addShortcut = true,
Some(implant.definition.implantType.shortcut) Some(implant.definition.implantType.shortcut)
) )
) )
@ -2303,6 +2444,30 @@ class AvatarActor(
out.future 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 = { def startIfStoppedStaminaRegen(initialDelay: FiniteDuration): Unit = {
if (staminaRegenTimer.isCancelled) { if (staminaRegenTimer.isCancelled) {
defaultStaminaRegen(initialDelay) defaultStaminaRegen(initialDelay)

View file

@ -1,31 +1,37 @@
package net.psforever.actors.session package net.psforever.actors.session
import akka.actor.Cancellable import akka.actor.Cancellable
import akka.actor.typed.receptionist.Receptionist
import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy} 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.{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.actors.zone.BuildingActor
import net.psforever.login.WorldSession
import net.psforever.objects.{Default, Player, Session}
import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Cosmetic} import net.psforever.objects.avatar.{BattleRank, Certification, CommandRank, Cosmetic}
import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} 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.resourcesilo.ResourceSilo
import net.psforever.objects.serverobject.structures.{Amenity, Building} import net.psforever.objects.serverobject.structures.{Amenity, Building}
import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets} import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets}
import net.psforever.objects.zones.Zoning import net.psforever.objects.zones.Zoning
import net.psforever.packet.game.{ChatMsg, DeadState, RequestDestroyMessage, ZonePopulationUpdateMessage} 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.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, Vector3}
import net.psforever.util.{Config, PointOfInterest} import net.psforever.util.{Config, PointOfInterest}
import net.psforever.zones.Zones 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 { object ChatActor {
def apply( def apply(
@ -501,12 +507,17 @@ class ChatActor(
case Some(all) if all.toLowerCase.startsWith("all") => case Some(all) if all.toLowerCase.startsWith("all") =>
session.zone.Buildings.values session.zone.Buildings.values
case Some(x) => case Some(x) =>
session.zone.Buildings.values.find { _.Name.equalsIgnoreCase(x) }.toList session.zone.Buildings.values.find {
_.Name.equalsIgnoreCase(x)
}.toList
case _ => case _ =>
session.zone.Buildings.values session.zone.Buildings.values
}) })
.flatMap { building => building.Amenities.filter { _.isInstanceOf[ResourceSilo] } } .flatMap { building => building.Amenities.filter {
ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent=s"$facility") _.isInstanceOf[ResourceSilo]
}
}
ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent = s"$facility")
case (_, _, content) if content.startsWith("!zonerotate") && gmCommandAllowed => case (_, _, content) if content.startsWith("!zonerotate") && gmCommandAllowed =>
val buffer = contents.toLowerCase.split("\\s+") val buffer = contents.toLowerCase.split("\\s+")
@ -523,9 +534,54 @@ class ChatActor(
tplayer.Revive tplayer.Revive
tplayer.Actor ! Player.Die() tplayer.Actor ! Player.Die()
case _ => 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 // unknown ! commands are ignored
} }
}
case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed => case (CMT_CAPTUREBASE, _, contents) if gmCommandAllowed =>
val args = contents.split(" ").filter(_ != "") val args = contents.split(" ").filter(_ != "")
@ -1168,7 +1224,107 @@ class ChatActor(
case _ => case _ =>
Behaviors.same 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))
))
}
}
} }

View file

@ -9,7 +9,7 @@ import net.psforever.actors.net.MiddlewareActor
import net.psforever.actors.zone.ZoneActor import net.psforever.actors.zone.ZoneActor
import net.psforever.login.WorldSession._ import net.psforever.login.WorldSession._
import net.psforever.objects._ 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.ballistics._
import net.psforever.objects.ce._ import net.psforever.objects.ce._
import net.psforever.objects.definition._ import net.psforever.objects.definition._
@ -1649,6 +1649,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
player.Armor = results.armor player.Armor = results.armor
player.ExoSuit = ExoSuitType(results.exosuitNum) player.ExoSuit = ExoSuitType(results.exosuitNum)
AvatarActor.buildContainedEquipmentFromClob(player, results.loadout, log) AvatarActor.buildContainedEquipmentFromClob(player, results.loadout, log)
if (player.ExoSuit == ExoSuitType.MAX) {
player.DrawnSlot = 0
player.ResistArmMotion(PlayerControl.maxRestriction)
}
} else { } else {
player.ExoSuit = ExoSuitType.Standard player.ExoSuit = ExoSuitType.Standard
DefinitionUtil.applyDefaultLoadout(player) DefinitionUtil.applyDefaultLoadout(player)
@ -2311,9 +2315,21 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(ObjectDeleteMessage(item_guid, unk)) sendResponse(ObjectDeleteMessage(item_guid, unk))
} }
case AvatarResponse.ObjectHeld(slot) => case AvatarResponse.ObjectHeld(slot, previousSLot) =>
if (tplayer_guid != guid) { if (tplayer_guid == guid) {
sendResponse(ObjectHeldMessage(guid, slot, false)) 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) => case AvatarResponse.OxygenState(player, vehicle) =>
@ -2565,7 +2581,7 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
sendResponse(PlanetsideAttributeMessage(target, 4, armor)) sendResponse(PlanetsideAttributeMessage(target, 4, armor))
if (tplayer_guid == target) { if (tplayer_guid == target) {
//happening to this player //happening to this player
sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, false)) sendResponse(ObjectHeldMessage(target, Player.HandsDownSlot, true))
//cleanup //cleanup
(old_holsters ++ old_inventory).foreach { (old_holsters ++ old_inventory).foreach {
case (obj, objGuid) => case (obj, objGuid) =>
@ -3382,10 +3398,10 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
CancelAllProximityUnits() CancelAllProximityUnits()
if (player.VisibleSlots.contains(player.DrawnSlot)) { if (player.VisibleSlots.contains(player.DrawnSlot)) {
player.DrawnSlot = Player.HandsDownSlot player.DrawnSlot = Player.HandsDownSlot
sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, true)) sendResponse(ObjectHeldMessage(player.GUID, Player.HandsDownSlot, unk1=true))
continent.AvatarEvents ! AvatarServiceMessage( continent.AvatarEvents ! AvatarServiceMessage(
continent.id, 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 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)) sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
//TODO if Medkit does not have shortcut, add to a free slot or write over slot 64 //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)) sendResponse(ChangeShortcutBankMessage(guid, 0))
//Favorites lists //Favorites lists
avatarActor ! AvatarActor.InitialRefreshLoadouts() avatarActor ! AvatarActor.InitialRefreshLoadouts()
sendResponse( sendResponse(
SetChatFilterMessage(ChatChannel.Platoon, false, ChatChannel.values.toList) SetChatFilterMessage(ChatChannel.Platoon, origin=false, ChatChannel.values.toList)
) //TODO will not always be "on" like this ) //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) //looking for squad (members)
if (tplayer.avatar.lookingForSquad || lfsm) { if (tplayer.avatar.lookingForSquad || lfsm) {
sendResponse(PlanetsideAttributeMessage(guid, 53, 1)) 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") 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) => case ObjectHeldMessage(_, held_holsters, _) =>
val before = player.DrawnSlot player.Actor ! PlayerControl.ObjectHeld(held_holsters)
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 msg @ AvatarJumpMessage(state) => case msg @ AvatarJumpMessage(state) =>
avatarActor ! AvatarActor.ConsumeStamina(10) avatarActor ! AvatarActor.ConsumeStamina(10)
@ -6490,7 +6468,14 @@ class SessionActor(middlewareActor: typed.ActorRef[MiddlewareActor.Command], con
log.info(lament) log.info(lament)
log.debug(s"Battleplan: $lament - $msg") 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) => case FriendsRequest(action, name) =>
avatarActor ! AvatarActor.MemberListRequest(action, name) avatarActor ! AvatarActor.MemberListRequest(action, name)

View file

@ -3,6 +3,7 @@ package net.psforever.login
import akka.actor.ActorRef import akka.actor.ActorRef
import akka.pattern.{AskTimeoutException, ask} import akka.pattern.{AskTimeoutException, ask}
import akka.util.Timeout import akka.util.Timeout
import net.psforever.objects._
import net.psforever.objects.equipment.{Ammo, Equipment, EquipmentSize} import net.psforever.objects.equipment.{Ammo, Equipment, EquipmentSize}
import net.psforever.objects.guid._ import net.psforever.objects.guid._
import net.psforever.objects.inventory.{Container, InventoryItem} 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.PlanetSideServerObject
import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.serverobject.containable.Containable
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects._ import net.psforever.types.{ExoSuitType, PlanetSideGUID, TransactionType, Vector3}
import net.psforever.packet.game.ObjectHeldMessage
import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3}
import net.psforever.services.Service import net.psforever.services.Service
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage} import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@ -35,7 +34,7 @@ object WorldSession {
private implicit val timeout = new Timeout(5000 milliseconds) 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). * 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. * 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. * It will also be dropped if it takes too long to be placed.
@ -299,11 +298,22 @@ object WorldSession {
if (player.VisibleSlots.contains(slot)) { if (player.VisibleSlots.contains(slot)) {
val localZone = player.Zone val localZone = player.Zone
TaskBundle( TaskBundle(
TaskToHoldEquipmentUp(player)(item, slot),
GUIDTask.registerEquipment(localZone.GUID, item)
)
} else {
//TODO log.error
throw new RuntimeException(s"provided slot $slot is not a player visible slot (holsters)")
}
}
def TaskToHoldEquipmentUp(player: Player)(item: Equipment, slot: Int): Task = {
new StraightforwardTask() { new StraightforwardTask() {
private val localPlayer = player private val localPlayer = player
private val localGUID = player.GUID private val localGUID = player.GUID
private val localItem = item private val localItem = item
private val localSlot = slot private val localSlot = slot
private val localZone = player.Zone
def action(): Future[Any] = { def action(): Future[Any] = {
ask(localPlayer.Actor, Containable.PutItemInSlotOnly(localItem, localSlot)) ask(localPlayer.Actor, Containable.PutItemInSlotOnly(localItem, localSlot))
@ -311,34 +321,15 @@ object WorldSession {
case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) => case Failure(_) | Success(_: Containable.CanNotPutItemInSlot) =>
TaskWorkflow.execute(GUIDTask.unregisterEquipment(localZone.GUID, localItem)) TaskWorkflow.execute(GUIDTask.unregisterEquipment(localZone.GUID, localItem))
case _ => case _ =>
if (localPlayer.DrawnSlot != Player.HandsDownSlot) { forcedTolowerRaisedArm(localPlayer, localPlayer.GUID, localZone)
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 localPlayer.DrawnSlot = localSlot
localZone.AvatarEvents ! AvatarServiceMessage( localZone.AvatarEvents ! AvatarServiceMessage(
localZone.id, localZone.id,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectHeldMessage(localGUID, localSlot, false)) AvatarAction.ObjectHeld(localGUID, localSlot, localSlot)
) )
} }
Future(this) Future(this)
} }
},
GUIDTask.registerEquipment(localZone.GUID, item)
)
} else {
//TODO log.error
throw new RuntimeException(s"provided slot $slot is not a player visible slot (holsters)")
} }
} }
@ -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.<br>
* <br>
* This is not vanilla behavior.<br>
* <br>
* 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. * If a timeout occurs on the manipulation, declare a terminal transaction failure.
* @see `AskTimeoutException` * @see `AskTimeoutException`

View file

@ -14,6 +14,7 @@ import net.psforever.objects.serverobject.environment.InteractWithEnvironment
import net.psforever.objects.serverobject.mount.MountableEntity import net.psforever.objects.serverobject.mount.MountableEntity
import net.psforever.objects.vital.resistance.ResistanceProfile import net.psforever.objects.vital.resistance.ResistanceProfile
import net.psforever.objects.vital.Vitality 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.interaction.DamageInteraction
import net.psforever.objects.vital.resolution.DamageResistanceModel import net.psforever.objects.vital.resolution.DamageResistanceModel
import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation} import net.psforever.objects.zones.blockmap.{BlockMapEntity, SectorPopulation}
@ -79,6 +80,8 @@ class Player(var avatar: Avatar)
val squadLoadouts = new LoadoutManager(10) val squadLoadouts = new LoadoutManager(10)
var resistArmMotion: (Player,Int)=>Boolean = Player.neverRestrict
//init //init
Health = 0 //player health is artificially managed as a part of their lifecycle; start entity as dead Health = 0 //player health is artificially managed as a part of their lifecycle; start entity as dead
Destroyed = true //see isAlive Destroyed = true //see isAlive
@ -176,8 +179,8 @@ class Player(var avatar: Avatar)
capacitorState capacitorState
} }
def CapacitorLastUsedMillis = capacitorLastUsedMillis def CapacitorLastUsedMillis: Long = capacitorLastUsedMillis
def CapacitorLastChargedMillis = capacitorLastChargedMillis def CapacitorLastChargedMillis: Long = capacitorLastChargedMillis
def VisibleSlots: Set[Int] = def VisibleSlots: Set[Int] =
if (exosuit.SuitType == ExoSuitType.MAX) { 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] = { def FreeHand_=(item: Option[Equipment]): Option[Equipment] = {
if (freeHand.Equipment.isEmpty || item.isEmpty) { 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: Int = drawnSlot
def DrawnSlot_=(slot: Int): Int = { def DrawnSlot_=(slot: Int): Int = {
@ -339,15 +354,15 @@ class Player(var avatar: Avatar)
ChangeSpecialAbility() 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 def FacingYawUpper: Float = facingYawUpper
@ -533,7 +548,7 @@ class Player(var avatar: Avatar)
ZoningRequest ZoningRequest
} }
def DamageModel = exosuit.asInstanceOf[DamageResistanceModel] def DamageModel: DamageResistanceModel = exosuit.asInstanceOf[DamageResistanceModel]
def canEqual(other: Any): Boolean = other.isInstanceOf[Player] def canEqual(other: Any): Boolean = other.isInstanceOf[Player]
@ -601,6 +616,10 @@ object Player {
player player
} }
} }
def neverRestrict(player: Player, slot: Int): Boolean = {
false
}
} }
private class InteractWithMinesUnlessSpectating( private class InteractWithMinesUnlessSpectating(

View file

@ -439,13 +439,9 @@ object Players {
} }
if (player.DrawnSlot == Player.HandsDownSlot) { if (player.DrawnSlot == Player.HandsDownSlot) {
player.DrawnSlot = index player.DrawnSlot = index
events ! AvatarServiceMessage(
name,
AvatarAction.SendResponse(Service.defaultPlayerGUID, ObjectHeldMessage(pguid, index, true))
)
events ! AvatarServiceMessage( events ! AvatarServiceMessage(
zone.id, zone.id,
AvatarAction.ObjectHeld(pguid, index) AvatarAction.ObjectHeld(pguid, index, index)
) )
} }
} }

View file

@ -123,6 +123,7 @@ case class Avatar(
fatigued: Boolean = false, fatigued: Boolean = false,
certifications: Set[Certification] = Set(), certifications: Set[Certification] = Set(),
implants: Seq[Option[Implant]] = Seq(None, None, None), implants: Seq[Option[Implant]] = Seq(None, None, None),
shortcuts: Array[Option[Shortcut]] = Array.fill[Option[Shortcut]](64)(None),
locker: LockerContainer = Avatar.makeLocker(), locker: LockerContainer = Avatar.makeLocker(),
deployables: DeployableToolbox = new DeployableToolbox(), deployables: DeployableToolbox = new DeployableToolbox(),
lookingForSquad: Boolean = false, lookingForSquad: Boolean = false,

View file

@ -306,10 +306,53 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
case PlayerControl.SetExoSuit(exosuit: ExoSuitType.Value, subtype: Int) => case PlayerControl.SetExoSuit(exosuit: ExoSuitType.Value, subtype: Int) =>
setExoSuit(exosuit, subtype) 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) => case Terminal.TerminalMessage(_, msg, order) =>
order match { order match {
case Terminal.BuyExosuit(exosuit, subtype) => case Terminal.BuyExosuit(exosuit, subtype) =>
val result = setExoSuit(exosuit, subtype) val result = setExoSuit(exosuit, subtype)
if (exosuit == ExoSuitType.MAX) {
player.ResistArmMotion(PlayerControl.maxRestriction)
}
player.Zone.AvatarEvents ! AvatarServiceMessage( player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Name, player.Name,
AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result) AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result)
@ -345,6 +388,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
if ( if (
Players.CertificationToUseExoSuit(player, exosuit, subtype) && Players.CertificationToUseExoSuit(player, exosuit, subtype) &&
(if (exosuit == ExoSuitType.MAX) { (if (exosuit == ExoSuitType.MAX) {
player.ResistArmMotion(PlayerControl.maxRestriction)
val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction) val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction)
player.avatar.purchaseCooldown(weapon) match { player.avatar.purchaseCooldown(weapon) match {
case Some(_) => false case Some(_) => false
@ -353,6 +397,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
true true
} }
} else { } else {
player.ResistArmMotion(Player.neverRestrict)
true true
}) })
) { ) {
@ -1329,6 +1374,12 @@ object PlayerControl {
/** na */ /** na */
final case class SetExoSuit(exosuit: ExoSuitType.Value, subtype: Int) 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. * Transform an applicable Aura effect into its `PlanetsideAttributeMessage` value.
* @see `Aura` * @see `Aura`
@ -1347,4 +1398,13 @@ object PlayerControl {
def sendResponse(zone: Zone, channel: String, msg: PlanetSideGamePacket): Unit = { def sendResponse(zone: Zone, channel: String, msg: PlanetSideGamePacket): Unit = {
zone.AvatarEvents ! AvatarServiceMessage(channel, AvatarAction.SendResponse(Service.defaultPlayerGUID, msg)) 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
}
}
} }

View file

@ -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
}
}
}

View file

@ -2,9 +2,10 @@
package net.psforever.packet.game package net.psforever.packet.game
import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket}
import net.psforever.types.PlanetSideGUID import net.psforever.types.{ImplantType, PlanetSideGUID}
import scodec.Codec import scodec.{Codec, TransformSyntax}
import scodec.codecs._ import scodec.codecs._
import shapeless.{::, HNil}
/** /**
* Details regarding this shortcut.<br> * Details regarding this shortcut.<br>
@ -18,33 +19,24 @@ import scodec.codecs._
* The `shortcut_macro` setting displays a word bubble superimposed by the (first three letters of) `effect1` text.<br> * The `shortcut_macro` setting displays a word bubble superimposed by the (first three letters of) `effect1` text.<br>
* Implants and the medkit should have self-explanatory graphics. * Implants and the medkit should have self-explanatory graphics.
* <br> * <br>
* Purpose:<br> * Tile - Code<br>
* `0 - Medkit`<br> * `advanced_regen` (regeneration) - 2<br>
* `1 - Macro`<br> * `audio_amplifier` - 2<br>
* `2 - Implant`<br> * `darklight_vision` - 2<br>
* <br> * `medkit` - 0<br>
* Tile:<br> * `melee_booster` - 2<br>
* `advanced_regen` (regeneration)<br> * `personal_shield` - 2<br>
* `audio_amplifier`<br> * `range_magnifier` - 2<br>
* `darklight_vision`<br> * `second_wind` - 2<br>
* `medkit`<br> * `shortcut_macro` - 1<br>
* `melee_booster`<br> * `silent_run` (sensor shield) - 2<br>
* `personal_shield`<br> * `surge` - 2<br>
* `range_magnifier`<br> * `targeting` (enhanced targeting) - 2
* `second_wind`<br> * @param code the primary use of this shortcut
* `shortcut_macro`<br>
* `silent_run` (sensor shield)<br>
* `surge`<br>
* `targeting` (enhanced targetting)<br>
* <br>
* Exploration:<br>
* 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
*/ */
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.<br> * Facilitate a quick-use button for the hotbar.<br>
@ -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. * The prior functionality will rarely be appreciated, however, as players rarely never have their medkit shortcut unbound.
* @param player_guid the player * @param player_guid the player
* @param slot the hotbar slot number (one-indexed) * @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 * @param shortcut optional; details about the shortcut to be created
* @see ChangeShortcutBankMessage * @see `ChangeShortcutBankMessage`
*/ */
final case class CreateShortcutMessage( final case class CreateShortcutMessage(
player_guid: PlanetSideGUID, player_guid: PlanetSideGUID,
slot: Int, slot: Int,
unk: Int, shortcut: Option[Shortcut]
addShortcut: Boolean,
shortcut: Option[Shortcut] = None
) extends PlanetSideGamePacket { ) extends PlanetSideGamePacket {
type Packet = CreateShortcutMessage type Packet = CreateShortcutMessage
def opcode = GamePacketOpcode.CreateShortcutMessage def opcode = GamePacketOpcode.CreateShortcutMessage
@ -82,33 +70,110 @@ final case class CreateShortcutMessage(
} }
object Shortcut extends Marshallable[Shortcut] { object Shortcut extends Marshallable[Shortcut] {
/** Preset for the medkit quick-use option. */ /** 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. * Converter for text macro parameters that acts like a preset.
* @param effect1 a three letter acronym displayed in the hotbar * @param acronym a three letter acronym displayed in the hotbar
* @param effect2 the chat message content * @param msg the chat message content
* @return `Some` shortcut that represents a voice macro command * @return `Some` shortcut that represents a voice macro command
*/ */
def MACRO(effect1: String, effect2: String): Some[Shortcut] = Some(Shortcut(1, "shortcut_macro", effect1, effect2)) 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) :: * Converter for an implant name token that acts like a preset.
("tile" | PacketHelpers.encodedStringAligned(5)) :: * @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) :: ("effect1" | PacketHelpers.encodedWideString) ::
("effect2" | 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] { object CreateShortcutMessage extends Marshallable[CreateShortcutMessage] {
implicit val codec: Codec[CreateShortcutMessage] = ( implicit val codec: Codec[CreateShortcutMessage] = (
("player_guid" | PlanetSideGUID.codec) :: ("player_guid" | PlanetSideGUID.codec) ::
("slot" | uint8L) :: ("slot" | uint16L) ::
("unk" | uint8L) :: ("shortcut" | optional(bool, Shortcut.codec))
(("addShortcut" | bool) >>:~ { value =>
conditional(value, "shortcut" | Shortcut.codec).hlist
})
).as[CreateShortcutMessage] ).as[CreateShortcutMessage]
} }

View file

@ -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
)

View file

@ -173,9 +173,9 @@ class AvatarService(zone: Zone) extends Actor {
AvatarEvents.publish( AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ObjectDelete(item_guid, unk)) 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( 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) => case AvatarAction.OxygenState(player, vehicle) =>
AvatarEvents.publish( AvatarEvents.publish(

View file

@ -67,7 +67,7 @@ object AvatarAction {
cdata: ConstructorData cdata: ConstructorData
) extends Action ) extends Action
final case class ObjectDelete(player_guid: PlanetSideGUID, item_guid: PlanetSideGUID, unk: Int = 0) 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 OxygenState(player: OxygenStateTarget, vehicle: Option[OxygenStateTarget]) extends Action
final case class PlanetsideAttribute(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long) final case class PlanetsideAttribute(player_guid: PlanetSideGUID, attribute_type: Int, attribute_value: Long)
extends Action extends Action

View file

@ -47,7 +47,7 @@ object AvatarResponse {
final case class LoadPlayer(pkt: ObjectCreateMessage) extends Response final case class LoadPlayer(pkt: ObjectCreateMessage) extends Response
final case class LoadProjectile(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 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 OxygenState(player: OxygenStateTarget, vehicle: Option[OxygenStateTarget]) extends Response
final case class PlanetsideAttribute(attribute_type: Int, attribute_value: Long) 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 final case class PlanetsideAttributeToAll(attribute_type: Int, attribute_value: Long) extends Response

View file

@ -5,6 +5,7 @@ import enumeratum.values.{IntEnum, IntEnumEntry}
import net.psforever.packet.PacketHelpers import net.psforever.packet.PacketHelpers
import net.psforever.packet.game.Shortcut import net.psforever.packet.game.Shortcut
import net.psforever.packet.game.objectcreate.ImplantEffects import net.psforever.packet.game.objectcreate.ImplantEffects
import scodec.Codec
import scodec.codecs._ import scodec.codecs._
sealed abstract class ImplantType( sealed abstract class ImplantType(
@ -16,52 +17,60 @@ sealed abstract class ImplantType(
) extends IntEnumEntry ) extends IntEnumEntry
case object ImplantType extends IntEnum[ImplantType] { case object ImplantType extends IntEnum[ImplantType] {
case object AdvancedRegen case object AdvancedRegen
extends ImplantType( extends ImplantType(
value = 0, value = 0,
shortcut = Shortcut(2, "advanced_regen"), shortcut = Shortcut.Implant("advanced_regen"),
effect = Some(ImplantEffects.RegenEffects) 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 case object DarklightVision
extends ImplantType( extends ImplantType(
value = 3, value = 3,
shortcut = Shortcut(2, "darklight_vision"), shortcut = Shortcut.Implant("darklight_vision"),
effect = Some(ImplantEffects.DarklightEffects) 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 case object PersonalShield
extends ImplantType( extends ImplantType(
value = 5, value = 5,
shortcut = Shortcut(2, "personal_shield"), shortcut = Shortcut.Implant("personal_shield"),
disabledFor = Set(ExoSuitType.Infiltration), disabledFor = Set(ExoSuitType.Infiltration),
effect = Some(ImplantEffects.PersonalShieldEffects) 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 case object Surge extends ImplantType(
extends ImplantType(
value = 9, value = 9,
shortcut = Shortcut(2, "surge"), shortcut = Shortcut.Implant("surge"),
disabledFor = Set(ExoSuitType.MAX), disabledFor = Set(ExoSuitType.MAX),
effect = Some(ImplantEffects.SurgeEffects) 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 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)
} }

View file

@ -15,16 +15,13 @@ class CreateShortcutMessageTest extends Specification {
"decode (medkit)" in { "decode (medkit)" in {
PacketCoding.decodePacket(stringMedkit).require match { PacketCoding.decodePacket(stringMedkit).require match {
case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) => case CreateShortcutMessage(player_guid, slot, shortcut) =>
player_guid mustEqual PlanetSideGUID(4210) player_guid mustEqual PlanetSideGUID(4210)
slot mustEqual 1 slot mustEqual 1
unk mustEqual 0 shortcut match {
addShortcut mustEqual true case Some(Shortcut.Medkit()) => ok
shortcut.isDefined mustEqual true case _ => ko
shortcut.get.purpose mustEqual 0 }
shortcut.get.tile mustEqual "medkit"
shortcut.get.effect1 mustEqual ""
shortcut.get.effect2 mustEqual ""
case _ => case _ =>
ko ko
} }
@ -32,16 +29,13 @@ class CreateShortcutMessageTest extends Specification {
"decode (macro)" in { "decode (macro)" in {
PacketCoding.decodePacket(stringMacro).require match { PacketCoding.decodePacket(stringMacro).require match {
case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) => case CreateShortcutMessage(player_guid, slot, shortcut) =>
player_guid mustEqual PlanetSideGUID(1356) player_guid mustEqual PlanetSideGUID(1356)
slot mustEqual 8 slot mustEqual 8
unk mustEqual 0 shortcut match {
addShortcut mustEqual true case Some(Shortcut.Macro("NTU", "/platoon Incoming NTU spam!")) => ok
shortcut.isDefined mustEqual true case _ => ko
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 _ => case _ =>
ko ko
} }
@ -49,11 +43,9 @@ class CreateShortcutMessageTest extends Specification {
"decode (remove)" in { "decode (remove)" in {
PacketCoding.decodePacket(stringRemove).require match { PacketCoding.decodePacket(stringRemove).require match {
case CreateShortcutMessage(player_guid, slot, unk, addShortcut, shortcut) => case CreateShortcutMessage(player_guid, slot, shortcut) =>
player_guid mustEqual PlanetSideGUID(1356) player_guid mustEqual PlanetSideGUID(1356)
slot mustEqual 1 slot mustEqual 1
unk mustEqual 0
addShortcut mustEqual false
shortcut.isDefined mustEqual false shortcut.isDefined mustEqual false
case _ => case _ =>
ko ko
@ -61,7 +53,7 @@ class CreateShortcutMessageTest extends Specification {
} }
"encode (medkit)" in { "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 val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual stringMedkit pkt mustEqual stringMedkit
@ -71,9 +63,7 @@ class CreateShortcutMessageTest extends Specification {
val msg = CreateShortcutMessage( val msg = CreateShortcutMessage(
PlanetSideGUID(1356), PlanetSideGUID(1356),
8, 8,
0, Some(Shortcut.Macro("NTU", "/platoon Incoming NTU spam!"))
true,
Some(Shortcut(1, "shortcut_macro", "NTU", "/platoon Incoming NTU spam!"))
) )
val pkt = PacketCoding.encodePacket(msg).require.toByteVector val pkt = PacketCoding.encodePacket(msg).require.toByteVector
@ -81,42 +71,40 @@ class CreateShortcutMessageTest extends Specification {
} }
"encode (remove)" in { "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 val pkt = PacketCoding.encodePacket(msg).require.toByteVector
pkt mustEqual stringRemove pkt mustEqual stringRemove
} }
"macro" in { "macro" in {
val MACRO: Some[Shortcut] = Shortcut.MACRO("NTU", "/platoon Incoming NTU spam!") val MACRO: Shortcut.Macro = Shortcut.Macro("NTU", "/platoon Incoming NTU spam!")
MACRO.get.purpose mustEqual 1 MACRO.acronym mustEqual "NTU"
MACRO.get.tile mustEqual "shortcut_macro" MACRO.msg mustEqual "/platoon Incoming NTU spam!"
MACRO.get.effect1 mustEqual "NTU"
MACRO.get.effect2 mustEqual "/platoon Incoming NTU spam!"
} }
"presets" in { "presets" in {
ImplantType.AudioAmplifier.shortcut.purpose mustEqual 2 ImplantType.AudioAmplifier.shortcut.code mustEqual 2
ImplantType.AudioAmplifier.shortcut.tile mustEqual "audio_amplifier" 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.DarklightVision.shortcut.tile mustEqual "darklight_vision"
ImplantType.Targeting.shortcut.purpose mustEqual 2 ImplantType.Targeting.shortcut.code mustEqual 2
ImplantType.Targeting.shortcut.tile mustEqual "targeting" ImplantType.Targeting.shortcut.tile mustEqual "targeting"
Shortcut.Medkit.get.purpose mustEqual 0 Shortcut.Medkit().code mustEqual 0
Shortcut.Medkit.get.tile mustEqual "medkit" Shortcut.Medkit().tile mustEqual "medkit"
ImplantType.MeleeBooster.shortcut.purpose mustEqual 2 ImplantType.MeleeBooster.shortcut.code mustEqual 2
ImplantType.MeleeBooster.shortcut.tile mustEqual "melee_booster" 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.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.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.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.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.SilentRun.shortcut.tile mustEqual "silent_run"
ImplantType.Surge.shortcut.purpose mustEqual 2 ImplantType.Surge.shortcut.code mustEqual 2
ImplantType.Surge.shortcut.tile mustEqual "surge" ImplantType.Surge.shortcut.tile mustEqual "surge"
} }
} }