diff --git a/common/src/main/scala/net/psforever/objects/zones/Zoning.scala b/common/src/main/scala/net/psforever/objects/zones/Zoning.scala
index b55735fee..e1e8f651e 100644
--- a/common/src/main/scala/net/psforever/objects/zones/Zoning.scala
+++ b/common/src/main/scala/net/psforever/objects/zones/Zoning.scala
@@ -10,7 +10,8 @@ object Zoning {
val
None,
InstantAction,
- Recall
+ Recall,
+ Quit
= Value
}
@@ -35,6 +36,8 @@ object Zoning {
final val Enemy = TimeType(30, "Enemy")
}
+ final case class Quit()
+
object InstantAction {
final case class Request(faction : PlanetSideEmpire.Value)
diff --git a/common/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala b/common/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala
index 3c4df4ba8..8ce98ab8d 100644
--- a/common/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala
+++ b/common/src/main/scala/net/psforever/packet/game/GenericActionMessage.scala
@@ -26,9 +26,10 @@ import scodec.codecs._
* 24 - message: you have been imprinted (updates imprinted status; does it?)
* 25 - message: you are no longer imprinted (updates imprinted status; does it?)
* 27 - event: purchase timers reset (does it?)
- * 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
+ * 32 - forced into first person view, attempt to deconstruct but fail;
* event: fail to deconstruct due to having a "parent vehicle"
- * 32 - switch to first person view
* 33 - event: fail to deconstruct
* 43 - prompt: friendly fire in virtual reality zone
* 45 - ?
diff --git a/common/src/main/scala/services/account/AccountPersistenceService.scala b/common/src/main/scala/services/account/AccountPersistenceService.scala
index 3c7f290d4..c9710153b 100644
--- a/common/src/main/scala/services/account/AccountPersistenceService.scala
+++ b/common/src/main/scala/services/account/AccountPersistenceService.scala
@@ -11,9 +11,8 @@ import net.psforever.objects._
import net.psforever.objects.serverobject.mount.Mountable
import net.psforever.objects.zones.Zone
import net.psforever.types.Vector3
-import services.{RemoverActor, Service, ServiceManager}
+import services.{Service, ServiceManager}
import services.avatar.{AvatarAction, AvatarServiceMessage}
-import services.vehicle.VehicleServiceMessage
/**
* 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) =>
ref ! msg
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)
}
+ 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?
accounts.remove(target)
@@ -114,7 +137,7 @@ class AccountPersistenceService extends Actor {
}
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
*/
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
/** the last-reported game coordinate position of this player */
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 */
var timer : Cancellable = Default.Cancellable
/** the sparingly-used log */
@@ -204,17 +249,44 @@ class PersistenceMonitor(name : String, squadService : ActorRef, taskResolver :
def receive : Receive = {
case AccountPersistenceService.Login(_) =>
- sender ! PlayerToken.LoginInfo(name, inZone, lastPosition)
- UpdateTimer()
+ sender ! (if(kicked) {
+ 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
lastPosition = p
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(_) =>
- context.parent ! Logout(name)
- context.stop(self)
+ kickTime match {
+ 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 _ => ;
}
@@ -224,7 +296,7 @@ class PersistenceMonitor(name : String, squadService : ActorRef, taskResolver :
*/
def UpdateTimer() : Unit = {
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.
*/
def PerformLogout() : Unit = {
- log.info(s"logout of $name")
(inZone.Players.find(p => p.name == name), inZone.LivePlayers.find(p => p.Name == name)) match {
case (Some(avatar), Some(player)) if player.VehicleSeated.nonEmpty =>
//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)
Deployables.Disown(inZone, 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)
object PlayerToken {
+ object DeniedLoginReason extends Enumeration {
+ val
+ Denied, //generic
+ Kicked
+ = Value
+ }
+
/**
* Message dispatched to confirm that a player with given locational attributes exists.
* 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
*/
final case class LoginInfo(name : String, zone : Zone, position : Vector3)
+
+ final case class CanNotLogin(name : String, reason : DeniedLoginReason.Value)
}
diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/pslogin/src/main/scala/WorldSessionActor.scala
index d76c864ff..30f29ea51 100644
--- a/pslogin/src/main/scala/WorldSessionActor.scala
+++ b/pslogin/src/main/scala/WorldSessionActor.scala
@@ -1092,7 +1092,7 @@ class WorldSessionActor extends Actor
case msg@Zoning.InstantAction.Located(zone, _, spawn_point) =>
//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)
- if(ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction))) {
+ if(ContemplateZoningResponse(Zoning.InstantAction.Request(player.Faction), cluster)) {
val (pos, ori) = spawn_point.SpecificPoint(player)
SpawnThroughZoningProcess(zone, pos, ori)
}
@@ -1103,7 +1103,7 @@ class WorldSessionActor extends Actor
case Zoning.InstantAction.NotLocated() =>
instantActionFallbackDestination match {
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)
SpawnThroughZoningProcess(zone, pos, ori)
}
@@ -1116,7 +1116,7 @@ class WorldSessionActor extends Actor
}
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)
SpawnThroughZoningProcess(zone, pos, ori)
}
@@ -1124,6 +1124,12 @@ class WorldSessionActor extends Actor
case Zoning.Recall.Denied(reason) =>
CancelZoningProcessWithReason(s"@norecall_sanctuary_$reason", Some(ChatMessageType.CMT_QUIT))
+ case Zoning.Quit() =>
+ if(ContemplateZoningResponse(Zoning.Quit(), self)) {
+ log.info("Good-bye")
+ ImmediateDisconnect()
+ }
+
case ZoningReset() =>
CancelZoningProcess()
@@ -1375,6 +1381,10 @@ class WorldSessionActor extends Actor
inZone.AvatarEvents ! AvatarServiceMessage(playerName, AvatarAction.TeardownConnection())
//find and reload previous player
(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 =>
//rejoin current avatar/player
log.info(s"LoginInfo: player $playerName is alive")
@@ -1415,10 +1425,17 @@ class WorldSessionActor extends Actor
case _ =>
//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)
}
+ 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]) =>
log.info(s"$msg")
@@ -1506,7 +1523,7 @@ class WorldSessionActor extends Actor
* @return `true`, if the zoning transportation process should start;
* `false`, otherwise
*/
- def ContemplateZoningResponse(nextStepMsg : Any) : Boolean = {
+ def ContemplateZoningResponse(nextStepMsg : Any, to : ActorRef) : Boolean = {
val descriptor = zoningType.toString.toLowerCase
if(zoningStatus == Zoning.Status.Request) {
DeactivateImplants()
@@ -1518,7 +1535,7 @@ class WorldSessionActor extends Actor
zoningReset.cancel
zoningTimer.cancel
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
}
else if(zoningStatus == Zoning.Status.Countdown) {
@@ -1531,7 +1548,7 @@ class WorldSessionActor extends Actor
}
//again
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
}
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")
shotsWhileDead = 0
}
- import scala.concurrent.ExecutionContext.Implicits.global
- reviveTimer = context.system.scheduler.scheduleOnce(respawnTimer milliseconds, cluster, Zone.Lattice.RequestSpawnPoint(Zones.SanctuaryZoneNumber(player.Faction), player, 7))
+ reviveTimer.cancel
+ 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) =>
if(tplayer_guid != guid) {
@@ -2781,14 +2804,8 @@ class WorldSessionActor extends Actor
if(player.VehicleSeated.contains(vehicle_guid)) {
player.Position = pos
GetVehicleAndSeat() match {
- case (Some(_), Some(0)) => ;
- case (Some(_), Some(_)) =>
+ case (Some(_), Some(seatNum)) if seatNum > 0 =>
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 _ => ;
}
}
@@ -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))
updateSquad()
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"))
+ KickedByAdministration()
}
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")
}
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"))
+ KickedByAdministration()
}
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 _ => ;
}
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"))
+ KickedByAdministration()
}
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) {
makeReply = false
val sanctuary = Zones.SanctuaryZoneId(player.Faction)
- if(zoningType == Zoning.Method.InstantAction) {
- sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_instantactionting", None))
+ if(zoningType == Zoning.Method.Quit) {
+ 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) {
- 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)) {
- //nonstandard message
- sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You can't recall when you are already on your faction's sanctuary continent.", None))
+ 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))
}
- else if(deadState != DeadState.Alive) {
- sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@norecall_dead", None))
+ else if(!player.isAlive || deadState != DeadState.Alive) {
+ 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) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@norecall_invehicle", None))
@@ -4067,15 +4086,22 @@ class WorldSessionActor extends Actor
}
else if(messagetype == ChatMessageType.CMT_INSTANTACTION) {
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))
}
else if(zoningType == Zoning.Method.Recall) {
- //nonstandard message
- sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You already requested to recall to your sanctuary continent.", None))
+ sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "You won't instant action. You already requested to recall to your sanctuary continent", None))
}
- else if(deadState != DeadState.Alive) {
- sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_dead", None))
+ else if(!player.isAlive || deadState != DeadState.Alive) {
+ 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) {
sendResponse(ChatMsg(ChatMessageType.CMT_QUIT, false, "", "@noinstantaction_invehicle", None))
@@ -4088,6 +4114,33 @@ class WorldSessionActor extends Actor
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 {
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" ||
@@ -4174,9 +4227,6 @@ class WorldSessionActor extends Actor
case _ =>
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
if(trimContents.equals("!loc")) {
@@ -4351,25 +4401,26 @@ class WorldSessionActor extends Actor
// StopBundlingPackets()
}
else if (trimContents.contains("!kick") && admin) {
- val CharIDorName : String = contents.drop(contents.indexOf(" ") + 1)
- try {
- val charID : Long = CharIDorName.toLong
- if(charID != player.CharId) {
- var charToKick = continent.LivePlayers.filter(_.CharId == charID)
- if (charToKick.nonEmpty) {
- charToKick.head.death_by = -1
- }
- else {
- charToKick = continent.Corpses.filter(_.CharId == charID)
- if (charToKick.nonEmpty) charToKick.head.death_by = -1
- }
+ val input = trimContents.split("\\s+").drop(1)
+ if(input.length > 0) {
+ val numRegex = raw"(\d+)".r
+ val id = input(0)
+ val determination : Player=>Boolean = id match {
+ case numRegex(_) => { _.CharId == id.toLong }
+ case _ => { _.Name.equals(id) }
}
- }
- catch {
- case _ : Throwable =>
- {
- val charToKick = continent.LivePlayers.filter(_.Name.equalsIgnoreCase(CharIDorName))
- if(charToKick.nonEmpty) charToKick.head.death_by = -1
+ continent.LivePlayers.find(determination).orElse(continent.Corpses.find(determination)) match {
+ case Some(tplayer) if AdministrativeKick(tplayer) =>
+ if(input.length > 1) {
+ val time = input(1)
+ time match {
+ 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 delay = delayedGratificationEntries.getOrElse(kid, 0L)
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 {
val indexOpt = player.Find(kit)
@@ -7616,6 +7668,7 @@ class WorldSessionActor extends Actor
val obj = Player.Respawn(tplayer)
obj.ResetAllImplants()
LoadClassicDefault(obj)
+ obj.death_by = tplayer.death_by
obj
}
@@ -7688,7 +7741,15 @@ class WorldSessionActor extends Actor
/**
* Creates a player that has the characteristics of a corpse.
* 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 `DepictPlayerAsCorpse`
+ * @see `Player.Release`
+ * @see `Zone.AvatarEvents`
+ * @see `Zone.Corpse.Add`
+ * @see `Zone.Population`
* @param tplayer the player
*/
def TurnPlayerIntoCorpse(tplayer : Player, zone : Zone) : Unit = {
@@ -7729,7 +7790,7 @@ class WorldSessionActor extends Actor
*/
def TryDisposeOfLootedCorpse(obj : Player) : Boolean = {
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
}
else {
@@ -8149,7 +8210,7 @@ class WorldSessionActor extends Actor
target match {
case obj : Player if obj.CanDamage && obj.Actor != Default.Actor =>
if(obj.spectator) {
- player.death_by = -1 // little thing for auto kick
+ AdministrativeKick(player, obj != player) // little thing for auto kick
}
else {
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) = {
log.error(error)
sendResponse(ConnectionClose())