From 82c68ebedba6aa8e291b759b6830632fcf90ffc9 Mon Sep 17 00:00:00 2001 From: FateJH Date: Mon, 9 Apr 2018 19:12:23 -0400 Subject: [PATCH] Modifying AvatarDeadStateMessage to manipulate visible respawn points on deployment mpa; improving postStop conditions on WSA to consider player in different situations; added basic Zone.Population.(message) case statements --- .../net/psforever/objects/zones/Zone.scala | 2 +- .../objects/zones/ZonePopulationActor.scala | 2 +- .../packet/game/AvatarDeadStateMessage.scala | 64 +++++++++++-- .../game/AvatarDeadStateMessageTest.scala | 11 ++- .../src/main/scala/WorldSessionActor.scala | 89 +++++++++++++++---- 5 files changed, 136 insertions(+), 32 deletions(-) diff --git a/common/src/main/scala/net/psforever/objects/zones/Zone.scala b/common/src/main/scala/net/psforever/objects/zones/Zone.scala index 37254fb7..5104c7dc 100644 --- a/common/src/main/scala/net/psforever/objects/zones/Zone.scala +++ b/common/src/main/scala/net/psforever/objects/zones/Zone.scala @@ -359,7 +359,7 @@ object Zone { * Message that acts in reply to `Spawn(avatar, player)`, but the avatar already has a player. * @param player the `Player` object */ - final case class PlayerAlreadySpawned(player : Player) + final case class PlayerAlreadySpawned(zone : Zone, player : Player) /** * Message that acts in reply to `Spawn(avatar, player)`, but the avatar did not initially `Join` this zone. * @param zone the `Zone` object diff --git a/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala b/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala index 76840df8..a22e9b43 100644 --- a/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala +++ b/common/src/main/scala/net/psforever/objects/zones/ZonePopulationActor.scala @@ -35,7 +35,7 @@ class ZonePopulationActor(zone : Zone, playerMap : TrieMap[Avatar, Option[Player PopulationSpawn(avatar, player, playerMap) match { case Some(tplayer) => if(tplayer ne player) { - sender ! Zone.Population.PlayerAlreadySpawned(player) + sender ! Zone.Population.PlayerAlreadySpawned(zone, player) } case None => sender ! Zone.Population.PlayerCanNotSpawn(zone, player) diff --git a/common/src/main/scala/net/psforever/packet/game/AvatarDeadStateMessage.scala b/common/src/main/scala/net/psforever/packet/game/AvatarDeadStateMessage.scala index 4b7ad326..5e17f544 100644 --- a/common/src/main/scala/net/psforever/packet/game/AvatarDeadStateMessage.scala +++ b/common/src/main/scala/net/psforever/packet/game/AvatarDeadStateMessage.scala @@ -2,15 +2,19 @@ package net.psforever.packet.game import net.psforever.packet.{GamePacketOpcode, Marshallable, PacketHelpers, PlanetSideGamePacket} -import net.psforever.types.Vector3 -import scodec.Codec +import net.psforever.types.{PlanetSideEmpire, Vector3} +import scodec.Attempt.{Failure, Successful} +import scodec.{Codec, Err} import scodec.codecs._ +/** + * An `Enumeration` of the various states a `Player` may possess in the cycle of nanite life and death. + */ object DeadState extends Enumeration { type Type = Value val - Nothing, + Alive, Dead, Release, RespawnTime @@ -20,19 +24,41 @@ object DeadState extends Enumeration { } /** - * na - * @param state avatar's relationship with the world + * Dispatched by the server to manipulate the client's management of the `Player` object owned by the user as his "avatar."
+ *
+ * The cycle of a player is generally `Alive` to `Dead` and `Dead` to `Release` and `Release` to `RespawnTimer` to `Alive`. + * When deconstructing oneself, the user makes a jump between `Alive` and `Release`; + * and, he may make a further jump from `Release` to `Alive` depending on spawning choices. + * Being `Alive` is the most common state. + * (Despite what anyone says.) + * Being `Dead` is just a technical requirement to initialize the revive timer. + * The player should be sufficiently "dead" by having his health points decreased to zero. + * If the timer is reduced to zero, the player is sent back to their faction-appropriate sanctuary continent.
+ *
+ * `Release` causes a "dead" player to have its character model converted into a backpack or a form of pastry. + * This cancels the revival timer - the player may no longer be revived - and brings the user to the deployment map. + * From the deployment map, the user may select a place where they may respawn a new character. + * The options available form this spawn are not only related to the faction affinity of the bases compared to the user's player(s) + * but also to the field `faction` as is provided in the packet. + * If the player is converted to a state of `Release` while being alive, the deployment map is still displayed. + * Their character model is not replaced by a backpack or pastry.
+ *
+ * `RespawnTimer` is like `Dead` as it is just a formal distinction to cause the client to display a timer. + * The state indicates that the player is being resurrected at a previously-selected location in the state `Alive`. + * @param state avatar's mortal relationship with the world; + * the following timers are applicable during `Death` and `RespawnTimer`; + * `faction` is applicable mainly during `Release` * @param timer_max total length of respawn countdown, in milliseconds * @param timer initial length of the respawn timer, in milliseconds - * @param pos last position - * @param unk4 na + * @param pos player's last position + * @param faction spawn points available for this faction on redeployment map * @param unk5 na */ final case class AvatarDeadStateMessage(state : DeadState.Value, timer_max : Long, timer : Long, pos : Vector3, - unk4 : Long, + faction : PlanetSideEmpire.Value, unk5 : Boolean) extends PlanetSideGamePacket { type Packet = AvatarDeadStateMessage @@ -41,12 +67,32 @@ final case class AvatarDeadStateMessage(state : DeadState.Value, } object AvatarDeadStateMessage extends Marshallable[AvatarDeadStateMessage] { + /** + * allocate all values from the `PlanetSideEmpire` `Enumeration` + */ + private val factionLongValues = PlanetSideEmpire.values map { _.id.toLong } + + /** + * `Codec` for converting between the limited `PlanetSideEmpire` `Enumeration` and a `Long` value. + */ + private val factionLongCodec = uint32L.exmap[PlanetSideEmpire.Value] ( + fv => + if(factionLongValues.contains(fv)) { + Successful(PlanetSideEmpire(fv.toInt)) + } + else { + Failure(Err(s"$fv is not mapped to a PlanetSideEmpire value")) + }, + f => + Successful(f.id.toLong) + ) + implicit val codec : Codec[AvatarDeadStateMessage] = ( ("state" | DeadState.codec) :: ("timer_max" | uint32L) :: ("timer" | uint32L) :: ("pos" | Vector3.codec_pos) :: - ("unk4" | uint32L) :: + ("unk4" | factionLongCodec) :: ("unk5" | bool) ).as[AvatarDeadStateMessage] } diff --git a/common/src/test/scala/game/AvatarDeadStateMessageTest.scala b/common/src/test/scala/game/AvatarDeadStateMessageTest.scala index ef874727..f1db1008 100644 --- a/common/src/test/scala/game/AvatarDeadStateMessageTest.scala +++ b/common/src/test/scala/game/AvatarDeadStateMessageTest.scala @@ -4,11 +4,12 @@ package game import org.specs2.mutable._ import net.psforever.packet._ import net.psforever.packet.game._ -import net.psforever.types.Vector3 +import net.psforever.types.{PlanetSideEmpire, Vector3} import scodec.bits._ class AvatarDeadStateMessageTest extends Specification { val string = hex"ad3c1260801c12608009f99861fb0741e040000010" + val string_invalid = hex"ad3c1260801c12608009f99861fb0741e0400000F0" "decode" in { PacketCoding.DecodePacket(string).require match { @@ -17,15 +18,19 @@ class AvatarDeadStateMessageTest extends Specification { unk2 mustEqual 300000 unk3 mustEqual 300000 pos mustEqual Vector3(6552.617f,4602.375f,60.90625f) - unk4 mustEqual 2 + unk4 mustEqual PlanetSideEmpire.VS unk5 mustEqual true case _ => ko } } + "decode (failure)" in { + PacketCoding.DecodePacket(string_invalid).isFailure mustEqual true + } + "encode" in { - val msg = AvatarDeadStateMessage(DeadState.Dead, 300000, 300000, Vector3(6552.617f,4602.375f,60.90625f), 2, true) + val msg = AvatarDeadStateMessage(DeadState.Dead, 300000, 300000, Vector3(6552.617f,4602.375f,60.90625f), PlanetSideEmpire.VS, true) val pkt = PacketCoding.EncodePacket(msg).require.toByteVector pkt mustEqual string diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala index ddf16df9..28a66dd8 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/pslogin/src/main/scala/WorldSessionActor.scala @@ -56,7 +56,7 @@ class WorldSessionActor extends Actor with MDCContextAware { var vehicleService : ActorRef = ActorRef.noSender var taskResolver : ActorRef = Actor.noSender var galaxy : ActorRef = Actor.noSender - var continent : Zone = null + var continent : Zone = Zone.Nowhere var player : Player = null var avatar : Avatar = null var progressBarValue : Option[Float] = None @@ -74,7 +74,7 @@ class WorldSessionActor extends Actor with MDCContextAware { override def postStop() = { clientKeepAlive.cancel reviveTimer.cancel - PlayerActionsToCancel() //progressBarUpdate.cancel + PlayerActionsToCancel() localService ! Service.Leave() vehicleService ! Service.Leave() avatarService ! Service.Leave() @@ -82,12 +82,49 @@ class WorldSessionActor extends Actor with MDCContextAware { LivePlayerList.Remove(sessionId) if(player != null && player.HasGUID) { val player_guid = player.GUID - player.VehicleSeated match { - case Some(vehicle_guid) => - //TODO do this at some other time - vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, 0, true, vehicle_guid)) - case None => ; + if(player.isAlive) { + //actually being alive or manually deconstructing + player.VehicleSeated match { + case Some(vehicle_guid) => + DismountVehicleOnLogOut(vehicle_guid, player_guid) + case None => ; + } + + continent.Population ! Zone.Population.Release(avatar) + player.Position = Vector3.Zero //save character before doing this + avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectDelete(player_guid, player_guid)) + taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) + //TODO normally, the actual player avatar persists a minute or so after the user disconnects } + else if(continent.LivePlayers.contains(player) && !continent.Corpses.contains(player)) { + //player disconnected while waiting for a revive + //similar to handling ReleaseAvatarRequestMessage + player.Release + continent.Population ! Zone.Population.Release(avatar) + player.VehicleSeated match { + case None => + continent.Population ! Zone.Corpse.Add(player) + FriskCorpse(player) //TODO eliminate dead letters + if(!WellLootedCorpse(player)) { + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.Release(player, continent)) + taskResolver ! GUIDTask.UnregisterLocker(player.Locker)(continent.GUID) //rest of player will be cleaned up with corpses + } + else { //no items in inventory; leave no corpse + val player_guid = player.GUID + player.Position = Vector3.Zero //save character before doing this + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) + taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) + } + + case Some(vehicle_guid) => + val player_guid = player.GUID + player.Position = Vector3.Zero //save character before doing this + avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player_guid, player_guid, 0)) + taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) + DismountVehicleOnLogOut(vehicle_guid, player_guid) + } + } + player.VehicleOwned match { case Some(vehicle_guid) => continent.GUID(vehicle_guid) match { @@ -100,15 +137,25 @@ class WorldSessionActor extends Actor with MDCContextAware { } case None => ; } - continent.Population ! Zone.Population.Release(avatar) continent.Population ! Zone.Population.Leave(avatar) - player.Position = Vector3.Zero //save character before doing this - avatarService ! AvatarServiceMessage(player.Continent, AvatarAction.ObjectDelete(player_guid, player_guid)) - taskResolver ! GUIDTask.UnregisterAvatar(player)(continent.GUID) - //TODO normally, the actual player avatar persists a minute or so after the user disconnects } } + /** + * Vehicle cleanup that is specific to log out behavior. + * @param vehicle_guid the vehicle being occupied + * @param player_guid the player + */ + def DismountVehicleOnLogOut(vehicle_guid : PlanetSideGUID, player_guid : PlanetSideGUID) : Unit = { + val vehicle = continent.GUID(vehicle_guid).get.asInstanceOf[Vehicle] + vehicle.Seat(vehicle.PassengerInSeat(player).get).get.Occupant = None + if(vehicle.Seats.values.count(_.isOccupied) == 0) { + vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(vehicle, continent, 600L) //start vehicle decay (10m) + } + vehicleService ! Service.Leave(Some(s"${vehicle.Actor}")) + vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, 0, true, vehicle_guid)) + } + def receive = Initializing def Initializing : Receive = { @@ -554,7 +601,7 @@ class WorldSessionActor extends Actor with MDCContextAware { else { vehicleService ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(player_guid, seat_num, true, obj.GUID)) } - if(obj.Seats.values.count(seat => seat.isOccupied) == 0) { + if(obj.Seats.values.count(_.isOccupied) == 0) { vehicleService ! VehicleServiceMessage.DelayedVehicleDeconstruction(obj, continent, 600L) //start vehicle decay (10m) } @@ -1008,6 +1055,12 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"$tplayer has left zone ${zone.Id}") } + case Zone.Population.PlayerCanNotSpawn(zone, tplayer) => + log.warn(s"$tplayer can not spawn in zone ${zone.Id}; why?") + + case Zone.Population.PlayerAlreadySpawned(zone, tplayer) => + log.warn(s"$tplayer is already spawned on zone ${zone.Id}; a clerical error?") + case Zone.Lattice.SpawnPoint(zone_id, building, spawn_tube) => log.info(s"Zone.Lattice.SpawnPoint: spawn point on $zone_id in ${building.Id} @ ${spawn_tube.GUID.guid} selected") reviveTimer.cancel @@ -1015,7 +1068,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val backpack = player.isBackpack val respawnTime : Long = if(sameZone) { 10 } else { 0 } //s val respawnTimeMillis = respawnTime * 1000 //ms - sendResponse(AvatarDeadStateMessage(DeadState.RespawnTime, respawnTimeMillis, respawnTimeMillis, Vector3.Zero, 2, true)) + sendResponse(AvatarDeadStateMessage(DeadState.RespawnTime, respawnTimeMillis, respawnTimeMillis, Vector3.Zero, player.Faction, true)) val tplayer = if(backpack) { RespawnClone(player) //new player } @@ -1122,7 +1175,7 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(ChangeShortcutBankMessage(guid, 0)) //FavoritesMessage sendResponse(SetChatFilterMessage(ChatChannel.Local, false, ChatChannel.values.toList)) //TODO will not always be "on" like this - sendResponse(AvatarDeadStateMessage(DeadState.Nothing, 0,0, tplayer.Position, 0, true)) + sendResponse(AvatarDeadStateMessage(DeadState.Alive, 0,0, tplayer.Position, player.Faction, true)) sendResponse(PlanetsideAttributeMessage(guid, 53, 1)) sendResponse(AvatarSearchCriteriaMessage(guid, List(0,0,0,0,0,0))) (1 to 73).foreach(i => { @@ -1500,7 +1553,7 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"ReleaseAvatarRequest: ${player.GUID} on ${continent.Id} has released") reviveTimer.cancel player.Release - sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, 2, true)) + sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true)) continent.Population ! Zone.Population.Release(avatar) player.VehicleSeated match { case None => @@ -2153,7 +2206,7 @@ class WorldSessionActor extends Actor with MDCContextAware { //deconstruction PlayerActionsToCancel() player.Release - sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, 2, true)) + sendResponse(AvatarDeadStateMessage(DeadState.Release, 0, 0, player.Position, player.Faction, true)) continent.Population ! Zone.Population.Release(avatar) case Some(obj : PlanetSideGameObject) => @@ -3418,7 +3471,7 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(PlanetsideAttributeMessage(player_guid, 2, 0)) avatarService ! AvatarServiceMessage(continent.Id, AvatarAction.PlanetsideAttribute(player_guid, 0, 0)) sendResponse(DestroyMessage(player_guid, player_guid, PlanetSideGUID(0), pos)) //how many players get this message? - sendResponse(AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, 2, true)) + sendResponse(AvatarDeadStateMessage(DeadState.Dead, respawnTimer, respawnTimer, pos, player.Faction, true)) if(tplayer.VehicleSeated.nonEmpty) { //make player invisible (if not, the cadaver sticks out the side in a seated position) sendResponse(PlanetsideAttributeMessage(player_guid, 29, 1))