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 b55735fe..e1e8f651 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 3c4df4ba..8ce98ab8 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 3c7f290d..c9710153 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 d76c864f..30f29ea5 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())