From fa7342264eb4dc2b074cf03848865daf47b9b30c Mon Sep 17 00:00:00 2001 From: Fate-JH Date: Mon, 29 Jun 2020 14:03:51 -0400 Subject: [PATCH] Passenger interim (#501) * properly handle interim and, thus, persistence for a pure passenger * persistence maintained during relog; message handling case reset at death --- .../src/main/scala/WorldSessionActor.scala | 157 +++++++++++++++--- 1 file changed, 130 insertions(+), 27 deletions(-) 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