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,