From 40c93d81050d153a1dcce7b15263947f8e42cedd Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 10 Jun 2024 15:13:32 -0400 Subject: [PATCH] redid (cleaned-up) implant logic --- .../actors/session/AvatarActor.scala | 755 ++++++++---------- .../session/normal/AvatarHandlerLogic.scala | 33 + .../actors/session/normal/GeneralLogic.scala | 9 +- .../session/normal/MountHandlerLogic.scala | 2 +- .../session/support/ZoningOperations.scala | 40 +- .../objects/avatar/PlayerControl.scala | 10 +- .../definition/ImplantDefinition.scala | 1 + .../packet/game/CreateShortcutMessage.scala | 19 +- .../services/avatar/AvatarService.scala | 4 + .../avatar/AvatarServiceMessage.scala | 2 + .../avatar/AvatarServiceResponse.scala | 3 +- .../scala/objects/PlayerControlTest.scala | 8 +- 12 files changed, 425 insertions(+), 461 deletions(-) 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(