diff --git a/README.md b/README.md index 8e45ec90..20786359 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -Welcome to the recreated login and world servers for PlanetSide 1. We are a awesome community of players and developers who took +Welcome to the recreated login and world servers for PlanetSide 1. We are a community of players and developers who took it upon ourselves to preserve PlanetSide 1's unique gameplay and history _forever_. The login and world servers (this repo runs both by default) are built to work with PlanetSide version 3.15.84.0. diff --git a/src/main/scala/net/psforever/actors/session/AvatarActor.scala b/src/main/scala/net/psforever/actors/session/AvatarActor.scala index 17b02612..20a7b46b 100644 --- a/src/main/scala/net/psforever/actors/session/AvatarActor.scala +++ b/src/main/scala/net/psforever/actors/session/AvatarActor.scala @@ -663,7 +663,13 @@ class AvatarActor( case LoadoutType.Infantry => storeLoadout(player, name, number).onComplete { case Success(_) => - context.self ! RefreshLoadouts() + loadLoadouts().onComplete { + case Success(loadouts) => + avatar = avatar.copy(loadouts = loadouts) + context.self ! RefreshLoadouts() + case Failure(exception) => log.error(exception)("db failure") + } + case Failure(exception) => log.error(exception)("db failure") } @@ -693,23 +699,18 @@ class AvatarActor( Behaviors.same case RefreshLoadouts() => - loadLoadouts().onComplete { - case Success(loadouts) => - avatar = avatar.copy(loadouts = loadouts) - loadouts.zipWithIndex.foreach { - case (Some(loadout: InfantryLoadout), index) => - sessionActor ! SessionActor.SendResponse( - FavoritesMessage( - LoadoutType.Infantry, - session.get.player.GUID, - index, - loadout.label, - InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype) - ) - ) - case _ => ; - } - case Failure(exception) => log.error(exception)("db failure") + avatar.loadouts.zipWithIndex.foreach { + case (Some(loadout: InfantryLoadout), index) => + sessionActor ! SessionActor.SendResponse( + FavoritesMessage( + LoadoutType.Infantry, + session.get.player.GUID, + index, + loadout.label, + InfantryLoadout.DetermineSubtypeB(loadout.exosuit, loadout.subtype) + ) + ) + case _ => ; } Behaviors.same @@ -772,7 +773,6 @@ class AvatarActor( Behaviors.same case ActivateImplant(implantType) => - log.info(s"ActivateImplant ${implantType}") val res = avatar.implants.zipWithIndex.collectFirst { case (Some(implant), index) if implant.definition.implantType == implantType => (implant, index) } @@ -780,14 +780,14 @@ class AvatarActor( case Some((implant, slot)) => if (!implant.initialized) { log.error(s"requested activation of uninitialized implant $implant") - } else if (!consumeStamina(implant.definition.ActivationStaminaCost)) { - sessionActor ! SessionActor.SendResponse( - AvatarImplantMessage(session.get.player.GUID, ImplantAction.OutOfStamina, slot, 1) - ) + } else if ( + !consumeStamina(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 { - avatar = avatar.copy( implants = avatar.implants.updated(slot, Some(implant.copy(active = true))) ) @@ -828,39 +828,15 @@ class AvatarActor( Behaviors.same case DeactivateImplant(implantType) => - val res = avatar.implants.zipWithIndex.collectFirst { - case (Some(implant), index) if implant.definition.implantType == implantType => (implant, index) - } - res match { - case Some((implant, slot)) => - implantTimers(slot).cancel() - avatar = 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") - } + 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) - context.self ! DeactivateImplant(implant.definition.implantType) + if (implant.active && implant.definition.GetCostIntervalByExoSuit(session.get.player.ExoSuit) > 0) { + deactivateImplant(implant.definition.implantType) + } } } Behaviors.same @@ -870,13 +846,18 @@ class AvatarActor( if (session.get.player.HasGUID) { val totalStamina = math.min(avatar.maxStamina, avatar.stamina + stamina) val fatigued = if (avatar.fatigued && totalStamina >= 20) { - context.self ! InitializeImplants(instant = true) + avatar.implants.zipWithIndex.foreach { + case (Some(implant), slot) => + sessionActor ! SessionActor.SendResponse( + AvatarImplantMessage(session.get.player.GUID, ImplantAction.OutOfStamina, slot, 0) + ) + case _ => () + } false } else { avatar.fatigued } avatar = avatar.copy(stamina = totalStamina, fatigued = fatigued) - sessionActor ! SessionActor.SendResponse( PlanetsideAttributeMessage(session.get.player.GUID, 2, avatar.stamina) ) @@ -1027,6 +1008,19 @@ class AvatarActor( } else { totalStamina == 0 } + if (!avatar.fatigued && fatigued) { + avatar.implants.zipWithIndex.foreach { + case (Some(implant), slot) => + if (implant.active) { + deactivateImplant(implant.definition.implantType) + } + sessionActor ! SessionActor.SendResponse( + AvatarImplantMessage(session.get.player.GUID, ImplantAction.OutOfStamina, slot, 1) + ) + case _ => () + } + } + avatar = avatar.copy(stamina = totalStamina, fatigued = fatigued) sessionActor ! SessionActor.SendResponse(PlanetsideAttributeMessage(session.get.player.GUID, 2, avatar.stamina)) consumed @@ -1047,6 +1041,14 @@ class AvatarActor( ) ) + // 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)) + ) + implantTimers.get(slot).foreach(_.cancel()) implantTimers(slot) = context.system.scheduler.scheduleOnce( if (instant) 0.seconds else implant.definition.InitializationDuration.seconds, @@ -1084,6 +1086,35 @@ class AvatarActor( }) } + def deactivateImplant(implantType: ImplantType): Unit = { + val res = avatar.implants.zipWithIndex.collectFirst { + case (Some(implant), index) if implant.definition.implantType == implantType => (implant, index) + } + res match { + case Some((implant, slot)) => + implantTimers(slot).cancel() + avatar = 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._ diff --git a/src/main/scala/net/psforever/actors/session/ChatActor.scala b/src/main/scala/net/psforever/actors/session/ChatActor.scala index 91021579..3f89f923 100644 --- a/src/main/scala/net/psforever/actors/session/ChatActor.scala +++ b/src/main/scala/net/psforever/actors/session/ChatActor.scala @@ -488,7 +488,7 @@ class ChatActor( case (CMT_GMTELL, _, _) if gmCommandAllowed => chatService ! ChatService.Message( session, - message.copy(recipient = session.player.Name), + message, ChatChannel.Default() ) diff --git a/src/main/scala/net/psforever/actors/session/SessionActor.scala b/src/main/scala/net/psforever/actors/session/SessionActor.scala index 07b17793..93fe8034 100644 --- a/src/main/scala/net/psforever/actors/session/SessionActor.scala +++ b/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -90,18 +90,6 @@ import akka.util.Timeout import scala.collection.mutable object SessionActor { - - /** Object use cooldowns.
- * key - object id
- * value - time last used (ms) - */ - val delayedGratificationEntries: Map[Int, Long] = Map( - GlobalDefinitions.medkit.ObjectId -> 5000, //5s - GlobalDefinitions.super_armorkit.ObjectId -> 1200000, //20min - GlobalDefinitions.super_medkit.ObjectId -> 1200000, //20min - GlobalDefinitions.super_staminakit.ObjectId -> 1200000 //20min - ) - sealed trait Command final case class ResponseToSelf(pkt: PlanetSideGamePacket) diff --git a/src/main/scala/net/psforever/login/psadmin/PsAdminActor.scala b/src/main/scala/net/psforever/login/psadmin/PsAdminActor.scala index 2f9013b9..fc8dea96 100644 --- a/src/main/scala/net/psforever/login/psadmin/PsAdminActor.scala +++ b/src/main/scala/net/psforever/login/psadmin/PsAdminActor.scala @@ -10,7 +10,6 @@ import org.json4s._ import org.json4s.native.Serialization.write import scodec.bits._ import scodec.interop.akka._ -import net.psforever.services.ServiceManager.Lookup import net.psforever.services._ import scala.collection.mutable.Map import akka.actor.typed.scaladsl.adapter._ diff --git a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala index 1f8415d2..00f38e4e 100644 --- a/src/main/scala/net/psforever/objects/GlobalDefinitions.scala +++ b/src/main/scala/net/psforever/objects/GlobalDefinitions.scala @@ -6528,7 +6528,7 @@ object GlobalDefinitions { */ private def initMiscellaneous(): Unit = { ams_respawn_tube.Name = "ams_respawn_tube" - ams_respawn_tube.Delay = 5 + ams_respawn_tube.Delay = 10 ams_respawn_tube.SpecificPointFunc = SpawnPoint.AMS ams_respawn_tube.Damageable = false ams_respawn_tube.Repairable = false diff --git a/src/main/scala/net/psforever/objects/avatar/Avatar.scala b/src/main/scala/net/psforever/objects/avatar/Avatar.scala index 63033453..aa3cdc43 100644 --- a/src/main/scala/net/psforever/objects/avatar/Avatar.scala +++ b/src/main/scala/net/psforever/objects/avatar/Avatar.scala @@ -110,10 +110,9 @@ case class Avatar( times.get(definition.Name) match { case Some(purchaseTime) => val secondsSincePurchase = Seconds.secondsBetween(purchaseTime, LocalDateTime.now()) - val duration = secondsSincePurchase.toStandardDuration cooldowns.get(definition) match { case Some(cooldown) if (cooldown.toSeconds - secondsSincePurchase.getSeconds) > 0 => - Some(duration) + Some(Seconds.seconds((cooldown.toSeconds - secondsSincePurchase.getSeconds).toInt).toStandardDuration) case _ => None } case None => diff --git a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala index 7f9db7df..63058277 100644 --- a/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala +++ b/src/main/scala/net/psforever/objects/avatar/PlayerControl.scala @@ -358,12 +358,13 @@ class PlayerControl(player: Player, avatarActor: typed.ActorRef[AvatarActor.Comm val originalArmor = player.Armor player.ExoSuit = nextSuit val toMaxArmor = player.MaxArmor - val toArmor = if (originalSuit != nextSuit || originalSubtype != nextSubtype || originalArmor > toMaxArmor) { - player.History(HealFromExoSuitChange(PlayerSource(player), nextSuit)) - player.Armor = toMaxArmor - } else { - player.Armor = originalArmor - } + val toArmor = + if (originalSuit != nextSuit || originalSubtype != nextSubtype || originalArmor > toMaxArmor) { + player.History(HealFromExoSuitChange(PlayerSource(player), nextSuit)) + 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 diff --git a/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala b/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala index a5fc7738..1ef2e60a 100644 --- a/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala +++ b/src/main/scala/net/psforever/objects/serverobject/terminals/EquipmentTerminalDefinition.scala @@ -31,15 +31,15 @@ object EquipmentTerminalDefinition { * value - a `Tuple` containing exo-suit specifications */ val maxSuits: Map[String, (ExoSuitType.Value, Int)] = Map( - "trhev_antiaircraft" -> (ExoSuitType.MAX, 3), - "trhev_antipersonnel" -> (ExoSuitType.MAX, 1), - "trhev_antivehicular" -> (ExoSuitType.MAX, 2), - "nchev_antiaircraft" -> (ExoSuitType.MAX, 3), - "nchev_antipersonnel" -> (ExoSuitType.MAX, 1), - "nchev_antivehicular" -> (ExoSuitType.MAX, 2), - "vshev_antiaircraft" -> (ExoSuitType.MAX, 3), - "vshev_antipersonnel" -> (ExoSuitType.MAX, 1), - "vshev_antivehicular" -> (ExoSuitType.MAX, 2) + "trhev_antiaircraft" -> (ExoSuitType.MAX, 1), + "trhev_antipersonnel" -> (ExoSuitType.MAX, 2), + "trhev_antivehicular" -> (ExoSuitType.MAX, 3), + "nchev_antiaircraft" -> (ExoSuitType.MAX, 1), + "nchev_antipersonnel" -> (ExoSuitType.MAX, 2), + "nchev_antivehicular" -> (ExoSuitType.MAX, 3), + "vshev_antiaircraft" -> (ExoSuitType.MAX, 1), + "vshev_antipersonnel" -> (ExoSuitType.MAX, 2), + "vshev_antivehicular" -> (ExoSuitType.MAX, 3) ) import net.psforever.objects.GlobalDefinitions._ diff --git a/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala b/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala index ef1022a3..56aeb11d 100644 --- a/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala +++ b/src/main/scala/net/psforever/packet/game/AvatarImplantMessage.scala @@ -33,7 +33,7 @@ object ImplantAction extends Enumeration { * `Initialization` - 0 to revoke slot; 1 to allocate implant slot
* `Activation` - 0 to deactivate implant; 1 to activate implant
* `UnlockMessage` - 0-3 as an unlocked implant slot; display a message
- * `OutOfStamina` - lock implant; 0 to lock; 1 to unlock; display a message + * `OutOfStamina` - lock implant; 1 to lock; 0 to unlock; display a message */ final case class AvatarImplantMessage( player_guid: PlanetSideGUID,