diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf index 0bc62ab18..8504f693a 100644 --- a/src/main/resources/application.conf +++ b/src/main/resources/application.conf @@ -82,6 +82,9 @@ game { # Modify the amount of NTU drain per autorepair tick for facility amenities amenity-autorepair-drain-rate = 0.5 + # Purchases timers for the mechanized assault exo-suits all update at the same time when any of them would update + shared-max-cooldown = no + # HART system, shuttles and facilities hart { # How long the shuttle is not boarding passengers (going through the motions) diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 2981b5c4e..e5435fd80 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -5,57 +5,16 @@ import java.util.concurrent.atomic.AtomicInteger import akka.actor.Cancellable import akka.actor.typed.scaladsl.{ActorContext, Behaviors, StashBuffer} import akka.actor.typed.{ActorRef, Behavior, PostStop, SupervisorStrategy} -import net.psforever.objects.avatar.{Avatar, BattleRank, Certification, Cosmetic, Implant} +import net.psforever.objects.avatar._ import net.psforever.objects.definition.converter.CharacterSelectConverter -import net.psforever.objects.definition.{ - AmmoBoxDefinition, - BasicDefinition, - ConstructionItemDefinition, - ImplantDefinition, - KitDefinition, - SimpleItemDefinition, - ToolDefinition -} +import net.psforever.objects.definition._ import net.psforever.objects.equipment.Equipment import net.psforever.objects.inventory.InventoryItem import net.psforever.objects.loadouts.{InfantryLoadout, Loadout} -import net.psforever.objects.{ - Account, - AmmoBox, - ConstructionItem, - GlobalDefinitions, - Kit, - Player, - Session, - SimpleItem, - Tool -} +import net.psforever.objects._ import net.psforever.packet.game.objectcreate.ObjectClass -import net.psforever.packet.game.{ - ActionProgressMessage, - ActionResultMessage, - AvatarImplantMessage, - AvatarVehicleTimerMessage, - BattleExperienceMessage, - CharacterInfoMessage, - CreateShortcutMessage, - FavoritesMessage, - ImplantAction, - ItemTransactionResultMessage, - ObjectCreateDetailedMessage, - PlanetSideZoneID, - PlanetsideAttributeMessage -} -import net.psforever.types.{ - CharacterSex, - CharacterVoice, - ExoSuitType, - ImplantType, - LoadoutType, - PlanetSideEmpire, - PlanetSideGUID, - TransactionType -} +import net.psforever.packet.game._ +import net.psforever.types._ import net.psforever.util.Database._ import net.psforever.persistence import net.psforever.util.{Config, DefinitionUtil} @@ -269,8 +228,8 @@ class AvatarActor( def postStartBehaviour(): Behavior[Command] = { account match { - case Some(account) => - buffer.unstashAll(active(account)) + case Some(_account) => + buffer.unstashAll(active(_account)) case _ => Behaviors.same } @@ -328,7 +287,7 @@ class AvatarActor( result.onComplete { case Success(_) => - log.debug(s"AvatarActor: created character ${name} for account ${account.name}") + log.debug(s"AvatarActor: created character $name for account ${account.name}") sessionActor ! SessionActor.SendResponse(ActionResultMessage.Pass) sendAvatars(account) case Failure(e) => log.error(e)("db failure") @@ -445,8 +404,8 @@ class AvatarActor( sessionActor ! SessionActor.SendResponse( ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = false) ) - case Success(replace) => - replace.foreach { cert => + case Success(_replace) => + _replace.foreach { cert => sessionActor ! SessionActor.SendResponse( PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value) ) @@ -519,15 +478,31 @@ class AvatarActor( ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = false) ) case Success(certs) => + val player = session.get.player context.self ! ReplaceAvatar(avatar.copy(certifications = avatar.certifications.diff(certs))) certs.foreach { cert => sessionActor ! SessionActor.SendResponse( - PlanetsideAttributeMessage(session.get.player.GUID, 25, cert.value) + PlanetsideAttributeMessage(player.GUID, 25, cert.value) ) } sessionActor ! SessionActor.SendResponse( ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true) ) + //wearing invalid armor? + if ( + if (certification == Certification.ReinforcedExoSuit) player.ExoSuit == ExoSuitType.Reinforced + else if (certification == Certification.InfiltrationSuit) player.ExoSuit == ExoSuitType.Infiltration + else if (player.ExoSuit == ExoSuitType.MAX) { + lazy val subtype = InfantryLoadout.DetermineSubtypeA(ExoSuitType.MAX, player.Slot(slot = 0).Equipment) + if (certification == Certification.UniMAX) true + else if (certification == Certification.AAMAX) subtype == 1 + else if (certification == Certification.AIMAX) subtype == 2 + else if (certification == Certification.AVMAX) subtype == 3 + else false + } else false + ) { + player.Actor ! PlayerControl.SetExoSuit(ExoSuitType.Standard, 0) + } } } Behaviors.same @@ -591,24 +566,24 @@ class AvatarActor( case LearnImplant(terminalGuid, definition) => // TODO there used to be a terminal check here, do we really need it? val index = avatar.implants.zipWithIndex.collectFirst { - case (Some(implant), index) if implant.definition.implantType == definition.implantType => index - case (None, index) if index < avatar.br.implantSlots => index + case (Some(implant), _index) if implant.definition.implantType == definition.implantType => _index + case (None, _index) if _index < avatar.br.implantSlots => _index } index match { - case Some(index) => + case Some(_index) => import ctx._ ctx .run(query[persistence.Implant].insert(_.name -> lift(definition.Name), _.avatarId -> lift(avatar.id))) .onComplete { case Success(_) => context.self ! ReplaceAvatar( - avatar.copy(implants = avatar.implants.updated(index, Some(Implant(definition)))) + avatar.copy(implants = avatar.implants.updated(_index, Some(Implant(definition)))) ) sessionActor ! SessionActor.SendResponse( AvatarImplantMessage( session.get.player.GUID, ImplantAction.Add, - index, + _index, definition.implantType.value ) ) @@ -630,10 +605,10 @@ class AvatarActor( case SellImplant(terminalGuid, definition) => // TODO there used to be a terminal check here, do we really need it? val index = avatar.implants.zipWithIndex.collectFirst { - case (Some(implant), index) if implant.definition.implantType == definition.implantType => index + case (Some(implant), _index) if implant.definition.implantType == definition.implantType => _index } index match { - case Some(index) => + case Some(_index) => import ctx._ ctx .run( @@ -644,9 +619,9 @@ class AvatarActor( ) .onComplete { case Success(_) => - context.self ! ReplaceAvatar(avatar.copy(implants = avatar.implants.updated(index, None))) + context.self ! ReplaceAvatar(avatar.copy(implants = avatar.implants.updated(_index, None))) sessionActor ! SessionActor.SendResponse( - AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, index, 0) + AvatarImplantMessage(session.get.player.GUID, ImplantAction.Remove, _index, 0) ) sessionActor ! SessionActor.SendResponse( ItemTransactionResultMessage(terminalGuid, TransactionType.Sell, success = true) @@ -721,15 +696,19 @@ class AvatarActor( Behaviors.same case UpdatePurchaseTime(definition, time) => - //only send for items with cooldowns - Avatar.purchaseCooldowns.get(definition) match { - case Some(cooldown) => - // TODO save to db - avatar = avatar.copy(purchaseTimes = avatar.purchaseTimes.updated(definition.Name, time)) - updatePurchaseTimer(definition.Name, cooldown.toSeconds, unk1 = true) - case None => ; - //log.warn(s"UpdatePurchaseTime message for item '${definition.Name}' without cooldown") + // TODO save to db + var newTimes = avatar.purchaseTimes + resolveSharedPurchaseTimeNames(resolvePurchaseTimeName(avatar.faction, definition)).foreach { + case (item, name) => + Avatar.purchaseCooldowns.get(item) match { + case Some(cooldown) => + //only send for items with cooldowns + newTimes = newTimes.updated(item.Name, time) + updatePurchaseTimer(name, cooldown.toSeconds, unk1 = true) + case _ => ; + } } + avatar = avatar.copy(purchaseTimes = newTimes) Behaviors.same case UpdateUseTime(definition, time) => @@ -858,7 +837,7 @@ class AvatarActor( Behaviors.same case ConsumeStamina(stamina) => - assert(stamina > 0, s"consumed stamina must be larger than 0, but is: ${stamina}") + assert(stamina > 0, s"consumed stamina must be larger than 0, but is: $stamina") consumeStamina(stamina) Behaviors.same @@ -1260,7 +1239,7 @@ class AvatarActor( doll.ExoSuit = ExoSuitType(loadout.exosuitId) loadout.items.split("/").foreach { - case value => + value => val (objectType, objectIndex, objectId, toolAmmo) = value.split(",") match { case Array(a, b: String, c: String) => (a, b.toInt, c.toInt, None) case Array(a, b: String, c: String, d) => (a, b.toInt, c.toInt, Some(d)) @@ -1318,9 +1297,9 @@ class AvatarActor( def defaultStaminaRegen(): Cancellable = { context.system.scheduler.scheduleWithFixedDelay(0.5 seconds, 0.5 seconds)(() => { (session, _avatar) match { - case (Some(session), Some(_)) => + case (Some(_session), Some(_)) => if ( - !avatar.staminaFull && (session.player.VehicleSeated.nonEmpty || !session.player.isMoving && !session.player.Jumping) + !avatar.staminaFull && (_session.player.VehicleSeated.nonEmpty || !_session.player.isMoving && !_session.player.Jumping) ) { context.self ! RestoreStamina(1) } @@ -1340,6 +1319,47 @@ class AvatarActor( }) } + def resolvePurchaseTimeName(faction: PlanetSideEmpire.Value, item: BasicDefinition): (BasicDefinition, String) = { + val factionName : String = faction.toString.toLowerCase + val name = item match { + case GlobalDefinitions.trhev_dualcycler | + GlobalDefinitions.nchev_scattercannon | + GlobalDefinitions.vshev_quasar => + s"${factionName}hev_antipersonnel" + case GlobalDefinitions.trhev_pounder | + GlobalDefinitions.nchev_falcon | + GlobalDefinitions.vshev_comet => + s"${factionName}hev_antivehicular" + case GlobalDefinitions.trhev_burster | + GlobalDefinitions.nchev_sparrow | + GlobalDefinitions.vshev_starfire => + s"${factionName}hev_antiaircraft" + case _ => + item.Name + } + (item, name) + } + + def resolveSharedPurchaseTimeNames(pair: (BasicDefinition, String)): Seq[(BasicDefinition, String)] = { + val (_, name) = pair + if (name.matches("(tr|nc|vs)hev_.+") && Config.app.game.sharedMaxCooldown) { + val faction = name.take(2) + (if (faction.equals("nc")) { + Seq(GlobalDefinitions.nchev_scattercannon, GlobalDefinitions.nchev_falcon, GlobalDefinitions.nchev_sparrow) + } + else if (faction.equals("vs")) { + Seq(GlobalDefinitions.vshev_quasar, GlobalDefinitions.vshev_comet, GlobalDefinitions.vshev_starfire) + } + else { + Seq(GlobalDefinitions.trhev_dualcycler, GlobalDefinitions.trhev_pounder, GlobalDefinitions.trhev_burster) + }).zip( + Seq(s"${faction}hev_antipersonnel", s"${faction}hev_antivehicular", s"${faction}hev_antiaircraft") + ) + } else { + Seq(pair) + } + } + def refreshPurchaseTimes(keys: Set[String]): Unit = { var keysToDrop: Seq[String] = Nil keys.foreach { key => @@ -1348,23 +1368,7 @@ class AvatarActor( val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()).getSeconds Avatar.purchaseCooldowns.find(_._1.Name == name) match { case Some((obj, cooldown)) if cooldown.toSeconds - secondsSincePurchase > 0 => - val faction : String = avatar.faction.toString.toLowerCase - val name = obj match { - case GlobalDefinitions.trhev_dualcycler | - GlobalDefinitions.nchev_scattercannon | - GlobalDefinitions.vshev_quasar => - s"${faction}hev_antipersonnel" - case GlobalDefinitions.trhev_pounder | - GlobalDefinitions.nchev_falcon | - GlobalDefinitions.vshev_comet => - s"${faction}hev_antivehicular" - case GlobalDefinitions.trhev_burster | - GlobalDefinitions.nchev_sparrow | - GlobalDefinitions.vshev_starfire => - s"${faction}hev_antiaircraft" - case _ => - obj.Name - } + val (_, name) = resolvePurchaseTimeName(avatar.faction, obj) updatePurchaseTimer(name, cooldown.toSeconds - secondsSincePurchase, unk1 = true) case _ => diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 2b6a0e4a6..cde96181f 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -221,107 +221,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm } } + case PlayerControl.SetExoSuit(exosuit: ExoSuitType.Value, subtype: Int) => + setExoSuit(exosuit, subtype) + case Terminal.TerminalMessage(_, msg, order) => order match { case Terminal.BuyExosuit(exosuit, subtype) => - var toDelete: List[InventoryItem] = Nil - val originalSuit = player.ExoSuit - val originalSubtype = Loadout.DetermineSubtype(player) - val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype - val allowedToChangeArmor = Players.CertificationToUseExoSuit(player, exosuit, subtype) && - (if (exosuit == ExoSuitType.MAX) { - val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction) - player.avatar.purchaseCooldown(weapon) match { - case Some(_) => - false - case None => - avatarActor ! AvatarActor.UpdatePurchaseTime(weapon) - true - } - } else { - true - }) - val result = if (requestToChangeArmor && allowedToChangeArmor) { - log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit") - val beforeHolsters = Players.clearHolsters(player.Holsters().iterator) - val beforeInventory = player.Inventory.Clear() - //change suit - val originalArmor = player.Armor - player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit - val toMaxArmor = player.MaxArmor - val toArmor = if (originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) { - player.History(HealFromExoSuitChange(PlayerSource(player), exosuit)) - player.Armor = toMaxArmor - } else { - player.Armor = originalArmor - } - //ensure arm is down, even if it needs to go back up - if (player.DrawnSlot != Player.HandsDownSlot) { - player.DrawnSlot = Player.HandsDownSlot - } - val normalHolsters = if (originalSuit == ExoSuitType.MAX) { - val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max) - toDelete ++= maxWeapons - normalWeapons - } else { - beforeHolsters - } - //populate holsters - val (afterHolsters, finalInventory) = if (exosuit == ExoSuitType.MAX) { - ( - normalHolsters, - Players.fillEmptyHolsters(List(player.Slot(4)).iterator, normalHolsters) ++ beforeInventory - ) - } else if (originalSuit == exosuit) { //note - this will rarely be the situation - (normalHolsters, Players.fillEmptyHolsters(player.Holsters().iterator, normalHolsters)) - } else { - val (afterHolsters, toInventory) = - normalHolsters.partition(elem => elem.obj.Size == player.Slot(elem.start).Size) - afterHolsters.foreach({ elem => player.Slot(elem.start).Equipment = elem.obj }) - val remainder = Players.fillEmptyHolsters(player.Holsters().iterator, toInventory ++ beforeInventory) - ( - player.HolsterItems(), - remainder - ) - } - //put items back into inventory - val (stow, drop) = if (originalSuit == exosuit) { - (finalInventory, Nil) - } else { - val (a, b) = GridInventory.recoverInventory(finalInventory, player.Inventory) - ( - a, - b.map { - InventoryItem(_, -1) - } - ) - } - stow.foreach { elem => - player.Inventory.InsertQuickly(elem.start, elem.obj) - } - //deactivate non-passive implants - avatarActor ! AvatarActor.DeactivateActiveImplants() - player.Zone.AvatarEvents ! AvatarServiceMessage( - player.Zone.id, - AvatarAction.ChangeExosuit( - player.GUID, - toArmor, - exosuit, - subtype, - player.LastDrawnSlot, - exosuit == ExoSuitType.MAX && requestToChangeArmor, - beforeHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, - afterHolsters, - beforeInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, - stow, - drop, - toDelete.map { case InventoryItem(obj, _) => (obj, obj.GUID) } - ) - ) - true - } else { - false - } + val result = setExoSuit(exosuit, subtype) player.Zone.AvatarEvents ! AvatarServiceMessage( player.Name, AvatarAction.TerminalOrderResult(msg.terminal_guid, msg.transaction_type, result) @@ -511,6 +417,107 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm case _ => ; } + def setExoSuit(exosuit: ExoSuitType.Value, subtype: Int): Boolean = { + var toDelete: List[InventoryItem] = Nil + val originalSuit = player.ExoSuit + val originalSubtype = Loadout.DetermineSubtype(player) + val requestToChangeArmor = originalSuit != exosuit || originalSubtype != subtype + val allowedToChangeArmor = Players.CertificationToUseExoSuit(player, exosuit, subtype) && + (if (exosuit == ExoSuitType.MAX) { + val weapon = GlobalDefinitions.MAXArms(subtype, player.Faction) + player.avatar.purchaseCooldown(weapon) match { + case Some(_) => + false + case None => + avatarActor ! AvatarActor.UpdatePurchaseTime(weapon) + true + } + } else { + true + }) + if (requestToChangeArmor && allowedToChangeArmor) { + log.info(s"${player.Name} wants to change to a different exo-suit - $exosuit") + val beforeHolsters = Players.clearHolsters(player.Holsters().iterator) + val beforeInventory = player.Inventory.Clear() + //change suit + val originalArmor = player.Armor + player.ExoSuit = exosuit //changes the value of MaxArmor to reflect the new exo-suit + val toMaxArmor = player.MaxArmor + val toArmor = if (originalSuit != exosuit || originalSubtype != subtype || originalArmor > toMaxArmor) { + player.History(HealFromExoSuitChange(PlayerSource(player), exosuit)) + player.Armor = toMaxArmor + } else { + player.Armor = originalArmor + } + //ensure arm is down, even if it needs to go back up + if (player.DrawnSlot != Player.HandsDownSlot) { + player.DrawnSlot = Player.HandsDownSlot + } + val normalHolsters = if (originalSuit == ExoSuitType.MAX) { + val (maxWeapons, normalWeapons) = beforeHolsters.partition(elem => elem.obj.Size == EquipmentSize.Max) + toDelete ++= maxWeapons + normalWeapons + } else { + beforeHolsters + } + //populate holsters + val (afterHolsters, finalInventory) = if (exosuit == ExoSuitType.MAX) { + ( + normalHolsters, + Players.fillEmptyHolsters(List(player.Slot(4)).iterator, normalHolsters) ++ beforeInventory + ) + } else if (originalSuit == exosuit) { //note - this will rarely be the situation + (normalHolsters, Players.fillEmptyHolsters(player.Holsters().iterator, normalHolsters)) + } else { + val (afterHolsters, toInventory) = + normalHolsters.partition(elem => elem.obj.Size == player.Slot(elem.start).Size) + afterHolsters.foreach({ elem => player.Slot(elem.start).Equipment = elem.obj }) + val remainder = Players.fillEmptyHolsters(player.Holsters().iterator, toInventory ++ beforeInventory) + ( + player.HolsterItems(), + remainder + ) + } + //put items back into inventory + val (stow, drop) = if (originalSuit == exosuit) { + (finalInventory, Nil) + } else { + val (a, b) = GridInventory.recoverInventory(finalInventory, player.Inventory) + ( + a, + b.map { + InventoryItem(_, -1) + } + ) + } + stow.foreach { elem => + player.Inventory.InsertQuickly(elem.start, elem.obj) + } + //deactivate non-passive implants + avatarActor ! AvatarActor.DeactivateActiveImplants() + player.Zone.AvatarEvents ! AvatarServiceMessage( + player.Zone.id, + AvatarAction.ChangeExosuit( + player.GUID, + toArmor, + exosuit, + subtype, + player.LastDrawnSlot, + exosuit == ExoSuitType.MAX && requestToChangeArmor, + beforeHolsters.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, + afterHolsters, + beforeInventory.map { case InventoryItem(obj, _) => (obj, obj.GUID) }, + stow, + drop, + toDelete.map { case InventoryItem(obj, _) => (obj, obj.GUID) } + ) + ) + true + } else { + false + } + } + override protected def PerformDamage( target: Target, applyDamageTo: Output @@ -1139,6 +1146,9 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm } object PlayerControl { + /** na */ + final case class SetExoSuit(exosuit: ExoSuitType.Value, subtype: Int) + /** * Transform an applicable Aura effect into its `PlanetsideAttributeMessage` value. * @see `Aura` diff --git a/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala b/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala index 97498cc72..1f307e084 100644 --- a/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala +++ b/src/main/scala/net/psforever/objects/loadouts/InfantryLoadout.scala @@ -88,8 +88,7 @@ object InfantryLoadout { /** * The sub-type of the player's uniform, as used in `FavoritesMessage`.
*
- * The values for `Standard`, `Infiltration`, and the generic `MAX` are not perfectly known. - * The latter-most exo-suit option is presumed. + * The values for a specific `MAX` type is only known by knowing the subtype. * @param suit the player's uniform * @param subtype the mechanized assault exo-suit subtype as determined by their arm weapons * @return the numeric subtype diff --git a/src/main/scala/net/psforever/util/Config.scala b/src/main/scala/net/psforever/util/Config.scala index e80a5bbf0..d3057fcb7 100644 --- a/src/main/scala/net/psforever/util/Config.scala +++ b/src/main/scala/net/psforever/util/Config.scala @@ -137,7 +137,8 @@ case class GameConfig( bepRate: Double, cepRate: Double, newAvatar: NewAvatar, - hart: HartConfig + hart: HartConfig, + sharedMaxCooldown: Boolean ) case class NewAvatar(