diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index 485432d6..ceae0ab2 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -158,12 +158,13 @@ class WorldSessionActor extends Actor
var squadUpdateCounter : Int = 0
val queuedSquadActions : Seq[() => Unit] = Seq(SquadUpdates, NoSquadUpdates, NoSquadUpdates, NoSquadUpdates)
/** Upstream message counter
- * Checks for server acknowledgement of the following messages:
- * `PlayerStateMessageUpstream`
+ * Checks for server acknowledgement of the following messages in the following conditions:
+ * `PlayerStateMessageUpstream` (infantry)
* `VehicleStateMessage` (driver seat only)
- * `ChildObjectStateMessage` (any seat but driver)
+ * `ChildObjectStateMessage` (any gunner seat that is not the driver)
+ * `KeepAliveMessage` (any passenger seat that is not the driver)
* As they should arrive roughly every 250 milliseconds this allows for a very crude method of scheduling tasks up to four times per second */
- private var upstreamMessageCount : Int = 0
+ var upstreamMessageCount : Int = 0
var zoningType : Zoning.Method.Value = Zoning.Method.None
var zoningChatMessageType : ChatMessageType.Value = ChatMessageType.CMT_QUIT
var zoningStatus : Zoning.Status.Value = Zoning.Status.None
@@ -180,7 +181,10 @@ class WorldSessionActor extends Actor
var zoneLoaded : Option[Boolean] = None
/** a flag that forces the current zone to reload itself during a zoning operation */
var zoneReload : Boolean = false
- var turnCounter : PlanetSideGUID=>Unit = TurnCounterDuringInterim
+ var interimUngunnedVehicle : Option[PlanetSideGUID] = None
+ var interimUngunnedVehicleSeat : Option[Int] = None
+ var keepAliveFunc : ()=>Unit = NormalKeepAlive
+ var turnCounterFunc : PlanetSideGUID=>Unit = TurnCounterDuringInterim
var clientKeepAlive : Cancellable = Default.Cancellable
var progressBarUpdate : Cancellable = Default.Cancellable
@@ -299,7 +303,6 @@ class WorldSessionActor extends Actor
handleGamePkt(pkt)
case PokeClient() =>
- persist()
sendResponse(KeepAliveMessage())
case AvatarServiceResponse(toChannel, guid, reply) =>
@@ -1142,7 +1145,14 @@ class WorldSessionActor extends Actor
//important! the LoadMapMessage must be processed by the client before the avatar is created
player = tplayer
setupAvatarFunc()
- turnCounter = TurnCounterDuringInterim
+ //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable
+ turnCounterFunc = interimUngunnedVehicle match {
+ case Some(_) =>
+ TurnCounterDuringInterimWhileInPassengerSeat
+ case None =>
+ TurnCounterDuringInterim
+ }
+ keepAliveFunc = NormalKeepAlive
upstreamMessageCount = 0
persist()
@@ -1151,7 +1161,14 @@ class WorldSessionActor extends Actor
log.info(s"Player ${tplayer.Name} will respawn")
player = tplayer
setupAvatarFunc()
- turnCounter = TurnCounterDuringInterim
+ //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable
+ turnCounterFunc = interimUngunnedVehicle match {
+ case Some(_) =>
+ TurnCounterDuringInterimWhileInPassengerSeat
+ case None =>
+ TurnCounterDuringInterim
+ }
+ keepAliveFunc = NormalKeepAlive
upstreamMessageCount = 0
persist()
@@ -1835,6 +1852,7 @@ class WorldSessionActor extends Actor
case AvatarResponse.Killed(mount) =>
val respawnTimer = 300000 //milliseconds
ToggleMaxSpecialState(enable = false)
+ keepAliveFunc = NormalKeepAlive
zoningStatus = Zoning.Status.None
deadState = DeadState.Dead
continent.GUID(mount) match {
@@ -1850,6 +1868,7 @@ class WorldSessionActor extends Actor
log.warn(s"KillPlayer/SHOTS_WHILE_DEAD: client of ${avatar.name} fired $shotsWhileDead rounds while character was dead on server")
shotsWhileDead = 0
}
+ import scala.concurrent.ExecutionContext.Implicits.global
reviveTimer.cancel
if(player.death_by == 0) {
import scala.concurrent.ExecutionContext.Implicits.global
@@ -2458,6 +2477,10 @@ class WorldSessionActor extends Actor
obj.Cloaked = tplayer.Cloaked
}
}
+ else if(obj.Seats(seat_num).ControlledWeapon.isEmpty) {
+ //the player will receive no messages consistently except the KeepAliveMessage echo
+ keepAliveFunc = KeepAlivePersistence
+ }
AccessContents(obj)
UpdateWeaponAtSeatPosition(obj, seat_num)
MountingAction(tplayer, obj, seat_num)
@@ -2812,11 +2835,6 @@ class WorldSessionActor extends Actor
sendResponse(VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, unk2, unk3, unk4, wheel_direction, unk5, unk6))
if(player.VehicleSeated.contains(vehicle_guid)) {
player.Position = pos
- GetVehicleAndSeat() match {
- case (Some(_), Some(seatNum)) if seatNum > 0 =>
- turnCounter(guid)
- case _ => ;
- }
}
}
case VehicleResponse.SendResponse(msg) =>
@@ -3536,8 +3554,8 @@ class WorldSessionActor extends Actor
log.error("Unsupported " + default + " in " + msg)
}
- case KeepAliveMessage(code) =>
- sendResponse(KeepAliveMessage())
+ case KeepAliveMessage(_) =>
+ keepAliveFunc()
case msg@BeginZoningMessage() =>
log.info("Reticulating splines ...")
@@ -3844,7 +3862,8 @@ class WorldSessionActor extends Actor
case msg @ PlayerStateMessageUpstream(avatar_guid, pos, vel, yaw, pitch, yaw_upper, seq_time, unk3, is_crouching, is_jumping, jump_thrust, is_cloaking, unk5, unk6) =>
//log.info(s"$msg")
- turnCounter(avatar_guid)
+ persist()
+ turnCounterFunc(avatar_guid)
val isMoving = WorldEntity.isMoving(vel)
val isMovingPlus = isMoving || is_jumping || jump_thrust
if(isMovingPlus) {
@@ -3911,7 +3930,8 @@ class WorldSessionActor extends Actor
}) match {
case None | Some(0) => ;
case Some(_) =>
- turnCounter(player.GUID)
+ persist()
+ turnCounterFunc(player.GUID)
}
if(tool.GUID == object_guid) {
//TODO set tool orientation?
@@ -3936,7 +3956,8 @@ class WorldSessionActor extends Actor
GetVehicleAndSeat() match {
case (Some(obj), Some(0)) =>
//we're driving the vehicle
- turnCounter(player.GUID)
+ persist()
+ turnCounterFunc(player.GUID)
val seat = obj.Seats(0)
player.Position = pos //convenient
if(seat.ControlledWeapon.isEmpty) {
@@ -7377,6 +7398,8 @@ class WorldSessionActor extends Actor
interstellarFerry = None
val vdef = vehicle.Definition
val vguid = vehicle.GUID
+ vehicle.Position = shiftPosition.getOrElse(vehicle.Position)
+ vehicle.Orientation = shiftOrientation.getOrElse(vehicle.Orientation)
val vdata = if(seat == 0) {
//driver
continent.Transport ! Zone.Vehicle.Spawn(vehicle)
@@ -7522,9 +7545,15 @@ class WorldSessionActor extends Actor
val pdata = pdef.Packet.DetailedConstructorData(tplayer).get
tplayer.VehicleSeated = vguid
sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata))
- sendResponse(ObjectAttachMessage(vguid, pguid, seat))
- AccessContents(vehicle)
- UpdateWeaponAtSeatPosition(vehicle, seat)
+ if(seat == 0 || vehicle.Seats(seat).ControlledWeapon.nonEmpty) {
+ sendResponse(ObjectAttachMessage(vguid, pguid, seat))
+ AccessContents(vehicle)
+ UpdateWeaponAtSeatPosition(vehicle, seat)
+ }
+ else {
+ interimUngunnedVehicle = Some(vguid)
+ interimUngunnedVehicleSeat = Some(seat)
+ }
continent.AvatarEvents ! AvatarServiceMessage(
continent.Id,
AvatarAction.LoadPlayer(
@@ -7602,10 +7631,16 @@ class WorldSessionActor extends Actor
val pdata = pdef.Packet.DetailedConstructorData(player).get
player.VehicleSeated = vguid
sendResponse(ObjectCreateDetailedMessage(pdef.ObjectId, pguid, pdata))
- sendResponse(ObjectAttachMessage(vguid, pguid, seat))
//log.info(s"AvatarRejoin: $vguid -> $vdata")
- AccessContents(vehicle)
- UpdateWeaponAtSeatPosition(vehicle, seat)
+ if(seat == 0 || vehicle.Seats(seat).ControlledWeapon.nonEmpty) {
+ sendResponse(ObjectAttachMessage(vguid, pguid, seat))
+ AccessContents(vehicle)
+ UpdateWeaponAtSeatPosition(vehicle, seat)
+ }
+ else {
+ interimUngunnedVehicle = Some(vguid)
+ interimUngunnedVehicleSeat = Some(seat)
+ }
log.info(s"AvatarRejoin: ${player.Name} in ${vehicle.Definition.Name}")
case _ =>
@@ -8192,6 +8227,7 @@ class WorldSessionActor extends Actor
def DismountAction(tplayer : Player, obj : PlanetSideGameObject with Mountable, seatNum : Int) : Unit = {
val player_guid : PlanetSideGUID = tplayer.GUID
log.info(s"DismountVehicleMsg: ${tplayer.Name} dismounts $obj from $seatNum")
+ keepAliveFunc = NormalKeepAlive
sendResponse(DismountVehicleMsg(player_guid, BailType.Normal, false))
continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.DismountVehicle(player_guid, BailType.Normal, false))
}
@@ -10225,14 +10261,81 @@ class WorldSessionActor extends Actor
* @param guid the player's globally unique identifier number
*/
def TurnCounterDuringInterim(guid : PlanetSideGUID) : Unit = {
+ upstreamMessageCount = 0
if(player.GUID == guid && player.Zone == continent) {
- turnCounter = NormalTurnCounter
+ turnCounterFunc = NormalTurnCounter
}
- else {
- upstreamMessageCount = 0
+ }
+
+ /**
+ * During the interim period between the avatar being in one place/zone
+ * and completing the process of transitioning to another place/zone,
+ * the upstream message counter is zero'd
+ * awaiting new activity from the client.
+ * Until new upstream messages that pass some tests against their data start being reported,
+ * the counter does not accumulate properly.
+ *
+ * In the case that the transitioning player is seated in a vehicle seat
+ * that is not the driver and does not have a mounted weapon under its control,
+ * no obvious feedback will be provided by the client.
+ * For example, when as infantry, a `PlayerStateMessageUpstream` packet is dispatched by the client.
+ * For example, when in the driver seat, a `VehicleStateMessage` is dispatched by the client.
+ * In the given case, the only packet that indicates the player is seated is a `KeepAliveMessage`.
+ * Detection of this `KeepALiveMessage`, for the purpose of transitioning logic,
+ * can not be instantaneous to the zoning process or other checks for proper zoning conditions that will be disrupted.
+ * To avoid complications, the player in such a seat is initially spawned as infantry on their own client,
+ * realizes the state transition confirmation for infantry (turn counter),
+ * and is forced to transition into being seated,
+ * and only at that time will begin registering `KeepAliveMessage` to mark the end of their interim period.
+ * @param guid the player's globally unique identifier number
+ */
+ def TurnCounterDuringInterimWhileInPassengerSeat(guid : PlanetSideGUID) : Unit = {
+ upstreamMessageCount = 0
+ val pguid = player.GUID
+ if(pguid == guid && player.Zone == continent) {
+ (continent.GUID(interimUngunnedVehicle), interimUngunnedVehicle, interimUngunnedVehicleSeat) match {
+ case (Some(vehicle : Vehicle), Some(vguid), Some(seat)) =>
+ //sit down
+ sendResponse(ObjectAttachMessage(vguid, pguid, seat))
+ AccessContents(vehicle)
+ keepAliveFunc = KeepAlivePersistence
+ case _ => ;
+ //we can't find a vehicle? and we're still here? that's bad
+ player.VehicleSeated = None
+ }
+ interimUngunnedVehicle = None
+ interimUngunnedVehicleSeat = None
+ turnCounterFunc = NormalTurnCounter
}
}
+ /**
+ * The normal response to receiving a `KeepAliveMessage` packet from the client.
+ *
+ * Even though receiving a `KeepAliveMessage` outside of zoning is uncommon,
+ * the behavior should be configured to maintain a neutral action.
+ * @see `KeepAliveMessage`
+ * @see `keepAliveFunc`
+ */
+ def NormalKeepAlive() : Unit = { }
+
+ /**
+ * The atypical response to receiving a `KeepAliveMessage` packet from the client.
+ *
+ * `KeepAliveMessage` packets are the primary vehicle for persistence due to client reporting
+ * in the case where the player's avatar is riding in a vehicle in a seat with no vehicle.
+ * @see `KeepAliveMessage`
+ * @see `keepAliveFunc`
+ * @see `turnCounterFunc`
+ * @see `persist`
+ */
+ def KeepAlivePersistence() : Unit = {
+ //log.info(s"KeepAlive in a vehicle - $upstreamMessageCount")
+ interimUngunnedVehicle = None
+ persist()
+ turnCounterFunc(player.GUID)
+ }
+
def AdministrativeKick(tplayer : Player, permitKickSelf : Boolean = false) : Boolean = {
if(permitKickSelf || tplayer != player) { //stop kicking yourself
tplayer.death_by = -1