mirror of
https://github.com/psforever/PSF-LoginServer.git
synced 2026-01-19 18:44:45 +00:00
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:
parent
1369da22f0
commit
630c2809cb
|
|
@ -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)
|
||||||
|
);
|
||||||
|
|
@ -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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,11 +507,16 @@ 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 {
|
||||||
|
_.isInstanceOf[ResourceSilo]
|
||||||
|
}
|
||||||
|
}
|
||||||
ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent = s"$facility")
|
ChatActor.setBaseResources(sessionActor, customNtuValue, silos, debugContent = s"$facility")
|
||||||
|
|
||||||
case (_, _, content) if content.startsWith("!zonerotate") && gmCommandAllowed =>
|
case (_, _, content) if content.startsWith("!zonerotate") && gmCommandAllowed =>
|
||||||
|
|
@ -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))
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
76
src/main/scala/net/psforever/objects/avatar/Shortcut.scala
Normal file
76
src/main/scala/net/psforever/objects/avatar/Shortcut.scala
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
src/main/scala/net/psforever/persistence/Shortcut.scala
Normal file
11
src/main/scala/net/psforever/persistence/Shortcut.scala
Normal 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
|
||||||
|
)
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue