ChatActor

This removes roughly 1k LOC from WorldSessionActor and moves
them to a new ChatActor. That was the initial goal anyway, but it
wasn't that simple. There was no clear location to put this new actor,
I didn't want to put it in pslogin since it isn't part of the "login server"
(and neither is WSA). But since the new actor would have to talk to
WSA and common does not depend on pslogin, I had a choice of
putting more actors in pslogin or putting everything in common. I
chose the latter.

ChatActor and SessionActor (formerly WorldSessionActor) now live
in `common/actors/session`. Since WSA also depends on other
actors in pslogin, most of the pslogin code was moved to either
common/login or common/util. PsLogin as the main entry point
remains in pslogin since having the main code compile to a library
has some advantages, and it will allow us to produce binaries
for distinct login/world servers in the future if desired. For a second
take, I'd suggest moving common to /src in the root directory.

This change is enabled by a new immutable `Zone` object that is
passed from SessionActor to ChatActor. Most of its members are
still mutable references, and the code at the moment does depend
on this being the case. Changes to the session object in
SessionActor are forwarded through a SetZone message to
ChatActor. As we split out more code into actors, we could
use EventBus or typed Topic's instead.

Also included is a reworked ChatService that was converted to a
typed actor and uses the built-in Receptionist facility for service
discovery. By receiving the session object from ChatActor, it can
be much smarter about who to send messages to, rather than
sending all messages to everyone and having them figure it out.
But as this session object is not updated, it can only use static
properties like player name and faction and not fluid properties
like position.

The following chat commands were added:
command, note, gmbroadcast, [nc|tr|vs|broadcast, gmtell, gmpopup
and !whitetext
This commit is contained in:
Jakob Gillich 2020-07-11 12:50:29 +02:00
parent 144804139f
commit 4634dffe00
66 changed files with 1560 additions and 1838 deletions

View file

@ -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)
)

View file

@ -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 // [[<empire>|none [<timer>]]
(Some(0), None, Some(1), None, Some(_)) | (Some(0), None, None, None, None) |
(None, None, None, None, None) |
// [<building name> [<empire>|none [timer]]]
(None | Some(1), Some(0), None, Some(_), None) | (Some(1), Some(0), Some(2), Some(_), Some(_)) |
// [all [<empire>|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 [[<empire>|none [<timer>]] | [<building name> [<empire>|none [timer]]] | [all [<empire>|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 <zone> -list')", None)
)
case (_, _, _) if buffer.isEmpty || buffer(0).equals("-help") =>
sessionActor ! SessionActor.SendResponse(
ChatMsg(UNK_229, true, "", "usage: /zone <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 <x><y><z> OR /warp to <character> OR /warp near <object> OR /warp above <object> 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
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -1,4 +1,4 @@
package net.psforever.pslogin
package net.psforever.login
import java.net.{InetAddress, InetSocketAddress}

View file

@ -1,4 +1,4 @@
package net.psforever.pslogin
package net.psforever.login
import java.net.{InetAddress, InetSocketAddress}

View file

@ -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
*

View file

@ -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 {

View file

@ -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 {

View file

@ -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)

View file

@ -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 {

View file

@ -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
}

View file

@ -1,4 +1,4 @@
package net.psforever.pslogin.psadmin
package net.psforever.login.psadmin
import scala.collection.mutable

View file

@ -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
)

View file

@ -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

View file

@ -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(

View file

@ -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 {

View file

@ -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))

View file

@ -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}

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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] {

View file

@ -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

View file

@ -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
}

View file

@ -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]
)
}

View file

@ -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 <name> [<time>]"), None),
channel
)
case (None, _, _, _) =>
log.error("received message from non-subscribed actor")
}
case CMT_NOTE =>
subs.filter(_.session.player.Name == message.recipient).foreach {
case JoinChannel(actor, _, _) =>
actor ! MessageResponse(session, message.copy(recipient = session.player.Name), channel)
}
// faction commands
case CMT_OPEN | CMT_PLATOON | CMT_COMMAND =>
subs.filter(_.session.player.Faction == session.player.Faction).foreach {
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
}
case CMT_GMBROADCAST_NC =>
subs.filter(_.session.player.Faction == PlanetSideEmpire.NC).foreach {
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
}
case CMT_GMBROADCAST_TR =>
subs.filter(_.session.player.Faction == PlanetSideEmpire.TR).foreach {
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
}
case CMT_GMBROADCAST_VS =>
subs.filter(_.session.player.Faction == PlanetSideEmpire.VS).foreach {
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
}
// cross faction commands
case CMT_BROADCAST | CMT_VOICE | CMT_GMBROADCAST | CMT_GMBROADCASTPOPUP | UNK_227 =>
subs.foreach {
case JoinChannel(actor, _, _) => actor ! MessageResponse(session, message, channel)
}
case _ =>
log.warn(s"unhandled chat message, add a case for $message")
}
this
}
}
}

View file

@ -1,4 +0,0 @@
// Copyright (c) 2017 PSForever
package services.chat
final case class ChatServiceMessage(forChannel: String, actionMessage: ChatAction.Action)

View file

@ -1,18 +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}
import services.GenericEventBusMsg
final case class ChatServiceResponse(
toChannel: String,
avatar_guid: PlanetSideGUID,
avatar_name: String,
cont: Zone = Zone.Nowhere,
avatar_pos: Vector3 = Vector3(0f, 0f, 0f),
avatar_faction: PlanetSideEmpire.Value = PlanetSideEmpire.NEUTRAL,
target: Int,
replyMessage: ChatMsg
) extends GenericEventBusMsg

View file

@ -8,7 +8,7 @@ import net.psforever.packet.game.BuildingInfoUpdateMessage
import net.psforever.types.PlanetSideGUID
import services.GenericEventBusMsg
final case class GalaxyServiceResponse(toChannel: String, replyMessage: GalaxyResponse.Response)
final case class GalaxyServiceResponse(channel: String, replyMessage: GalaxyResponse.Response)
extends GenericEventBusMsg
object GalaxyResponse {

View file

@ -10,7 +10,7 @@ import net.psforever.types.{PlanetSideEmpire, PlanetSideGUID, Vector3}
import services.GenericEventBusMsg
final case class LocalServiceResponse(
toChannel: String,
channel: String,
avatar_guid: PlanetSideGUID,
replyMessage: LocalResponse.Response
) extends GenericEventBusMsg

View file

@ -2,15 +2,22 @@
package services.teamwork
import akka.actor.{Actor, ActorRef, Terminated}
import net.psforever.objects.{Avatar, LivePlayerList, Player}
import net.psforever.objects.definition.converter.StatConverter
import net.psforever.objects.loadouts.SquadLoadout
import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures}
import net.psforever.objects.zones.Zone
import net.psforever.packet.game._
import net.psforever.objects.{Avatar, LivePlayerList, Player}
import net.psforever.packet.game.{
SquadDetail,
SquadInfo,
WaypointEventAction,
SquadPositionEntry,
SquadPositionDetail,
WaypointInfo,
PlanetSideZoneID
}
import net.psforever.types._
import services.{GenericEventBus, Service}
import services.teamwork.SquadAction
import scala.collection.concurrent.TrieMap
import scala.collection.mutable

View file

@ -2,12 +2,11 @@
package services.teamwork
import net.psforever.objects.teamwork.Squad
import net.psforever.packet.game._
import net.psforever.packet.game.{SquadDetail, SquadInfo, WaypointEventAction, WaypointInfo}
import net.psforever.types.{PlanetSideGUID, SquadResponseType, SquadWaypoints}
import services.GenericEventBusMsg
import services.teamwork.SquadAction
final case class SquadServiceResponse(toChannel: String, exclude: Iterable[Long], response: SquadResponse.Response)
final case class SquadServiceResponse(channel: String, exclude: Iterable[Long], response: SquadResponse.Response)
extends GenericEventBusMsg
object SquadServiceResponse {

View file

@ -14,7 +14,7 @@ import net.psforever.types.{BailType, DriveState, PlanetSideGUID, Vector3}
import services.GenericEventBusMsg
final case class VehicleServiceResponse(
toChannel: String,
channel: String,
avatar_guid: PlanetSideGUID,
replyMessage: VehicleResponse.Response
) extends GenericEventBusMsg

View file

@ -2,7 +2,9 @@ package net.psforever.pslogin
import java.net.InetAddress
import java.util.Locale
import akka.actor.{ActorSystem, Props}
import akka.{actor => classic}
import akka.actor.typed.ActorSystem
import akka.routing.RandomPool
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.joran.JoranConfigurator
@ -10,7 +12,6 @@ import net.psforever.crypto.CryptoInterface
import net.psforever.objects.Default
import net.psforever.objects.zones._
import net.psforever.objects.guid.TaskResolver
import net.psforever.pslogin.psadmin.PsAdminActor
import org.slf4j
import org.fusesource.jansi.Ansi._
import org.fusesource.jansi.Ansi.Color._
@ -26,6 +27,22 @@ import org.flywaydb.core.Flyway
import java.nio.file.Paths
import scopt.OParser
import akka.actor.typed.scaladsl.adapter._
import net.psforever.actors.session.SessionActor
import net.psforever.login.psadmin.PsAdminActor
import net.psforever.login.{
CryptoSessionActor,
LoginSessionActor,
NetworkSimulatorParameters,
PacketCodingActor,
SessionPipeline,
SessionRouter,
TcpListener,
UdpListener
}
import net.psforever.util.Config
import net.psforever.zones.Zones
object PsLogin {
private val logger = org.log4s.getLogger
@ -74,9 +91,11 @@ object PsLogin {
}
/** Start up the main actor system. This "system" is the home for all actors running on this server */
implicit val system: ActorSystem = ActorSystem("PsLogin")
implicit val system = classic.ActorSystem("PsLogin")
Default(system)
val typedSystem: ActorSystem[Nothing] = system.toTyped
/** Create pipelines for the login and world servers
*
* The first node in the pipe is an Actor that handles the crypto for protecting packets.
@ -87,14 +106,14 @@ object PsLogin {
* See SessionRouter.scala for a diagram
*/
val loginTemplate = List(
SessionPipeline("crypto-session-", Props[CryptoSessionActor]),
SessionPipeline("packet-session-", Props[PacketCodingActor]),
SessionPipeline("login-session-", Props[LoginSessionActor])
SessionPipeline("crypto-session-", classic.Props[CryptoSessionActor]),
SessionPipeline("packet-session-", classic.Props[PacketCodingActor]),
SessionPipeline("login-session-", classic.Props[LoginSessionActor])
)
val worldTemplate = List(
SessionPipeline("crypto-session-", Props[CryptoSessionActor]),
SessionPipeline("packet-session-", Props[PacketCodingActor]),
SessionPipeline("world-session-", Props[WorldSessionActor])
SessionPipeline("crypto-session-", classic.Props[CryptoSessionActor]),
SessionPipeline("packet-session-", classic.Props[PacketCodingActor]),
SessionPipeline("world-session-", classic.Props[SessionActor])
)
val netSim: Option[NetworkSimulatorParameters] = if (Config.app.developer.netSim.enable) {
@ -113,37 +132,38 @@ object PsLogin {
val continents = Zones.zones.values ++ Seq(Zone.Nowhere)
val serviceManager = ServiceManager.boot
serviceManager ! ServiceManager.Register(Props[AccountIntermediaryService], "accountIntermediary")
serviceManager ! ServiceManager.Register(RandomPool(150).props(Props[TaskResolver]), "taskResolver")
serviceManager ! ServiceManager.Register(Props[ChatService], "chat")
serviceManager ! ServiceManager.Register(Props[GalaxyService], "galaxy")
serviceManager ! ServiceManager.Register(Props[SquadService], "squad")
serviceManager ! ServiceManager.Register(Props(classOf[InterstellarCluster], continents), "cluster")
serviceManager ! ServiceManager.Register(Props[AccountPersistenceService], "accountPersistence")
serviceManager ! ServiceManager.Register(Props[PropertyOverrideManager], "propertyOverrideManager")
system.spawnAnonymous(ChatService())
val loginRouter = Props(new SessionRouter("Login", loginTemplate))
val worldRouter = Props(new SessionRouter("World", worldTemplate))
val serviceManager = ServiceManager.boot
serviceManager ! ServiceManager.Register(classic.Props[AccountIntermediaryService], "accountIntermediary")
serviceManager ! ServiceManager.Register(RandomPool(150).props(classic.Props[TaskResolver]), "taskResolver")
serviceManager ! ServiceManager.Register(classic.Props[GalaxyService], "galaxy")
serviceManager ! ServiceManager.Register(classic.Props[SquadService], "squad")
serviceManager ! ServiceManager.Register(classic.Props(classOf[InterstellarCluster], continents), "cluster")
serviceManager ! ServiceManager.Register(classic.Props[AccountPersistenceService], "accountPersistence")
serviceManager ! ServiceManager.Register(classic.Props[PropertyOverrideManager], "propertyOverrideManager")
val loginRouter = classic.Props(new SessionRouter("Login", loginTemplate))
val worldRouter = classic.Props(new SessionRouter("World", worldTemplate))
val loginListener = system.actorOf(
Props(new UdpListener(loginRouter, "login-session-router", bindAddress, Config.app.login.port, netSim)),
classic.Props(new UdpListener(loginRouter, "login-session-router", bindAddress, Config.app.login.port, netSim)),
"login-udp-endpoint"
)
val worldListener = system.actorOf(
Props(new UdpListener(worldRouter, "world-session-router", bindAddress, Config.app.world.port, netSim)),
classic.Props(new UdpListener(worldRouter, "world-session-router", bindAddress, Config.app.world.port, netSim)),
"world-udp-endpoint"
)
val adminListener = system.actorOf(
Props(
classic.Props(
new TcpListener(
classOf[PsAdminActor],
"psadmin-client-",
"net.psforever.login.psadmin-client-",
InetAddress.getByName(Config.app.admin.bind),
Config.app.admin.port
)
),
"psadmin-tcp-endpoint"
"net.psforever.login.psadmin-tcp-endpoint"
)
logger.info(

View file

@ -1,131 +0,0 @@
package net.psforever.pslogin.csr
import net.psforever.packet.PacketCoding
import net.psforever.packet.game.ChatMsg
import net.psforever.types.{ChatMessageType, Vector3}
import scala.collection.mutable.ArrayBuffer
import scala.util.Try
/*
The following is STILL for development and fun.
*/
/**
* An implementation of the CSR command `/warp`, highly modified to serve the purposes of the testing phases of the server.
* See `help()` for details.
*/
object CSRWarp {
/**
* Accept and confirm that a message sent to a player is a valid `/warp` invocation.
* If so, parse the message and send the player to whichever destination in this zone was requested.
* @param traveler the player
* @param msg the message the player received
* @return true, if the player is being transported to another place; false, otherwise
*/
def read(traveler: Traveler, msg: ChatMsg): (Boolean, Vector3) = {
if (!isProperRequest(msg))
return (false, Vector3.Zero) //we do not handle this message
val buffer = decomposeMessage(msg.contents)
if (buffer.length == 0 || buffer(0).equals("") || buffer(0).equals("-help")) {
CSRWarp.help(traveler) //print usage information to chat
return (false, Vector3.Zero)
}
var destId: String = ""
var coords: ArrayBuffer[Int] = ArrayBuffer.empty[Int]
var list: Boolean = false
var failedCoordInput = false
for (o <- buffer) {
val toInt = Try(o.toInt)
if (toInt.isSuccess) {
coords += toInt.get
} else if (coords.nonEmpty && coords.size < 3)
failedCoordInput = true
if (o.equals("-list"))
list = true
else if (destId.equals(""))
destId = o
}
if (failedCoordInput || (coords.nonEmpty && coords.size < 3)) {
CSRWarp.error(traveler, "Needs three integer components (<x> <y> <z>)")
return (false, Vector3.Zero)
} else {
coords
.slice(0, 3)
.foreach(x => {
if (x < 0 || x > 8191) {
CSRWarp.error(traveler, "Out of range - 0 < n < 8191, but n = " + x)
return (false, Vector3.Zero)
}
})
}
val zone = CSRZoneImpl.get(traveler.zone).get //the traveler is already in the appropriate zone
if (list && coords.isEmpty && destId.equals("")) {
CSRWarp.reply(traveler, CSRZoneImpl.listLocations(zone) + "; " + CSRZoneImpl.listWarpgates(zone))
return (false, Vector3.Zero)
}
val dest: Option[Vector3] =
if (coords.nonEmpty) Some(Vector3(coords(0).toFloat, coords(1).toFloat, coords(2).toFloat))
else CSRZoneImpl.getWarpLocation(zone, destId) //coords before destId
if (dest.isEmpty) {
CSRWarp.error(traveler, "Invalid location")
return (false, Vector3.Zero)
}
(true, dest.get)
}
/**
* Check that the incoming message is an appropriate type for this command.
* @param msg the message
* @return true, if we will handle it; false, otherwise
*/
def isProperRequest(msg: ChatMsg): Boolean = {
msg.messageType == ChatMessageType.CMT_WARP
}
/**
* Break the message in the packet down for parsing.
* @param msg the contents portion of the message, a space-separated `String`
* @return the contents portion of the message, transformed into an `Array`
*/
private def decomposeMessage(msg: String): Array[String] = {
msg.trim.toLowerCase.split("\\s+")
}
/**
* Send a message back to the `Traveler` that will be printed into his chat window.
* @param traveler the player
* @param msg the message to be sent
*/
private def reply(traveler: Traveler, msg: String): Unit = {
traveler ! PacketCoding.CreateGamePacket(0, ChatMsg(ChatMessageType.CMT_OPEN, true, "", msg, None))
}
/**
* Print usage information to the `Traveler`'s chat window.<br>
* <br>
* The "official" use information for help dictates the command should follow this format:
* `/warp &lt;x&gt;&lt;y&gt;&lt;z&gt; | to &lt;character&gt; | near &lt;object&gt; | above &lt;object&gt; | waypoint`.
* In our case, creating fixed coordinate points of interest is not terribly dissimilar from the "near" and "to" aspect.
* We can not currently implement most of the options for now, however.<br>
* <br>
* The destination prioritizes evaluation of the coordinates before the location string.
* When the user provides coordinates, he must provide all three components of the coordinate at once, else none will be accepted.
* If the coordinates are invalid, the location string will still be checked.
* "-list" is accepted while no serious attempt is made to indicate a destination (no location string or not enough coordinates).
* @param traveler the player
*/
private def help(traveler: Traveler): Unit = {
CSRWarp.reply(traveler, "usage: /warp <location> | <gatename> | <x> <y> <z> | [-list]")
}
/**
* Print error information to the `Traveler`'s chat window.<br>
* The most common reason for error is the lack of information, or wrong information.
* @param traveler the player
*/
private def error(traveler: Traveler, msg: String): Unit = {
CSRWarp.reply(traveler, "Error! " + msg)
}
}

View file

@ -1,113 +0,0 @@
package net.psforever.pslogin.csr
import net.psforever.packet.PacketCoding
import net.psforever.packet.game.ChatMsg
import net.psforever.types.{ChatMessageType, Vector3}
/*
The following is STILL for development and fun.
*/
/**
* An implementation of the CSR command `/zone`, slightly modified to serve the purposes of the testing phases of the server.
*/
object CSRZone {
/**
* Accept and confirm that a message sent to a player is a valid `/zone` invocation.
* If so, parse the message and send the player to whichever zone was requested.
* @param traveler the player
* @param msg the message the player received
* @return true, if the player is being transported to another zone; false, otherwise
*/
def read(traveler: Traveler, msg: ChatMsg): (Boolean, String, Vector3) = {
if (!isProperRequest(msg))
return (false, "", Vector3.Zero) //we do not handle this message
val buffer = decomposeMessage(msg.contents)
if (buffer.length == 0 || buffer(0).equals("-help")) {
CSRZone.help(traveler) //print usage information to chat
return (false, "", Vector3.Zero)
}
var zoneId = ""
var gateId = "" //the user can define which warpgate they may visit (actual keyword protocol missing)
var list = false //if the user wants a printed list of destination locations
for (o <- buffer) {
if (o.equals("-list")) {
if (zoneId.equals("") || gateId.equals("")) {
list = true
}
} else if (zoneId.equals(""))
zoneId = o
else if (gateId.equals(""))
gateId = o
}
val zoneOpt = CSRZoneImpl.get(zoneId)
if (zoneOpt.isEmpty) {
if (list)
CSRZone.reply(traveler, CSRZoneImpl.list)
else
CSRZone.error(traveler, "Give a valid zonename (use '/zone -list')")
return (false, "", Vector3.Zero)
}
val zone = zoneOpt.get
var destination: Vector3 = CSRZoneImpl.selectRandom(zone) //the destination in the new zone starts as random
if (!gateId.equals("")) { //if we've defined a warpgate, and can find that warpgate, we re-assign the destination
val gateOpt = CSRZoneImpl.getWarpgate(zone, gateId)
if (gateOpt.isDefined)
destination = gateOpt.get
else
CSRZone.error(traveler, "Gate id not defined (use '/zone <zone> -list')")
} else if (list) {
CSRZone.reply(traveler, CSRZoneImpl.listWarpgates(zone))
return (false, "", Vector3.Zero)
}
(true, zone.zonename, destination)
}
/**
* Check that the incoming message is an appropriate type for this command.
* @param msg the message
* @return true, if we will handle it; false, otherwise
*/
def isProperRequest(msg: ChatMsg): Boolean = {
msg.messageType == ChatMessageType.CMT_ZONE
}
/**
* Break the message in the packet down for parsing.
* @param msg the contents portion of the message, a space-separated `String`
* @return the contents portion of the message, transformed into an `Array`
*/
private def decomposeMessage(msg: String): Array[String] = {
msg.trim.toLowerCase.split("\\s+")
}
/**
* Send a message back to the `Traveler` that will be printed into his chat window.
* @param traveler the player
* @param msg the message to be sent
*/
private def reply(traveler: Traveler, msg: String): Unit = {
traveler ! PacketCoding.CreateGamePacket(0, ChatMsg(ChatMessageType.CMT_OPEN, true, "", msg, None))
}
/**
* Print usage information to the `Traveler`'s chat window.
* @param traveler the player
*/
private def help(traveler: Traveler): Unit = {
CSRZone.reply(traveler, "usage: /zone <zone> [gatename] | [-list]")
}
/**
* Print error information to the `Traveler`'s chat window.<br>
* The most common reason for error is the lack of information, or wrong information.
* @param traveler the player
*/
private def error(traveler: Traveler, msg: String): Unit = {
CSRZone.reply(traveler, "Error! " + msg)
}
}

View file

@ -1,38 +0,0 @@
package net.psforever.pslogin.csr
import akka.actor.ActorRef
import net.psforever.packet.PlanetSidePacketContainer
/*
The following is STILL for development and fun.
*/
/**
* The traveler is synonymous with the player.
* The primary purpose of the object is to keep track of but not expose the player's session so that packets may be relayed back to him.
* csr.Traveler also keeps track of which zone the player currently occupies.
* @param session the player's session
*/
class Traveler(private val session: ActorRef, var zone: String) {
/**
* `sendToSelf` is a call that permits the session to gain access to its internal `rightRef` so that it can dispatch a packet.
* @param msg the byte-code translation of a packet
*/
def sendToSelf(msg: PlanetSidePacketContainer): Unit = {
// this.session.sendResponse(msg)
}
def !(msg: Any): Unit = {
session ! msg
}
}
object Traveler {
/**
* An abbreviated constructor for creating `csr.Traveler`s without invocation of `new`.
* @param session the player's session
* @return a traveler object for this player
*/
def apply(session: ActorRef, zoneId: String): Traveler = new Traveler(session, zoneId)
}

View file

@ -2,6 +2,7 @@ package net.psforever.pslogin
import akka.actor.{ActorRef, MDCContextAware}
import akka.testkit.TestProbe
import net.psforever.login.HelloFriend
import net.psforever.packet.{ControlPacket, GamePacket}
final case class MDCGamePacket(packet: GamePacket)

View file

@ -3,6 +3,7 @@ package net.psforever.pslogin
import actor.base.ActorTest
import akka.actor.{ActorRef, Props}
import akka.testkit.TestProbe
import net.psforever.login.{HelloFriend, PacketCodingActor, RawPacket}
import net.psforever.packet.control.{ControlSync, MultiPacketBundle, SlottedMetaPacket}
import net.psforever.packet.{ControlPacket, GamePacket, GamePacketOpcode, PacketCoding}
import net.psforever.packet.game._

View file

@ -534,7 +534,7 @@ class AvatarReleaseTest extends ActorTest {
val reply1 = receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[AvatarServiceResponse])
val reply1msg = reply1.asInstanceOf[AvatarServiceResponse]
assert(reply1msg.toChannel == "/test/Avatar")
assert(reply1msg.channel == "/test/Avatar")
assert(reply1msg.avatar_guid == guid)
assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release])
assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj)
@ -542,7 +542,7 @@ class AvatarReleaseTest extends ActorTest {
val reply2 = receiveOne(2 seconds)
assert(reply2.isInstanceOf[AvatarServiceResponse])
val reply2msg = reply2.asInstanceOf[AvatarServiceResponse]
assert(reply2msg.toChannel.equals("/test/Avatar"))
assert(reply2msg.channel.equals("/test/Avatar"))
assert(reply2msg.avatar_guid == Service.defaultPlayerGUID)
assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete])
assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid)
@ -585,7 +585,7 @@ class AvatarReleaseEarly1Test extends ActorTest {
val reply1 = receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[AvatarServiceResponse])
val reply1msg = reply1.asInstanceOf[AvatarServiceResponse]
assert(reply1msg.toChannel == "/test/Avatar")
assert(reply1msg.channel == "/test/Avatar")
assert(reply1msg.avatar_guid == guid)
assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release])
assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj)
@ -594,7 +594,7 @@ class AvatarReleaseEarly1Test extends ActorTest {
val reply2 = receiveOne(200 milliseconds)
assert(reply2.isInstanceOf[AvatarServiceResponse])
val reply2msg = reply2.asInstanceOf[AvatarServiceResponse]
assert(reply2msg.toChannel.equals("/test/Avatar"))
assert(reply2msg.channel.equals("/test/Avatar"))
assert(reply2msg.avatar_guid == Service.defaultPlayerGUID)
assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete])
assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid)
@ -641,7 +641,7 @@ class AvatarReleaseEarly2Test extends ActorTest {
val reply1 = receiveOne(200 milliseconds)
assert(reply1.isInstanceOf[AvatarServiceResponse])
val reply1msg = reply1.asInstanceOf[AvatarServiceResponse]
assert(reply1msg.toChannel == "/test/Avatar")
assert(reply1msg.channel == "/test/Avatar")
assert(reply1msg.avatar_guid == guid)
assert(reply1msg.replyMessage.isInstanceOf[AvatarResponse.Release])
assert(reply1msg.replyMessage.asInstanceOf[AvatarResponse.Release].player == obj)
@ -652,7 +652,7 @@ class AvatarReleaseEarly2Test extends ActorTest {
val reply2 = receiveOne(100 milliseconds)
assert(reply2.isInstanceOf[AvatarServiceResponse])
val reply2msg = reply2.asInstanceOf[AvatarServiceResponse]
assert(reply2msg.toChannel.equals("/test/Avatar"))
assert(reply2msg.channel.equals("/test/Avatar"))
assert(reply2msg.avatar_guid == Service.defaultPlayerGUID)
assert(reply2msg.replyMessage.isInstanceOf[AvatarResponse.ObjectDelete])
assert(reply2msg.replyMessage.asInstanceOf[AvatarResponse.ObjectDelete].item_guid == guid)