diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
index b27f882ba..2f9ebcbef 100644
--- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala
+++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala
@@ -11,7 +11,8 @@ import net.psforever.objects.Session
import net.psforever.objects.avatar.ModePermissions
import net.psforever.objects.avatar.scoring.{Assist, Death, EquipmentStat, KDAStat, Kill, Life, ScoreCard, SupportActivity}
import net.psforever.objects.sourcing.{TurretSource, VehicleSource}
-import net.psforever.objects.vital.ReconstructionActivity
+import net.psforever.packet.game.ImplantAction
+import net.psforever.services.avatar.AvatarServiceResponse
import net.psforever.types.{ChatMessageType, StatisticalCategory, StatisticalElement}
import org.joda.time.{LocalDateTime, Seconds}
@@ -117,9 +118,6 @@ object AvatarActor {
/** Log in the currently selected avatar. Must have first sent SelectAvatar. */
final case class LoginAvatar(replyTo: ActorRef[AvatarLoginResponse]) extends Command
- /** Send implants to client */
- final case class CreateImplants() extends Command
-
/** Replace avatar instance with the provided one */
final case class ReplaceAvatar(avatar: Avatar) extends Command
@@ -173,23 +171,23 @@ object AvatarActor {
/** Activate an implant (must already be initialized) */
final case class ActivateImplant(implantType: ImplantType) extends Command
- /** Deactivate an implant */
+ /** Deactivate an implant (must already be activated) */
final case class DeactivateImplant(implantType: ImplantType) extends Command
- /** Deactivate all non-passive implants that are in use */
- final case class DeactivateActiveImplants() extends Command
+ /** Deactivate all non-passive implants that have been activated */
+ final case object DeactivateActiveImplants extends Command
- /** Start implant initialization timers (after zoning or respawn) */
- final case class InitializeImplants() extends Command
+ /** Start all implant initialization timers (this will also hard restart all active timers) */
+ final case object InitializeImplants extends Command
- /** Deinitialize implants (before zoning or respawning) */
- final case class DeinitializeImplants() extends Command
+ /** Set all implants to deactivated and deinitialized; do not restart the initialization process */
+ final case object DeinitializeImplants extends Command
- /** Deinitialize a certain implant, then initialize it again */
+ /** Set a certain implant to deactivated and deinitialized; restart the initialization process */
final case class ResetImplant(implant: ImplantType) extends Command
- /** Shorthand for DeinitializeImplants and InitializeImplants */
- final case class ResetImplants() extends Command
+ /** Set all active non-passive implants to deactivated and restart the initialization process all un-initialized implants */
+ final case object SoftResetImplants extends Command
/** Set the avatar's lookingForSquad */
final case class SetLookingForSquad(lfs: Boolean) extends Command
@@ -234,7 +232,7 @@ object AvatarActor {
final case class SetStamina(stamina: Int) extends Command
- final case class SetImplantInitialized(implantType: ImplantType) extends Command
+ private case class SetImplantInitialized(implantType: ImplantType) extends Command
final case class MemberListRequest(action: MemberAction.Value, name: String) extends Command
@@ -1015,7 +1013,7 @@ class AvatarActor(
private[this] val log = org.log4s.getLogger
var account: Option[Account] = None
var session: Option[Session] = None
- val implantTimers: mutable.Map[Int, Cancellable] = mutable.Map()
+ val implantTimers: Array[Cancellable] = Array.fill(3)(Default.Cancellable)
var staminaRegenTimer: Cancellable = Default.Cancellable
var _avatar: Option[Avatar] = None
var saveLockerFunc: () => Unit = storeNewLocker
@@ -1292,22 +1290,6 @@ class AvatarActor(
Behaviors.same
- case CreateImplants() =>
- avatar.implants.zipWithIndex.foreach {
- case (Some(implant), index) =>
- sessionActor ! SessionActor.SendResponse(
- AvatarImplantMessage(
- session.get.player.GUID,
- ImplantAction.Add,
- index,
- implant.definition.implantType.value
- )
- )
- case _ => ()
- }
- deinitializeImplants()
- Behaviors.same
-
case LearnImplant(terminalGuid, definition) =>
// TODO there used to be a terminal check here, do we really need it?
buyImplantAction(terminalGuid, definition)
@@ -1466,113 +1448,36 @@ class AvatarActor(
avatarCopy(avatar.copy(vehicle = vehicle))
Behaviors.same
- case ActivateImplant(implantType) =>
- avatar.implants.zipWithIndex.collectFirst {
- case (Some(implant), index) if implant.definition.implantType == implantType => (implant, index)
- } match {
- case Some((implant, slot)) =>
- if (!implant.initialized) {
- log.warn(s"requested activation of uninitialized implant $implantType")
- } else if (
- !consumeThisMuchStamina(implant.definition.ActivationStaminaCost) ||
- avatar.stamina < implant.definition.StaminaCost
- ) {
- // not enough stamina to activate
- } else if (implant.definition.implantType.disabledFor.contains(session.get.player.ExoSuit)) {
- // TODO can this really happen? can we prevent it?
- } else {
- avatarCopy(
- avatar.copy(
- implants = avatar.implants.updated(slot, Some(implant.copy(active = true)))
- )
- )
- sessionActor ! SessionActor.SendResponse(
- AvatarImplantMessage(session.get.player.GUID, ImplantAction.Activation, slot, 1)
- )
- // Activation sound / effect
- session.get.zone.AvatarEvents ! AvatarServiceMessage(
- session.get.zone.id,
- AvatarAction.PlanetsideAttribute(
- session.get.player.GUID,
- 28,
- implant.definition.implantType.value * 2 + 1
- )
- )
- implantTimers.get(slot).foreach(_.cancel())
- val interval = implant.definition.GetCostIntervalByExoSuit(session.get.player.ExoSuit).milliseconds
- // TODO costInterval should be an option ^
- if (interval.toMillis > 0) {
- implantTimers(slot) = context.system.scheduler.scheduleWithFixedDelay(interval, interval)(() => {
- val player = session.get.player
- if (
- implantType match {
- case ImplantType.AdvancedRegen =>
- // for every 1hp: 2sp (running), 1.5sp (standing), 1sp (crouched)
- // to simulate '1.5sp (standing)', find if 0.0...1.0 * 100 is an even number
- val cost = implant.definition.StaminaCost -
- (if (player.Crouching || (!player.isMoving && (math.random() * 100) % 2 == 1)) 1 else 0)
- val aliveAndWounded = player.isAlive && player.Health < player.MaxHealth
- if (aliveAndWounded && consumeThisMuchStamina(cost)) {
- //heal
- val originalHealth = player.Health
- val zone = player.Zone
- val events = zone.AvatarEvents
- val guid = player.GUID
- val newHealth = player.Health = originalHealth + 1
- player.LogActivity(HealFromImplant(implantType, 1))
- events ! AvatarServiceMessage(
- zone.id,
- AvatarAction.PlanetsideAttributeToAll(guid, 0, newHealth)
- )
- false
- } else {
- !aliveAndWounded
- }
- case _ =>
- !player.isAlive || !consumeThisMuchStamina(implant.definition.StaminaCost)
- }
- ) {
- context.self ! DeactivateImplant(implantType)
- }
- })
- }
- }
-
- case None => log.error(s"requested activation of unknown implant $implantType")
- }
+ case SetImplantInitialized(implantType) =>
+ setImplantInitialized(implantType)
Behaviors.same
- case SetImplantInitialized(implantType) =>
- avatar.implants.zipWithIndex.collectFirst {
- case (Some(implant), index) if implant.definition.implantType == implantType => index
- } match {
- case Some(index) =>
- sessionActor ! SessionActor.SendResponse(
- AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, index, 1)
- )
- avatarCopy(avatar.copy(implants = avatar.implants.map {
- case Some(implant) if implant.definition.implantType == implantType =>
- Some(implant.copy(initialized = true))
- case other => other
- }))
-
- case None => log.error(s"set initialized called for unknown implant $implantType")
- }
-
+ case ActivateImplant(implantType) =>
+ activateImplant(implantType)
Behaviors.same
case DeactivateImplant(implantType) =>
deactivateImplant(implantType)
Behaviors.same
- case DeactivateActiveImplants() =>
- avatar.implants.indices.foreach { index =>
- avatar.implants(index).foreach { implant =>
- if (implant.active && implant.definition.GetCostIntervalByExoSuit(session.get.player.ExoSuit) > 0) {
- deactivateImplant(implant.definition.implantType)
- }
- }
- }
+ case DeactivateActiveImplants =>
+ deactivateActiveImplants()
+ Behaviors.same
+
+ case InitializeImplants =>
+ initializeImplants()
+ Behaviors.same
+
+ case DeinitializeImplants =>
+ deinitializeImplants()
+ Behaviors.same
+
+ case ResetImplant(implantType) =>
+ reinitializeImplant(implantType)
+ Behaviors.same
+
+ case SoftResetImplants =>
+ softResetImplants()
Behaviors.same
case RestoreStamina(stamina) =>
@@ -1596,83 +1501,6 @@ class AvatarActor(
defaultStaminaRegen(duration)
Behaviors.same
- case InitializeImplants() =>
- initializeImplants()
- Behaviors.same
-
- case DeinitializeImplants() =>
- deinitializeImplants()
- Behaviors.same
-
- case ResetImplant(implantType) =>
- resetAnImplant(implantType)
- Behaviors.same
-
- case ResetImplants() =>
- val player = session.get.player
- // Get time of when you spawned after a deconstruction or zoning activity.
- val lastDecon: Long = player.History.findLast {entry => entry.isInstanceOf[ReconstructionActivity]} match {
- case Some(entry) => entry.time
- case _ => 0L
- }
- // Get time of when you entered the world or respawned after death.
- val lastRespawn: Long = player.History.findLast {entry => entry.isInstanceOf[SpawningActivity]} match {
- case Some(entry) => entry.time
- case _ => 0L
- }
- // You didn't die. You deconstructed or changed zones via warpgate/IA/recall.
- // When you respawn after death, it does both recon and spawn activities, hence the minus 3000 to make sure
- // this doesn't happen at respawn after death.
- if (lastDecon - 3000 > lastRespawn) {
- deinitializeImplants()
- val implants = avatar.implants
- implants.zipWithIndex.foreach {
- case (Some(implant), slot) =>
- sessionActor ! SessionActor.SendResponse(
- CreateShortcutMessage(
- session.get.player.GUID,
- slot + 2,
- Some(implant.definition.implantType.shortcut)
- )
- )
- // If the amount of time that has passed since you entered the world or died is > how long it takes to
- // initialize this implant, initialize it after 1 second.
- if ((System.currentTimeMillis() / 1000) - (lastRespawn / 1000) > implant.definition.InitializationDuration) {
- implantTimers.get(slot).foreach(_.cancel())
- implantTimers(slot) = context.scheduleOnce(
- 1.seconds,
- context.self,
- SetImplantInitialized(implant.definition.implantType)
- )
- session.get.zone.AvatarEvents ! AvatarServiceMessage(
- avatar.name,
- AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 0))
- )
- }
- // If the implant initialization timer hasn't quite finished, calculate a reduced timer based on last spawn activity
- else {
- val remainingTime = (lastRespawn / 1000).seconds - (System.currentTimeMillis() / 1000).seconds + implant.definition.InitializationDuration.seconds
- implantTimers.get(slot).foreach(_.cancel())
- implantTimers(slot) = context.scheduleOnce(
- remainingTime,
- context.self,
- SetImplantInitialized(implant.definition.implantType)
- )
- session.get.zone.AvatarEvents ! AvatarServiceMessage(
- avatar.name,
- AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 0))
- )
- }
- case (None, _) =>
- }
- }
- // You just entered the world or died. Implants reset and timers start from scratch
- else {
- deinitializeImplants()
- initializeImplants()
- }
- Behaviors.same
-
case UpdateToolDischarge(stats) =>
updateToolDischarge(stats)
Behaviors.same
@@ -1898,7 +1726,7 @@ class AvatarActor(
.receiveSignal {
case (_, PostStop) =>
staminaRegenTimer.cancel()
- implantTimers.values.foreach(_.cancel())
+ implantTimers.foreach(_.cancel())
supportExperienceTimer.cancel()
if (supportExperiencePool > 0) {
AvatarActor.setBepOnly(avatar.id, avatar.bep + supportExperiencePool)
@@ -1940,63 +1768,6 @@ class AvatarActor(
def performAvatarLogin(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
performAvatarLogin0(avatarId, accountId, replyTo)
- /*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),
- _.avatarLoggedIn -> lift(avatarId)
- )
- )
- //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)
- debt <- AvatarActor.loadExperienceDebt(avatarId)
- card <- AvatarActor.loadCampaignKdaData(avatarId)
- } yield (loadouts, implants, certs, locker, friends, ignored, shortcuts, saved, debt, card)
- result.onComplete {
- case Success((_loadouts, implants, certs, lockerInv, friendsList, ignoredList, shortcutList, saved, debt, card)) =>
- 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 = lockerInv,
- people = MemberLists(
- friend = friendsList,
- ignored = ignoredList
- ),
- cooldowns = Cooldowns(
- purchase = AvatarActor.buildCooldownsFromClob(saved.purchaseCooldowns, Avatar.purchaseCooldowns, log),
- use = AvatarActor.buildCooldownsFromClob(saved.useCooldowns, Avatar.useCooldowns, log)
- ),
- scorecard = card
- )
- )
- // if we need to start stamina regeneration
- tryRestoreStaminaForSession(stamina = 1).collect { _ => defaultStaminaRegen(initialDelay = 0.5f seconds) }
- experienceDebt = debt
- replyTo ! AvatarLoginResponse(avatar)
- case Failure(e) =>
- log.error(e)("db failure")
- }*/
}
def performAvatarLogin0(avatarId: Long, accountId: Long, replyTo: ActorRef[AvatarLoginResponse]): Unit = {
@@ -2163,7 +1934,7 @@ class AvatarActor(
if (originalFatigued && !isFatigued) {
avatar.implants.zipWithIndex.foreach {
case (Some(_), slot) =>
- sessionActor ! SessionActor.SendResponse(AvatarImplantMessage(guid, ImplantAction.OutOfStamina, slot, 0))
+ sendAvatarImplantMessageToSelf(guid, ImplantAction.OutOfStamina, slot, value = 0)
case _ => ()
}
}
@@ -2183,8 +1954,8 @@ class AvatarActor(
* meaning that he will only be able to walk, all implants will deactivate,
* and all exertion that require stamina use will become impossible until a threshold of stamina is regained.
* @param stamina an amount to drain
- * @return `true`, as long as the requested amount of stamina can be drained in total;
- * `false`, otherwise
+ * @return `false`, as long as the requested amount of stamina can be drained in total, or tif stamina equals zero;
+ * `true`, otherwise
*/
def consumeThisMuchStamina(stamina: Int): Boolean = {
if (stamina < 1) {
@@ -2204,9 +1975,7 @@ class AvatarActor(
if (implant.active) {
deactivateImplant(implant.definition.implantType)
}
- sessionActor ! SessionActor.SendResponse(
- AvatarImplantMessage(player.GUID, ImplantAction.OutOfStamina, slot, 1)
- )
+ sendAvatarImplantMessageToSelf(player.GUID, ImplantAction.OutOfStamina, slot, value = 1)
case _ => ()
}
}
@@ -2214,7 +1983,8 @@ class AvatarActor(
} else if (becomeFatigued) {
avatarCopy(avatar.copy(implants = avatar.implants.zipWithIndex.collect {
case (Some(implant), slot) if implant.active =>
- implantTimers.get(slot).foreach(_.cancel())
+ implantTimers.lift(slot).foreach(_.cancel())
+ implantTimers.update(slot, Default.Cancellable)
Some(implant.copy(active = false))
case (out, _) =>
out
@@ -2224,119 +1994,6 @@ class AvatarActor(
}
}
- def initializeImplants(): Unit = {
- avatar.implants.zipWithIndex.foreach {
- case (Some(implant), slot) =>
- // TODO if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63
- // for now, just write into slots 2, 3 and 4
- sessionActor ! SessionActor.SendResponse(
- CreateShortcutMessage(
- session.get.player.GUID,
- slot + 2,
- Some(implant.definition.implantType.shortcut)
- )
- )
-
- implantTimers.get(slot).foreach(_.cancel())
- implantTimers(slot) = context.scheduleOnce(
- implant.definition.InitializationDuration.seconds,
- context.self,
- SetImplantInitialized(implant.definition.implantType)
- )
-
- // Start client side initialization timer, visible on the character screen
- // Progress accumulates according to the client's knowledge of the implant initialization time
- // What is normally a 60s timer that is set to 120s on the server will still visually update as if 60s\
- session.get.zone.AvatarEvents ! AvatarServiceMessage(
- avatar.name,
- AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 0))
- )
-
- case (None, _) => ()
- }
- }
-
- def deinitializeImplants(): Unit = {
- avatarCopy(avatar.copy(implants = avatar.implants.zipWithIndex.map {
- case (Some(implant), slot) =>
- if (implant.active) {
- deactivateImplant(implant.definition.implantType)
- }
- if (implant.initialized) {
- session.get.zone.AvatarEvents ! AvatarServiceMessage(
- session.get.zone.id,
- AvatarAction.SendResponse(
- Service.defaultPlayerGUID,
- AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, slot, 0)
- )
- )
- }
- Some(implant.copy(initialized = false, active = false))
- case (None, _) => None
- }))
- }
-
- def resetAnImplant(implantType: ImplantType): Unit = {
- avatar.implants.zipWithIndex.find {
- case (Some(imp), _) => imp.definition.implantType == implantType
- case (None, _) => false
- } match {
- case Some((Some(imp), index)) =>
- //deactivate
- if (imp.active) {
- deactivateImplant(implantType)
- }
- //deinitialize
- session.get.zone.AvatarEvents ! AvatarServiceMessage(
- session.get.zone.id,
- AvatarAction.SendResponse(
- Service.defaultPlayerGUID,
- AvatarImplantMessage(session.get.player.GUID, ImplantAction.Initialization, index, 0)
- )
- )
- avatarCopy(
- avatar.copy(
- implants = avatar.implants.updated(index, Some(imp.copy(initialized = false, active = false)))
- )
- )
- //restart initialization process
- implantTimers.get(index).foreach(_.cancel())
- implantTimers(index) = context.scheduleOnce(
- imp.definition.InitializationDuration.seconds,
- context.self,
- SetImplantInitialized(implantType)
- )
- session.get.zone.AvatarEvents ! AvatarServiceMessage(
- avatar.name,
- AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(index + 6, 0))
- )
- case _ => ()
- }
- }
-
- def deactivateImplant(implantType: ImplantType): Unit = {
- avatar.implants.zipWithIndex.collectFirst {
- case (Some(implant), index) if implant.definition.implantType == implantType => (implant, index)
- } match {
- case Some((implant, slot)) =>
- implantTimers.get(slot).foreach(_.cancel())
- avatarCopy(
- avatar.copy(
- implants = avatar.implants.updated(slot, Some(implant.copy(active = false)))
- )
- )
- // Deactivation sound / effect
- session.get.zone.AvatarEvents ! AvatarServiceMessage(
- session.get.zone.id,
- AvatarAction.PlanetsideAttribute(session.get.player.GUID, 28, implant.definition.implantType.value * 2)
- )
- sessionActor ! SessionActor.SendResponse(
- AvatarImplantMessage(session.get.player.GUID, ImplantAction.Activation, slot, 0)
- )
- case None => log.error(s"requested deactivation of unknown implant $implantType")
- }
- }
-
/** Send list of avatars to client (show character selection screen) */
def sendAvatars(account: Account): Unit = {
import ctx._
@@ -3115,9 +2772,7 @@ class AvatarActor(
)
.onComplete {
case Success(_) =>
- sessionActor ! SessionActor.SendResponse(
- AvatarImplantMessage(pguid, ImplantAction.Remove, index, 0)
- )
+ sendAvatarImplantMessageToSelf(pguid, ImplantAction.Remove, index, value = 0)
case Failure(exception) =>
log.error(exception)("db failure")
}
@@ -3573,16 +3228,29 @@ class AvatarActor(
AvatarActor.basicLoginCertifications.diff(certs).foreach { learnCertificationInTheFuture }
}
- def buyImplantAction(
- terminalGuid: PlanetSideGUID,
- definition: ImplantDefinition
- ): Unit = {
+ private def sendAvatarImplantMessageToSelf(
+ guid: PlanetSideGUID,
+ action: ImplantAction.Value,
+ index: Int,
+ value: Int
+ ): Unit = {
+ import akka.actor.typed.scaladsl.adapter.TypedActorRefOps
+ import net.psforever.services.avatar.{AvatarResponse => RESP}
+ sessionActor.toClassic ! AvatarServiceResponse("", guid, RESP.AvatarImplant(action, index, value))
+ }
+
+ private def buyImplantAction(
+ terminalGuid: PlanetSideGUID,
+ definition: ImplantDefinition
+ ): Unit = {
buyImplantInTheFuture(definition).onComplete {
case Success(true) =>
sessionActor ! SessionActor.SendResponse(
ItemTransactionResultMessage(terminalGuid, TransactionType.Buy, success = true)
)
- resetAnImplant(definition.implantType)
+ findImplantByType(definition.implantType).foreach {
+ case (implant, slot) => updateAvatarForImplant(implant, slot, initializeImplant(implant.definition.InitializationDuration.seconds))
+ }
sessionActor ! SessionActor.CharSaved
case _ =>
sessionActor ! SessionActor.SendResponse(
@@ -3591,7 +3259,7 @@ class AvatarActor(
}
}
- def buyImplantInTheFuture(definition: ImplantDefinition): Future[Boolean] = {
+ private def buyImplantInTheFuture(definition: ImplantDefinition): Future[Boolean] = {
val out: Promise[Boolean] = Promise()
avatar.implants.zipWithIndex.collectFirst {
case (Some(implant), _) if implant.definition.implantType == definition.implantType => None
@@ -3604,14 +3272,7 @@ class AvatarActor(
.onComplete {
case Success(_) =>
replaceAvatar(avatar.copy(implants = avatar.implants.updated(index, Some(Implant(definition)))))
- sessionActor ! SessionActor.SendResponse(
- AvatarImplantMessage(
- session.get.player.GUID,
- ImplantAction.Add,
- index,
- definition.implantType.value
- )
- )
+ sendAvatarImplantMessageToSelf(session.get.player.GUID, ImplantAction.Add, index, definition.implantType.value)
out.completeWith(Future(true))
case Failure(exception) =>
log.error(exception)("db failure")
@@ -3624,10 +3285,10 @@ class AvatarActor(
out.future
}
- def sellImplantAction(
- terminalGuid: PlanetSideGUID,
- definition: ImplantDefinition
- ): Unit = {
+ private def sellImplantAction(
+ terminalGuid: PlanetSideGUID,
+ definition: ImplantDefinition
+ ): Unit = {
sellImplantInTheFuture(definition).onComplete {
case Success(true) =>
sessionActor ! SessionActor.SendResponse(
@@ -3641,7 +3302,7 @@ class AvatarActor(
}
}
- def sellImplantInTheFuture(definition: ImplantDefinition): Future[Boolean] = {
+ private def sellImplantInTheFuture(definition: ImplantDefinition): Future[Boolean] = {
val out: Promise[Boolean] = Promise()
avatar.implants.zipWithIndex.collectFirst {
case (Some(implant), index) if implant.definition.implantType == definition.implantType => index
@@ -3657,10 +3318,8 @@ class AvatarActor(
)
.onComplete {
case Success(_) =>
- replaceAvatar(avatar.copy(implants = avatar.implants.updated(index, None)))
- sessionActor ! SessionActor.SendResponse(
- AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, index, 0)
- )
+ updateAvatarForImplant(index)
+ sendAvatarImplantMessageToSelf(session.get.player.GUID, ImplantAction.Remove, index, value = 0)
out.completeWith(Future(true))
case Failure(exception) =>
log.error(exception)("db failure")
@@ -3673,9 +3332,275 @@ class AvatarActor(
out.future
}
- def removeAllImplants(): Unit = {
- avatar.implants.collect { case Some(imp) => imp.definition }.foreach { sellImplantInTheFuture }
- context.self ! ResetImplants()
+ private def findImplantByType(implantType: ImplantType): Option[(Implant, Int)] = {
+ avatar
+ .implants
+ .zipWithIndex
+ .collectFirst {
+ case (Some(implant), index) if implant.definition.implantType == implantType => (implant, index)
+ }
+ }
+
+ private def updateAvatarForImplant(
+ implant: Implant,
+ slot: Int,
+ implantFunc: (Implant, Int) => Implant
+ ): Unit = {
+ avatarCopy(avatar.copy(implants = avatar.implants.updated(slot, Some(implantFunc(implant, slot)))))
+ }
+
+ private def updateAvatarForImplant(slot: Int): Unit = {
+ avatarCopy(avatar.copy(implants = avatar.implants.updated(slot, None)))
+ }
+
+ private def initializeImplants(): Unit = {
+ avatar.implants.zipWithIndex.foreach {
+ case (Some(implant), slot) =>
+ // TODO if this implant is Installed but does not have shortcut, add to a free slot or write over slot 61/62/63
+ // for now, just write into slots 2, 3 and 4
+ sessionActor ! SessionActor.SendResponse(
+ CreateShortcutMessage(
+ session.get.player.GUID,
+ slot + 2,
+ Some(implant.definition.implantType.shortcut)
+ )
+ )
+ initializeImplant(implant.definition.InitializationDuration.seconds)(implant, slot)
+ case (None, _) => ()
+ }
+ }
+
+ private def reinitializeImplant(implantType: ImplantType): Unit = {
+ findImplantByType(implantType).collect {
+ case (implant, slot) if implant.active =>
+ updateAvatarForImplant(deactivateImplant(implant, slot), slot, reinitializeImplant)
+ case (implant, slot) =>
+ updateAvatarForImplant(implant, slot, reinitializeImplant)
+ }
+ }
+
+ private def reinitializeImplant(implant: Implant, slot: Int): Implant = {
+ //deinitialize
+ session.get.zone.AvatarEvents ! AvatarServiceMessage(
+ session.get.zone.id,
+ AvatarAction.AvatarImplant(session.get.player.GUID, ImplantAction.Initialization, slot, 0)
+ )
+ initializeImplant(implant.definition.InitializationDuration.seconds)(implant, slot)
+ }
+
+ private def initializeImplant(delay: FiniteDuration)(implant: Implant, slot: Int): Implant = {
+ //start initialization process
+ setImplantInitializedTimer(implant, slot, delay)
+ // Start client side initialization timer, visible on the character screen
+ // Progress accumulates according to the client's knowledge of the implant initialization time
+ // What is normally a 60s timer that is set to 120s on the server will still visually update as if 60s\
+ session.get.zone.AvatarEvents ! AvatarServiceMessage(
+ avatar.name,
+ AvatarAction.SendResponse(Service.defaultPlayerGUID, ActionProgressMessage(slot + 6, 0))
+ )
+ implant.copy(initialized = false, active = false)
+ }
+
+ private def deinitializeImplants(): Unit = {
+ avatarCopy(avatar.copy(implants = avatar
+ .implants
+ .zipWithIndex
+ .collect {
+ case (Some(implant), slot) if implant.active =>
+ Some(deinitializeImplant(deactivateImplant(implant, slot), slot))
+ case (Some(implant), slot) if implant.initialized =>
+ Some(deinitializeImplant(implant, slot))
+ case (implantOpt, _) =>
+ implantOpt
+ }
+ ))
+ }
+
+ private def deinitializeImplant(implant: Implant, slot: Int): Implant = {
+ session.get.zone.AvatarEvents ! AvatarServiceMessage(
+ session.get.zone.id,
+ AvatarAction.AvatarImplant(session.get.player.GUID, ImplantAction.Initialization, slot, 0)
+ )
+ implant.copy(initialized = false, active = false)
+ }
+
+ private def setImplantInitialized(implantType: ImplantType): Unit = {
+ findImplantByType(implantType)
+ .collect {
+ case (implant, slot) =>
+ sendAvatarImplantMessageToSelf(session.get.player.GUID, ImplantAction.Initialization, slot, value = 1)
+ avatarCopy(avatar.copy(implants = avatar.implants.map {
+ case Some(implant)
+ if implant.definition.implantType == implantType && implant.definition.Passive =>
+ activateImplantPackets(implant, slot)
+ Some(implant.copy(initialized = true, active = true))
+ case Some(implant)
+ if implant.definition.implantType == implantType =>
+ Some(implant.copy(initialized = true))
+ case other => other
+ }))
+ Some(implant)
+ }
+ .orElse {
+ log.error(s"set initialized called for unknown implant $implantType")
+ None
+ }
+ }
+
+ private def setImplantInitializedTimer(implant: Implant, slot: Int, delay: FiniteDuration): Unit = {
+ implantTimers.lift(slot).foreach(_.cancel())
+ implantTimers.update(slot, context.scheduleOnce(
+ delay,
+ context.self,
+ SetImplantInitialized(implant.definition.implantType)
+ ))
+ }
+
+ private def deactivateImplant(implantType: ImplantType): Unit = {
+ avatar.implants.zipWithIndex.collectFirst {
+ case (Some(implant), index) if implant.definition.implantType == implantType => (implant, index)
+ } match {
+ case Some((implant, slot)) =>
+ updateAvatarForImplant(implant, slot, deactivateImplant)
+ case None =>
+ log.error(s"requested deactivation of unknown implant $implantType")
+ }
+ }
+
+ private def deactivateActiveImplants(): Unit = {
+ avatar
+ .implants
+ .zipWithIndex
+ .collect {
+ case (Some(implant), slot) if implant.active && !implant.definition.Passive =>
+ updateAvatarForImplant(implant, slot, deactivateImplant)
+ }
+ }
+
+ private def deactivateImplant(implant: Implant, slot: Int): Implant = {
+ implantTimers.lift(slot).foreach(_.cancel())
+ implantTimers.update(slot, Default.Cancellable)
+ // Deactivation sound / effect
+ session.get.zone.AvatarEvents ! AvatarServiceMessage(
+ session.get.zone.id,
+ AvatarAction.PlanetsideAttribute(session.get.player.GUID, 28, implant.definition.implantType.value * 2)
+ )
+ sendAvatarImplantMessageToSelf(session.get.player.GUID, ImplantAction.Activation, slot, value = 0)
+ implant.copy(active = false)
+ }
+
+ private def activateImplant(implantType: ImplantType): Unit = {
+ findImplantByType(implantType)
+ .collect { case (implant, slot) =>
+ activateImplant(implant, slot)
+ Some(true)
+ }
+ .orElse {
+ log.error(s"requested activation of unknown implant $implantType")
+ None
+ }
+ }
+
+ private def activateImplant(implant: Implant, slot: Int): Unit = {
+ if (!implant.initialized) {
+ log.warn(s"requested activation of uninitialized implant ${implant.definition.implantType}")
+ } else if (
+ !consumeThisMuchStamina(implant.definition.ActivationStaminaCost) ||
+ avatar.stamina < implant.definition.StaminaCost
+ ) {
+ // not enough stamina to activate
+ } else if (implant.definition.implantType.disabledFor.contains(session.get.player.ExoSuit)) {
+ // TODO can this really happen? can we prevent it?
+ } else {
+ avatarCopy(
+ avatar.copy(
+ implants = avatar.implants.updated(slot, Some(implant.copy(active = true)))
+ )
+ )
+ activateImplantPackets(implant, slot)
+ implantTimers.lift(slot).foreach(_.cancel())
+ val interval = implant.definition.GetCostIntervalByExoSuit(session.get.player.ExoSuit).milliseconds
+ if (interval.toMillis > 0) {
+ val stopConditionTest: (Implant, Player) => Boolean = implant.definition.implantType match {
+ case ImplantType.AdvancedRegen => staminaDrainByIntervalAdvancedRegen
+ case _ => staminaDrainByIntervalSomeImplant
+ }
+ val stopConditionFunc: () => Unit = staminaDrainByIntervalOngoing(implant, slot, session.get.player, stopConditionTest)
+ implantTimers.update(slot, context.system.scheduler.scheduleWithFixedDelay(interval, interval)(() => stopConditionFunc()))
+ }
+ }
+ }
+
+ private def staminaDrainByIntervalOngoing(
+ implant: Implant,
+ slot: Int,
+ player: Player,
+ func: (Implant, Player) => Boolean
+ )(): Unit = {
+ if (func(implant, player)) {
+ updateAvatarForImplant(implant, slot, deactivateImplant)
+ }
+ }
+
+ private def staminaDrainByIntervalSomeImplant(implant: Implant, player: Player): Boolean = {
+ !player.isAlive || !consumeThisMuchStamina(implant.definition.StaminaCost)
+ }
+
+ private def staminaDrainByIntervalAdvancedRegen(implant: Implant, player: Player): Boolean = {
+ // for every 1hp: 2sp (running), 1.5sp (standing), 1sp (crouched)
+ // to simulate '1.5sp (standing)', find if 0.0...1.0 * 100 is an even number
+ val cost = implant.definition.StaminaCost -
+ (if (player.Crouching || (!player.isMoving && (math.random() * 100) % 2 == 1)) 1 else 0)
+ val aliveAndWounded = player.isAlive && player.Health < player.MaxHealth
+ if (aliveAndWounded && consumeThisMuchStamina(cost)) {
+ //heal
+ val originalHealth = player.Health
+ val zone = player.Zone
+ val guid = player.GUID
+ val newHealth = player.Health = originalHealth + 1
+ val events = zone.AvatarEvents
+ player.LogActivity(HealFromImplant(implant.definition.implantType, 1))
+ events ! AvatarServiceMessage(
+ zone.id,
+ AvatarAction.PlanetsideAttributeToAll(guid, 0, newHealth)
+ )
+ false
+ } else {
+ !aliveAndWounded
+ }
+ }
+
+ private def activateImplantPackets(implant: Implant, slot: Int): Unit = {
+ sendAvatarImplantMessageToSelf(session.get.player.GUID, ImplantAction.Activation, slot, value = 1)
+ // Activation sound / effect
+ session.get.zone.AvatarEvents ! AvatarServiceMessage(
+ session.get.zone.id,
+ AvatarAction.PlanetsideAttribute(
+ session.get.player.GUID,
+ 28,
+ implant.definition.implantType.value * 2 + 1
+ )
+ )
+ }
+
+ private def softResetImplants() : Unit = {
+ avatarCopy(
+ avatar.copy(implants = avatar
+ .implants
+ .zipWithIndex
+ .map {
+ case (Some(implant), slot) if implant.active && !implant.definition.Passive =>
+ //deactivate active non-passive implant
+ Some(deactivateImplant(implant, slot))
+ case (Some(implant), slot) if !implant.initialized && implantTimers(slot).isCancelled =>
+ //restart stopped/unstarted initialization process
+ Some(reinitializeImplant(implant, slot))
+ case (implantOpt, _) =>
+ //fine as is
+ implantOpt
+ }
+ )
+ )
}
def resetSupportExperienceTimer(previousBep: Long, previousDelay: Long): Unit = {
diff --git a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala
index 6c4460023..bdda54dab 100644
--- a/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/normal/AvatarHandlerLogic.scala
@@ -3,6 +3,8 @@ package net.psforever.actors.session.normal
import akka.actor.{ActorContext, typed}
import net.psforever.actors.session.support.AvatarHandlerFunctions
+import net.psforever.packet.game.{AvatarImplantMessage, CreateShortcutMessage, ImplantAction}
+import net.psforever.types.ImplantType
import scala.concurrent.duration._
//
@@ -155,6 +157,37 @@ class AvatarHandlerLogic(val ops: SessionAvatarHandlers, implicit val context: A
}
}
+ case AvatarResponse.AvatarImplant(ImplantAction.Add, implant_slot, value)
+ if value == ImplantType.SecondWind.value =>
+ sendResponse(AvatarImplantMessage(resolvedPlayerGuid, ImplantAction.Add, implant_slot, 7))
+ //second wind does not normally load its icon into the shortcut hotbar
+ avatar
+ .shortcuts
+ .zipWithIndex
+ .find { case (s, _) => s.isEmpty}
+ .foreach { case (_, index) =>
+ sendResponse(CreateShortcutMessage(resolvedPlayerGuid, index + 1, Some(ImplantType.SecondWind.shortcut)))
+ }
+
+ case AvatarResponse.AvatarImplant(ImplantAction.Remove, implant_slot, value)
+ if value == ImplantType.SecondWind.value =>
+ sendResponse(AvatarImplantMessage(resolvedPlayerGuid, ImplantAction.Remove, implant_slot, value))
+ //second wind does not normally unload its icon from the shortcut hotbar
+ val shortcut = {
+ val imp = ImplantType.SecondWind.shortcut
+ net.psforever.objects.avatar.Shortcut(imp.code, imp.tile) //case class
+ }
+ avatar
+ .shortcuts
+ .zipWithIndex
+ .find { case (s, _) => s.contains(shortcut) }
+ .foreach { case (_, index) =>
+ sendResponse(CreateShortcutMessage(resolvedPlayerGuid, index + 1, None))
+ }
+
+ case AvatarResponse.AvatarImplant(action, implant_slot, value) =>
+ sendResponse(AvatarImplantMessage(resolvedPlayerGuid, action, implant_slot, value))
+
case AvatarResponse.ObjectHeld(slot, _)
if isSameTarget && player.VisibleSlots.contains(slot) =>
sendResponse(ObjectHeldMessage(guid, slot, unk1=true))
diff --git a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala
index 1d8b02624..65881caf9 100644
--- a/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/normal/GeneralLogic.scala
@@ -40,7 +40,7 @@ import net.psforever.objects.vital.interaction.DamageInteraction
import net.psforever.objects.zones.{Zone, ZoneProjectile, Zoning}
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.objectcreate.ObjectClass
-import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BindStatus, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, ItemTransactionMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
+import net.psforever.packet.game.{ActionCancelMessage, ActionResultMessage, AvatarFirstTimeEventMessage, AvatarImplantMessage, AvatarJumpMessage, BattleplanMessage, BindPlayerMessage, BindStatus, BugReportMessage, ChangeFireModeMessage, ChangeShortcutBankMessage, CharacterCreateRequestMessage, CharacterRequestAction, CharacterRequestMessage, ChatMsg, CollisionIs, ConnectToWorldRequestMessage, CreateShortcutMessage, DeadState, DeployObjectMessage, DisplayedAwardMessage, DropItemMessage, EmoteMsg, FacilityBenefitShieldChargeRequestMessage, FriendsRequest, GenericAction, GenericActionMessage, GenericCollisionMsg, GenericObjectActionAtPositionMessage, GenericObjectActionMessage, GenericObjectStateMsg, HitHint, ImplantAction, InvalidTerrainMessage, ItemTransactionMessage, LootItemMessage, MoveItemMessage, ObjectDeleteMessage, ObjectDetectedMessage, ObjectHeldMessage, PickupItemMessage, PlanetsideAttributeMessage, PlayerStateMessageUpstream, PlayerStateShiftMessage, RequestDestroyMessage, ShiftState, Shortcut, TargetInfo, TargetingImplantRequest, TargetingInfoMessage, TerrainCondition, TradeMessage, UnuseItemMessage, UseItemMessage, VoiceHostInfo, VoiceHostRequest, ZipLineMessage}
import net.psforever.services.RemoverActor
import net.psforever.services.account.{AccountPersistenceService, RetrieveAccountData}
import net.psforever.services.avatar.{AvatarAction, AvatarServiceMessage}
@@ -117,9 +117,10 @@ class GeneralLogic(val ops: GeneralOperations, implicit val context: ActorContex
}
}
ops.fallHeightTracker(pos.z)
- // if (isCrouching && !player.Crouching) {
- // //dev stuff goes here
- // }
+ if (isCrouching && !player.Crouching) {
+ //dev stuff goes here
+ sendResponse(CreateShortcutMessage(player.GUID, 2, Some(Shortcut.Implant("second_wind"))))
+ }
player.Position = pos
player.Velocity = vel
player.Orientation = Vector3(player.Orientation.x, pitch, yaw)
diff --git a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala
index 92f4eebef..211cc48ef 100644
--- a/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala
+++ b/src/main/scala/net/psforever/actors/session/normal/MountHandlerLogic.scala
@@ -450,7 +450,7 @@ class MountHandlerLogic(val ops: SessionMountHandlers, implicit val context: Act
val playerGuid: PlanetSideGUID = tplayer.GUID
val objGuid: PlanetSideGUID = obj.GUID
sessionLogic.actionsToCancel()
- avatarActor ! AvatarActor.DeactivateActiveImplants()
+ avatarActor ! AvatarActor.DeactivateActiveImplants
avatarActor ! AvatarActor.SuspendStaminaRegeneration(3.seconds)
sendResponse(ObjectAttachMessage(objGuid, playerGuid, seatNum))
continent.VehicleEvents ! VehicleServiceMessage(
diff --git a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
index 47a87c748..dc85da3d3 100644
--- a/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
+++ b/src/main/scala/net/psforever/actors/session/support/ZoningOperations.scala
@@ -17,7 +17,7 @@ import net.psforever.objects.serverobject.tube.SpawnTube
import net.psforever.objects.serverobject.turret.auto.AutomatedTurret
import net.psforever.objects.sourcing.{PlayerSource, SourceEntry, VehicleSource}
import net.psforever.objects.vital.{InGameHistory, IncarnationActivity, ReconstructionActivity, SpawningActivity}
-import net.psforever.packet.game.{CampaignStatistic, ChangeFireStateMessage_Start, MailMessage, ObjectDetectedMessage, SessionStatistic}
+import net.psforever.packet.game.{AvatarImplantMessage, CampaignStatistic, ChangeFireStateMessage_Start, ImplantAction, MailMessage, ObjectDetectedMessage, SessionStatistic}
import net.psforever.services.chat.DefaultChannel
import scala.collection.mutable
@@ -918,7 +918,7 @@ class ZoningOperations(
def beginZoningCountdown(runnable: Runnable): Unit = {
val descriptor = zoningType.toString.toLowerCase
if (zoningStatus == Zoning.Status.Request) {
- avatarActor ! AvatarActor.DeactivateActiveImplants()
+ avatarActor ! AvatarActor.DeactivateActiveImplants
zoningStatus = Zoning.Status.Countdown
val (time, origin) = ZoningStartInitialMessageAndTimer()
zoningCounter = time
@@ -2047,8 +2047,6 @@ class ZoningOperations(
sessionLogic.persist = UpdatePersistenceAndRefs
tplayer.avatar = avatar
session = session.copy(player = tplayer)
- avatarActor ! AvatarActor.CreateImplants()
- avatarActor ! AvatarActor.InitializeImplants()
//LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar
val weaponsEnabled = !(mapName.equals("map11") || mapName.equals("map12") || mapName.equals("map13"))
sendResponse(LoadMapMessage(mapName, id, 40100, 25, weaponsEnabled, map.checksum))
@@ -2264,7 +2262,7 @@ class ZoningOperations(
val armor = player.Armor
val events = continent.VehicleEvents
val zoneid = continent.id
- avatarActor ! AvatarActor.ResetImplants()
+ avatarActor ! AvatarActor.SoftResetImplants
player.Spawn()
if (health != 0) {
player.Health = health
@@ -2492,7 +2490,7 @@ class ZoningOperations(
// workaround to make sure player is spawned with full stamina
player.avatar = player.avatar.copy(stamina = avatar.maxStamina)
avatarActor ! AvatarActor.RestoreStamina(avatar.maxStamina)
- avatarActor ! AvatarActor.ResetImplants()
+ avatarActor ! AvatarActor.DeinitializeImplants
zones.exp.ToDatabase.reportRespawns(tplayer.CharId, ScoreCard.reviveCount(player.avatar.scorecard.CurrentLife))
val obj = Player.Respawn(tplayer)
DefinitionUtil.applyDefaultLoadout(obj)
@@ -2794,9 +2792,9 @@ class ZoningOperations(
// new player is spawning
val newPlayer = RespawnClone(player)
newPlayer.LogActivity(SpawningActivity(PlayerSource(newPlayer), toZoneNumber, toSpawnPoint))
- LoadZoneAsPlayUsing(newPlayer, pos, ori, toSide, zoneId)
+ LoadZoneAsPlayerUsing(newPlayer, pos, ori, toSide, zoneId)
} else {
- avatarActor ! AvatarActor.DeactivateActiveImplants()
+ avatarActor ! AvatarActor.DeactivateActiveImplants
val betterSpawnPoint = physSpawnPoint.collect { case o: PlanetSideGameObject with FactionAffinity with InGameHistory => o }
interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match {
case Some(vehicle: Vehicle) => // driver or passenger in vehicle using a warp gate, or a droppod
@@ -2813,11 +2811,11 @@ class ZoningOperations(
AvatarAction.ObjectDelete(player_guid, player_guid, 4)
)
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
- LoadZoneAsPlayUsing(player, pos, ori, toSide, zoneId)
+ LoadZoneAsPlayerUsing(player, pos, ori, toSide, zoneId)
case _ => //player is logging in
InGameHistory.SpawnReconstructionActivity(player, toZoneNumber, betterSpawnPoint)
- LoadZoneAsPlayUsing(player, pos, ori, toSide, zoneId)
+ LoadZoneAsPlayerUsing(player, pos, ori, toSide, zoneId)
}
}
}
@@ -2831,7 +2829,7 @@ class ZoningOperations(
* @param onThisSide description of the containing environment
* @param goingToZone common designation for the zone
*/
- private def LoadZoneAsPlayUsing(
+ private def LoadZoneAsPlayerUsing(
target: Player,
position: Vector3,
orientation: Vector3,
@@ -2958,9 +2956,22 @@ class ZoningOperations(
tplayer.Actor ! JammableUnit.ClearJammeredStatus()
tplayer.Actor ! JammableUnit.ClearJammeredSound()
}
+ avatarActor ! AvatarActor.SoftResetImplants
+ tavatar.implants.zipWithIndex.collect {
+ case (Some(implant), slot) if !implant.initialized =>
+ sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 0))
+ }
+// tavatar.implants.zipWithIndex.collect {
+// case (Some(implant), slot) if implant.active && implant.definition.Passive =>
+// sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1))
+// sendResponse(AvatarImplantMessage(guid, ImplantAction.Activation, slot, 1))
+// case (Some(implant), slot) if implant.initialized =>
+// sendResponse(AvatarImplantMessage(guid, ImplantAction.Initialization, slot, 1))
+// case (Some(implant), _) =>
+// () //avatarActor ! AvatarActor.ResetImplant(implant.definition.implantType)
+// }
val originalDeadState = deadState
deadState = DeadState.Alive
- avatarActor ! AvatarActor.ResetImplants()
sendResponse(PlanetsideAttributeMessage(PlanetSideGUID(0), 82, 0))
initializeShortcutsAndBank(guid, tavatar.shortcuts)
//Favorites lists
@@ -3588,10 +3599,7 @@ class ZoningOperations(
def startDeconstructing(obj: SpawnTube): Unit = {
log.info(s"${player.Name} is deconstructing at the ${obj.Owner.Definition.Name}'s spawns")
- avatar.implants.collect {
- case Some(implant) if implant.active && !implant.definition.Passive =>
- avatarActor ! AvatarActor.DeactivateImplant(implant.definition.implantType)
- }
+ avatarActor ! AvatarActor.DeactivateActiveImplants
if (player.ExoSuit != ExoSuitType.MAX) {
player.Actor ! PlayerControl.ObjectHeld(Player.HandsDownSlot, updateMyHolsterArm = true)
}
diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
index c2ee863c9..4245c7688 100644
--- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
+++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala
@@ -476,7 +476,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
Deployables.initializeConstructionItem(player.avatar.certifications, citem)
}
//deactivate non-passive implants
- avatarActor ! AvatarActor.DeactivateActiveImplants()
+ avatarActor ! AvatarActor.DeactivateActiveImplants
val zone = player.Zone
zone.AvatarEvents ! AvatarServiceMessage(
zone.id,
@@ -659,7 +659,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
afterHolsters.foreach(elem => player.Slot(elem.start).Equipment = elem.obj)
afterInventory.foreach(elem => player.Inventory.InsertQuickly(elem.start, elem.obj))
//deactivate non-passive implants
- avatarActor ! AvatarActor.DeactivateActiveImplants()
+ avatarActor ! AvatarActor.DeactivateActiveImplants
player.Zone.AvatarEvents ! AvatarServiceMessage(
player.Zone.id,
AvatarAction.ChangeExosuit(
@@ -944,7 +944,7 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
CancelJammeredSound(target)
super.CancelJammeredStatus(target)
//uninitialize implants
- avatarActor ! AvatarActor.DeinitializeImplants()
+ avatarActor ! AvatarActor.DeinitializeImplants
//log historical event
target.LogActivity(cause)
@@ -1073,13 +1073,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm
* @param dur the duration of the timer, in milliseconds
*/
override def StartJammeredStatus(target: Any, dur: Int): Unit = {
- avatarActor ! AvatarActor.DeinitializeImplants()
+ avatarActor ! AvatarActor.DeinitializeImplants
avatarActor ! AvatarActor.SuspendStaminaRegeneration(5 seconds)
super.StartJammeredStatus(target, dur)
}
override def CancelJammeredStatus(target: Any): Unit = {
- avatarActor ! AvatarActor.InitializeImplants()
+ avatarActor ! AvatarActor.SoftResetImplants
super.CancelJammeredStatus(target)
}
diff --git a/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala b/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala
index d1e6b7247..bb770afc8 100644
--- a/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala
+++ b/src/main/scala/net/psforever/objects/definition/ImplantDefinition.scala
@@ -83,5 +83,6 @@ class ImplantDefinition(val implantType: ImplantType) extends BasicDefinition {
def GetCostIntervalByExoSuit(exosuit: ExoSuitType.Value): Int =
costIntervalByExoSuit.getOrElse(exosuit, CostIntervalDefault)
+
def CostIntervalByExoSuitHashMap: mutable.Map[ExoSuitType.Value, Int] = costIntervalByExoSuit
}
diff --git a/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala b/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala
index d194bc880..2a99f4bd3 100644
--- a/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala
+++ b/src/main/scala/net/psforever/packet/game/CreateShortcutMessage.scala
@@ -13,25 +13,14 @@ import shapeless.{::, HNil}
* The parameters `purpose` and `tile` are closely related.
* These two fields are consistent for all shortcuts of the same type.
* `purpose` indicates the purpose of the shortcut.
+ * The medkit icon is 0, chat shortcuts are 1, and implants are 2.
* `tile` is related to what kind of graphic is displayed in this shortcut's slot on the hotbar based on its purpose.
- * The parameters `effect1` and `effect2` are exclusive to text macro shortcuts and are defaulted to empty `String`s.
+ * The medkit tile use "medkit", chat shortcuts use "shortcut_macro", and implants are the internal name of the implant.
*
+ * The parameters `effect1` and `effect2` are exclusive to text macro shortcuts and are defaulted to empty `String`s.
* The `shortcut_macro` setting displays a word bubble superimposed by the (first three letters of) `effect1` text.
* Implants and the medkit should have self-explanatory graphics.
- *
- * Tile - Code
- * `advanced_regen` (regeneration) - 2
- * `audio_amplifier` - 2
- * `darklight_vision` - 2
- * `medkit` - 0
- * `melee_booster` - 2
- * `personal_shield` - 2
- * `range_magnifier` - 2
- * `second_wind` - 2
- * `shortcut_macro` - 1
- * `silent_run` (sensor shield) - 2
- * `surge` - 2
- * `targeting` (enhanced targeting) - 2
+ * The implant second wind does not have a graphic shortcut icon.
* @param code the primary use of this shortcut
*/
abstract class Shortcut(val code: Int) {
diff --git a/src/main/scala/net/psforever/services/avatar/AvatarService.scala b/src/main/scala/net/psforever/services/avatar/AvatarService.scala
index 8ee7edb52..a927da4a0 100644
--- a/src/main/scala/net/psforever/services/avatar/AvatarService.scala
+++ b/src/main/scala/net/psforever/services/avatar/AvatarService.scala
@@ -39,6 +39,10 @@ class AvatarService(zone: Zone) extends Actor {
AvatarEvents.publish(
AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.ArmorChanged(suit, subtype))
)
+ case AvatarAction.AvatarImplant(player_guid, action, implantSlot, status) =>
+ AvatarEvents.publish(
+ AvatarServiceResponse(s"/$forChannel/Avatar", player_guid, AvatarResponse.AvatarImplant(action, implantSlot, status))
+ )
case AvatarAction.ChangeAmmo(
player_guid,
weapon_guid,
diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala
index 68333969f..7f4134a82 100644
--- a/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala
+++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceMessage.scala
@@ -11,6 +11,7 @@ import net.psforever.objects.serverobject.environment.interaction.common.Watery.
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.objects.zones.Zone
import net.psforever.packet.PlanetSideGamePacket
+import net.psforever.packet.game.ImplantAction
import net.psforever.packet.game.objectcreate.{ConstructorData, ObjectCreateMessageParent}
import net.psforever.types.{ExoSuitType, ExperienceType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
@@ -27,6 +28,7 @@ object AvatarAction {
sealed trait Action
final case class ArmorChanged(player_guid: PlanetSideGUID, suit: ExoSuitType.Value, subtype: Int) extends Action
+ final case class AvatarImplant(player_guid: PlanetSideGUID, action: ImplantAction.Value, implantSlot: Int, status: Int) extends Action
final case class ChangeAmmo(
player_guid: PlanetSideGUID,
weapon_guid: PlanetSideGUID,
diff --git a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala
index 3e0c020cf..c90505842 100644
--- a/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala
+++ b/src/main/scala/net/psforever/services/avatar/AvatarServiceResponse.scala
@@ -10,7 +10,7 @@ import net.psforever.objects.serverobject.environment.interaction.common.Watery.
import net.psforever.objects.sourcing.SourceEntry
import net.psforever.packet.PlanetSideGamePacket
import net.psforever.packet.game.objectcreate.ConstructorData
-import net.psforever.packet.game.ObjectCreateMessage
+import net.psforever.packet.game.{ImplantAction, ObjectCreateMessage}
import net.psforever.types.{ExoSuitType, ExperienceType, PlanetSideEmpire, PlanetSideGUID, TransactionType, Vector3}
import net.psforever.services.GenericEventBusMsg
@@ -24,6 +24,7 @@ object AvatarResponse {
sealed trait Response
final case class ArmorChanged(suit: ExoSuitType.Value, subtype: Int) extends Response
+ final case class AvatarImplant(action: ImplantAction.Value, implantSlot: Int, status: Int) extends Response
final case class ChangeAmmo(
weapon_guid: PlanetSideGUID,
weapon_slot: Int,
diff --git a/src/test/scala/objects/PlayerControlTest.scala b/src/test/scala/objects/PlayerControlTest.scala
index 7add65cc4..d10e7a85b 100644
--- a/src/test/scala/objects/PlayerControlTest.scala
+++ b/src/test/scala/objects/PlayerControlTest.scala
@@ -544,8 +544,8 @@ class PlayerControlDeathStandingTest extends ActorTest {
)
assert(
msg_stamina match {
- case AvatarActor.DeinitializeImplants() => true
- case _ => false
+ case AvatarActor.DeinitializeImplants => true
+ case _ => false
}
)
assert(
@@ -685,8 +685,8 @@ class PlayerControlDeathStandingTest extends ActorTest {
// activityProbe.expectNoMessage(200 milliseconds)
// assert(
// msg_stamina match {
-// case AvatarActor.DeinitializeImplants() => true
-// case _ => false
+// case AvatarActor.DeinitializeImplants => true
+// case _ => false
// }
// )
// assert(