Disconnect (#499)

* better kicking; a quitting that eliminates persistence

* GenericActionMessage comments; integer delay time

* TeardownConnection corresponds to closing the client directly

* messaging path for CMT_QUIT immediate logout that intersects zoning logic for IA and Recall

* slightly improved kicking, and the posibility of longer kicking

* restoring a turn counter instance

* player character will now clean up like normal; immediately turns into corpse; kick delay exists only on the persistence monitor
This commit is contained in:
Fate-JH 2020-06-24 23:08:22 -04:00 committed by GitHub
parent a5403298e3
commit e91e282d3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 254 additions and 75 deletions

View file

@ -10,7 +10,8 @@ object Zoning {
val val
None, None,
InstantAction, InstantAction,
Recall Recall,
Quit
= Value = Value
} }
@ -35,6 +36,8 @@ object Zoning {
final val Enemy = TimeType(30, "Enemy") final val Enemy = TimeType(30, "Enemy")
} }
final case class Quit()
object InstantAction { object InstantAction {
final case class Request(faction : PlanetSideEmpire.Value) final case class Request(faction : PlanetSideEmpire.Value)

View file

@ -26,9 +26,10 @@ import scodec.codecs._
* 24 - message: you have been imprinted (updates imprinted status; does it?)<br> * 24 - message: you have been imprinted (updates imprinted status; does it?)<br>
* 25 - message: you are no longer imprinted (updates imprinted status; does it?)<br> * 25 - message: you are no longer imprinted (updates imprinted status; does it?)<br>
* 27 - event: purchase timers reset (does it?)<br> * 27 - event: purchase timers reset (does it?)<br>
* 31 - switch to first person view, attempt to deconstruct but fail; * 31 - forced into first person view;
* in third person view, player character sinks into the ground; green deconstruction particle effect under feet<br>
* 32 - forced into first person view, attempt to deconstruct but fail;
* event: fail to deconstruct due to having a "parent vehicle"<br> * event: fail to deconstruct due to having a "parent vehicle"<br>
* 32 - switch to first person view<br>
* 33 - event: fail to deconstruct<br> * 33 - event: fail to deconstruct<br>
* 43 - prompt: friendly fire in virtual reality zone<br> * 43 - prompt: friendly fire in virtual reality zone<br>
* 45 - ?<br> * 45 - ?<br>

View file

@ -11,9 +11,8 @@ import net.psforever.objects._
import net.psforever.objects.serverobject.mount.Mountable import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.types.Vector3 import net.psforever.types.Vector3
import services.{RemoverActor, Service, ServiceManager} import services.{Service, ServiceManager}
import services.avatar.{AvatarAction, AvatarServiceMessage} import services.avatar.{AvatarAction, AvatarServiceMessage}
import services.vehicle.VehicleServiceMessage
/** /**
* A global service that manages user behavior as divided into the following three categories: * A global service that manages user behavior as divided into the following three categories:
@ -85,10 +84,34 @@ class AccountPersistenceService extends Actor {
case Some(ref) => case Some(ref) =>
ref ! msg ref ! msg
case None => case None =>
log.warn(s"tried to update a player entry ($name) that did not yet exist; rebuilding entry ...") log.warn(s"tried to update a player entry for $name that did not yet exist; rebuilding entry ...")
CreateNewPlayerToken(name).tell(msg, sender) CreateNewPlayerToken(name).tell(msg, sender)
} }
case msg @ AccountPersistenceService.PersistDelay(name, _) =>
accounts.get(name) match {
case Some(ref) =>
ref ! msg
case _ =>
log.warn(s"player entry for $name not found; not logged in")
}
case msg @ AccountPersistenceService.Kick(name, _) =>
accounts.get(name) match {
case Some(ref) =>
ref ! msg
case _ =>
log.warn(s"player entry for $name not found; not logged in")
}
case AccountPersistenceService.Logout(name) =>
accounts.remove(name) match {
case Some(ref) =>
ref ! Logout(name)
case _ =>
log.warn(s"player entry for $name not found; not logged in")
}
case Logout(target) => //TODO use context.watch and Terminated? case Logout(target) => //TODO use context.watch and Terminated?
accounts.remove(target) accounts.remove(target)
@ -114,7 +137,7 @@ class AccountPersistenceService extends Actor {
} }
case msg => case msg =>
log.warn(s"Not yet started; received a $msg that will go unhandled") log.warn(s"not yet started; received a $msg that will go unhandled")
} }
/** /**
@ -166,6 +189,22 @@ object AccountPersistenceService {
* @param position the location of the player in game world coordinates * @param position the location of the player in game world coordinates
*/ */
final case class Update(name : String, zone : Zone, position : Vector3) final case class Update(name : String, zone : Zone, position : Vector3)
final case class Kick(name : String, time : Option[Long] = None)
/**
* Update the persistence monitor that was setup for a user for a custom persistence delay.
* If set to `None`, the default persistence time should assert itself.
* @param name the unique name of the player
* @param time the duration that this user's player characters will persist without update in seconds
*/
final case class PersistDelay(name : String, time : Option[Long])
/**
* Message that indicates that persistence is no longer necessary for this player character.
* @param name the unique name of the player
*/
final case class Logout(name : String)
} }
/** /**
@ -189,6 +228,12 @@ class PersistenceMonitor(name : String, squadService : ActorRef, taskResolver :
var inZone : Zone = Zone.Nowhere var inZone : Zone = Zone.Nowhere
/** the last-reported game coordinate position of this player */ /** the last-reported game coordinate position of this player */
var lastPosition : Vector3 = Vector3.Zero var lastPosition : Vector3 = Vector3.Zero
/** */
var kicked : Boolean = false
/** */
var kickTime : Option[Long] = None
/** a custom logout time for this player; 60s by default */
var persistTime : Option[Long] = None
/** the ongoing amount of permissible inactivity */ /** the ongoing amount of permissible inactivity */
var timer : Cancellable = Default.Cancellable var timer : Cancellable = Default.Cancellable
/** the sparingly-used log */ /** the sparingly-used log */
@ -204,17 +249,44 @@ class PersistenceMonitor(name : String, squadService : ActorRef, taskResolver :
def receive : Receive = { def receive : Receive = {
case AccountPersistenceService.Login(_) => case AccountPersistenceService.Login(_) =>
sender ! PlayerToken.LoginInfo(name, inZone, lastPosition) sender ! (if(kicked) {
UpdateTimer() PlayerToken.CanNotLogin(name, PlayerToken.DeniedLoginReason.Kicked)
}
else {
UpdateTimer()
PlayerToken.LoginInfo(name, inZone, lastPosition)
})
case AccountPersistenceService.Update(_, z, p) => case AccountPersistenceService.Update(_, z, p) if !kicked =>
inZone = z inZone = z
lastPosition = p lastPosition = p
UpdateTimer() UpdateTimer()
case AccountPersistenceService.PersistDelay(_, delay) if !kicked =>
persistTime = delay
UpdateTimer()
case AccountPersistenceService.Kick(_, time) =>
persistTime = None
kickTime match {
case None if kicked =>
UpdateTimer()
case _ => ;
}
kicked = true
kickTime = time.orElse(Some(300L))
case Logout(_) => case Logout(_) =>
context.parent ! Logout(name) kickTime match {
context.stop(self) case Some(time) =>
PerformLogout()
kickTime = None
timer.cancel
timer = context.system.scheduler.scheduleOnce(time seconds, self, Logout(name))
case None =>
context.parent ! Logout(name)
context.stop(self)
}
case _ => ; case _ => ;
} }
@ -224,7 +296,7 @@ class PersistenceMonitor(name : String, squadService : ActorRef, taskResolver :
*/ */
def UpdateTimer() : Unit = { def UpdateTimer() : Unit = {
timer.cancel timer.cancel
timer = context.system.scheduler.scheduleOnce(60 seconds, self, Logout(name)) timer = context.system.scheduler.scheduleOnce(persistTime.getOrElse(60L) seconds, self, Logout(name))
} }
/** /**
@ -248,7 +320,6 @@ class PersistenceMonitor(name : String, squadService : ActorRef, taskResolver :
* but should be uncommon. * but should be uncommon.
*/ */
def PerformLogout() : Unit = { def PerformLogout() : Unit = {
log.info(s"logout of $name")
(inZone.Players.find(p => p.name == name), inZone.LivePlayers.find(p => p.Name == name)) match { (inZone.Players.find(p => p.name == name), inZone.LivePlayers.find(p => p.Name == name)) match {
case (Some(avatar), Some(player)) if player.VehicleSeated.nonEmpty => case (Some(avatar), Some(player)) if player.VehicleSeated.nonEmpty =>
//alive or dead in a vehicle //alive or dead in a vehicle
@ -336,6 +407,7 @@ class PersistenceMonitor(name : String, squadService : ActorRef, taskResolver :
squadService.tell(Service.Leave(Some(charId.toString)), parent) squadService.tell(Service.Leave(Some(charId.toString)), parent)
Deployables.Disown(inZone, avatar, parent) Deployables.Disown(inZone, avatar, parent)
inZone.Population.tell(Zone.Population.Leave(avatar), parent) inZone.Population.tell(Zone.Population.Leave(avatar), parent)
log.info(s"logout of ${avatar.name}")
} }
} }
@ -347,6 +419,13 @@ class PersistenceMonitor(name : String, squadService : ActorRef, taskResolver :
private[this] case class Logout(name : String) private[this] case class Logout(name : String)
object PlayerToken { object PlayerToken {
object DeniedLoginReason extends Enumeration {
val
Denied, //generic
Kicked
= Value
}
/** /**
* Message dispatched to confirm that a player with given locational attributes exists. * Message dispatched to confirm that a player with given locational attributes exists.
* Agencies outside of the `AccountPersistanceService`/`PlayerToken` system make use of this message. * Agencies outside of the `AccountPersistanceService`/`PlayerToken` system make use of this message.
@ -356,4 +435,6 @@ object PlayerToken {
* @param position where in the zone the player is located * @param position where in the zone the player is located
*/ */
final case class LoginInfo(name : String, zone : Zone, position : Vector3) final case class LoginInfo(name : String, zone : Zone, position : Vector3)
final case class CanNotLogin(name : String, reason : DeniedLoginReason.Value)
} }

View file

@ -1092,7 +1092,7 @@ class WorldSessionActor extends Actor
case msg@Zoning.InstantAction.Located(zone, _, spawn_point) => case msg@Zoning.InstantAction.Located(zone, _, spawn_point) =>
//in between subsequent reply messages, it does not matter if the destination changes //in between subsequent reply messages, it does not matter if the destination changes
//so long as there is at least one destination at all (including the fallback) //so long as there is at least one destination at all (including the fallback)
if(ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction))) { if(ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction), cluster)) {
val (pos, ori) = spawn_point.SpecificPoint(player) val (pos, ori) = spawn_point.SpecificPoint(player)
SpawnThroughZoningProcess(zone, pos, ori) SpawnThroughZoningProcess(zone, pos, ori)
} }
@ -1103,7 +1103,7 @@ class WorldSessionActor extends Actor
case Zoning.InstantAction.NotLocated() => case Zoning.InstantAction.NotLocated() =>
instantActionFallbackDestination match { instantActionFallbackDestination match {
case Some(Zoning.InstantAction.Located(zone, _, spawn_point)) if spawn_point.Owner.Faction == player.Faction && !spawn_point.Offline => case Some(Zoning.InstantAction.Located(zone, _, spawn_point)) if spawn_point.Owner.Faction == player.Faction && !spawn_point.Offline =>
if(ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction))) { if(ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction), cluster)) {
val (pos, ori) = spawn_point.SpecificPoint(player) val (pos, ori) = spawn_point.SpecificPoint(player)
SpawnThroughZoningProcess(zone, pos, ori) SpawnThroughZoningProcess(zone, pos, ori)
} }
@ -1116,7 +1116,7 @@ class WorldSessionActor extends Actor
} }
case Zoning.Recall.Located(zone, spawn_point) => case Zoning.Recall.Located(zone, spawn_point) =>
if(ContemplateZoningResponse(Zoning.Recall.Request(player.Faction, zone.Id))) { if(ContemplateZoningResponse(Zoning.Recall.Request(player.Faction, zone.Id), cluster)) {
val (pos, ori) = spawn_point.SpecificPoint(player) val (pos, ori) = spawn_point.SpecificPoint(player)
SpawnThroughZoningProcess(zone, pos, ori) SpawnThroughZoningProcess(zone, pos, ori)
} }
@ -1124,6 +1124,12 @@ class WorldSessionActor extends Actor
case Zoning.Recall.Denied(reason) => case Zoning.Recall.Denied(reason) =>
CancelZoningProcessWithReason(s"@norecall_sanctuary_$reason", Some(ChatMessageType.CMT_QUIT)) CancelZoningProcessWithReason(s"@norecall_sanctuary_$reason", Some(ChatMessageType.CMT_QUIT))
case Zoning.Quit() =>
if(ContemplateZoningResponse(Zoning.Quit(), self)) {
log.info("Good-bye")
ImmediateDisconnect()
}
case ZoningReset() => case ZoningReset() =>
CancelZoningProcess() CancelZoningProcess()
@ -1375,6 +1381,10 @@ class WorldSessionActor extends Actor
inZone.AvatarEvents ! AvatarServiceMessage(playerName, AvatarAction.TeardownConnection()) inZone.AvatarEvents ! AvatarServiceMessage(playerName, AvatarAction.TeardownConnection())
//find and reload previous player //find and reload previous player
(inZone.Players.find(p => p.name.equals(playerName)), inZone.LivePlayers.find(p => p.Name.equals(playerName))) match { (inZone.Players.find(p => p.name.equals(playerName)), inZone.LivePlayers.find(p => p.Name.equals(playerName))) match {
case (_, Some(p)) if p.death_by == -1 =>
//player is not allowed
KickedByAdministration()
case (Some(a), Some(p)) if p.isAlive => case (Some(a), Some(p)) if p.isAlive =>
//rejoin current avatar/player //rejoin current avatar/player
log.info(s"LoginInfo: player $playerName is alive") log.info(s"LoginInfo: player $playerName is alive")
@ -1415,10 +1425,17 @@ class WorldSessionActor extends Actor
case _ => case _ =>
//fall back to sanctuary/prior? //fall back to sanctuary/prior?
log.error(s"LoginInfo: player ${player.Name}Name could not be found in game world") log.error(s"LoginInfo: player $playerName could not be found in game world")
self ! PlayerToken.LoginInfo(playerName, Zone.Nowhere, pos) self ! PlayerToken.LoginInfo(playerName, Zone.Nowhere, pos)
} }
case PlayerToken.CanNotLogin(playerName, reason) =>
log.warn(s"LoginInfo: player $playerName is denied login for reason: $reason")
reason match {
case PlayerToken.DeniedLoginReason.Kicked => KickedByAdministration()
case _ => sendResponse(DisconnectMessage("You will be logged out."))
}
case msg @ Containable.ItemPutInSlot(_ : PlanetSideServerObject with Container, _ : Equipment, _ : Int, _ : Option[Equipment]) => case msg @ Containable.ItemPutInSlot(_ : PlanetSideServerObject with Container, _ : Equipment, _ : Int, _ : Option[Equipment]) =>
log.info(s"$msg") log.info(s"$msg")
@ -1506,7 +1523,7 @@ class WorldSessionActor extends Actor
* @return `true`, if the zoning transportation process should start; * @return `true`, if the zoning transportation process should start;
* `false`, otherwise * `false`, otherwise
*/ */
def ContemplateZoningResponse(nextStepMsg : Any) : Boolean = { def ContemplateZoningResponse(nextStepMsg : Any, to : ActorRef) : Boolean = {
val descriptor = zoningType.toString.toLowerCase val descriptor = zoningType.toString.toLowerCase
if(zoningStatus == Zoning.Status.Request) { if(zoningStatus == Zoning.Status.Request) {
DeactivateImplants() DeactivateImplants()
@ -1518,7 +1535,7 @@ class WorldSessionActor extends Actor
zoningReset.cancel zoningReset.cancel
zoningTimer.cancel zoningTimer.cancel
zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset()) zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset())
zoningTimer = context.system.scheduler.scheduleOnce(5 seconds, cluster, nextStepMsg) zoningTimer = context.system.scheduler.scheduleOnce(5 seconds, to, nextStepMsg)
false false
} }
else if(zoningStatus == Zoning.Status.Countdown) { else if(zoningStatus == Zoning.Status.Countdown) {
@ -1531,7 +1548,7 @@ class WorldSessionActor extends Actor
} }
//again //again
zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset()) zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset())
zoningTimer = context.system.scheduler.scheduleOnce(5 seconds, cluster, nextStepMsg) zoningTimer = context.system.scheduler.scheduleOnce(5 seconds, to, nextStepMsg)
false false
} }
else { else {
@ -1833,8 +1850,14 @@ class WorldSessionActor extends Actor
log.warn(s"KillPlayer/SHOTS_WHILE_DEAD: client of ${avatar.name} fired $shotsWhileDead rounds while character was dead on server") log.warn(s"KillPlayer/SHOTS_WHILE_DEAD: client of ${avatar.name} fired $shotsWhileDead rounds while character was dead on server")
shotsWhileDead = 0 shotsWhileDead = 0
} }
import scala.concurrent.ExecutionContext.Implicits.global reviveTimer.cancel
reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, cluster, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(player.Faction), player, 7)) if(player.death_by == 0) {
import scala.concurrent.ExecutionContext.Implicits.global
reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, cluster, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(player.Faction), player, 7))
}
else {
HandleReleaseAvatar(player, continent)
}
case AvatarResponse.LoadPlayer(pkt) => case AvatarResponse.LoadPlayer(pkt) =>
if(tplayer_guid != guid) { if(tplayer_guid != guid) {
@ -2781,14 +2804,8 @@ class WorldSessionActor extends Actor
if(player.VehicleSeated.contains(vehicle_guid)) { if(player.VehicleSeated.contains(vehicle_guid)) {
player.Position = pos player.Position = pos
GetVehicleAndSeat() match { GetVehicleAndSeat() match {
case (Some(_), Some(0)) => ; case (Some(_), Some(seatNum)) if seatNum > 0 =>
case (Some(_), Some(_)) =>
turnCounter(guid) turnCounter(guid)
if (player.death_by == -1) {
sendResponse(ChatMsg(ChatMessageType.UNK_71, true, "", "Your account has been logged out by a Customer Service Representative.", None))
Thread.sleep(300)
sendResponse(DropSession(sessionId, "kick by GM"))
}
case _ => ; case _ => ;
} }
} }
@ -3873,9 +3890,7 @@ class WorldSessionActor extends Actor
continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, player.Position, player.Velocity, yaw, pitch, yaw_upper, seq_time, is_crouching, is_jumping, jump_thrust, is_cloaking, player.spectator, wepInHand)) continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.PlayerState(avatar_guid, player.Position, player.Velocity, yaw, pitch, yaw_upper, seq_time, is_crouching, is_jumping, jump_thrust, is_cloaking, player.spectator, wepInHand))
updateSquad() updateSquad()
if(player.death_by == -1) { if(player.death_by == -1) {
sendResponse(ChatMsg(ChatMessageType.UNK_71, true, "", "Your account has been logged out by a Customer Service Representative.", None)) KickedByAdministration()
Thread.sleep(300)
sendResponse(DropSession(sessionId, "kick by GM"))
} }
case msg@ChildObjectStateMessage(object_guid, pitch, yaw) => case msg@ChildObjectStateMessage(object_guid, pitch, yaw) =>
@ -3906,9 +3921,7 @@ class WorldSessionActor extends Actor
//log.warn(s"ChildObjectState: player ${player.Name} not related to anything with a controllable agent") //log.warn(s"ChildObjectState: player ${player.Name} not related to anything with a controllable agent")
} }
if (player.death_by == -1) { if (player.death_by == -1) {
sendResponse(ChatMsg(ChatMessageType.UNK_71, true, "", "Your account has been logged out by a Customer Service Representative.", None)) KickedByAdministration()
Thread.sleep(300)
sendResponse(DropSession(sessionId, "kick by GM"))
} }
case msg@VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, flying, unk6, unk7, wheels, is_decelerating, is_cloaked) => case msg@VehicleStateMessage(vehicle_guid, unk1, pos, ang, vel, flying, unk6, unk7, wheels, is_decelerating, is_cloaked) =>
@ -3954,9 +3967,7 @@ class WorldSessionActor extends Actor
case _ => ; case _ => ;
} }
if (player.death_by == -1) { if (player.death_by == -1) {
sendResponse(ChatMsg(ChatMessageType.UNK_71, true, "", "Your account has been logged out by a Customer Service Representative.", None)) KickedByAdministration()
Thread.sleep(300)
sendResponse(DropSession(sessionId, "kick by GM"))
} }
case msg@VehicleSubStateMessage(vehicle_guid, player_guid, vehicle_pos, vehicle_ang, vel, unk1, unk2) => case msg@VehicleSubStateMessage(vehicle_guid, player_guid, vehicle_pos, vehicle_ang, vel, unk1, unk2) =>
@ -4041,18 +4052,26 @@ class WorldSessionActor extends Actor
else if(messagetype == ChatMessageType.CMT_RECALL) { else if(messagetype == ChatMessageType.CMT_RECALL) {
makeReply = false makeReply = false
val sanctuary = Zones.SanctuaryZoneId(player.Faction) val sanctuary = Zones.SanctuaryZoneId(player.Faction)
if(zoningType == Zoning.Method.InstantAction) { if(zoningType == Zoning.Method.Quit) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_instantactionting", None)) sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You can't recall to your sanctuary continent while quitting", None))
}
else if(zoningType == Zoning.Method.InstantAction) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You can't recall to your sanctuary continent while instant actioning", None))
} }
else if(zoningType == Zoning.Method.Recall) { else if(zoningType == Zoning.Method.Recall) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You already requested to recall to your sanctuary continent.", None)) sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You already requested to recall to your sanctuary continent", None))
} }
else if(continent.Id.equals(sanctuary)) { else if(continent.Id.equals(sanctuary)) {
//nonstandard message sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You can't recall to your sanctuary continent when you are already on your faction's sanctuary continent", None))
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You can't recall when you are already on your faction's sanctuary continent.", None))
} }
else if(deadState != DeadState.Alive) { else if(!player.isAlive || deadState != DeadState.Alive) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@norecall_dead", None)) if (player.isAlive) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@norecall_deconstructing", None))
//sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You can't recall to your sanctuary continent while deconstructing.", None))
}
else {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@norecall_dead", None))
}
} }
else if(player.VehicleSeated.nonEmpty) { else if(player.VehicleSeated.nonEmpty) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@norecall_invehicle", None)) sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@norecall_invehicle", None))
@ -4067,15 +4086,22 @@ class WorldSessionActor extends Actor
} }
else if(messagetype == ChatMessageType.CMT_INSTANTACTION) { else if(messagetype == ChatMessageType.CMT_INSTANTACTION) {
makeReply = false makeReply = false
if(zoningType == Zoning.Method.InstantAction) { if(zoningType == Zoning.Method.Quit) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You can't instant action while quitting.", None))
}
else if(zoningType == Zoning.Method.InstantAction) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_instantactionting", None)) sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_instantactionting", None))
} }
else if(zoningType == Zoning.Method.Recall) { else if(zoningType == Zoning.Method.Recall) {
//nonstandard message sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You won't instant action. You already requested to recall to your sanctuary continent", None))
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You already requested to recall to your sanctuary continent.", None))
} }
else if(deadState != DeadState.Alive) { else if(!player.isAlive || deadState != DeadState.Alive) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_dead", None)) if(player.isAlive) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_deconstructing", None))
}
else {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_dead", None))
}
} }
else if(player.VehicleSeated.nonEmpty) { else if(player.VehicleSeated.nonEmpty) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_invehicle", None)) sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_invehicle", None))
@ -4088,6 +4114,33 @@ class WorldSessionActor extends Actor
cluster ! Zoning.InstantAction.Request(player.Faction) cluster ! Zoning.InstantAction.Request(player.Faction)
} }
} }
else if(messagetype == ChatMessageType.CMT_QUIT) {
makeReply = false
if(zoningType == Zoning.Method.Quit) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noquit_quitting", None))
}
else if(!player.isAlive || deadState != DeadState.Alive) {
if(player.isAlive) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noquit_deconstructing", None))
}
else {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noquit_dead", None))
}
}
else if(player.VehicleSeated.nonEmpty) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noquit_invehicle", None))
}
else {
//priority to quitting is given to quit over other zoning methods
if(zoningType == Zoning.Method.InstantAction || zoningType == Zoning.Method.Recall) {
CancelZoningProcessWithDescriptiveReason("cancel")
}
zoningType = Zoning.Method.Quit
zoningChatMessageType = messagetype
zoningStatus = Zoning.Status.Request
self ! Zoning.Quit()
}
}
CSRZone.read(traveler, msg) match { CSRZone.read(traveler, msg) match {
case (true, zone, pos) => case (true, zone, pos) =>
if (player.isAlive && zone != player.Continent && (admin || zone == "z8" || zone == "c1" || zone == "c2" || zone == "c3" || zone == "c4" || zone == "c5" || zone == "c6" || if (player.isAlive && zone != player.Continent && (admin || zone == "z8" || zone == "c1" || zone == "c2" || zone == "c3" || zone == "c4" || zone == "c5" || zone == "c6" ||
@ -4174,9 +4227,6 @@ class WorldSessionActor extends Actor
case _ => case _ =>
self ! PacketCoding.CreateGamePacket(0, RequestDestroyMessage(PlanetSideGUID(guid))) self ! PacketCoding.CreateGamePacket(0, RequestDestroyMessage(PlanetSideGUID(guid)))
} }
} else if(messagetype == ChatMessageType.CMT_QUIT) { // TODO: handle this appropriately
sendResponse(DropCryptoSession())
sendResponse(DropSession(sessionId, "user quit"))
} }
//dev hack; consider bang-commands to complement slash-commands in future //dev hack; consider bang-commands to complement slash-commands in future
if(trimContents.equals("!loc")) { if(trimContents.equals("!loc")) {
@ -4351,25 +4401,26 @@ class WorldSessionActor extends Actor
// StopBundlingPackets() // StopBundlingPackets()
} }
else if (trimContents.contains("!kick") && admin) { else if (trimContents.contains("!kick") && admin) {
val CharIDorName : String = contents.drop(contents.indexOf(" ") + 1) val input = trimContents.split("\\s+").drop(1)
try { if(input.length > 0) {
val charID : Long = CharIDorName.toLong val numRegex = raw"(\d+)".r
if(charID != player.CharId) { val id = input(0)
var charToKick = continent.LivePlayers.filter(_.CharId == charID) val determination : Player=>Boolean = id match {
if (charToKick.nonEmpty) { case numRegex(_) => { _.CharId == id.toLong }
charToKick.head.death_by = -1 case _ => { _.Name.equals(id) }
}
else {
charToKick = continent.Corpses.filter(_.CharId == charID)
if (charToKick.nonEmpty) charToKick.head.death_by = -1
}
} }
} continent.LivePlayers.find(determination).orElse(continent.Corpses.find(determination)) match {
catch { case Some(tplayer) if AdministrativeKick(tplayer) =>
case _ : Throwable => if(input.length > 1) {
{ val time = input(1)
val charToKick = continent.LivePlayers.filter(_.Name.equalsIgnoreCase(CharIDorName)) time match {
if(charToKick.nonEmpty) charToKick.head.death_by = -1 case numRegex(_) =>
accountPersistence ! AccountPersistenceService.Kick(tplayer.Name, Some(time.toLong))
case _ =>
accountPersistence ! AccountPersistenceService.Kick(tplayer.Name, None)
}
}
case _ => ;
} }
} }
} }
@ -5011,7 +5062,8 @@ class WorldSessionActor extends Actor
val lastUse = player.GetLastUsedTime(kid) val lastUse = player.GetLastUsedTime(kid)
val delay = delayedGratificationEntries.getOrElse(kid, 0L) val delay = delayedGratificationEntries.getOrElse(kid, 0L)
if((time - lastUse) < delay) { if((time - lastUse) < delay) {
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^${((delay / 1000) - math.ceil((time - lastUse).toDouble) / 1000)}~", None)) val displayedDelay = math.min(5, ((delay.toDouble / 1000) - math.ceil((time - lastUse).toDouble) / 1000) + 1).toInt
sendResponse(ChatMsg(ChatMessageType.UNK_225, false, "", s"@TimeUntilNextUse^$displayedDelay~", None))
} }
else { else {
val indexOpt = player.Find(kit) val indexOpt = player.Find(kit)
@ -7616,6 +7668,7 @@ class WorldSessionActor extends Actor
val obj = Player.Respawn(tplayer) val obj = Player.Respawn(tplayer)
obj.ResetAllImplants() obj.ResetAllImplants()
LoadClassicDefault(obj) LoadClassicDefault(obj)
obj.death_by = tplayer.death_by
obj obj
} }
@ -7688,7 +7741,15 @@ class WorldSessionActor extends Actor
/** /**
* Creates a player that has the characteristics of a corpse. * Creates a player that has the characteristics of a corpse.
* To the game, that is a backpack (or some pastry, festive graphical modification allowing). * To the game, that is a backpack (or some pastry, festive graphical modification allowing).
* A player who has been kicked may not turn into a corpse.
* @see `AvatarAction.Release`
* @see `AvatarServiceMessage`
* @see `CorpseConverter.converter` * @see `CorpseConverter.converter`
* @see `DepictPlayerAsCorpse`
* @see `Player.Release`
* @see `Zone.AvatarEvents`
* @see `Zone.Corpse.Add`
* @see `Zone.Population`
* @param tplayer the player * @param tplayer the player
*/ */
def TurnPlayerIntoCorpse(tplayer : Player, zone : Zone) : Unit = { def TurnPlayerIntoCorpse(tplayer : Player, zone : Zone) : Unit = {
@ -7729,7 +7790,7 @@ class WorldSessionActor extends Actor
*/ */
def TryDisposeOfLootedCorpse(obj : Player) : Boolean = { def TryDisposeOfLootedCorpse(obj : Player) : Boolean = {
if(obj.isBackpack && WellLootedDeadBody(obj)) { if(obj.isBackpack && WellLootedDeadBody(obj)) {
continent.AvatarEvents ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), continent)) obj.Zone.AvatarEvents ! AvatarServiceMessage.Corpse(RemoverActor.HurrySpecific(List(obj), obj.Zone))
true true
} }
else { else {
@ -8149,7 +8210,7 @@ class WorldSessionActor extends Actor
target match { target match {
case obj : Player if obj.CanDamage && obj.Actor != Default.Actor => case obj : Player if obj.CanDamage && obj.Actor != Default.Actor =>
if(obj.spectator) { if(obj.spectator) {
player.death_by = -1 // little thing for auto kick AdministrativeKick(player, obj != player) // little thing for auto kick
} }
else { else {
obj.Actor ! Vitality.Damage(func) obj.Actor ! Vitality.Damage(func)
@ -10169,6 +10230,39 @@ class WorldSessionActor extends Actor
} }
} }
def AdministrativeKick(tplayer : Player, permitKickSelf : Boolean = false) : Boolean = {
if(permitKickSelf || tplayer != player) { //stop kicking yourself
tplayer.death_by = -1
accountPersistence ! AccountPersistenceService.Kick(tplayer.Name)
//get out of that vehicle
GetMountableAndSeat(None, tplayer, continent) match {
case (Some(obj), Some(seatNum)) =>
tplayer.VehicleSeated = None
obj.Seats(seatNum).Occupant = None
continent.VehicleEvents ! VehicleServiceMessage(continent.Id, VehicleAction.KickPassenger(tplayer.GUID, seatNum, false, obj.GUID))
case _ => ;
}
true
}
else {
false
}
}
def KickedByAdministration() : Unit = {
sendResponse(DisconnectMessage("Your account has been logged out by a Customer Service Representative."))
Thread.sleep(300)
sendResponse(DropSession(sessionId, "kick by GM"))
}
def ImmediateDisconnect() : Unit = {
if(avatar != null) {
accountPersistence ! AccountPersistenceService.Logout(avatar.name)
}
sendResponse(DropCryptoSession())
sendResponse(DropSession(sessionId, "user quit"))
}
def failWithError(error : String) = { def failWithError(error : String) = {
log.error(error) log.error(error)
sendResponse(ConnectionClose()) sendResponse(ConnectionClose())