diff --git a/build.sbt b/build.sbt index 938a7b61..29b0a77d 100644 --- a/build.sbt +++ b/build.sbt @@ -4,6 +4,7 @@ lazy val commonSettings = Seq( organization := "net.psforever", version := "1.0.2-SNAPSHOT", scalaVersion := "2.13.2", + Global / cancelable := false, semanticdbEnabled := true, semanticdbVersion := scalafixSemanticdb.revision, scalacOptions := Seq( @@ -110,7 +111,7 @@ lazy val pslogin = (project in file("pslogin")) // ActorTests have specific timing requirements and will be flaky if run in parallel parallelExecution in Test := false, // TODO(chord): remove exclusion when WorldSessionActor is refactored: https://github.com/psforever/PSF-LoginServer/issues/279 - coverageExcludedPackages := "net.psforever.pslogin.WorldSessionActor.*;net.psforever.pslogin.zonemaps.*", + coverageExcludedPackages := "net.psforever.actors.session.SessionActor.*;net.psforever.zones.zonemaps.*", // Copy all tests from Test -> QuietTest (we're only changing the run options) inConfig(QuietTest)(Defaults.testTasks) ) diff --git a/common/src/main/scala/net/psforever/actors/session/ChatActor.scala b/common/src/main/scala/net/psforever/actors/session/ChatActor.scala new file mode 100644 index 00000000..ba3252e8 --- /dev/null +++ b/common/src/main/scala/net/psforever/actors/session/ChatActor.scala @@ -0,0 +1,755 @@ +package net.psforever.actors.session + +import akka.actor.Cancellable +import akka.actor.typed.receptionist.Receptionist +import akka.actor.typed.{ActorRef, Behavior, PostStop, Signal} +import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} +import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} +import net.psforever.objects.{Default, GlobalDefinitions, Player, Session} +import net.psforever.objects.serverobject.resourcesilo.ResourceSilo +import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurrets} +import net.psforever.objects.zones.Zoning +import net.psforever.packet.PacketCoding +import net.psforever.packet.game.{ChatMsg, DeadState, RequestDestroyMessage, ZonePopulationUpdateMessage} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID, Vector3} +import net.psforever.util.PointOfInterest +import net.psforever.zones.Zones +import services.chat.ChatService +import services.chat.ChatService.ChatChannel +import services.local.{LocalAction, LocalServiceMessage} + +import scala.concurrent.duration._ +import scala.concurrent.ExecutionContext.Implicits.global + +object ChatActor { + def apply(sessionActor: ActorRef[SessionActor.Command]): Behavior[Command] = + Behaviors.setup(context => new ChatActor(context, sessionActor)) + + sealed trait Command + + final case class JoinChannel(channel: ChatChannel) extends Command + final case class LeaveChannel(channel: ChatChannel) extends Command + final case class Message(message: ChatMsg) extends Command + final case class SetSession(session: Session) extends Command + + private case class ListingResponse(listing: Receptionist.Listing) extends Command + private case class IncomingMessage(session: Session, message: ChatMsg, channel: ChatChannel) extends Command +} + +class ChatActor(context: ActorContext[ChatActor.Command], sessionActor: ActorRef[SessionActor.Command]) + extends AbstractBehavior[ChatActor.Command](context) { + import ChatActor._ + + private[this] val log = org.log4s.getLogger + var channels: List[ChatChannel] = List() + var session: Option[Session] = None + var chatService: Option[ActorRef[ChatService.Command]] = None + var silenceTimer: Cancellable = Default.Cancellable + + val chatServiceAdapter: ActorRef[ChatService.MessageResponse] = context.messageAdapter[ChatService.MessageResponse] { + case ChatService.MessageResponse(session, message, channel) => IncomingMessage(session, message, channel) + } + + override def onSignal: PartialFunction[Signal, Behavior[Command]] = { + case PostStop => + silenceTimer.cancel() + if (chatService.isDefined) chatService.get ! ChatService.LeaveAllChannels(chatServiceAdapter) + this + } + + override def onMessage(msg: Command): Behavior[Command] = { + import ChatMessageType._ + + msg match { + case ListingResponse(ChatService.ChatServiceKey.Listing(listings)) => + chatService = Some(listings.head) + chatService.get ! ChatService.JoinChannel(chatServiceAdapter, session.get, ChatChannel.Default()) + channels ++= List(ChatChannel.Default()) + this + + case SetSession(newSession) => + session = Some(newSession) + if (chatService.isEmpty && newSession.player != null) { // TODO the player check sucks... + context.system.receptionist ! Receptionist.Find( + ChatService.ChatServiceKey, + context.messageAdapter[Receptionist.Listing](ListingResponse) + ) + } + this + + case JoinChannel(channel) => + chatService.get ! ChatService.JoinChannel(chatServiceAdapter, session.get, channel) + channels ++= List(channel) + this + + case LeaveChannel(channel) => + chatService.get ! ChatService.LeaveChannel(chatServiceAdapter, channel) + channels = channels.filter(_ == channel) + this + + /** Some messages are sent during login so we handle them prematurely because main message handler requires the + * session object and chat service and they may not be set yet + */ + case Message(ChatMsg(CMT_CULLWATERMARK, _, _, contents, _)) => + val connectionState = + if (contents.contains("40 80")) 100 + else if (contents.contains("120 200")) 25 + else 50 + sessionActor ! SessionActor.SetConnectionState(connectionState) + this + + case Message(ChatMsg(CMT_ANONYMOUS, _, _, _, _)) => + // ?? + this + + case Message(ChatMsg(CMT_TOGGLE_GM, _, _, _, _)) => + // ?? + this + + case Message(message) => + log.info("Chat: " + message) + + (session, chatService) match { + case (Some(session), Some(chatService)) => + (message.messageType, message.recipient.trim, message.contents.trim) match { + case (CMT_FLY, recipient, contents) if session.admin => + val flying = contents match { + case "on" => true + case "off" => false + case _ => !session.flying + } + sessionActor ! SessionActor.SetFlying(flying) + sessionActor ! SessionActor.SendResponse( + ChatMsg(CMT_FLY, false, recipient, if (flying) "on" else "off", None) + ) + + case (CMT_SPEED, recipient, contents) => + val speed = + try { + contents.toFloat + } catch { + case _: Throwable => + 1f + } + sessionActor ! SessionActor.SetSpeed(speed) + sessionActor ! SessionActor.SendResponse(message.copy(contents = f"$speed%.3f")) + + case (CMT_TOGGLESPECTATORMODE, _, contents) if session.admin => + val spectator = contents match { + case "on" => true + case "off" => false + case _ => !session.player.spectator + } + sessionActor ! SessionActor.SetSpectator(spectator) + sessionActor ! SessionActor.SendResponse(message.copy(contents = if (spectator) "on" else "off")) + + case (CMT_RECALL, _, _) => + val sanctuary = Zones.SanctuaryZoneId(session.player.Faction) + val errorMessage = session.zoningType match { + case Zoning.Method.Quit => Some("You can't recall to your sanctuary continent while quitting") + case Zoning.Method.InstantAction => + Some("You can't recall to your sanctuary continent while instant actioning") + case Zoning.Method.Recall => Some("You already requested to recall to your sanctuary continent") + case _ if session.zone.Id == sanctuary => + Some("You can't recall to your sanctuary when you are already in your sanctuary") + case _ if !session.player.isAlive || session.deadState != DeadState.Alive => + Some(if (session.player.isAlive) "@norecall_deconstructing" else "@norecall_dead") + case _ if session.player.VehicleSeated.nonEmpty => Some("@norecall_invehicle") + case _ => None + } + errorMessage match { + case Some(errorMessage) => + sessionActor ! SessionActor.SendResponse( + ChatMsg( + CMT_QUIT, + false, + "", + errorMessage, + None + ) + ) + case None => + sessionActor ! SessionActor.Recall(sanctuary) + } + + case (CMT_INSTANTACTION, _, _) => + if (session.zoningType == Zoning.Method.Quit) { + sessionActor ! SessionActor.SendResponse( + ChatMsg(CMT_QUIT, false, "", "You can't instant action while quitting.", None) + ) + } else if (session.zoningType == Zoning.Method.InstantAction) { + sessionActor ! SessionActor.SendResponse( + ChatMsg(CMT_QUIT, false, "", "@noinstantaction_instantactionting", None) + ) + } else if (session.zoningType == Zoning.Method.Recall) { + sessionActor ! SessionActor.SendResponse( + ChatMsg( + CMT_QUIT, + false, + "", + "You won't instant action. You already requested to recall to your sanctuary continent", + None + ) + ) + } else if (!session.player.isAlive || session.deadState != DeadState.Alive) { + if (session.player.isAlive) { + sessionActor ! SessionActor.SendResponse( + ChatMsg(CMT_QUIT, false, "", "@noinstantaction_deconstructing", None) + ) + } else { + sessionActor ! SessionActor.SendResponse( + ChatMsg(CMT_QUIT, false, "", "@noinstantaction_dead", None) + ) + } + } else if (session.player.VehicleSeated.nonEmpty) { + sessionActor ! SessionActor.SendResponse( + ChatMsg(CMT_QUIT, false, "", "@noinstantaction_invehicle", None) + ) + } else { + sessionActor ! SessionActor.InstantAction() + } + + case (CMT_QUIT, _, _) => + if (session.zoningType == Zoning.Method.Quit) { + sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_quitting", None)) + } else if (!session.player.isAlive || session.deadState != DeadState.Alive) { + if (session.player.isAlive) { + sessionActor ! SessionActor.SendResponse( + ChatMsg(CMT_QUIT, false, "", "@noquit_deconstructing", None) + ) + } else { + sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_dead", None)) + } + } else if (session.player.VehicleSeated.nonEmpty) { + sessionActor ! SessionActor.SendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_invehicle", None)) + } else { + sessionActor ! SessionActor.Quit() + } + + case (CMT_SUICIDE, _, _) => + if (session.player.isAlive && session.deadState != DeadState.Release) { + sessionActor ! SessionActor.Suicide() + } + + case (CMT_DESTROY, recipient, contents) => + val guid = contents.toInt + session.zone.GUID(session.zone.Map.TerminalToSpawnPad.getOrElse(guid, guid)) match { + case Some(pad: VehicleSpawnPad) => + pad.Actor ! VehicleSpawnControl.ProcessControl.Flush + case Some(turret: FacilityTurret) if turret.isUpgrading => + WeaponTurrets.FinishUpgradingMannedTurret(turret, TurretUpgrade.None) + case _ => + sessionActor ! SessionActor.SendResponse( + PacketCoding.CreateGamePacket(0, RequestDestroyMessage(PlanetSideGUID(guid))).packet + ) + } + sessionActor ! SessionActor.SendResponse(message) + + case (_, _, "!loc") => + val continent = session.zone + val player = session.player + val loc = + s"zone=${continent.Id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}" + log.info(loc) + sessionActor ! SessionActor.SendResponse(message.copy(contents = loc)) + + case (_, _, contents) if contents.startsWith("!list") => + val zone = contents.split(" ").lift(1) match { + case None => + Some(session.zone) + case Some(id) => + Zones.zones.get(id) + } + + zone match { + case Some(zone) => + sessionActor ! SessionActor.SendResponse( + ChatMsg( + CMT_GMOPEN, + message.wideContents, + "Server", + "\\#8Name (Faction) [ID] at PosX PosY PosZ", + message.note + ) + ) + + (zone.LivePlayers ++ zone.Corpses) + .filter(_.CharId != session.player.CharId) + .sortBy(_.Name) + .foreach(player => { + sessionActor ! SessionActor.SendResponse( + ChatMsg( + CMT_GMOPEN, + message.wideContents, + "Server", + s"\\#7${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}", + message.note + ) + ) + }) + case None => + sessionActor ! SessionActor.SendResponse( + ChatMsg( + CMT_GMOPEN, + message.wideContents, + "Server", + "Invalid zone ID", + message.note + ) + ) + } + + case (_, _, contents) if session.admin && contents.startsWith("!kick") => + val input = contents.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) + } + session.zone.LivePlayers + .find(determination) + .orElse(session.zone.Corpses.find(determination)) match { + case Some(player) => + input.lift(1) match { + case Some(numRegex(time)) => + sessionActor ! SessionActor.Kick(player, Some(time.toLong)) + case _ => + sessionActor ! SessionActor.Kick(player) + } + case None => + sessionActor ! SessionActor.SendResponse( + ChatMsg( + CMT_GMOPEN, + message.wideContents, + "Server", + "Invalid player", + message.note + ) + ) + } + } + + case (CMT_CAPTUREBASE, _, contents) if session.admin => + val args = contents.split(" ").filter(_ != "") + + val (faction, factionPos) = args.zipWithIndex + .map { case (faction, pos) => (faction.toLowerCase, pos) } + .flatMap { + case ("tr", pos) => Some(PlanetSideEmpire.TR, pos) + case ("nc", pos) => Some(PlanetSideEmpire.NC, pos) + case ("vs", pos) => Some(PlanetSideEmpire.VS, pos) + case ("none", pos) => Some(PlanetSideEmpire.NEUTRAL, pos) + case _ => None + } + .headOption match { + case Some((faction, pos)) => (faction, Some(pos)) + case None => (session.player.Faction, None) + } + + val (buildingsOption, buildingPos) = args.zipWithIndex.flatMap { + case (_, pos) if factionPos.isDefined && factionPos.get == pos => None + case ("all", pos) => + Some( + Some( + session.zone.Buildings + .filter { + case (_, building) => building.CaptureTerminal.isDefined + } + .values + .toSeq + ), + Some(pos) + ) + case (name, pos) => + session.zone.Buildings.find { + case (_, building) => name.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined + } match { + case Some((_, building)) => Some(Some(Seq(building)), Some(pos)) + case None => + try { + // check if we have a timer + name.toInt + None + } catch { + case _: Throwable => + Some(None, Some(pos)) + } + } + }.headOption match { + case Some((buildings, pos)) => (buildings, pos) + case None => (None, None) + } + + val (timerOption, timerPos) = args.zipWithIndex.flatMap { + case (_, pos) + if factionPos.isDefined && factionPos.get == pos || buildingPos.isDefined && buildingPos.get == pos => + None + case (timer, pos) => + try { + val t = timer.toInt // TODO what is the timer format supposed to be? + Some(Some(t), Some(pos)) + } catch { + case _: Throwable => + Some(None, Some(pos)) + } + }.headOption match { + case Some((timer, posOption)) => (timer, posOption) + case None => (None, None) + } + + (factionPos, buildingPos, timerPos, buildingsOption, timerOption) match { + case // [[|none []] + (Some(0), None, Some(1), None, Some(_)) | (Some(0), None, None, None, None) | + (None, None, None, None, None) | + // [ [|none [timer]]] + (None | Some(1), Some(0), None, Some(_), None) | (Some(1), Some(0), Some(2), Some(_), Some(_)) | + // [all [|none]] + (Some(1) | None, Some(0), None, Some(_), None) => + val buildings = buildingsOption.getOrElse( + session.zone.Buildings + .filter { + case (_, building) => + building.PlayersInSOI.exists { soiPlayer => + session.player.CharId == soiPlayer.CharId + } + } + .map { case (_, building) => building } + ) + buildings foreach { building => + // TODO implement timer + building.Faction = faction + session.zone.LocalEvents ! LocalServiceMessage( + session.zone.Id, + LocalAction.SetEmpire(building.GUID, faction) + ) + } + case (_, Some(0), _, None, _) => + sessionActor ! SessionActor.SendResponse( + ChatMsg( + UNK_229, + true, + "", + s"\\#FF4040ERROR - \'${args(0)}\' is not a valid building name.", + None + ) + ) + case (Some(0), _, Some(1), _, None) | (Some(1), Some(0), Some(2), _, None) => + sessionActor ! SessionActor.SendResponse( + ChatMsg( + UNK_229, + true, + "", + s"\\#FF4040ERROR - \'${args(timerPos.get)}\' is not a valid timer value.", + None + ) + ) + case _ => + sessionActor ! SessionActor.SendResponse( + ChatMsg( + UNK_229, + true, + "", + "usage: /capturebase [[|none []] | [ [|none [timer]]] | [all [|none]]", + None + ) + ) + } + + case (CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_VS | CMT_GMBROADCAST_TR, _, _) + if session.admin => + chatService ! ChatService.Message( + session, + message.copy(recipient = session.player.Name), + ChatChannel.Default() + ) + + case (CMT_GMTELL, _, _) if session.admin => + chatService ! ChatService.Message( + session, + message.copy(recipient = session.player.Name), + ChatChannel.Default() + ) + + case (CMT_GMBROADCASTPOPUP, _, _) if session.admin => + chatService ! ChatService.Message( + session, + message.copy(recipient = session.player.Name), + ChatChannel.Default() + ) + + case (_, _, contents) if contents.startsWith("!whitetext ") && session.admin => + chatService ! ChatService.Message( + session, + ChatMsg(UNK_227, true, "", contents.replace("!whitetext ", ""), None), + ChatChannel.Default() + ) + + case (_, "tr", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, contents.toInt, 138, contents.toInt / 2, 138, 0, 138, 0) + ) + + case (_, "nc", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, 0, 138, contents.toInt, 138, contents.toInt / 3, 138, 0) + ) + + case (_, "vs", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, contents.toInt * 2, 138, 0, 138, contents.toInt, 138, 0) + ) + + case (_, "bo", contents) => + sessionActor ! SessionActor.SendResponse( + ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt) + ) + + case (_, _, contents) if contents.startsWith("!ntu") && session.admin => + session.zone.Buildings.values.foreach(building => + building.Amenities.foreach(amenity => + amenity.Definition match { + case GlobalDefinitions.resource_silo => + val r = new scala.util.Random + val silo = amenity.asInstanceOf[ResourceSilo] + val ntu: Int = 900 + r.nextInt(100) - silo.ChargeLevel + // val ntu: Int = 0 + r.nextInt(100) - silo.ChargeLevel + silo.Actor ! ResourceSilo.UpdateChargeLevel(ntu) + + case _ => ; + } + ) + ) + + case (CMT_OPEN, _, _) if !session.player.silenced => + chatService ! ChatService.Message( + session, + message.copy(recipient = session.player.Name), + ChatChannel.Default() + ) + + case (CMT_VOICE, _, _) => + chatService ! ChatService.Message( + session, + message.copy(recipient = session.player.Name), + ChatChannel.Default() + ) + + case (CMT_TELL, _, _) if !session.player.silenced => + chatService ! ChatService.Message( + session, + message, + ChatChannel.Default() + ) + + case (CMT_BROADCAST, _, _) if !session.player.silenced => + chatService ! ChatService.Message( + session, + message.copy(recipient = session.player.Name), + ChatChannel.Default() + ) + + case (CMT_PLATOON, _, _) if !session.player.silenced => + chatService ! ChatService.Message( + session, + message.copy(recipient = session.player.Name), + ChatChannel.Default() + ) + + case (CMT_COMMAND, _, _) if session.admin => + chatService ! ChatService.Message( + session, + message.copy(recipient = session.player.Name), + ChatChannel.Default() + ) + + case (CMT_NOTE, _, _) => + chatService ! ChatService.Message(session, message, ChatChannel.Default()) + + case (CMT_SILENCE, _, _) if session.admin => + chatService ! ChatService.Message(session, message, ChatChannel.Default()) + + case (CMT_SQUAD, _, _) => + channels.foreach { + case channel: ChatChannel.Squad => + chatService ! ChatService.Message(session, message.copy(recipient = session.player.Name), channel) + case _ => + } + + case ( + CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, + _, + _ + ) => + val players = session.zone.Players + val popTR = players.count(_.faction == PlanetSideEmpire.TR) + val popNC = players.count(_.faction == PlanetSideEmpire.NC) + val popVS = players.count(_.faction == PlanetSideEmpire.VS) + val contName = session.zone.Map.Name + + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None) + ) + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None) + ) + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None) + ) + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None) + ) + + case (CMT_ZONE, _, contents) if session.admin => + val buffer = contents.toLowerCase.split("\\s+") + val (zone, gate, list) = (buffer.lift(0), buffer.lift(1)) match { + case (Some("-list"), None) => + (None, None, true) + case (Some(zoneId), Some("-list")) => + (PointOfInterest.get(zoneId), None, true) + case (Some(zoneId), gateId) => + val zone = PointOfInterest.get(zoneId) + val gate = (zone, gateId) match { + case (Some(zone), Some(gateId)) => PointOfInterest.getWarpgate(zone, gateId) + case (Some(zone), None) => Some(PointOfInterest.selectRandom(zone)) + case _ => None + } + (zone, gate, false) + case _ => + (None, None, false) + } + (zone, gate, list) match { + case (None, None, true) => + sessionActor ! SessionActor.SendResponse(ChatMsg(UNK_229, true, "", PointOfInterest.list, None)) + case (Some(zone), None, true) => + sessionActor ! SessionActor.SendResponse( + ChatMsg(UNK_229, true, "", PointOfInterest.listWarpgates(zone), None) + ) + case (Some(zone), Some(gate), false) => + sessionActor ! SessionActor.SetZone(zone.zonename, gate) + case (_, None, false) => + sessionActor ! SessionActor.SendResponse( + ChatMsg(UNK_229, true, "", "Gate id not defined (use '/zone -list')", None) + ) + case (_, _, _) if buffer.isEmpty || buffer(0).equals("-help") => + sessionActor ! SessionActor.SendResponse( + ChatMsg(UNK_229, true, "", "usage: /zone [gatename] | [-list]", None) + ) + } + + case (CMT_WARP, _, contents) if session.admin => + val buffer = contents.toLowerCase.split("\\s+") + val (coordinates, waypoint) = (buffer.lift(0), buffer.lift(1), buffer.lift(2)) match { + case (Some(x), Some(y), Some(z)) => (Some(x, y, z), None) + case (Some("to"), Some(character), None) => (None, None) // TODO not implemented + case (Some("near"), Some(objectName), None) => (None, None) // TODO not implemented + case (Some(waypoint), None, None) => (None, Some(waypoint)) + case _ => (None, None) + } + (coordinates, waypoint) match { + case (Some((x, y, z)), None) if List(x, y, z).forall { str => + val coordinate = str.toFloatOption + coordinate.isDefined && coordinate.get >= 0 && coordinate.get <= 8191 + } => + sessionActor ! SessionActor.SetPosition(Vector3(x.toFloat, y.toFloat, z.toFloat)) + case (None, Some(waypoint)) if waypoint != "-help" => + PointOfInterest.getWarpLocation(session.zone.Id, waypoint) match { + case Some(location) => sessionActor ! SessionActor.SetPosition(location) + case None => + sessionActor ! SessionActor.SendResponse( + ChatMsg(UNK_229, true, "", s"unknown location '$waypoint", None) + ) + } + case _ => + sessionActor ! SessionActor.SendResponse( + ChatMsg( + UNK_229, + true, + "", + s"usage: /warp OR /warp to OR /warp near OR /warp above OR /warp waypoint", + None + ) + ) + } + + case _ => + log.info("unhandled chat message $message") + } + case (None, _) | (_, None) => + log.error("failed to handle message because dependencies are missing") + } + this + + case IncomingMessage(fromSession, message, channel) => + (session) match { + case Some(session) => + message.messageType match { + case CMT_TELL | U_CMT_TELLFROM | CMT_BROADCAST | CMT_SQUAD | CMT_PLATOON | CMT_COMMAND | UNK_45 | UNK_71 | + CMT_NOTE | CMT_GMBROADCAST | CMT_GMBROADCAST_NC | CMT_GMBROADCAST_TR | CMT_GMBROADCAST_VS | + CMT_GMBROADCASTPOPUP | CMT_GMTELL | U_CMT_GMTELLFROM | UNK_227 => + sessionActor ! SessionActor.SendResponse(message) + case CMT_OPEN => + if ( + session.zone == fromSession.zone && + Vector3.Distance(session.player.Position, fromSession.player.Position) < 25 && + session.player.Faction == fromSession.player.Faction + ) { + sessionActor ! SessionActor.SendResponse(message) + } + case CMT_VOICE => + if ( + session.zone == fromSession.zone && + Vector3.Distance(session.player.Position, fromSession.player.Position) < 25 + ) { + sessionActor ! SessionActor.SendResponse(message) + } + case CMT_SILENCE => + val args = message.contents.split(" ") + val (name, time) = (args.lift(0), args.lift(1)) match { + case (Some(name), _) if name != session.player.Name => + log.error("received silence message for other player") + (None, None) + case (Some(name), None) => (Some(name), Some(5)) + case (Some(name), Some(time)) if time.toIntOption.isDefined => (Some(name), Some(time.toInt)) + case _ => (None, None) + } + (name, time) match { + case (Some(_), Some(time)) => + if (session.player.silenced) { + sessionActor ! SessionActor.SetSilenced(false) + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.UNK_71, true, "", "@silence_off", None) + ) + if (!silenceTimer.isCancelled) silenceTimer.cancel() + } else { + sessionActor ! SessionActor.SetSilenced(true) + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.UNK_71, true, "", "@silence_on", None) + ) + silenceTimer = context.system.scheduler.scheduleOnce( + time minutes, + () => { + sessionActor ! SessionActor.SetSilenced(false) + sessionActor ! SessionActor.SendResponse( + ChatMsg(ChatMessageType.UNK_71, true, "", "@silence_timeout", None) + ) + } + ) + } + + case (name, time) => + log.error(s"bad silence args $name $time") + } + case _ => + log.error(s"unexpected messageType $message") + } + case None => + log.error("failed to handle incoming message because dependencies are missing") + } + this + + } + } + +} diff --git a/pslogin/src/main/scala/WorldSessionActor.scala b/common/src/main/scala/net/psforever/actors/session/SessionActor.scala similarity index 92% rename from pslogin/src/main/scala/WorldSessionActor.scala rename to common/src/main/scala/net/psforever/actors/session/SessionActor.scala index f0f9152d..9efdd101 100644 --- a/pslogin/src/main/scala/WorldSessionActor.scala +++ b/common/src/main/scala/net/psforever/actors/session/SessionActor.scala @@ -1,48 +1,24 @@ -package net.psforever.pslogin +package net.psforever.actors.session -import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger -import org.log4s.MDC -import scala.collection.mutable.LongMap -import scala.concurrent.{Await, Future} -import scala.concurrent.duration._ -import scala.concurrent.ExecutionContext.Implicits.global -import scala.util.{Success, Failure} -import scodec.bits.ByteVector -import services.properties.PropertyOverrideManager -import org.joda.time.{LocalDateTime, Period} -import csr.{CSRWarp, CSRZone, Traveler} -import MDCContextAware.Implicits._ -import net.psforever.objects._ +import akka.actor.MDCContextAware.Implicits._ +import akka.actor.typed +import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} +import net.psforever.objects.{GlobalDefinitions, _} import net.psforever.objects.avatar.{Certification, DeployableToolbox, FirstTimeEvents} -import net.psforever.objects.ballistics.{ - PlayerSource, - Projectile, - ProjectileResolution, - ResolvedProjectile, - SourceEntry -} -import net.psforever.objects.ce.{ - ComplexDeployable, - Deployable, - DeployableCategory, - DeployedItem, - SimpleDeployable, - TelepadLike -} +import net.psforever.objects.ballistics._ +import net.psforever.objects.ce._ import net.psforever.objects.definition._ import net.psforever.objects.definition.converter.{CorpseConverter, DestroyedVehicleConverter} import net.psforever.objects.entity.{SimpleWorldEntity, WorldEntity} import net.psforever.objects.equipment.{EffectTarget, Equipment, FireModeSwitch, JammableUnit} -import net.psforever.objects.GlobalDefinitions import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.inventory.{Container, InventoryItem} import net.psforever.objects.loadouts.{InfantryLoadout, SquadLoadout, VehicleLoadout} -import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.serverobject.affinity.FactionAffinity -import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.containable.Containable +import net.psforever.objects.serverobject.damage.Damageable import net.psforever.objects.serverobject.deploy.Deployment import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.generator.Generator @@ -51,21 +27,15 @@ import net.psforever.objects.serverobject.implantmech.ImplantTerminalMech import net.psforever.objects.serverobject.locks.IFFLock import net.psforever.objects.serverobject.mblocker.Locker import net.psforever.objects.serverobject.mount.Mountable -import net.psforever.objects.serverobject.pad.{VehicleSpawnControl, VehicleSpawnPad} +import net.psforever.objects.serverobject.pad.VehicleSpawnPad import net.psforever.objects.serverobject.painbox.Painbox import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.structures.{Amenity, Building, StructureType, WarpGate} -import net.psforever.objects.serverobject.terminals.{ - CaptureTerminal, - MatrixTerminalDefinition, - MedicalTerminalDefinition, - ProximityDefinition, - ProximityUnit, - Terminal -} +import net.psforever.objects.serverobject.terminals._ import net.psforever.objects.serverobject.tube.SpawnTube -import net.psforever.objects.serverobject.turret.{FacilityTurret, TurretUpgrade, WeaponTurret, WeaponTurrets} +import net.psforever.objects.serverobject.turret.{FacilityTurret, WeaponTurret} import net.psforever.objects.serverobject.zipline.ZipLinePath +import net.psforever.objects.serverobject.{CommonMessages, PlanetSideServerObject} import net.psforever.objects.teamwork.Squad import net.psforever.objects.vehicles.{ AccessPermissionGroup, @@ -77,63 +47,188 @@ import net.psforever.objects.vehicles.{ VehicleLockState } import net.psforever.objects.vehicles.Utility.InternalTelepad -import net.psforever.objects.vital.{ - DamageFromPainbox, - HealFromKit, - HealFromTerm, - PlayerSuicide, - RepairFromKit, - Vitality -} +import net.psforever.objects.vehicles._ +import net.psforever.objects.vital._ import net.psforever.objects.zones.{InterstellarCluster, Zone, ZoneHotSpotProjector, Zoning} import net.psforever.packet._ import net.psforever.packet.control._ -import net.psforever.packet.game._ -import net.psforever.packet.game.objectcreate.{ - DetailedCharacterData, - DroppedItemData, - ObjectClass, - ObjectCreateMessageParent, - PlacementData -} -import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo} +import net.psforever.packet.game.objectcreate._ +import net.psforever.packet.game.{HotSpotInfo => PacketHotSpotInfo, _} import net.psforever.persistence import net.psforever.types._ -import services.{RemoverActor, Service, ServiceManager} +import org.joda.time.{LocalDateTime, Period} +import org.log4s.MDC +import scodec.bits.ByteVector +import services.ServiceManager.LookupResult import services.account.{AccountPersistenceService, PlayerToken, ReceiveAccountData, RetrieveAccountData} import services.avatar.{AvatarAction, AvatarResponse, AvatarServiceMessage, AvatarServiceResponse} -import services.chat.{ChatAction, ChatServiceMessage, ChatServiceResponse} import services.galaxy.{GalaxyAction, GalaxyResponse, GalaxyServiceMessage, GalaxyServiceResponse} -import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} import services.local.support.RouterTelepadActivation -import services.ServiceManager.LookupResult +import services.local.{LocalAction, LocalResponse, LocalServiceMessage, LocalServiceResponse} +import services.properties.PropertyOverrideManager import services.support.SupportActor import services.teamwork.{SquadResponse, SquadServiceMessage, SquadServiceResponse, SquadAction => SquadServiceAction} import services.vehicle.{VehicleAction, VehicleResponse, VehicleServiceMessage, VehicleServiceResponse} -import Database._ +import services.{RemoverActor, Service, ServiceManager} +import net.psforever.login.{DropCryptoSession, DropSession, HelloFriend, RawPacket} +import net.psforever.util.Config +import net.psforever.util.Database._ +import net.psforever.login.WorldSession._ +import net.psforever.zones.Zones +import services.chat.ChatService +import scala.collection.mutable.LongMap +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} +import scala.util.{Failure, Success} +import akka.actor.typed.scaladsl.adapter._ -class WorldSessionActor extends Actor with MDCContextAware { +object SessionActor { - import WorldSessionActor._ - import WorldSession._ + /** Object purchasing cooldowns.
+ * key - object id
+ * value - time last used (ms) + */ + val delayedPurchaseEntries: Map[Int, Long] = Map( + GlobalDefinitions.ams.ObjectId -> 300000, //5min + GlobalDefinitions.ant.ObjectId -> 300000, //5min + GlobalDefinitions.apc_nc.ObjectId -> 300000, //5min + GlobalDefinitions.apc_tr.ObjectId -> 300000, //5min + GlobalDefinitions.apc_vs.ObjectId -> 300000, //5min + GlobalDefinitions.aurora.ObjectId -> 300000, //5min + GlobalDefinitions.battlewagon.ObjectId -> 300000, //5min + GlobalDefinitions.dropship.ObjectId -> 300000, //5min + GlobalDefinitions.flail.ObjectId -> 300000, //5min + GlobalDefinitions.fury.ObjectId -> 300000, //5min + GlobalDefinitions.galaxy_gunship.ObjectId -> 600000, //10min + GlobalDefinitions.lodestar.ObjectId -> 300000, //5min + GlobalDefinitions.liberator.ObjectId -> 300000, //5min + GlobalDefinitions.lightgunship.ObjectId -> 300000, //5min + GlobalDefinitions.lightning.ObjectId -> 300000, //5min + GlobalDefinitions.magrider.ObjectId -> 300000, //5min + GlobalDefinitions.mediumtransport.ObjectId -> 300000, //5min + GlobalDefinitions.mosquito.ObjectId -> 300000, //5min + GlobalDefinitions.phantasm.ObjectId -> 300000, //5min + GlobalDefinitions.prowler.ObjectId -> 300000, //5min + GlobalDefinitions.quadassault.ObjectId -> 300000, //5min + GlobalDefinitions.quadstealth.ObjectId -> 300000, //5min + GlobalDefinitions.router.ObjectId -> 300000, //5min + GlobalDefinitions.switchblade.ObjectId -> 300000, //5min + GlobalDefinitions.skyguard.ObjectId -> 300000, //5min + GlobalDefinitions.threemanheavybuggy.ObjectId -> 300000, //5min + GlobalDefinitions.thunderer.ObjectId -> 300000, //5min + GlobalDefinitions.two_man_assault_buggy.ObjectId -> 300000, //5min + GlobalDefinitions.twomanhoverbuggy.ObjectId -> 300000, //5min + GlobalDefinitions.twomanheavybuggy.ObjectId -> 300000, //5min + GlobalDefinitions.vanguard.ObjectId -> 300000, //5min + GlobalDefinitions.vulture.ObjectId -> 300000, //5min + GlobalDefinitions.wasp.ObjectId -> 300000, //5min + GlobalDefinitions.flamethrower.ObjectId -> 180000 //3min + ) + + /** Object use cooldowns.
+ * key - object id
+ * value - time last used (ms) + */ + val delayedGratificationEntries: Map[Int, Long] = Map( + GlobalDefinitions.medkit.ObjectId -> 5000, //5s + GlobalDefinitions.super_armorkit.ObjectId -> 1200000, //20min + GlobalDefinitions.super_medkit.ObjectId -> 1200000, //20min + GlobalDefinitions.super_staminakit.ObjectId -> 1200000 //20min + ) + + sealed trait Command + + final case class ResponseToSelf(pkt: PlanetSideGamePacket) + + private final case class PokeClient() + private final case class ServerLoaded() + private final case class NewPlayerLoaded(tplayer: Player) + private final case class PlayerLoaded(tplayer: Player) + private final case class PlayerFailedToLoad(tplayer: Player) + private final case class CreateCharacter( + name: String, + head: Int, + voice: CharacterVoice.Value, + gender: CharacterGender.Value, + empire: PlanetSideEmpire.Value + ) + private final case class ListAccountCharacters() + private final case class SetCurrentAvatar(tplayer: Player, max_attempts: Int, attempt: Int = 0) + private final case class ZoningReset() + + final case class SendResponse(packet: PlanetSidePacket) extends Command + final case class SetSpeed(speed: Float) extends Command + final case class SetFlying(flying: Boolean) extends Command + final case class SetSpectator(spectator: Boolean) extends Command + final case class SetZone(zoneId: String, position: Vector3) extends Command + final case class SetPosition(position: Vector3) extends Command + final case class SetConnectionState(connectionState: Int) extends Command + final case class SetSilenced(silenced: Boolean) extends Command + final case class Recall(zoneId: String) extends Command + final case class InstantAction() extends Command + final case class Quit() extends Command + final case class Suicide() extends Command + final case class Kick(player: Player, time: Option[Long] = None) extends Command + + final val ftes = ( + FirstTimeEvents.Maps ++ FirstTimeEvents.Monoliths ++ + FirstTimeEvents.Standard.All ++ FirstTimeEvents.Cavern.All ++ + FirstTimeEvents.TR.All ++ FirstTimeEvents.NC.All ++ FirstTimeEvents.VS.All ++ + FirstTimeEvents.Generic + ).toList + + /** + * The message that progresses some form of user-driven activity with a certain eventual outcome + * and potential feedback per cycle. + * @param delta how much the progress value changes each tick, which will be treated as a percentage; + * must be a positive value + * @param completionAction a finalizing action performed once the progress reaches 100(%) + * @param tickAction an action that is performed for each increase of progress + */ + final case class ProgressEvent(delta: Float, completionAction: () => Unit, tickAction: Float => Boolean) + + private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20) + + protected final case class SquadUIElement( + name: String, + index: Int, + zone: Int, + health: Int, + armor: Int, + position: Vector3 + ) + + private final case class NtuCharging(tplayer: Player, vehicle: Vehicle) + + private final case class NtuDischarging(tplayer: Player, vehicle: Vehicle, silo_guid: PlanetSideGUID) + + private final case class FinalizeDeployable( + obj: PlanetSideGameObject with Deployable, + tool: ConstructionItem, + index: Int + ) + + private final case class LoadedRemoteProjectile(projectile_guid: PlanetSideGUID, projectile: Option[Projectile]) +} + +class SessionActor extends Actor with MDCContextAware { + + import SessionActor._ private[this] val log = org.log4s.getLogger private[this] val damageLog = org.log4s.getLogger(Damageable.LogChannel) - var sessionId: Long = 0 var leftRef: ActorRef = ActorRef.noSender var rightRef: ActorRef = ActorRef.noSender + var chatActor: typed.ActorRef[ChatActor.Command] = context.spawn(ChatActor(context.self), "chatActor") var accountIntermediary: ActorRef = ActorRef.noSender var accountPersistence: ActorRef = ActorRef.noSender - var chatService: ActorRef = ActorRef.noSender var galaxyService: ActorRef = ActorRef.noSender var squadService: ActorRef = ActorRef.noSender var taskResolver: ActorRef = Actor.noSender var propertyOverrideManager: ActorRef = Actor.noSender var cluster: ActorRef = Actor.noSender - var continent: Zone = Zone.Nowhere - var account: Account = null - var player: Player = null - var avatar: Avatar = null + var _session: Session = Session() var progressBarValue: Option[Float] = None var shooting: Option[PlanetSideGUID] = None //ChangeFireStateMessage_Start var prefire: Option[PlanetSideGUID] = None //if WeaponFireMessage precedes ChangeFireStateMessage_Start @@ -141,16 +236,12 @@ class WorldSessionActor extends Actor with MDCContextAware { var accessedContainer: Option[PlanetSideGameObject with Container] = None var connectionState: Int = 25 var flying: Boolean = false - var speed: Float = 1.0f - var admin: Boolean = false var loadConfZone: Boolean = false var noSpawnPointHere: Boolean = false var usingMedicalTerminal: Option[PlanetSideGUID] = None var controlled: Option[Int] = None - //keep track of avatar's ServerVehicleOverride state - var traveler: Traveler = null - var deadState: DeadState.Value = DeadState.Dead - var whenUsedLastMAXName: Array[String] = Array.fill[String](4)("") + var deadState: DeadState.Value = DeadState.Dead + var whenUsedLastMAXName: Array[String] = Array.fill[String](4)("") val projectiles: Array[Option[Projectile]] = Array.fill[Option[Projectile]](Projectile.RangeUID - Projectile.BaseUID)(None) val projectilesToCleanUp: Array[Boolean] = Array.fill[Boolean](Projectile.RangeUID - Projectile.BaseUID)(false) @@ -192,7 +283,6 @@ class WorldSessionActor extends Actor with MDCContextAware { * Control switching between the `Avatar`-local and the `WorldSessionActor`-local variable is contingent on `squadUI` being populated. */ var lfsm: Boolean = false - var squadChannel: Option[String] = None var squadSetup: () => Unit = FirstTimeSquadSetup var squadUpdateCounter: Int = 0 val queuedSquadActions: Seq[() => Unit] = Seq(SquadUpdates, NoSquadUpdates, NoSquadUpdates, NoSquadUpdates) @@ -237,13 +327,23 @@ class WorldSessionActor extends Actor with MDCContextAware { var zoningTimer: Cancellable = Default.Cancellable var zoningReset: Cancellable = Default.Cancellable + def session = _session + def session_=(session: Session) = { + chatActor ! ChatActor.SetSession(session) + _session = session + } + + def account: Account = _session.account + def continent: Zone = _session.zone // TODO continent -> zone + def player: Player = _session.player + def avatar: Avatar = _session.avatar + override def postStop(): Unit = { //normally, the player avatar persists a minute or so after disconnect; we are subject to the SessionReaper clientKeepAlive.cancel progressBarUpdate.cancel reviveTimer.cancel respawnTimer.cancel - chatService ! Service.Leave() galaxyService ! Service.Leave() continent.AvatarEvents ! Service.Leave() continent.LocalEvents ! Service.Leave() @@ -268,11 +368,11 @@ class WorldSessionActor extends Actor with MDCContextAware { def Initializing: Receive = { case HelloFriend(inSessionId, pipe) => - this.sessionId = inSessionId + session = session.copy(id = inSessionId) leftRef = sender() if (pipe.hasNext) { rightRef = pipe.next - rightRef !> HelloFriend(sessionId, pipe) + rightRef !> HelloFriend(session.id, pipe) } else { rightRef = sender() } @@ -281,7 +381,6 @@ class WorldSessionActor extends Actor with MDCContextAware { val serviceManager = ServiceManager.serviceManager serviceManager ! Lookup("accountIntermediary") serviceManager ! Lookup("accountPersistence") - serviceManager ! Lookup("chat") serviceManager ! Lookup("taskResolver") serviceManager ! Lookup("cluster") serviceManager ! Lookup("galaxy") @@ -313,28 +412,25 @@ class WorldSessionActor extends Actor with MDCContextAware { def Started: Receive = { case LookupResult("accountIntermediary", endpoint) => accountIntermediary = endpoint - log.info("ID: " + sessionId + " Got account intermediary service " + endpoint) + log.info("ID: " + session.id + " Got account intermediary service " + endpoint) case LookupResult("accountPersistence", endpoint) => accountPersistence = endpoint - log.info("ID: " + sessionId + " Got account persistence service " + endpoint) - case LookupResult("chat", endpoint) => - chatService = endpoint - log.info("ID: " + sessionId + " Got chat service " + endpoint) + log.info("ID: " + session.id + " Got account persistence service " + endpoint) case LookupResult("taskResolver", endpoint) => taskResolver = endpoint - log.info("ID: " + sessionId + " Got task resolver service " + endpoint) + log.info("ID: " + session.id + " Got task resolver service " + endpoint) case LookupResult("galaxy", endpoint) => galaxyService = endpoint - log.info("ID: " + sessionId + " Got galaxy service " + endpoint) + log.info("ID: " + session.id + " Got galaxy service " + endpoint) case LookupResult("cluster", endpoint) => cluster = endpoint - log.info("ID: " + sessionId + " Got cluster service " + endpoint) + log.info("ID: " + session.id + " Got cluster service " + endpoint) case LookupResult("squad", endpoint) => squadService = endpoint - log.info("ID: " + sessionId + " Got squad service " + endpoint) + log.info("ID: " + session.id + " Got squad service " + endpoint) case LookupResult("propertyOverrideManager", endpoint) => propertyOverrideManager = endpoint - log.info("ID: " + sessionId + " Got propertyOverrideManager service " + endpoint) + log.info("ID: " + session.id + " Got propertyOverrideManager service " + endpoint) case ControlPacket(_, ctrl) => handleControlPkt(ctrl) @@ -347,6 +443,93 @@ class WorldSessionActor extends Actor with MDCContextAware { case AvatarServiceResponse(toChannel, guid, reply) => HandleAvatarServiceResponse(toChannel, guid, reply) + case SendResponse(packet) => + sendResponse(packet) + + case SetSpeed(speed) => + session = session.copy(speed = speed) + + case SetFlying(flying) => + session = session.copy(flying = flying) + + case SetSpectator(spectator) => + session.player.spectator = spectator + + case Recall(zoneId) => + zoningType = Zoning.Method.Recall + zoningChatMessageType = ChatMessageType.CMT_RECALL + zoningStatus = Zoning.Status.Request + zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset()) + cluster ! Zoning.Recall.Request(player.Faction, zoneId) + + case InstantAction() => + zoningType = Zoning.Method.InstantAction + zoningChatMessageType = ChatMessageType.CMT_INSTANTACTION + zoningStatus = Zoning.Status.Request + zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset()) + cluster ! Zoning.InstantAction.Request(player.Faction) + + case Quit() => + //priority to quitting is given to quit over other zoning methods + if (session.zoningType == Zoning.Method.InstantAction || session.zoningType == Zoning.Method.Recall) { + CancelZoningProcessWithDescriptiveReason("cancel") + } + zoningType = Zoning.Method.Quit + zoningChatMessageType = ChatMessageType.CMT_QUIT + zoningStatus = Zoning.Status.Request + self ! Zoning.Quit() + + case Suicide() => + suicide(player) + + case Kick(player, time) => + AdministrativeKick(player) + accountPersistence ! AccountPersistenceService.Kick(player.Name, time) + + case SetZone(zoneId, position) => + PlayerActionsToCancel() + continent.GUID(player.VehicleSeated) match { + case Some(vehicle: Vehicle) if vehicle.MountedIn.isEmpty => + vehicle.PassengerInSeat(player) match { + case Some(0) => + deadState = DeadState.Release // cancel movement updates + vehicle.Position = position + LoadZonePhysicalSpawnPoint(zoneId, position, Vector3.Zero, 0) + case _ => // not seated as the driver, in which case we can't move + } + case None => + deadState = DeadState.Release // cancel movement updates + player.Position = position + // continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, player.GUID)) + LoadZonePhysicalSpawnPoint(zoneId, position, Vector3.Zero, 0) + case _ => // seated in something that is not a vehicle or the vehicle is cargo, in which case we can't move + } + + case SetPosition(position) => + PlayerActionsToCancel() + continent.GUID(player.VehicleSeated) match { + case Some(vehicle: Vehicle) if vehicle.MountedIn.isEmpty => + vehicle.PassengerInSeat(player) match { + case Some(0) => + deadState = DeadState.Release // cancel movement updates + vehicle.Position = position + LoadZonePhysicalSpawnPoint(continent.Id, position, Vector3.z(vehicle.Orientation.z), 0) + case _ => // not seated as the driver, in which case we can't move + } + case None => + deadState = DeadState.Release // cancel movement updates + player.Position = position + sendResponse(PlayerStateShiftMessage(ShiftState(0, position, player.Orientation.z, None))) + deadState = DeadState.Alive // must be set here + case _ => // seated in something that is not a vehicle or the vehicle is cargo, in which case we can't move + } + + case SetConnectionState(state) => + connectionState = state + + case SetSilenced(silenced) => + player.silenced = silenced + case CommonMessages.Progress(rate, finishedAction, stepAction) => if (progressBarValue.isEmpty) { progressBarValue = Some(-rate) @@ -418,9 +601,6 @@ class WorldSessionActor extends Actor with MDCContextAware { case LocalServiceResponse(toChannel, guid, reply) => HandleLocalServiceResponse(toChannel, guid, reply) - case ChatServiceResponse(toChannel, guid, avatar_name, cont, avatar_pos, avatar_faction, target, reply) => - HandleChatServiceResponse(toChannel, guid, avatar_name, cont, avatar_pos, avatar_faction, target, reply) - case Mountable.MountMessages(tplayer, reply) => HandleMountMessages(tplayer, reply) @@ -557,8 +737,7 @@ class WorldSessionActor extends Actor with MDCContextAware { //a finalization? what does this do? sendResponse(SquadDefinitionActionMessage(squad.GUID, 0, SquadAction.Unknown(18))) updateSquad = PeriodicUpdatesWhenEnrolledInSquad - squadChannel = Some(toChannel) - chatService ! Service.Join(squadChannel.get) + chatActor ! ChatActor.JoinChannel(ChatService.ChatChannel.Squad(squad.GUID)) case _ => //other player is joining our squad //load each member's entry @@ -630,8 +809,7 @@ class WorldSessionActor extends Actor with MDCContextAware { squad_supplement_id = 0 squadUpdateCounter = 0 updateSquad = NoSquadUpdates - chatService ! Service.Leave(squadChannel) - squadChannel = None + chatActor ! ChatActor.LeaveChannel(ChatService.ChatChannel.Squad(squad.GUID)) case _ => //remove each member's entry GiveSquadColorsInZone( @@ -782,7 +960,6 @@ class WorldSessionActor extends Actor with MDCContextAware { case CreateCharacter(name, head, voice, gender, empire) => import ctx._ log.info(s"Creating new character $name") - val result = ctx.run( query[persistence.Character].insert( _.name -> lift(name), @@ -946,7 +1123,9 @@ class WorldSessionActor extends Actor with MDCContextAware { ) reviveTimer.cancel if (spawn_group == 2) { - sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, false, "", "No friendly AMS is deployed in this region.", None)) + sendResponse( + ChatMsg(ChatMessageType.CMT_OPEN, false, "", "No friendly AMS is deployed in this region.", None) + ) cluster ! Zone.Lattice.RequestSpawnPoint(zone_number, player, 0) } else { RequestSanctuaryZoneSpawn(player, zone_number) @@ -987,7 +1166,7 @@ class WorldSessionActor extends Actor with MDCContextAware { context.system.scheduler.scheduleOnce( obj.Definition.DeployTime milliseconds, self, - WorldSessionActor.FinalizeDeployable(obj, tool, index) + SessionActor.FinalizeDeployable(obj, tool, index) ) } else { TryDropFDU(tool, index, obj.Position) @@ -997,7 +1176,7 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.Deployables ! Zone.Deployable.Dismiss(obj) } - case WorldSessionActor.FinalizeDeployable(obj: SensorDeployable, tool, index) => + case SessionActor.FinalizeDeployable(obj: SensorDeployable, tool, index) => //motion alarm sensor and sensor disruptor StartBundlingPackets() DeployableBuildActivity(obj) @@ -1009,7 +1188,7 @@ class WorldSessionActor extends Actor with MDCContextAware { FindReplacementConstructionItem(tool, index) StopBundlingPackets() - case WorldSessionActor.FinalizeDeployable(obj: BoomerDeployable, tool, index) => + case SessionActor.FinalizeDeployable(obj: BoomerDeployable, tool, index) => //boomers StartBundlingPackets() DeployableBuildActivity(obj) @@ -1030,7 +1209,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } StopBundlingPackets() - case WorldSessionActor.FinalizeDeployable(obj: ExplosiveDeployable, tool, index) => + case SessionActor.FinalizeDeployable(obj: ExplosiveDeployable, tool, index) => //mines StartBundlingPackets() DeployableBuildActivity(obj) @@ -1038,7 +1217,7 @@ class WorldSessionActor extends Actor with MDCContextAware { FindReplacementConstructionItem(tool, index) StopBundlingPackets() - case WorldSessionActor.FinalizeDeployable(obj: ComplexDeployable, tool, index) => + case SessionActor.FinalizeDeployable(obj: ComplexDeployable, tool, index) => //tank_traps, spitfires, deployable field turrets and the deployable_shield_generator StartBundlingPackets() DeployableBuildActivity(obj) @@ -1046,7 +1225,7 @@ class WorldSessionActor extends Actor with MDCContextAware { FindReplacementConstructionItem(tool, index) StopBundlingPackets() - case WorldSessionActor.FinalizeDeployable(obj: TelepadDeployable, tool, index) => + case SessionActor.FinalizeDeployable(obj: TelepadDeployable, tool, index) => StartBundlingPackets() if (obj.Health > 0) { val guid = obj.GUID @@ -1082,7 +1261,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } StopBundlingPackets() - case WorldSessionActor.FinalizeDeployable(obj: PlanetSideGameObject with Deployable, tool, index) => + case SessionActor.FinalizeDeployable(obj: PlanetSideGameObject with Deployable, tool, index) => val guid = obj.GUID val definition = obj.Definition StartBundlingPackets() @@ -1107,7 +1286,6 @@ class WorldSessionActor extends Actor with MDCContextAware { case InterstellarCluster.ClientInitializationComplete() => LivePlayerList.Add(avatar.CharId, avatar) - traveler = new Traveler(self, continent.Id) StartBundlingPackets() //PropertyOverrideMessage @@ -1125,14 +1303,6 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(FriendsResponse(FriendAction.InitializeFriendList, 0, true, true, Nil)) sendResponse(FriendsResponse(FriendAction.InitializeIgnoreList, 0, true, true, Nil)) //the following subscriptions last until character switch/logout - chatService ! Service.Join("local") - chatService ! Service.Join("squad") - chatService ! Service.Join("platoon") - chatService ! Service.Join("voice") - chatService ! Service.Join("tell") - chatService ! Service.Join("broadcast") - chatService ! Service.Join("note") - chatService ! Service.Join("gm") galaxyService ! Service.Join("galaxy") //for galaxy-wide messages galaxyService ! Service.Join(s"${avatar.faction}") //for hotspots squadService ! Service.Join(s"${avatar.faction}") //channel will be player.Faction @@ -1144,7 +1314,7 @@ class WorldSessionActor extends Actor with MDCContextAware { log.info(s"Zone ${zone.Id} will now load") loadConfZone = true val oldZone = continent - continent = zone + session = session.copy(zone = zone) //the only zone-level event system subscription necessary before BeginZoningMessage (for persistence purposes) continent.AvatarEvents ! Service.Join(player.Name) persist() @@ -1163,8 +1333,8 @@ class WorldSessionActor extends Actor with MDCContextAware { case InterstellarCluster.GiveWorld(zoneId, zone) => log.info(s"Zone $zoneId will now load") loadConfZone = true - val oldZone = continent - continent = zone + val oldZone = session.zone + session = session.copy(zone = zone) //the only zone-level event system subscription necessary before BeginZoningMessage (for persistence purposes) continent.AvatarEvents ! Service.Join(player.Name) persist() @@ -1182,8 +1352,8 @@ class WorldSessionActor extends Actor with MDCContextAware { 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), cluster)) { - val (pos, ori) = spawn_point.SpecificPoint(player) + if (ContemplateZoningResponse(Zoning.InstantAction.Request(session.player.Faction), cluster)) { + val (pos, ori) = spawn_point.SpecificPoint(session.player) SpawnThroughZoningProcess(zone, pos, ori) } else if (zoningStatus != Zoning.Status.None) { instantActionFallbackDestination = Some(msg) @@ -1192,9 +1362,9 @@ class WorldSessionActor extends Actor with MDCContextAware { 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), cluster)) { - val (pos, ori) = spawn_point.SpecificPoint(player) + if spawn_point.Owner.Faction == session.player.Faction && !spawn_point.Offline => + if (ContemplateZoningResponse(Zoning.InstantAction.Request(session.player.Faction), cluster)) { + val (pos, ori) = spawn_point.SpecificPoint(session.player) SpawnThroughZoningProcess(zone, pos, ori) } else if (zoningCounter == 0) { CancelZoningProcessWithReason("@InstantActionNoHotspotsAvailable") @@ -1205,8 +1375,8 @@ class WorldSessionActor extends Actor with MDCContextAware { } case Zoning.Recall.Located(zone, spawn_point) => - if (ContemplateZoningResponse(Zoning.Recall.Request(player.Faction, zone.Id), cluster)) { - val (pos, ori) = spawn_point.SpecificPoint(player) + if (ContemplateZoningResponse(Zoning.Recall.Request(session.player.Faction, zone.Id), cluster)) { + val (pos, ori) = spawn_point.SpecificPoint(session.player) SpawnThroughZoningProcess(zone, pos, ori) } @@ -1225,12 +1395,21 @@ class WorldSessionActor extends Actor with MDCContextAware { case NewPlayerLoaded(tplayer) => //new zone log.info(s"Player ${tplayer.Name} has been loaded") + session = session.copy(player = tplayer) //LoadMapMessage causes the client to send BeginZoningMessage, eventually leading to SetCurrentAvatar val weaponsEnabled = - (continent.Map.Name != "map11" && continent.Map.Name != "map12" && continent.Map.Name != "map13") - sendResponse(LoadMapMessage(continent.Map.Name, continent.Id, 40100, 25, weaponsEnabled, continent.Map.Checksum)) + (session.zone.Map.Name != "map11" && session.zone.Map.Name != "map12" && session.zone.Map.Name != "map13") + sendResponse( + LoadMapMessage( + session.zone.Map.Name, + session.zone.Id, + 40100, + 25, + weaponsEnabled, + session.zone.Map.Checksum + ) + ) //important! the LoadMapMessage must be processed by the client before the avatar is created - player = tplayer setupAvatarFunc() //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable turnCounterFunc = interimUngunnedVehicle match { @@ -1247,7 +1426,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case PlayerLoaded(tplayer) => //same zone log.info(s"Player ${tplayer.Name} will respawn") - player = tplayer + session = session.copy(player = tplayer) setupAvatarFunc() //interimUngunnedVehicle should have been setup by setupAvatarFunc, if it is applicable turnCounterFunc = interimUngunnedVehicle match { @@ -1363,8 +1542,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case ReceiveAccountData(account) => import ctx._ log.info(s"Received account data for accountId = ${account.AccountId}") - this.account = account - admin = account.GM + session = session.copy(account = account, admin = account.GM) ctx.run(query[persistence.Account].filter(_.id == lift(account.AccountId)).map(_.id)).onComplete { case Success(accounts) => accounts.headOption match { @@ -1373,7 +1551,7 @@ class WorldSessionActor extends Actor with MDCContextAware { self ! ListAccountCharacters() case None => log.error(s"ReceiveAccountData: ${account.Username} data not found") - sendResponse(DropSession(sessionId, "You should not exist!")) + sendResponse(DropSession(session.id, "You should not exist!")) } case Failure(e) => log.error(s"ReceiveAccountData: ${e.getMessage}") @@ -1470,7 +1648,7 @@ class WorldSessionActor extends Actor with MDCContextAware { avatar.Implants(2).Unlocked = true avatar.Implants(2).Implant = GlobalDefinitions.targeting - player = new Player(avatar) + session = session.copy(player = new Player(avatar)) //xy-coordinates indicate sanctuary spawn bias: player.Position = math.abs(scala.util.Random.nextInt() % avatar.name.hashCode % 4) match { case 0 => Vector3(8192, 8192, 0) //NE @@ -1502,8 +1680,7 @@ class WorldSessionActor extends Actor with MDCContextAware { //rejoin current avatar/player log.info(s"LoginInfo: player $playerName is alive") deadState = DeadState.Alive - avatar = a - player = p + session = session.copy(avatar = a, player = p) persist() setupAvatarFunc = AvatarRejoin UpdateLoginTimeThenDoClientInitialization() @@ -1512,8 +1689,7 @@ class WorldSessionActor extends Actor with MDCContextAware { //convert player to a corpse (unless in vehicle); automatic recall to closest spawn point log.info(s"LoginInfo: player $playerName is dead") deadState = DeadState.Dead - avatar = a - player = p + session = session.copy(avatar = a, player = p) persist() player.Zone = inZone HandleReleaseAvatar(p, inZone) @@ -1523,17 +1699,19 @@ class WorldSessionActor extends Actor with MDCContextAware { //respawn avatar as a new player; automatic recall to closest spawn point log.info(s"LoginInfo: player $playerName had released recently") deadState = DeadState.RespawnTime - avatar = a - player = inZone.Corpses.findLast(c => c.Name == playerName) match { - case Some(c) => - c //the last corpse of this user should be where they died - case None => - val tplayer = Player(a) //throwaway - tplayer.Position = pos - tplayer.Release //for proper respawn - tplayer.Zone = inZone - tplayer - } + session = session.copy( + avatar = a, + player = inZone.Corpses.findLast(c => c.Name == playerName) match { + case Some(c) => + c //the last corpse of this user should be where they died + case None => + val tplayer = Player(a) //throwaway + tplayer.Position = pos + tplayer.Release //for proper respawn + tplayer.Zone = inZone + tplayer + } + ) UpdateLoginTimeThenDoClientInitialization() case _ => @@ -2521,130 +2699,6 @@ class WorldSessionActor extends Actor with MDCContextAware { } } - /** - * na - * @param toChannel na - * @param avatar_guid na - * @param target na - * @param reply na - */ - def HandleChatServiceResponse( - toChannel: String, - avatar_guid: PlanetSideGUID, - avatar_name: String, - cont: Zone, - avatar_pos: Vector3, - avatar_faction: PlanetSideEmpire.Value, - target: Int, - reply: ChatMsg - ): Unit = { - val tplayer_guid = - if (player.HasGUID) player.GUID - else PlanetSideGUID(0) - target match { - case 0 => // for other(s) user(s) - if (player.GUID != avatar_guid) { - reply.messageType match { - case ChatMessageType.CMT_TELL => - if (player.Name.equalsIgnoreCase(reply.recipient)) { - sendResponse(ChatMsg(reply.messageType, reply.wideContents, avatar_name, reply.contents, reply.note)) - } - case ChatMessageType.CMT_SILENCE => - val args = avatar_name.split(" ") - var silence_name: String = "" - var silence_time: Int = 5 - if (args.length == 1) { - silence_name = args(0) - } else if (args.length == 2) { - silence_name = args(0) - silence_time = args(1).toInt - } - if (player.Name == args(0)) { - if (!player.silenced) { - sendResponse( - ChatMsg(ChatMessageType.UNK_71, reply.wideContents, reply.recipient, "@silence_on", reply.note) - ) - player.silenced = true - context.system.scheduler.scheduleOnce( - silence_time minutes, - chatService, - ChatServiceMessage( - "gm", - ChatAction.GM( - PlanetSideGUID(0), - player.Name, - ChatMsg(ChatMessageType.CMT_SILENCE, true, "", player.Name, None) - ) - ) - ) - } else { - sendResponse( - ChatMsg(ChatMessageType.UNK_71, reply.wideContents, reply.recipient, "@silence_off", reply.note) - ) - player.silenced = false - } - } - case _ => - sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note)) - } - } - case 1 => // for player - if (player.Name == avatar_name) { - if ( - (reply.contents.length > 1 && (reply.contents.dropRight(reply.contents.length - 1) != "!" || reply.contents - .drop(1) - .dropRight(reply.contents.length - 2) == "!")) || reply.contents.length == 1 - ) { - sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note)) - } - } - case 2 => // both case - if ( - (reply.contents.length > 1 && (reply.contents.dropRight(reply.contents.length - 1) != "!" || reply.contents - .drop(1) - .dropRight(reply.contents.length - 2) == "!")) || reply.contents.length == 1 - ) { - reply.messageType match { - case ChatMessageType.CMT_OPEN => - if ( - Vector3.Distance( - player.Position, - avatar_pos - ) < 25 && player.Faction == avatar_faction && player.Continent == cont.Id - ) { - sendResponse( - ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note) - ) - } - case ChatMessageType.CMT_COMMAND => - sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note)) - case ChatMessageType.CMT_SQUAD => - if (squadChannel.nonEmpty) { - if ("/Chat/" + squadChannel.get == toChannel) { - sendResponse( - ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note) - ) - } - } - case ChatMessageType.CMT_PLATOON => - if (player.Faction == avatar_faction) { - sendResponse( - ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note) - ) - } - case ChatMessageType.CMT_VOICE => - if (Vector3.Distance(player.Position, avatar_pos) < 25 && player.Continent == cont.Id) { - sendResponse( - ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note) - ) - } - case _ => - sendResponse(ChatMsg(reply.messageType, reply.wideContents, reply.recipient, reply.contents, reply.note)) - } - } - } - } - /** * na * @param tplayer na @@ -2740,7 +2794,9 @@ class WorldSessionActor extends Actor with MDCContextAware { case Mountable.CanNotMount(obj: Vehicle, seat_num) => log.warn(s"MountVehicleMsg: ${tplayer.Name} attempted to mount $obj's seat $seat_num, but was not allowed") if (obj.SeatPermissionGroup(seat_num).contains(AccessPermissionGroup.Driver)) { - sendResponse(ChatMsg(ChatMessageType.CMT_OPEN, false, "", "You are not the driver of this vehicle.", None)) + sendResponse( + ChatMsg(ChatMessageType.CMT_OPEN, false, "", "You are not the driver of this vehicle.", None) + ) } case Mountable.CanNotMount(obj: Mountable, seat_num) => @@ -3139,7 +3195,7 @@ class WorldSessionActor extends Actor with MDCContextAware { case VehicleResponse.ServerVehicleOverrideStart(vehicle, pad) => val vdef = vehicle.Definition - ServerVehicleOverride(vehicle, vdef.AutoPilotSpeed1, GlobalDefinitions.isFlightVehicle(vdef): Int) + ServerVehicleOverride(vehicle, vdef.AutoPilotSpeed1, if (GlobalDefinitions.isFlightVehicle(vdef)) 1 else 0) case VehicleResponse.ServerVehicleOverrideEnd(vehicle, pad) => DriverVehicleControl(vehicle, vehicle.Definition.AutoPilotSpeed2) @@ -3294,7 +3350,7 @@ class WorldSessionActor extends Actor with MDCContextAware { */ def HandleSetCurrentAvatar(tplayer: Player): Unit = { log.info(s"HandleSetCurrentAvatar - ${tplayer.Name}") - player = tplayer + session = session.copy(player = tplayer) val guid = tplayer.GUID StartBundlingPackets() UpdateDeployableUIElements(Deployables.InitializeDeployableUIElements(avatar)) @@ -3696,7 +3752,7 @@ class WorldSessionActor extends Actor with MDCContextAware { characters.headOption match { case Some(character) => log.info(s"CharacterRequest/Select: character ${character.name} found in records") - avatar = character.toAvatar + session = session.copy(avatar = character.toAvatar) val faction: String = avatar.faction.toString.toLowerCase whenUsedLastMAXName(0) = faction + "hev" whenUsedLastMAXName(1) = faction + "hev_antipersonnel" @@ -3722,9 +3778,8 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg@BeginZoningMessage() => log.info("Reticulating splines ...") zoneLoaded = None - val continentId = continent.Id - traveler.zone = continentId - val faction = player.Faction + val continentId = continent.Id + val faction = player.Faction val factionChannel = s"$faction" continent.AvatarEvents ! Service.Join(continentId) continent.AvatarEvents ! Service.Join(factionChannel) @@ -4289,525 +4344,8 @@ class WorldSessionActor extends Actor with MDCContextAware { case msg @ SetChatFilterMessage(send_channel, origin, whitelist) => //log.info("SetChatFilters: " + msg) - case msg @ ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents) => - import ChatMessageType._ - log.info("Chat: " + msg) - - (messagetype, admin, recipient.trim, contents.trim) match { - case (CMT_FLY, true, _, _) => - flying = !flying - sendResponse( - ChatMsg(CMT_FLY, msg.wideContents, recipient, if (flying) "on" else "off", msg.note) - ) - - case (CMT_SPEED, true, _, _) => - speed = - try { - contents.toFloat - } catch { - case _: Throwable => - 1f - } - sendResponse(ChatMsg(messagetype, has_wide_contents, recipient, f"$speed%.3f", note_contents)) - - case (CMT_TOGGLESPECTATORMODE, true, _, _) => - player.spectator = !player.spectator - sendResponse( - ChatMsg( - CMT_TOGGLESPECTATORMODE, - msg.wideContents, - msg.recipient, - if (player.spectator) "on" else "off", - msg.note - ) - ) - - case (CMT_RECALL, _, _, _) => - val sanctuary = Zones.SanctuaryZoneId(player.Faction) - val errorMessage = zoningType match { - case Zoning.Method.Quit => Some("You can't recall to your sanctuary continent while quitting") - case Zoning.Method.InstantAction => - Some("You can't recall to your sanctuary continent while instant actioning") - case Zoning.Method.Recall => Some("You already requested to recall to your sanctuary continent") - case _ if continent.Id == sanctuary => - Some("You can't recall to your sanctuary when you are already in your sanctuary") - case _$msg if !player.isAlive || deadState != DeadState.Alive => - Some(if (player.isAlive) "@norecall_deconstructing" else "@norecall_dead") - case _ if player.VehicleSeated.nonEmpty => Some("@norecall_invehicle") - case _ => None - } - errorMessage match { - case Some(errorMessage) => - sendResponse( - ChatMsg( - CMT_QUIT, - false, - "", - errorMessage, - None - ) - ) - case None => - zoningType = Zoning.Method.Recall - zoningChatMessageType = messagetype - zoningStatus = Zoning.Status.Request - zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset()) - cluster ! Zoning.Recall.Request(player.Faction, sanctuary) - } - - case (CMT_INSTANTACTION, _, _, _) => - if (zoningType == Zoning.Method.Quit) { - sendResponse( - ChatMsg(CMT_QUIT, false, "", "You can't instant action while quitting.", None) - ) - } else if (zoningType == Zoning.Method.InstantAction) { - sendResponse(ChatMsg(CMT_QUIT, false, "", "@noinstantaction_instantactionting", None)) - } else if (zoningType == Zoning.Method.Recall) { - sendResponse( - ChatMsg( - CMT_QUIT, - false, - "", - "You won't instant action. You already requested to recall to your sanctuary continent", - None - ) - ) - } else if (!player.isAlive || deadState != DeadState.Alive) { - if (player.isAlive) { - sendResponse(ChatMsg(CMT_QUIT, false, "", "@noinstantaction_deconstructing", None)) - } else { - sendResponse(ChatMsg(CMT_QUIT, false, "", "@noinstantaction_dead", None)) - } - } else if (player.VehicleSeated.nonEmpty) { - sendResponse(ChatMsg(CMT_QUIT, false, "", "@noinstantaction_invehicle", None)) - } else { - zoningType = Zoning.Method.InstantAction - zoningChatMessageType = messagetype - zoningStatus = Zoning.Status.Request - zoningReset = context.system.scheduler.scheduleOnce(10 seconds, self, ZoningReset()) - cluster ! Zoning.InstantAction.Request(player.Faction) - } - - case (CMT_QUIT, _, _, _) => - if (zoningType == Zoning.Method.Quit) { - sendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_quitting", None)) - } else if (!player.isAlive || deadState != DeadState.Alive) { - if (player.isAlive) { - sendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_deconstructing", None)) - } else { - sendResponse(ChatMsg(CMT_QUIT, false, "", "@noquit_dead", None)) - } - } else if (player.VehicleSeated.nonEmpty) { - sendResponse(ChatMsg(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() - } - - case (CMT_SUICIDE, _, _, _) => - if (player.isAlive && deadState != DeadState.Release) { - Suicide(player) - } - - case (CMT_CULLWATERMARK, _, _, contents) => - if (contents.contains("40 80")) connectionState = 100 - else if (contents.contains("120 200")) connectionState = 25 - else connectionState = 50 - - case (CMT_DESTROY, _, _, contents) => - val guid = contents.toInt - continent.GUID(continent.Map.TerminalToSpawnPad.getOrElse(guid, guid)) match { - case Some(pad: VehicleSpawnPad) => - pad.Actor ! VehicleSpawnControl.ProcessControl.Flush - case Some(turret: FacilityTurret) if turret.isUpgrading => - WeaponTurrets.FinishUpgradingMannedTurret(turret, TurretUpgrade.None) - case _ => - self ! PacketCoding.CreateGamePacket(0, RequestDestroyMessage(PlanetSideGUID(guid))) - } - sendResponse(ChatMsg(messagetype, has_wide_contents, recipient, contents, note_contents)) - - case (_, _, _, "!loc") => - val loc = - s"zone=${continent.Id} pos=${player.Position.x},${player.Position.y},${player.Position.z}; ori=${player.Orientation.x},${player.Orientation.y},${player.Orientation.z}" - log.info(loc) - sendResponse(ChatMsg(messagetype, has_wide_contents, recipient, loc, note_contents)) - - case (_, _, _, contents) if contents.startsWith("!list") => - val localString: String = contents.drop(contents.indexOf(" ") + 1) - val zone = contents.split(" ").lift(1) match { - case None => - Some(continent) - case Some(id) => - Zones.zones.get(id) - } - - zone match { - case Some(zone) => - sendResponse( - ChatMsg( - CMT_GMOPEN, - has_wide_contents, - "Server", - "\\#8Name (Faction) [ID] at PosX PosY PosZ", - note_contents - ) - ) - - (zone.LivePlayers ++ zone.Corpses) - .filter(_.CharId != player.CharId) - .sortBy(_.Name) - .foreach(player => { - sendResponse( - ChatMsg( - CMT_GMOPEN, - has_wide_contents, - "Server", - s"\\#7${player.Name} (${player.Faction}) [${player.CharId}] at ${player.Position.x.toInt} ${player.Position.y.toInt} ${player.Position.z.toInt}", - note_contents - ) - ) - }) - case None => - sendResponse( - ChatMsg( - CMT_GMOPEN, - has_wide_contents, - "Server", - "Invalid zone ID", - note_contents - ) - ) - } - - case (_, true, _, contents) if contents.startsWith("!kick") => - val input = contents.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) } - } - 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 None => - sendResponse( - ChatMsg( - CMT_GMOPEN, - has_wide_contents, - "Server", - "Invalid player", - note_contents - ) - ) - } - } - - case (CMT_CAPTUREBASE, true, _, contents) => - val args = contents.split(" ").filter(_ != "") - - val (faction, factionPos) = args.zipWithIndex - .map { case (faction, pos) => (faction.toLowerCase, pos) } - .map { - case ("tr", pos) => Some(PlanetSideEmpire.TR, pos) - case ("nc", pos) => Some(PlanetSideEmpire.NC, pos) - case ("vs", pos) => Some(PlanetSideEmpire.VS, pos) - case ("none", pos) => Some(PlanetSideEmpire.NEUTRAL, pos) - case _ => None - } - .flatten - .headOption match { - case Some((faction, pos)) => (faction, Some(pos)) - case None => (player.Faction, None) - } - - val (buildingsOption, buildingPos) = args.zipWithIndex - .map { - case (_, pos) if (factionPos.isDefined && factionPos.get == pos) => None - case ("all", pos) => - Some( - Some( - continent.Buildings - .filter { - case (_, building) => building.CaptureTerminal.isDefined - } - .values - .toSeq - ), - Some(pos) - ) - case (name, pos) => - continent.Buildings.find { - case (_, building) => name.equalsIgnoreCase(building.Name) && building.CaptureTerminal.isDefined - } match { - case Some((_, building)) => Some(Some(Seq(building)), Some(pos)) - case None => - try { - // check if we have a timer - name.toInt - None - } catch { - case _: Throwable => - Some(None, Some(pos)) - } - } - } - .flatten - .headOption match { - case Some((buildings, pos)) => (buildings, pos) - case None => (None, None) - } - val (timerOption, timerPos) = args.zipWithIndex - .map { - case (_, pos) - if (factionPos.isDefined && factionPos.get == pos || buildingPos.isDefined && buildingPos.get == pos) => - None - case (timer, pos) => - try { - val t = timer.toInt // TODO what is the timer format supposed to be? - Some(Some(t), Some(pos)) - } catch { - case _: Throwable => - Some(None, Some(pos)) - } - } - .flatten - .headOption match { - case Some((timer, posOption)) => (timer, posOption) - case None => (None, None) - } - - (factionPos, buildingPos, timerPos, buildingsOption, timerOption) match { - case // [[|none []] - (Some(0), None, Some(1), None, Some(_)) | (Some(0), None, None, None, None) | - (None, None, None, None, None) | - // [ [|none [timer]]] - (None | Some(1), Some(0), None, Some(_), None) | (Some(1), Some(0), Some(2), Some(_), Some(_)) | - // [all [|none]] - (Some(1) | None, Some(0), None, Some(_), None) => - val buildings = buildingsOption.getOrElse( - continent.Buildings - .filter { - case (_, building) => - building.PlayersInSOI.find { soiPlayer => - player.CharId == soiPlayer.CharId - }.isDefined - } - .map { case (_, building) => building } - ) - buildings foreach { building => - // TODO implement timer - building.Faction = faction - continent.LocalEvents ! LocalServiceMessage( - continent.Id, - LocalAction.SetEmpire(building.GUID, faction) - ) - } - case (_, Some(0), _, None, _) => - sendResponse( - ChatMsg( - UNK_229, - true, - "", - s"\\#FF4040ERROR - \'${args(0)}\' is not a valid building name.", - None - ) - ) - case (Some(0), _, Some(1), _, None) | (Some(1), Some(0), Some(2), _, None) => - sendResponse( - ChatMsg( - UNK_229, - true, - "", - s"\\#FF4040ERROR - \'${args(timerPos.get)}\' is not a valid timer value.", - None - ) - ) - case _ => - sendResponse( - ChatMsg( - UNK_229, - true, - "", - "usage: /capturebase [[|none []] | [ [|none [timer]]] | [all [|none]]", - None - ) - ) - } - - case (_, _, "tr", _) => - sendResponse( - ZonePopulationUpdateMessage(4, 414, 138, contents.toInt, 138, contents.toInt / 2, 138, 0, 138, 0) - ) - - case (_, _, "nc", _) => - sendResponse( - ZonePopulationUpdateMessage(4, 414, 138, 0, 138, contents.toInt, 138, contents.toInt / 3, 138, 0) - ) - - case (_, _, "vs", _) => - ZonePopulationUpdateMessage(4, 414, 138, contents.toInt * 2, 138, 0, 138, contents.toInt, 138, 0) - - case (_, _, "bo", _) => - sendResponse(ZonePopulationUpdateMessage(4, 414, 138, 0, 138, 0, 138, 0, 138, contents.toInt)) - - case (_, _, _, contents) if contents.startsWith("!ntu") => - continent.Buildings.values.foreach(building => - building.Amenities.foreach(amenity => - amenity.Definition match { - case GlobalDefinitions.resource_silo => - val r = new scala.util.Random - val silo = amenity.asInstanceOf[ResourceSilo] - val ntu: Int = 900 + r.nextInt(100) - silo.NtuCapacitor - silo.Actor ! ResourceSilo.UpdateChargeLevel(ntu) - - case _ => ; - } - ) - ) - - case (CMT_OPEN, _, _, _) if !player.silenced => - chatService ! ChatServiceMessage( - "local", - ChatAction.Local(player.GUID, player.Name, continent, player.Position, player.Faction, msg) - ) - - case (CMT_VOICE, _, _, _) => - chatService ! ChatServiceMessage( - "voice", - ChatAction.Voice(player.GUID, player.Name, continent, player.Position, player.Faction, msg) - ) - case (CMT_TELL, _, _, _) if !player.silenced => - chatService ! ChatServiceMessage("tell", ChatAction.Tell(player.GUID, player.Name, msg)) - - case (CMT_BROADCAST, _, _, _) if !player.silenced => - chatService ! ChatServiceMessage( - "broadcast", - ChatAction.Broadcast(player.GUID, player.Name, continent, player.Position, player.Faction, msg) - ) - - case (CMT_NOTE, _, _, _) => - chatService ! ChatServiceMessage("note", ChatAction.Note(player.GUID, player.Name, msg)) - - case (CMT_SILENCE, true, _, _) => - chatService ! ChatServiceMessage("gm", ChatAction.GM(player.GUID, player.Name, msg)) - - case (CMT_SQUAD, _, _, _) => - if (squadChannel.nonEmpty) { - chatService ! ChatServiceMessage( - squadChannel.get, - ChatAction.Squad(player.GUID, player.Name, continent, player.Position, player.Faction, msg) - ) - } - - case (CMT_PLATOON, _, _, _) if !player.silenced => - chatService ! ChatServiceMessage( - "platoon", - ChatAction.Platoon(player.GUID, player.Name, continent, player.Position, player.Faction, msg) - ) - - case (CMT_COMMAND, true, _, _) => - chatService ! ChatServiceMessage( - "command", - ChatAction.Command(player.GUID, player.Name, continent, player.Position, player.Faction, msg) - ) - - case ( - CMT_WHO | CMT_WHO_CSR | CMT_WHO_CR | CMT_WHO_PLATOONLEADERS | CMT_WHO_SQUADLEADERS | CMT_WHO_TEAMS, - _, - _, - _ - ) => - val poplist = continent.Players - val popTR = poplist.count(_.faction == PlanetSideEmpire.TR) - val popNC = poplist.count(_.faction == PlanetSideEmpire.NC) - val popVS = poplist.count(_.faction == PlanetSideEmpire.VS) - val contName = continent.Map.Name - StartBundlingPackets() - sendResponse(ChatMsg(ChatMessageType.CMT_WHO, true, "", "That command doesn't work for now, but : ", None)) - sendResponse(ChatMsg(ChatMessageType.CMT_WHO, true, "", "NC online : " + popNC + " on " + contName, None)) - sendResponse(ChatMsg(ChatMessageType.CMT_WHO, true, "", "TR online : " + popTR + " on " + contName, None)) - sendResponse(ChatMsg(ChatMessageType.CMT_WHO, true, "", "VS online : " + popVS + " on " + contName, None)) - StopBundlingPackets() - - case _ => - } - - 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" || - zone == "tzshtr" || zone == "tzcotr" || zone == "tzdrtr" || - zone == "tzshnc" || zone == "tzconc" || zone == "tzdrnc" || - zone == "tzshvs" || zone == "tzcovs" || zone == "tzdrvs") - ) { - deadState = DeadState.Release //cancel movement updates - PlayerActionsToCancel() - continent.GUID(player.VehicleSeated) match { - case Some(vehicle: Vehicle) if vehicle.MountedIn.isEmpty => - vehicle.PassengerInSeat(player) match { - case Some(0) => - vehicle.Position = pos - LoadZonePhysicalSpawnPoint(zone, pos, Vector3.Zero, 0) - case _ => //not seated as the driver, in which case we can't move - deadState = DeadState.Alive - } - case None => - player.Position = pos - //continent.AvatarEvents ! AvatarServiceMessage(continent.Id, AvatarAction.ObjectDelete(player.GUID, player.GUID)) - LoadZonePhysicalSpawnPoint(zone, pos, Vector3.Zero, 0) - case _ => //seated in something that is not a vehicle or the vehicle is cargo, in which case we can't move - deadState = DeadState.Alive - } - } - case (_, _, _) => ; - } - - CSRWarp.read(traveler, msg) match { - case (true, pos) => - // continent.Id == "c1" || continent.Id == "c2" || continent.Id == "c3" || continent.Id == "c4" || continent.Id == "c5" || continent.Id == "c6" || - if ( - player.isAlive && (admin || continent.Id == "z8" || - continent.Id == "tzshtr" || continent.Id == "tzcotr" || continent.Id == "tzdrtr" || - continent.Id == "tzshnc" || continent.Id == "tzconc" || continent.Id == "tzdrnc" || - continent.Id == "tzshvs" || continent.Id == "tzcovs" || continent.Id == "tzdrvs") - ) { - deadState = DeadState.Release //cancel movement updates - PlayerActionsToCancel() - continent.GUID(player.VehicleSeated) match { - case Some(vehicle: Vehicle) if vehicle.MountedIn.isEmpty => - vehicle.PassengerInSeat(player) match { - case Some(0) => - vehicle.Position = pos - LoadZonePhysicalSpawnPoint(continent.Id, pos, Vector3.z(vehicle.Orientation.z), 0) - case _ => //not seated as the driver, in which case we can't move - deadState = DeadState.Alive - } - case None => - player.Position = pos - sendResponse(PlayerStateShiftMessage(ShiftState(0, pos, player.Orientation.z, None))) - deadState = DeadState.Alive //must be set here - case _ => //seated in something that is not a vehicle or the vehicle is cargo, in which case we can't move - deadState = DeadState.Alive - } - } - case (_, _) => ; - } + case msg: ChatMsg => + chatActor ! ChatActor.Message(msg) case msg @ VoiceHostRequest(unk, PlanetSideGUID(player_guid), data) => log.info("Player " + player_guid + " requested in-game voice chat.") @@ -5146,7 +4684,7 @@ class WorldSessionActor extends Actor with MDCContextAware { /* line 1c: vehicle is the same faction as player and either the owner is absent or the vehicle is destroyed */ /* line 2: vehicle is not mounted in anything or, if it is, its seats are empty */ if ( - (admin || + (session.admin || (player.VehicleOwned.contains(object_guid) && vehicle.Owner.contains(player.GUID)) || (player.Faction == vehicle.Faction && ((vehicle.Owner.isEmpty || continent .GUID(vehicle.Owner.get) @@ -6717,12 +6255,9 @@ class WorldSessionActor extends Actor with MDCContextAware { def RegisterVehicleFromSpawnPad(obj: Vehicle, pad: VehicleSpawnPad): TaskResolver.GiveTask = { TaskResolver.GiveTask( new Task() { - private val localVehicle = obj - private val localPad = pad.Actor - private val localSession: String = sessionId.toString - private val localPlayer = player - private val localVehicleService = continent.VehicleEvents - private val localZone = continent + private val localVehicle = obj + private val localPad = pad.Actor + private val localPlayer = player override def Description: String = s"register a ${localVehicle.Definition.Name} for spawn pad" @@ -7777,7 +7312,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * `PlayerSuicide` * @param tplayer the player to be killed */ - def Suicide(tplayer: Player): Unit = { + def suicide(tplayer: Player): Unit = { tplayer.History(PlayerSuicide(PlayerSource(tplayer))) tplayer.Actor ! Player.Die() } @@ -7821,13 +7356,11 @@ class WorldSessionActor extends Actor with MDCContextAware { shooting = None case None => ; } - if (flying) { - sendResponse(ChatMsg(ChatMessageType.CMT_FLY, false, "", "off", None)) - flying = false + if (session.flying) { + chatActor ! ChatActor.Message(ChatMsg(ChatMessageType.CMT_FLY, false, "", "off", None)) } - if (speed > 1) { - sendResponse(ChatMsg(ChatMessageType.CMT_SPEED, false, "", "1.000", None)) - speed = 1f + if (session.speed > 1) { + chatActor ! ChatActor.Message(ChatMsg(ChatMessageType.CMT_SPEED, false, "", "1.000", None)) } } @@ -7958,7 +7491,7 @@ class WorldSessionActor extends Actor with MDCContextAware { * The priority of object confirmation is `direct` then `occupant.VehicleSeated`. * Once an object is found, the remainder are ignored. * @param direct a game object in which the player may be sat - * @param target the player who is sat and may have specified the game object in which mounted + * @param occupant the player who is sat and may have specified the game object in which mounted * @return a tuple consisting of a vehicle reference and a seat index * if and only if the vehicle is known to this client and the `WorldSessioNActor`-global `player` occupies it; * `(None, None)`, otherwise (even if the vehicle can be determined) @@ -8819,8 +8352,9 @@ class WorldSessionActor extends Actor with MDCContextAware { val func = data.damage_model.Calculate(data) target match { case obj: Player if obj.CanDamage && obj.Actor != Default.Actor => - if (obj.spectator) { - AdministrativeKick(player, obj != player) // little thing for auto kick + // auto kick spectators doing damage to others + if (obj.spectator && obj != player) { + AdministrativeKick(player) } else { obj.Actor ! Vitality.Damage(func) } @@ -9083,7 +8617,9 @@ class WorldSessionActor extends Actor with MDCContextAware { continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.ClearSpecific(List(old), continent)) continent.LocalEvents ! LocalServiceMessage.Deployables(RemoverActor.AddTask(old, continent, Some(0 seconds))) if (msg) { //max test - sendResponse(ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}OldestDestroyed", None)) + sendResponse( + ChatMsg(ChatMessageType.UNK_229, false, "", s"@${definition.Descriptor}OldestDestroyed", None) + ) } case None => ; //should be an invalid case log.warn( @@ -9402,14 +8938,14 @@ class WorldSessionActor extends Actor with MDCContextAware { * @see `interstellarFerry` * @see `LoadZoneAsPlayer` * @see `LoadZoneInVehicle` - * @param zone_id the zone in which the player will be placed + * @param zoneId the zone in which the player will be placed * @param pos the game world coordinates where the player will be positioned * @param ori the direction in which the player will be oriented * @param respawnTime the character downtime spent respawning, as clocked on the redeployment screen; * does not factor in any time required for loading zone or game objects */ - def LoadZonePhysicalSpawnPoint(zone_id: String, pos: Vector3, ori: Vector3, respawnTime: Long): Unit = { - log.info(s"Load in zone $zone_id at position $pos in $respawnTime seconds") + def LoadZonePhysicalSpawnPoint(zoneId: String, pos: Vector3, ori: Vector3, respawnTime: Long): Unit = { + log.info(s"Load in zone $zoneId at position $pos in $respawnTime seconds") respawnTimer.cancel reviveTimer.cancel val backpack = player.isBackpack @@ -9433,7 +8969,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val newPlayer = RespawnClone(player) newPlayer.Position = pos newPlayer.Orientation = ori - LoadZoneAsPlayer(newPlayer, zone_id) + LoadZoneAsPlayer(newPlayer, zoneId) } else { //deactivate non-passive implants player.Implants.indices.foreach { index => @@ -9444,7 +8980,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } interstellarFerry.orElse(continent.GUID(player.VehicleSeated)) match { case Some(vehicle: Vehicle) => //driver or passenger in vehicle using a warp gate, or a droppod - LoadZoneInVehicle(vehicle, pos, ori, zone_id) + LoadZoneInVehicle(vehicle, pos, ori, zoneId) case _ if player.HasGUID => //player is deconstructing self or instant action val player_guid = player.GUID @@ -9455,12 +8991,12 @@ class WorldSessionActor extends Actor with MDCContextAware { ) player.Position = pos player.Orientation = ori - LoadZoneAsPlayer(player, zone_id) + LoadZoneAsPlayer(player, zoneId) case _ => //player is logging in player.Position = pos player.Orientation = ori - LoadZoneAsPlayer(player, zone_id) + LoadZoneAsPlayer(player, zoneId) } } import scala.concurrent.ExecutionContext.Implicits.global @@ -9500,7 +9036,7 @@ class WorldSessionActor extends Actor with MDCContextAware { val original = player if (player.isBackpack) { //unregister avatar locker + GiveWorld - player = tplayer + session = session.copy(player = tplayer) (taskResolver, TaskBeforeZoneChange(GUIDTask.UnregisterLocker(original.Locker)(continent.GUID), zone_id)) } else if (player.HasGUID) { //unregister avatar whole + GiveWorld @@ -10996,31 +10532,26 @@ class WorldSessionActor extends Actor with MDCContextAware { turnCounterFunc(player.GUID) } - 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 AdministrativeKick(tplayer: Player) = { + 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 _ => ; } } 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")) + sendResponse(DropSession(session.id, "kick by GM")) } def ImmediateDisconnect(): Unit = { @@ -11028,7 +10559,7 @@ class WorldSessionActor extends Actor with MDCContextAware { accountPersistence ! AccountPersistenceService.Logout(avatar.name) } sendResponse(DropCryptoSession()) - sendResponse(DropSession(sessionId, "user quit")) + sendResponse(DropSession(session.id, "user quit")) } def HandleWeaponFire(weaponGUID : PlanetSideGUID, projectileGUID : PlanetSideGUID, shotOrigin : Vector3) : Unit = { @@ -11249,7 +10780,7 @@ class WorldSessionActor extends Actor with MDCContextAware { } def sendResponse(msg: Any): Unit = { - MDC("sessionId") = sessionId.toString + MDC("sessionId") = session.id.toString rightRef !> msg } @@ -11258,112 +10789,3 @@ class WorldSessionActor extends Actor with MDCContextAware { sendResponse(RawPacket(pkt)) } } - -object WorldSessionActor { - - /** Object purchasing cooldowns.
- * key - object id
- * value - time last used (ms) - */ - val delayedPurchaseEntries: Map[Int, Long] = Map( - GlobalDefinitions.ams.ObjectId -> 300000, //5min - GlobalDefinitions.ant.ObjectId -> 300000, //5min - GlobalDefinitions.apc_nc.ObjectId -> 300000, //5min - GlobalDefinitions.apc_tr.ObjectId -> 300000, //5min - GlobalDefinitions.apc_vs.ObjectId -> 300000, //5min - GlobalDefinitions.aurora.ObjectId -> 300000, //5min - GlobalDefinitions.battlewagon.ObjectId -> 300000, //5min - GlobalDefinitions.dropship.ObjectId -> 300000, //5min - GlobalDefinitions.flail.ObjectId -> 300000, //5min - GlobalDefinitions.fury.ObjectId -> 300000, //5min - GlobalDefinitions.galaxy_gunship.ObjectId -> 600000, //10min - GlobalDefinitions.lodestar.ObjectId -> 300000, //5min - GlobalDefinitions.liberator.ObjectId -> 300000, //5min - GlobalDefinitions.lightgunship.ObjectId -> 300000, //5min - GlobalDefinitions.lightning.ObjectId -> 300000, //5min - GlobalDefinitions.magrider.ObjectId -> 300000, //5min - GlobalDefinitions.mediumtransport.ObjectId -> 300000, //5min - GlobalDefinitions.mosquito.ObjectId -> 300000, //5min - GlobalDefinitions.phantasm.ObjectId -> 300000, //5min - GlobalDefinitions.prowler.ObjectId -> 300000, //5min - GlobalDefinitions.quadassault.ObjectId -> 300000, //5min - GlobalDefinitions.quadstealth.ObjectId -> 300000, //5min - GlobalDefinitions.router.ObjectId -> 300000, //5min - GlobalDefinitions.switchblade.ObjectId -> 300000, //5min - GlobalDefinitions.skyguard.ObjectId -> 300000, //5min - GlobalDefinitions.threemanheavybuggy.ObjectId -> 300000, //5min - GlobalDefinitions.thunderer.ObjectId -> 300000, //5min - GlobalDefinitions.two_man_assault_buggy.ObjectId -> 300000, //5min - GlobalDefinitions.twomanhoverbuggy.ObjectId -> 300000, //5min - GlobalDefinitions.twomanheavybuggy.ObjectId -> 300000, //5min - GlobalDefinitions.vanguard.ObjectId -> 300000, //5min - GlobalDefinitions.vulture.ObjectId -> 300000, //5min - GlobalDefinitions.wasp.ObjectId -> 300000, //5min - GlobalDefinitions.flamethrower.ObjectId -> 180000 //3min - ) - - /** Object use cooldowns.
- * key - object id
- * value - time last used (ms) - */ - val delayedGratificationEntries: Map[Int, Long] = Map( - GlobalDefinitions.medkit.ObjectId -> 5000, //5s - GlobalDefinitions.super_armorkit.ObjectId -> 1200000, //20min - GlobalDefinitions.super_medkit.ObjectId -> 1200000, //20min - GlobalDefinitions.super_staminakit.ObjectId -> 1200000 //20min - ) - - final case class ResponseToSelf(pkt: PlanetSideGamePacket) - - private final case class PokeClient() - private final case class ServerLoaded() - private final case class NewPlayerLoaded(tplayer: Player) - private final case class PlayerLoaded(tplayer: Player) - private final case class PlayerFailedToLoad(tplayer: Player) - private final case class CreateCharacter( - name: String, - head: Int, - voice: CharacterVoice.Value, - gender: CharacterGender.Value, - empire: PlanetSideEmpire.Value - ) - private final case class ListAccountCharacters() - private final case class SetCurrentAvatar(tplayer: Player, max_attempts: Int, attempt: Int = 0) - private final case class ZoningReset() - - final val ftes = ( - FirstTimeEvents.Maps ++ FirstTimeEvents.Monoliths ++ - FirstTimeEvents.Standard.All ++ FirstTimeEvents.Cavern.All ++ - FirstTimeEvents.TR.All ++ FirstTimeEvents.NC.All ++ FirstTimeEvents.VS.All ++ - FirstTimeEvents.Generic - ).toList - - /** - * The message that progresses some form of user-driven activity with a certain eventual outcome - * and potential feedback per cycle. - * @param delta how much the progress value changes each tick, which will be treated as a percentage; - * must be a positive value - * @param completionAction a finalizing action performed once the progress reaches 100(%) - * @param tickAction an action that is performed for each increase of progress - */ - final case class ProgressEvent(delta: Float, completionAction: () => Unit, tickAction: Float => Boolean) - - private final val zoningCountdownMessages: Seq[Int] = Seq(5, 10, 20) - - protected final case class SquadUIElement( - name: String, - index: Int, - zone: Int, - health: Int, - armor: Int, - position: Vector3 - ) - - private final case class FinalizeDeployable( - obj: PlanetSideGameObject with Deployable, - tool: ConstructionItem, - index: Int - ) - - private final case class LoadedRemoteProjectile(projectile_guid: PlanetSideGUID, projectile: Option[Projectile]) -} diff --git a/pslogin/src/main/scala/CryptoSessionActor.scala b/common/src/main/scala/net/psforever/login/CryptoSessionActor.scala similarity index 99% rename from pslogin/src/main/scala/CryptoSessionActor.scala rename to common/src/main/scala/net/psforever/login/CryptoSessionActor.scala index e0cf5c62..b30b70b5 100644 --- a/pslogin/src/main/scala/CryptoSessionActor.scala +++ b/common/src/main/scala/net/psforever/login/CryptoSessionActor.scala @@ -1,18 +1,18 @@ -package net.psforever.pslogin +package net.psforever.login -import akka.actor.{Actor, ActorRef, MDCContextAware} -import net.psforever.crypto.CryptoInterface.CryptoStateWithMAC -import net.psforever.crypto.CryptoInterface -import net.psforever.packet._ -import scodec.Attempt.{Failure, Successful} -import scodec.bits._ import java.security.SecureRandom +import akka.actor.MDCContextAware.Implicits._ +import akka.actor.{Actor, ActorRef, MDCContextAware} +import net.psforever.crypto.CryptoInterface +import net.psforever.crypto.CryptoInterface.CryptoStateWithMAC +import net.psforever.packet._ import net.psforever.packet.control._ import net.psforever.packet.crypto._ import net.psforever.packet.game.PingMsg import org.log4s.MDC -import MDCContextAware.Implicits._ +import scodec.Attempt.{Failure, Successful} +import scodec.bits._ sealed trait CryptoSessionAPI final case class DropCryptoSession() extends CryptoSessionAPI diff --git a/pslogin/src/main/scala/LoginSessionActor.scala b/common/src/main/scala/net/psforever/login/LoginSessionActor.scala similarity index 97% rename from pslogin/src/main/scala/LoginSessionActor.scala rename to common/src/main/scala/net/psforever/login/LoginSessionActor.scala index 60bd418e..ef42e3f5 100644 --- a/pslogin/src/main/scala/LoginSessionActor.scala +++ b/common/src/main/scala/net/psforever/login/LoginSessionActor.scala @@ -1,27 +1,28 @@ -package net.psforever.pslogin +package net.psforever.login -import java.net.InetSocketAddress +import java.net.{InetAddress, InetSocketAddress} + +import akka.actor.MDCContextAware.Implicits._ import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} -import net.psforever.packet.{PlanetSideGamePacket, _} +import com.github.t3hnar.bcrypt._ +import net.psforever.objects.{Account, Default} import net.psforever.packet.control._ +import net.psforever.packet.game.LoginRespMessage.{LoginError, StationError, StationSubscriptionStatus} import net.psforever.packet.game._ +import net.psforever.packet.{PlanetSideGamePacket, _} +import net.psforever.persistence +import net.psforever.types.PlanetSideEmpire +import net.psforever.util.Config +import net.psforever.util.Database._ import org.log4s.MDC import scodec.bits._ -import MDCContextAware.Implicits._ -import net.psforever.objects.Account -import net.psforever.objects.Default -import net.psforever.types.PlanetSideEmpire import services.ServiceManager import services.ServiceManager.Lookup import services.account.{ReceiveIPAddress, RetrieveIPAddress, StoreAccountData} -import com.github.t3hnar.bcrypt._ -import net.psforever.packet.game.LoginRespMessage.{LoginError, StationError, StationSubscriptionStatus} -import net.psforever.persistence + +import scala.concurrent.Future import scala.concurrent.duration._ import scala.util.{Failure, Success} -import scala.concurrent.Future -import Database._ -import java.net.InetAddress class LoginSessionActor extends Actor with MDCContextAware { private[this] val log = org.log4s.getLogger diff --git a/pslogin/src/main/scala/PacketCodingActor.scala b/common/src/main/scala/net/psforever/login/PacketCodingActor.scala similarity index 99% rename from pslogin/src/main/scala/PacketCodingActor.scala rename to common/src/main/scala/net/psforever/login/PacketCodingActor.scala index 9cd1e1c5..7ea56164 100644 --- a/pslogin/src/main/scala/PacketCodingActor.scala +++ b/common/src/main/scala/net/psforever/login/PacketCodingActor.scala @@ -1,13 +1,13 @@ -package net.psforever.pslogin +package net.psforever.login +import akka.actor.MDCContextAware.Implicits._ import akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware} +import net.psforever.objects.Default import net.psforever.packet._ +import net.psforever.packet.control.{HandleGamePacket, _} +import org.log4s.MDC import scodec.Attempt.{Failure, Successful} import scodec.bits._ -import org.log4s.MDC -import MDCContextAware.Implicits._ -import net.psforever.objects.Default -import net.psforever.packet.control.{HandleGamePacket, _} import scala.annotation.tailrec import scala.collection.mutable diff --git a/pslogin/src/main/scala/Session.scala b/common/src/main/scala/net/psforever/login/Session.scala similarity index 94% rename from pslogin/src/main/scala/Session.scala rename to common/src/main/scala/net/psforever/login/Session.scala index dd13c9a7..777ed596 100644 --- a/pslogin/src/main/scala/Session.scala +++ b/common/src/main/scala/net/psforever/login/Session.scala @@ -1,13 +1,11 @@ -package net.psforever.pslogin +package net.psforever.login import java.net.InetSocketAddress -import akka.actor._ -import scodec.bits._ - -import akka.actor.{ActorContext, ActorRef, PoisonPill} +import akka.actor.MDCContextAware.Implicits._ +import akka.actor.{ActorContext, ActorRef, PoisonPill, _} import com.github.nscala_time.time.Imports._ -import MDCContextAware.Implicits._ +import scodec.bits._ sealed trait SessionState final case class New() extends SessionState diff --git a/pslogin/src/main/scala/SessionRouter.scala b/common/src/main/scala/net/psforever/login/SessionRouter.scala similarity index 99% rename from pslogin/src/main/scala/SessionRouter.scala rename to common/src/main/scala/net/psforever/login/SessionRouter.scala index b6261aeb..1b1e197f 100644 --- a/pslogin/src/main/scala/SessionRouter.scala +++ b/common/src/main/scala/net/psforever/login/SessionRouter.scala @@ -1,19 +1,18 @@ -package net.psforever.pslogin +package net.psforever.login import java.net.InetSocketAddress -import akka.actor._ -import org.log4s.MDC -import scodec.bits._ - -import scala.collection.mutable import akka.actor.SupervisorStrategy.Stop +import akka.actor._ import net.psforever.packet.PacketCoding import net.psforever.packet.control.ConnectionClose +import net.psforever.util.Config +import org.log4s.MDC +import scodec.bits._ import services.ServiceManager import services.ServiceManager.Lookup import services.account.{IPAddress, StoreIPAddress} - +import scala.collection.mutable import scala.concurrent.duration._ sealed trait SessionRouterAPI diff --git a/pslogin/src/main/scala/TcpListener.scala b/common/src/main/scala/net/psforever/login/TcpListener.scala similarity index 98% rename from pslogin/src/main/scala/TcpListener.scala rename to common/src/main/scala/net/psforever/login/TcpListener.scala index f221071b..1a65c966 100644 --- a/pslogin/src/main/scala/TcpListener.scala +++ b/common/src/main/scala/net/psforever/login/TcpListener.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin +package net.psforever.login import java.net.{InetAddress, InetSocketAddress} diff --git a/pslogin/src/main/scala/UdpListener.scala b/common/src/main/scala/net/psforever/login/UdpListener.scala similarity index 98% rename from pslogin/src/main/scala/UdpListener.scala rename to common/src/main/scala/net/psforever/login/UdpListener.scala index dc281810..e48b0d92 100644 --- a/pslogin/src/main/scala/UdpListener.scala +++ b/common/src/main/scala/net/psforever/login/UdpListener.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin +package net.psforever.login import java.net.{InetAddress, InetSocketAddress} diff --git a/pslogin/src/main/scala/UdpNetworkSimulator.scala b/common/src/main/scala/net/psforever/login/UdpNetworkSimulator.scala similarity index 99% rename from pslogin/src/main/scala/UdpNetworkSimulator.scala rename to common/src/main/scala/net/psforever/login/UdpNetworkSimulator.scala index 378e71b8..bc823667 100644 --- a/pslogin/src/main/scala/UdpNetworkSimulator.scala +++ b/common/src/main/scala/net/psforever/login/UdpNetworkSimulator.scala @@ -1,11 +1,11 @@ -package net.psforever.pslogin +package net.psforever.login import akka.actor.{Actor, ActorRef} import akka.io._ -import scala.util.Random import scala.collection.mutable import scala.concurrent.duration._ +import scala.util.Random /** Parameters for the Network simulator * diff --git a/pslogin/src/main/scala/WorldSession.scala b/common/src/main/scala/net/psforever/login/WorldSession.scala similarity index 99% rename from pslogin/src/main/scala/WorldSession.scala rename to common/src/main/scala/net/psforever/login/WorldSession.scala index a43e1b26..fa06f1d8 100644 --- a/pslogin/src/main/scala/WorldSession.scala +++ b/common/src/main/scala/net/psforever/login/WorldSession.scala @@ -1,15 +1,15 @@ -package net.psforever.pslogin +package net.psforever.login import akka.actor.ActorRef import akka.pattern.{AskTimeoutException, ask} import akka.util.Timeout -import net.psforever.objects.{AmmoBox, GlobalDefinitions, Player, Tool} import net.psforever.objects.equipment.{Ammo, Equipment} import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.inventory.{Container, InventoryItem} import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.zones.Zone +import net.psforever.objects.{AmmoBox, GlobalDefinitions, Player, Tool} import net.psforever.packet.game.ObjectHeldMessage import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3} import services.Service @@ -19,7 +19,7 @@ import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future import scala.concurrent.duration._ import scala.language.implicitConversions -import scala.util.{Success, Failure} +import scala.util.{Failure, Success} object WorldSession { diff --git a/pslogin/src/main/scala/psadmin/CmdInternal.scala b/common/src/main/scala/net/psforever/login/psadmin/CmdInternal.scala similarity index 91% rename from pslogin/src/main/scala/psadmin/CmdInternal.scala rename to common/src/main/scala/net/psforever/login/psadmin/CmdInternal.scala index 8197b8a4..91ea3d32 100644 --- a/pslogin/src/main/scala/psadmin/CmdInternal.scala +++ b/common/src/main/scala/net/psforever/login/psadmin/CmdInternal.scala @@ -1,8 +1,7 @@ -package net.psforever.pslogin.psadmin +package net.psforever.login.psadmin +import net.psforever.util.Config import scala.collection.mutable -import net.psforever.pslogin.Config - import scala.jdk.CollectionConverters._ object CmdInternal { diff --git a/pslogin/src/main/scala/psadmin/CmdListPlayers.scala b/common/src/main/scala/net/psforever/login/psadmin/CmdListPlayers.scala similarity index 90% rename from pslogin/src/main/scala/psadmin/CmdListPlayers.scala rename to common/src/main/scala/net/psforever/login/psadmin/CmdListPlayers.scala index 639aa612..e6af302e 100644 --- a/pslogin/src/main/scala/psadmin/CmdListPlayers.scala +++ b/common/src/main/scala/net/psforever/login/psadmin/CmdListPlayers.scala @@ -1,10 +1,10 @@ -package net.psforever.pslogin.psadmin +package net.psforever.login.psadmin -import akka.actor.ActorRef -import akka.actor.Actor -import scala.collection.mutable.Map +import akka.actor.{Actor, ActorRef} import net.psforever.objects.zones.InterstellarCluster +import scala.collection.mutable.Map + class CmdListPlayers(args: Array[String], services: Map[String, ActorRef]) extends Actor { private[this] val log = org.log4s.getLogger(self.path.name) diff --git a/pslogin/src/main/scala/psadmin/CmdShutdown.scala b/common/src/main/scala/net/psforever/login/psadmin/CmdShutdown.scala similarity index 90% rename from pslogin/src/main/scala/psadmin/CmdShutdown.scala rename to common/src/main/scala/net/psforever/login/psadmin/CmdShutdown.scala index 75037125..9ddbcb22 100644 --- a/pslogin/src/main/scala/psadmin/CmdShutdown.scala +++ b/common/src/main/scala/net/psforever/login/psadmin/CmdShutdown.scala @@ -1,6 +1,7 @@ -package net.psforever.pslogin.psadmin +package net.psforever.login.psadmin import akka.actor.{Actor, ActorRef} + import scala.collection.mutable.Map class CmdShutdown(args: Array[String], services: Map[String, ActorRef]) extends Actor { diff --git a/pslogin/src/main/scala/psadmin/PsAdminActor.scala b/common/src/main/scala/net/psforever/login/psadmin/PsAdminActor.scala similarity index 97% rename from pslogin/src/main/scala/psadmin/PsAdminActor.scala rename to common/src/main/scala/net/psforever/login/psadmin/PsAdminActor.scala index ac0896eb..91da77db 100644 --- a/pslogin/src/main/scala/psadmin/PsAdminActor.scala +++ b/common/src/main/scala/net/psforever/login/psadmin/PsAdminActor.scala @@ -1,20 +1,19 @@ -package net.psforever.pslogin.psadmin +package net.psforever.login.psadmin import java.net.InetSocketAddress -import akka.actor.{ActorRef, Props} -import akka.actor.{Actor, Stash} -import akka.io.Tcp -import scodec.bits._ -import scodec.interop.akka._ -import scala.collection.mutable.Map -import akka.util.ByteString +import akka.actor.{Actor, ActorRef, Props, Stash} +import akka.io.Tcp +import akka.util.ByteString import org.json4s._ import org.json4s.native.Serialization.write - +import scodec.bits._ +import scodec.interop.akka._ import services.ServiceManager.Lookup import services._ +import scala.collection.mutable.Map + object PsAdminActor { val whiteSpaceRegex = """\s+""".r } diff --git a/pslogin/src/main/scala/psadmin/PsAdminCommands.scala b/common/src/main/scala/net/psforever/login/psadmin/PsAdminCommands.scala similarity index 97% rename from pslogin/src/main/scala/psadmin/PsAdminCommands.scala rename to common/src/main/scala/net/psforever/login/psadmin/PsAdminCommands.scala index a8ea6311..e5febd8a 100644 --- a/pslogin/src/main/scala/psadmin/PsAdminCommands.scala +++ b/common/src/main/scala/net/psforever/login/psadmin/PsAdminCommands.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.psadmin +package net.psforever.login.psadmin import scala.collection.mutable diff --git a/common/src/main/scala/net/psforever/objects/Session.scala b/common/src/main/scala/net/psforever/objects/Session.scala new file mode 100644 index 00000000..885a1d01 --- /dev/null +++ b/common/src/main/scala/net/psforever/objects/Session.scala @@ -0,0 +1,17 @@ +package net.psforever.objects + +import net.psforever.objects.zones.{Zone, Zoning} +import net.psforever.packet.game.DeadState + +case class Session( + id: Long = 0, + zone: Zone = Zone.Nowhere, + account: Account = null, + player: Player = null, + avatar: Avatar = null, + admin: Boolean = false, + zoningType: Zoning.Method.Value = Zoning.Method.None, + deadState: DeadState.Value = DeadState.Alive, + speed: Float = 1.0f, + flying: Boolean = false +) diff --git a/common/src/main/scala/net/psforever/types/ChatMessageType.scala b/common/src/main/scala/net/psforever/types/ChatMessageType.scala index aea7f4e7..603023e7 100644 --- a/common/src/main/scala/net/psforever/types/ChatMessageType.scala +++ b/common/src/main/scala/net/psforever/types/ChatMessageType.scala @@ -63,8 +63,8 @@ object ChatMessageType extends Enumeration { CMT_GMTELL, // /gmtell (actually causes normal /tell 0x20 when not a gm???) CMT_NOTE, // /note CMT_GMBROADCASTPOPUP, // /gmpopup - U_CMT_GMTELLFROM, // ??? Recipient of /gmtell? - U_CMT_TELLFROM, // ??? Recipient of /t? + U_CMT_GMTELLFROM, // Acknowledgement of /gmtell for sender + U_CMT_TELLFROM, // Acknowledgement of /tell for sender UNK_45, // ??? empty CMT_CULLWATERMARK, // ??? This actually causes the client to ping back to the server with some stringified numbers "80 120" (with the same 46 chatmsg causing infinite loop?) - may be incorrect decoding CMT_INSTANTACTION, // /instantaction OR /ia diff --git a/pslogin/src/main/scala/Config.scala b/common/src/main/scala/net/psforever/util/Config.scala similarity index 87% rename from pslogin/src/main/scala/Config.scala rename to common/src/main/scala/net/psforever/util/Config.scala index 4455b4c8..e89600ce 100644 --- a/pslogin/src/main/scala/Config.scala +++ b/common/src/main/scala/net/psforever/util/Config.scala @@ -1,25 +1,24 @@ -package net.psforever.pslogin +package net.psforever.util import java.nio.file.Paths import com.typesafe.config.{Config => TypesafeConfig} - -import scala.concurrent.duration._ -import net.psforever.packet.game.ServerType -import pureconfig.{ConfigConvert, ConfigSource} -import pureconfig.ConfigConvert.{viaNonEmptyStringOpt} import enumeratum.values.{IntEnum, IntEnumEntry} -import pureconfig.generic.auto._ // intellij: this is not unused - +import net.psforever.packet.game.ServerType +import pureconfig.ConfigConvert.viaNonEmptyStringOpt +import pureconfig.ConfigReader.Result +import pureconfig.{ConfigConvert, ConfigSource} +import scala.concurrent.duration._ import scala.reflect.ClassTag +import pureconfig.generic.auto._ // intellij: this is not unused object Config { // prog.home is defined when we are running from SBT pack val directory: String = System.getProperty("prog.home") match { case null => - Paths.get("config").toAbsolutePath().toString() + Paths.get("config").toAbsolutePath.toString case home => - Paths.get(home, "config").toAbsolutePath().toString() + Paths.get(home, "config").toAbsolutePath.toString } implicit def enumeratumIntConfigConvert[A <: IntEnumEntry](implicit @@ -42,7 +41,7 @@ object Config { ConfigSource.defaultApplication } - val result = source.load[AppConfig] + val result: Result[AppConfig] = source.load[AppConfig] // Raw config object - prefer app when possible lazy val config: TypesafeConfig = source.config().toOption.get @@ -88,7 +87,7 @@ case class DatabaseConfig( database: String, sslmode: String ) { - def toJdbc = s"jdbc:postgresql://${host}:${port}/${database}" + def toJdbc = s"jdbc:postgresql://$host:$port/$database" } case class AntiCheatConfig( diff --git a/pslogin/src/main/scala/Database.scala b/common/src/main/scala/net/psforever/util/Database.scala similarity index 89% rename from pslogin/src/main/scala/Database.scala rename to common/src/main/scala/net/psforever/util/Database.scala index 1e406518..dce3d563 100644 --- a/pslogin/src/main/scala/Database.scala +++ b/common/src/main/scala/net/psforever/util/Database.scala @@ -1,7 +1,6 @@ -package net.psforever.pslogin +package net.psforever.util -import io.getquill.PostgresJAsyncContext -import io.getquill.SnakeCase +import io.getquill.{PostgresJAsyncContext, SnakeCase} import net.psforever.persistence object Database { diff --git a/pslogin/src/main/scala/csr/CSRZoneImpl.scala b/common/src/main/scala/net/psforever/util/PointOfInterest.scala similarity index 84% rename from pslogin/src/main/scala/csr/CSRZoneImpl.scala rename to common/src/main/scala/net/psforever/util/PointOfInterest.scala index c3c20ebc..e6e70866 100644 --- a/pslogin/src/main/scala/csr/CSRZoneImpl.scala +++ b/common/src/main/scala/net/psforever/util/PointOfInterest.scala @@ -1,13 +1,10 @@ -package net.psforever.pslogin.csr +package net.psforever.util import net.psforever.types.Vector3 import scala.collection.mutable import scala.util.Random -/* -The following is STILL for development and fun. - */ /** * A crude representation of the information needed to describe a continent (hitherto, a "zone"). * The information is mainly catered to the simulation of the CSR commands `/zone` and `/warp`. @@ -16,7 +13,7 @@ The following is STILL for development and fun. * @param map the map name of the zone (this map is loaded) * @param zonename the zone's internal name */ -class CSRZoneImpl(val alias: String, val map: String, val zonename: String) { +class PointOfInterest(val alias: String, val map: String, val zonename: String) { /** * A listing of warpgates, geowarps, and island warpgates in this zone. @@ -33,49 +30,49 @@ class CSRZoneImpl(val alias: String, val map: String, val zonename: String) { private val locations: mutable.HashMap[String, Vector3] = mutable.HashMap() } -object CSRZoneImpl { +object PointOfInterest { /** * A listing of all zones that can be visited by their internal name. * The keys in this map should be directly usable by the `/zone` command. */ - private val zones = Map[String, CSRZoneImpl]( - "z1" -> CSRZoneImpl("Solsar", "map01", "z1"), - "z2" -> CSRZoneImpl("Hossin", "map02", "z2"), - "z3" -> CSRZoneImpl("Cyssor", "map03", "z3"), - "z4" -> CSRZoneImpl("Ishundar", "map04", "z4"), - "z5" -> CSRZoneImpl("Forseral", "map05", "z5"), - "z6" -> CSRZoneImpl("Ceryshen", "map06", "z6"), - "z7" -> CSRZoneImpl("Esamir", "map07", "z7"), - "z8" -> CSRZoneImpl("Oshur", "map08", "z8"), - "z9" -> CSRZoneImpl("Searhus", "map09", "z9"), - "z10" -> CSRZoneImpl("Amerish", "map10", "z10"), - "home1" -> CSRZoneImpl("NC Sanctuary", "map11", "home1"), - "home2" -> CSRZoneImpl("TR Sanctuary", "map12", "home2"), - "home3" -> CSRZoneImpl("VS Sanctuary", "map13", "home3"), - "tzshtr" -> CSRZoneImpl("VR Shooting Range TR", "map14", "tzshtr"), - "tzdrtr" -> CSRZoneImpl("VR Driving Range TR", "map15", "tzdrtr"), - "tzcotr" -> CSRZoneImpl("VR Combat Zone TR", "map16", "tzcotr"), - "tzshvs" -> CSRZoneImpl("VR Shooting Range VS", "map14", "tzshvs"), - "tzdrvs" -> CSRZoneImpl("VR Driving Range VS", "map15", "tzdrvs"), - "tzcovs" -> CSRZoneImpl("VR Combat Zone VS", "map16", "tzcovs"), - "tzshnc" -> CSRZoneImpl("VR Shooting Range NC", "map14", "tzshnc"), - "tzdrnc" -> CSRZoneImpl("VR Driving Range NC", "map15", "tzdrnc"), - "tzconc" -> CSRZoneImpl("VR Combat Zone NC", "map16", "tzconc"), - "c1" -> CSRZoneImpl("Supai", "ugd01", "c1"), - "c2" -> CSRZoneImpl("Hunhau", "ugd02", "c2"), - "c3" -> CSRZoneImpl("Adlivun", "ugd03", "c3"), - "c4" -> CSRZoneImpl("Byblos", "ugd04", "c4"), - "c5" -> CSRZoneImpl("Annwn", "ugd05", "c5"), - "c6" -> CSRZoneImpl("Drugaskan", "ugd06", "c6"), - "i4" -> CSRZoneImpl("Nexus", "map96", "i4"), - "i3" -> CSRZoneImpl("Desolation", "map97", "i3"), - "i2" -> CSRZoneImpl("Ascension", "map98", "i2"), - "i1" -> CSRZoneImpl("Extinction", "map99", "i1"), - "homebo" -> CSRZoneImpl("Black_ops_hq", "Black_ops_hq", "homebo"), - "station1" -> CSRZoneImpl("TR Station", "Station1", "station1"), - "station2" -> CSRZoneImpl("NC Station", "Station2", "station2"), - "station3" -> CSRZoneImpl("VS Station", "Station3", "station3") + private val zones = Map[String, PointOfInterest]( + "z1" -> PointOfInterest("Solsar", "map01", "z1"), + "z2" -> PointOfInterest("Hossin", "map02", "z2"), + "z3" -> PointOfInterest("Cyssor", "map03", "z3"), + "z4" -> PointOfInterest("Ishundar", "map04", "z4"), + "z5" -> PointOfInterest("Forseral", "map05", "z5"), + "z6" -> PointOfInterest("Ceryshen", "map06", "z6"), + "z7" -> PointOfInterest("Esamir", "map07", "z7"), + "z8" -> PointOfInterest("Oshur", "map08", "z8"), + "z9" -> PointOfInterest("Searhus", "map09", "z9"), + "z10" -> PointOfInterest("Amerish", "map10", "z10"), + "home1" -> PointOfInterest("NC Sanctuary", "map11", "home1"), + "home2" -> PointOfInterest("TR Sanctuary", "map12", "home2"), + "home3" -> PointOfInterest("VS Sanctuary", "map13", "home3"), + "tzshtr" -> PointOfInterest("VR Shooting Range TR", "map14", "tzshtr"), + "tzdrtr" -> PointOfInterest("VR Driving Range TR", "map15", "tzdrtr"), + "tzcotr" -> PointOfInterest("VR Combat Zone TR", "map16", "tzcotr"), + "tzshvs" -> PointOfInterest("VR Shooting Range VS", "map14", "tzshvs"), + "tzdrvs" -> PointOfInterest("VR Driving Range VS", "map15", "tzdrvs"), + "tzcovs" -> PointOfInterest("VR Combat Zone VS", "map16", "tzcovs"), + "tzshnc" -> PointOfInterest("VR Shooting Range NC", "map14", "tzshnc"), + "tzdrnc" -> PointOfInterest("VR Driving Range NC", "map15", "tzdrnc"), + "tzconc" -> PointOfInterest("VR Combat Zone NC", "map16", "tzconc"), + "c1" -> PointOfInterest("Supai", "ugd01", "c1"), + "c2" -> PointOfInterest("Hunhau", "ugd02", "c2"), + "c3" -> PointOfInterest("Adlivun", "ugd03", "c3"), + "c4" -> PointOfInterest("Byblos", "ugd04", "c4"), + "c5" -> PointOfInterest("Annwn", "ugd05", "c5"), + "c6" -> PointOfInterest("Drugaskan", "ugd06", "c6"), + "i4" -> PointOfInterest("Nexus", "map96", "i4"), + "i3" -> PointOfInterest("Desolation", "map97", "i3"), + "i2" -> PointOfInterest("Ascension", "map98", "i2"), + "i1" -> PointOfInterest("Extinction", "map99", "i1"), + "homebo" -> PointOfInterest("Black_ops_hq", "Black_ops_hq", "homebo"), + "station1" -> PointOfInterest("TR Station", "Station1", "station1"), + "station2" -> PointOfInterest("NC Station", "Station2", "station2"), + "station3" -> PointOfInterest("VS Station", "Station3", "station3") ) /** @@ -134,14 +131,14 @@ object CSRZoneImpl { * @param map the map name of the zone (this map is loaded) * @param zonename the zone's internal name */ - def apply(alias: String, map: String, zonename: String): CSRZoneImpl = new CSRZoneImpl(alias, map, zonename) + def apply(alias: String, map: String, zonename: String): PointOfInterest = new PointOfInterest(alias, map, zonename) /** * Get a valid `CSRZone`'s information. * @param zoneId a name that describes the zone and should be searchable * @return the `CSRZone`, or `None` */ - def get(zoneId: String): Option[CSRZoneImpl] = { + def get(zoneId: String): Option[PointOfInterest] = { var zId = zoneId.toLowerCase if (alias.get(zId).isDefined) zId = alias(zId) @@ -151,16 +148,19 @@ object CSRZoneImpl { /** * Get a location within the `CSRZone`. * The location should be a facility or a warpgate or interesting. - * @param zone the `CSRZone` - * @param locId a name that describes a known location in the provided `CSRZone` and is searchable + * @param zoneId the `CSRZone` + * @param locationId a name that describes a known location in the provided `CSRZone` and is searchable * @return the coordinates of that location, or None */ - def getWarpLocation(zone: CSRZoneImpl, locId: String): Option[Vector3] = { - val low_locId = locId.toLowerCase - var location = zone.locations.get(low_locId) - if (location.isEmpty) - location = zone.gates.get(low_locId) - location + def getWarpLocation(zoneId: String, locationId: String): Option[Vector3] = { + get(zoneId) match { + case Some(poi) => + poi.locations.get(locationId) match { + case Some(position) => Some(position) + case None => poi.gates.get(locationId) + } + case None => None + } } /** @@ -169,7 +169,7 @@ object CSRZoneImpl { * @param gateId a name that describes a known warpgate in the provided `CSRZone` and is searchable * @return the coordinates of that warpgate, or None */ - def getWarpgate(zone: CSRZoneImpl, gateId: String): Option[Vector3] = { + def getWarpgate(zone: PointOfInterest, gateId: String): Option[Vector3] = { zone.gates.get(gateId.toLowerCase) } @@ -178,7 +178,7 @@ object CSRZoneImpl { * @return all of the zonenames */ def list: String = { - "zonenames: z1 - z10, home1 - home3, tzshnc, tzdrnc, tzconc, tzshtr, tzdrtr, tzcotr, tzshvs, tzdrvs, tzcovs, c1 - c6, i1 - i4; zones are also aliased to their continent name" + "zone names: z1 - z10, home1 - home3, tzshnc, tzdrnc, tzconc, tzshtr, tzdrtr, tzcotr, tzshvs, tzdrvs, tzcovs, c1 - c6, i1 - i4; zones are also aliased to their continent name" } /** @@ -186,7 +186,7 @@ object CSRZoneImpl { * @param zone the `CSRZone` * @return all of the location keys */ - def listLocations(zone: CSRZoneImpl): String = { + def listLocations(zone: PointOfInterest): String = { var out: String = "warps: " if (zone.locations.nonEmpty) { out += zone.locations.keys.toArray.sorted.mkString(", ") @@ -200,7 +200,7 @@ object CSRZoneImpl { * @param zone the `CSRZone` * @return all of the warpgate keys */ - def listWarpgates(zone: CSRZoneImpl): String = { + def listWarpgates(zone: PointOfInterest): String = { var out: String = "gatenames: " if (zone.gates.isEmpty) out += "none" @@ -214,7 +214,7 @@ object CSRZoneImpl { * @param zone the `CSRZone` * @return the coordinates of the spawn point */ - def selectRandom(zone: CSRZoneImpl): Vector3 = { + def selectRandom(zone: PointOfInterest): Vector3 = { var outlets = zone.locations //random location? if (outlets.nonEmpty) { return outlets.values.toArray.apply(rand.nextInt(outlets.size)) diff --git a/pslogin/src/main/scala/Maps.scala b/common/src/main/scala/net/psforever/zones/Maps.scala similarity index 96% rename from pslogin/src/main/scala/Maps.scala rename to common/src/main/scala/net/psforever/zones/Maps.scala index 801b67a2..770576ee 100644 --- a/pslogin/src/main/scala/Maps.scala +++ b/common/src/main/scala/net/psforever/zones/Maps.scala @@ -1,9 +1,9 @@ -package net.psforever.pslogin +package net.psforever.zones import net.psforever.objects.LocalProjectile import net.psforever.objects.ballistics.Projectile import net.psforever.objects.zones.ZoneMap -import zonemaps._ +import net.psforever.zones.zonemaps._ import scala.concurrent.Future import scala.util.{Failure, Success} diff --git a/pslogin/src/main/scala/Zones.scala b/common/src/main/scala/net/psforever/zones/Zones.scala similarity index 99% rename from pslogin/src/main/scala/Zones.scala rename to common/src/main/scala/net/psforever/zones/Zones.scala index 6029be2d..d2cccbc5 100644 --- a/pslogin/src/main/scala/Zones.scala +++ b/common/src/main/scala/net/psforever/zones/Zones.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin +package net.psforever.zones import akka.actor.ActorContext import net.psforever.objects.GlobalDefinitions @@ -6,10 +6,9 @@ import net.psforever.objects.serverobject.resourcesilo.ResourceSilo import net.psforever.objects.serverobject.structures.WarpGate import net.psforever.objects.zones.Zone import net.psforever.types.PlanetSideEmpire - import scala.concurrent.duration._ -import scala.concurrent.Await import scala.collection.immutable.HashMap +import scala.concurrent.Await object Zones { val zones: HashMap[String, Zone] = HashMap( @@ -547,8 +546,8 @@ object Zones { * @return the duration */ def StandardTimeRules(defender: SourceEntry, attacker: SourceEntry): FiniteDuration = { - import net.psforever.objects.ballistics._ import net.psforever.objects.GlobalDefinitions + import net.psforever.objects.ballistics._ if (attacker.Faction == defender.Faction) { 0 seconds } else { diff --git a/pslogin/src/main/scala/zonemaps/Map01.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map01.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map01.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map01.scala index 8ff2b91a..001e2585 100644 --- a/pslogin/src/main/scala/zonemaps/Map01.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map01.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map02.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map02.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map02.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map02.scala index e479bcd8..33f0c620 100644 --- a/pslogin/src/main/scala/zonemaps/Map02.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map02.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map03.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map03.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map03.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map03.scala index 3c8d0513..6917b82a 100644 --- a/pslogin/src/main/scala/zonemaps/Map03.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map03.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map04.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map04.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map04.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map04.scala index 277a6d20..937f9ff6 100644 --- a/pslogin/src/main/scala/zonemaps/Map04.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map04.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map05.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map05.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map05.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map05.scala index 492e857e..20b1fb14 100644 --- a/pslogin/src/main/scala/zonemaps/Map05.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map05.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map06.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map06.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map06.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map06.scala index 41913d5c..8a7b7e6b 100644 --- a/pslogin/src/main/scala/zonemaps/Map06.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map06.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map07.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map07.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map07.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map07.scala index 2c88f59e..eed3bc7d 100644 --- a/pslogin/src/main/scala/zonemaps/Map07.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map07.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map08.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map08.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map08.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map08.scala index fdbc152a..ce4b8d01 100644 --- a/pslogin/src/main/scala/zonemaps/Map08.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map08.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map09.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map09.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map09.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map09.scala index cafef4e5..59994d89 100644 --- a/pslogin/src/main/scala/zonemaps/Map09.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map09.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map10.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map10.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map10.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map10.scala index 4dfc8ea5..ccb15318 100644 --- a/pslogin/src/main/scala/zonemaps/Map10.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map10.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map11.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map11.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map11.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map11.scala index 6a1e7f89..0d431c3b 100644 --- a/pslogin/src/main/scala/zonemaps/Map11.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map11.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map12.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map12.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map12.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map12.scala index f9203a99..71c6a715 100644 --- a/pslogin/src/main/scala/zonemaps/Map12.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map12.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map13.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map13.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map13.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map13.scala index 302b0f48..29eb4ad6 100644 --- a/pslogin/src/main/scala/zonemaps/Map13.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map13.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map96.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map96.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map96.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map96.scala index 7906dd90..c3e13377 100644 --- a/pslogin/src/main/scala/zonemaps/Map96.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map96.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map97.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map97.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map97.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map97.scala index 004becdd..a07a0f93 100644 --- a/pslogin/src/main/scala/zonemaps/Map97.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map97.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map98.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map98.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map98.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map98.scala index 11102374..34f35559 100644 --- a/pslogin/src/main/scala/zonemaps/Map98.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map98.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Map99.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Map99.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Map99.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Map99.scala index 1e59ed04..317169e8 100644 --- a/pslogin/src/main/scala/zonemaps/Map99.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Map99.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Ugd01.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd01.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Ugd01.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Ugd01.scala index 6b94cd07..12227cf5 100644 --- a/pslogin/src/main/scala/zonemaps/Ugd01.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd01.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Ugd02.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd02.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Ugd02.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Ugd02.scala index eda42b99..d3dbb5ad 100644 --- a/pslogin/src/main/scala/zonemaps/Ugd02.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd02.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Ugd03.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd03.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Ugd03.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Ugd03.scala index ba76df73..6558c683 100644 --- a/pslogin/src/main/scala/zonemaps/Ugd03.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd03.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Ugd04.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd04.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Ugd04.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Ugd04.scala index 2308559d..fbbcd316 100644 --- a/pslogin/src/main/scala/zonemaps/Ugd04.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd04.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Ugd05.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd05.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Ugd05.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Ugd05.scala index c2586ea3..89e2eba8 100644 --- a/pslogin/src/main/scala/zonemaps/Ugd05.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd05.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/pslogin/src/main/scala/zonemaps/Ugd06.scala b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd06.scala similarity index 99% rename from pslogin/src/main/scala/zonemaps/Ugd06.scala rename to common/src/main/scala/net/psforever/zones/zonemaps/Ugd06.scala index de3d0c89..2b188ee9 100644 --- a/pslogin/src/main/scala/zonemaps/Ugd06.scala +++ b/common/src/main/scala/net/psforever/zones/zonemaps/Ugd06.scala @@ -1,4 +1,4 @@ -package net.psforever.pslogin.zonemaps +package net.psforever.zones.zonemaps import net.psforever.objects.GlobalDefinitions._ import net.psforever.objects.serverobject.doors.Door diff --git a/common/src/main/scala/services/Service.scala b/common/src/main/scala/services/Service.scala index c06dac01..f5cdf0d2 100644 --- a/common/src/main/scala/services/Service.scala +++ b/common/src/main/scala/services/Service.scala @@ -15,14 +15,14 @@ object Service { } trait GenericEventBusMsg { - def toChannel: String + def channel: String } class GenericEventBus[A <: GenericEventBusMsg] extends ActorEventBus with SubchannelClassification { type Event = A type Classifier = String - protected def classify(event: Event): Classifier = event.toChannel + protected def classify(event: Event): Classifier = event.channel protected def subclassification = new Subclassification[Classifier] { diff --git a/common/src/main/scala/services/avatar/AvatarServiceResponse.scala b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala index c38a4f8b..a6ba345f 100644 --- a/common/src/main/scala/services/avatar/AvatarServiceResponse.scala +++ b/common/src/main/scala/services/avatar/AvatarServiceResponse.scala @@ -12,7 +12,7 @@ import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Trans import services.GenericEventBusMsg final case class AvatarServiceResponse( - toChannel: String, + channel: String, avatar_guid: PlanetSideGUID, replyMessage: AvatarResponse.Response ) extends GenericEventBusMsg diff --git a/common/src/main/scala/services/chat/ChatAction.scala b/common/src/main/scala/services/chat/ChatAction.scala deleted file mode 100644 index c56e40f8..00000000 --- a/common/src/main/scala/services/chat/ChatAction.scala +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2017 PSForever -package services.chat - -import net.psforever.objects.zones.Zone -import net.psforever.packet.game.ChatMsg -import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3} - -object ChatAction { - sealed trait Action - - final case class Local( - player_guid: PlanetSideGUID, - player_name: String, - continent: Zone, - player_pos: Vector3, - player_faction: PlanetSideEmpire.Value, - msg: ChatMsg - ) extends Action - final case class Tell(player_guid: PlanetSideGUID, player_name: String, msg: ChatMsg) extends Action - final case class Broadcast( - player_guid: PlanetSideGUID, - player_name: String, - continent: Zone, - player_pos: Vector3, - player_faction: PlanetSideEmpire.Value, - msg: ChatMsg - ) extends Action - final case class Voice( - player_guid: PlanetSideGUID, - player_name: String, - continent: Zone, - player_pos: Vector3, - player_faction: PlanetSideEmpire.Value, - msg: ChatMsg - ) extends Action - final case class Note(player_guid: PlanetSideGUID, player_name: String, msg: ChatMsg) extends Action - final case class Squad( - player_guid: PlanetSideGUID, - player_name: String, - continent: Zone, - player_pos: Vector3, - player_faction: PlanetSideEmpire.Value, - msg: ChatMsg - ) extends Action - final case class Platoon( - player_guid: PlanetSideGUID, - player_name: String, - continent: Zone, - player_pos: Vector3, - player_faction: PlanetSideEmpire.Value, - msg: ChatMsg - ) extends Action - final case class Command( - player_guid: PlanetSideGUID, - player_name: String, - continent: Zone, - player_pos: Vector3, - player_faction: PlanetSideEmpire.Value, - msg: ChatMsg - ) extends Action - final case class GM(player_guid: PlanetSideGUID, player_name: String, msg: ChatMsg) extends Action -} diff --git a/common/src/main/scala/services/chat/ChatResponse.scala b/common/src/main/scala/services/chat/ChatResponse.scala deleted file mode 100644 index f618951d..00000000 --- a/common/src/main/scala/services/chat/ChatResponse.scala +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) 2017 PSForever -package services.chat - -import net.psforever.types.{ChatMessageType, PlanetSideGUID} - -object ChatResponse { - sealed trait Response - - final case class Local( - sender: String, - messageType: ChatMessageType.Value, - wideContents: Boolean, - recipient: String, - contents: String, - note: Option[String] - ) extends Response - final case class Tell( - sender: String, - messageType: ChatMessageType.Value, - wideContents: Boolean, - recipient: String, - contents: String, - note: Option[String] - ) extends Response - final case class UTell( - sender: String, - messageType: ChatMessageType.Value, - wideContents: Boolean, - recipient: String, - contents: String, - note: Option[String] - ) extends Response - final case class Broadcast( - messageType: ChatMessageType.Value, - wideContents: Boolean, - recipient: String, - contents: String, - note: Option[String] - ) extends Response - final case class Voice( - messageType: ChatMessageType.Value, - wideContents: Boolean, - recipient: String, - contents: String, - note: Option[String] - ) extends Response - final case class Unk45( - sender: String, - messageType: ChatMessageType.Value, - wideContents: Boolean, - recipient: String, - contents: String, - note: Option[String] - ) extends Response - final case class Squad( - sender: String, - messageType: ChatMessageType.Value, - wideContents: Boolean, - recipient: String, - contents: String, - note: Option[String] - ) extends Response - final case class Platoon( - sender: String, - messageType: ChatMessageType.Value, - wideContents: Boolean, - recipient: String, - contents: String, - note: Option[String] - ) extends Response - final case class Command( - sender: String, - messageType: ChatMessageType.Value, - wideContents: Boolean, - recipient: String, - contents: String, - note: Option[String] - ) extends Response - - final case class Text( - toChannel: String, - avatar_guid: PlanetSideGUID, - personal: Int, - messageType: ChatMessageType.Value, - wideContents: Boolean, - recipient: String, - contents: String, - note: Option[String] - ) -} diff --git a/common/src/main/scala/services/chat/ChatService.scala b/common/src/main/scala/services/chat/ChatService.scala index ec6e51dc..460d070a 100644 --- a/common/src/main/scala/services/chat/ChatService.scala +++ b/common/src/main/scala/services/chat/ChatService.scala @@ -1,227 +1,188 @@ // Copyright (c) 2017 PSForever package services.chat -import akka.actor.Actor -import net.psforever.objects.LivePlayerList +import akka.actor.typed.receptionist.{Receptionist, ServiceKey} +import akka.actor.typed.{ActorRef, Behavior} +import akka.actor.typed.scaladsl.{AbstractBehavior, ActorContext, Behaviors} +import net.psforever.objects.Session import net.psforever.packet.game.ChatMsg -import net.psforever.types.ChatMessageType -import services.{GenericEventBus, Service} +import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID} -class ChatService extends Actor { - private[this] val log = org.log4s.getLogger +object ChatService { + val ChatServiceKey: ServiceKey[Command] = ServiceKey[ChatService.Command]("chatService") - override def preStart = { - log.info("Starting....") + def apply(): Behavior[Command] = + Behaviors.setup { context => + context.system.receptionist ! Receptionist.Register(ChatServiceKey, context.self) + new ChatService(context) + } + + sealed trait Command + + final case class JoinChannel(actor: ActorRef[MessageResponse], session: Session, channel: ChatChannel) extends Command + final case class LeaveChannel(actor: ActorRef[MessageResponse], channel: ChatChannel) extends Command + final case class LeaveAllChannels(actor: ActorRef[MessageResponse]) extends Command + + final case class Message(session: Session, message: ChatMsg, channel: ChatChannel) extends Command + final case class MessageResponse(session: Session, message: ChatMsg, channel: ChatChannel) + + trait ChatChannel + object ChatChannel { + // one of the default channels that the player is always subscribed to (local, broadcast, command...) + final case class Default() extends ChatChannel + final case class Squad(guid: PlanetSideGUID) extends ChatChannel } - val ChatEvents = new GenericEventBus[ChatServiceResponse] +} - def receive = { - case Service.Join(channel) => - val path = s"/Chat/$channel" - val who = sender() - log.info(s"$who has joined $path") - ChatEvents.subscribe(who, path) - case Service.Leave(None) => - ChatEvents.unsubscribe(sender()) - case Service.Leave(Some(channel)) => - val path = s"/Chat/$channel" - val who = sender() - log.info(s"$who has left $path") - ChatEvents.unsubscribe(who, path) - case Service.LeaveAll() => - ChatEvents.unsubscribe(sender()) +class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBehavior[ChatService.Command](context) { - case ChatServiceMessage(forChannel, action) => - action match { - case ChatAction.Local(player_guid, player_name, cont, player_pos, player_faction, msg) => // local - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - cont, - player_pos, - player_faction, - 2, - ChatMsg(ChatMessageType.CMT_OPEN, msg.wideContents, player_name, msg.contents, None) - ) - ) - case ChatAction.Tell(player_guid, player_name, msg) => // tell - var good: Boolean = false - LivePlayerList - .WorldPopulation(_ => true) - .foreach(char => { - if (char.name.equalsIgnoreCase(msg.recipient)) { - good = true - } - }) - if (good) { - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - target = 0, - replyMessage = ChatMsg(ChatMessageType.CMT_TELL, msg.wideContents, msg.recipient, msg.contents, None) - ) - ) - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - target = 1, - replyMessage = - ChatMsg(ChatMessageType.U_CMT_TELLFROM, msg.wideContents, msg.recipient, msg.contents, None) - ) - ) - } else { - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - target = 1, - replyMessage = - ChatMsg(ChatMessageType.U_CMT_TELLFROM, msg.wideContents, msg.recipient, msg.contents, None) - ) - ) - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - target = 1, - replyMessage = ChatMsg(ChatMessageType.UNK_45, msg.wideContents, "", "@NoTell_Target", None) - ) - ) - } - case ChatAction.Broadcast(player_guid, player_name, cont, player_pos, player_faction, msg) => // broadcast - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - cont, - player_pos, - player_faction, - 2, - ChatMsg(msg.messageType, msg.wideContents, player_name, msg.contents, None) - ) - ) - case ChatAction.Voice(player_guid, player_name, cont, player_pos, player_faction, msg) => // voice - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - cont, - player_pos, - player_faction, - 2, - ChatMsg(ChatMessageType.CMT_VOICE, false, player_name, msg.contents, None) - ) - ) + import ChatService._ + import ChatMessageType._ - case ChatAction.Note(player_guid, player_name, msg) => // note - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - target = 1, - replyMessage = ChatMsg(ChatMessageType.U_CMT_GMTELLFROM, true, msg.recipient, msg.contents, None) - ) - ) - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - target = 1, - replyMessage = ChatMsg( - ChatMessageType.CMT_GMTELL, - true, - "Server", - "Why do you try to /note ? That's a GM command ! ... Or not, nobody can /note", - None - ) - ) - ) - case ChatAction.Squad(player_guid, player_name, cont, player_pos, player_faction, msg) => // squad - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - cont, - player_pos, - player_faction, - 2, - ChatMsg(ChatMessageType.CMT_SQUAD, msg.wideContents, player_name, msg.contents, None) - ) - ) - case ChatAction.Platoon(player_guid, player_name, cont, player_pos, player_faction, msg) => // platoon - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - cont, - player_pos, - player_faction, - 2, - ChatMsg(ChatMessageType.CMT_PLATOON, msg.wideContents, player_name, msg.contents, None) - ) - ) - case ChatAction.Command(player_guid, player_name, cont, player_pos, player_faction, msg) => // command - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - player_name, - cont, - player_pos, - player_faction, - 2, - ChatMsg(ChatMessageType.CMT_COMMAND, msg.wideContents, player_name, msg.contents, None) - ) - ) - case ChatAction.GM(player_guid, player_name, msg) => // GM - msg.messageType match { - case ChatMessageType.CMT_SILENCE => - ChatEvents.publish( - ChatServiceResponse( - s"/Chat/$forChannel", - player_guid, - msg.contents, - target = 0, - replyMessage = ChatMsg(ChatMessageType.CMT_SILENCE, true, "", "", None) + private[this] val log = org.log4s.getLogger + var subscriptions: List[JoinChannel] = List[JoinChannel]() + + override def onMessage(msg: Command): Behavior[Command] = { + msg match { + case subscription: JoinChannel => + subscriptions ++= List(subscription) + this + + case LeaveChannel(actor, channel) => + subscriptions = subscriptions.filter { + case JoinChannel(a, _, c) => actor != a && channel != c + } + this + + case LeaveAllChannels(actor) => + subscriptions = subscriptions.filter { + case JoinChannel(a, _, _) => actor != a + } + this + + case Message(session, message, channel) => + (channel, message.messageType) match { + case (ChatChannel.Squad(_), CMT_SQUAD) => + case (ChatChannel.Default(), messageType) if messageType != CMT_SQUAD => + case _ => + log.error(s"invalid chat channel $channel for messageType ${message.messageType}") + return this + } + val subs = subscriptions.filter(_.channel == channel) + message.messageType match { + case CMT_TELL | CMT_GMTELL => + subs.find(_.session.player.Name == session.player.Name).foreach { + case JoinChannel(sender, _, _) => + sender ! MessageResponse( + session, + message.copy(messageType = if (message.messageType == CMT_TELL) U_CMT_TELLFROM else U_CMT_GMTELLFROM), + channel ) - ) -// if(player_guid != PlanetSideGUID(0)) { -// -// val args = msg.contents.split(" ") -// var silence_name : String = "" -// var silence_time : Int = 5 -// if (args.length == 1) { -// silence_name = args(0) -// } -// else if (args.length == 2) { -// silence_name = args(0) -// silence_time = args(1).toInt -// } -// ChatEvents.publish( -// ChatServiceResponse(s"/Chat/$forChannel", player_guid, player_name, target = 1, replyMessage = ChatMsg(ChatMessageType.UNK_45, true, "", silence_name + " silenced for " + silence_time + " min(s)", None)) -// ) -// } - case _ => ; - } - case _ => ; - } + subs.find(_.session.player.Name == message.recipient) match { + case Some(JoinChannel(receiver, _, _)) => + receiver ! MessageResponse(session, message, channel) + case None => + sender ! MessageResponse( + session, + ChatMsg(ChatMessageType.UNK_45, false, "", "@NoTell_Target", None), + channel + ) + } - case msg => - log.info(s"Unhandled message $msg from $sender") + } + + case CMT_SILENCE => + val args = message.contents.split(" ") + val (name, time, error) = (args.lift(0), args.lift(1)) match { + case (Some(name), None) => (Some(name), Some(5), None) + case (Some(name), Some(time)) => + time.toIntOption match { + case Some(time) => + (Some(name), Some(time), None) + case None => + (None, None, Some("bad time format")) + } + case _ => (None, None, None) + } + + val sender = subs.find(_.session.player.Name == session.player.Name) + + (sender, name, time, error) match { + case (Some(sender), Some(name), Some(_), None) => + val recipient = subs.find(_.session.player.Name == name) + recipient match { + case Some(recipient) => + if (recipient.session.player.silenced) { + sender.actor ! MessageResponse( + session, + ChatMsg(UNK_71, true, "", "@silence_disabled_ack", None), + channel + ) + } else { + sender.actor ! MessageResponse( + session, + ChatMsg(UNK_71, true, "", "@silence_enabled_ack", None), + channel + ) + } + recipient.actor ! MessageResponse(session, message, channel) + case None => + sender.actor ! MessageResponse( + session, + ChatMsg(UNK_71, true, "", s"unknown player '$name'", None), + channel + ) + } + case (Some(sender), _, _, error) => + sender.actor ! MessageResponse( + session, + ChatMsg(UNK_71, false, "", error.getOrElse("usage: /silence [