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", organization := "net.psforever",
version := "1.0.2-SNAPSHOT", version := "1.0.2-SNAPSHOT",
scalaVersion := "2.13.2", scalaVersion := "2.13.2",
Global / cancelable := false,
semanticdbEnabled := true, semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision, semanticdbVersion := scalafixSemanticdb.revision,
scalacOptions := Seq( 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 // ActorTests have specific timing requirements and will be flaky if run in parallel
parallelExecution in Test := false, parallelExecution in Test := false,
// TODO(chord): remove exclusion when WorldSessionActor is refactored: https://github.com/psforever/PSF-LoginServer/issues/279 // 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) // Copy all tests from Test -> QuietTest (we're only changing the run options)
inConfig(QuietTest)(Defaults.testTasks) 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 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.control._
import net.psforever.packet.crypto._ import net.psforever.packet.crypto._
import net.psforever.packet.game.PingMsg import net.psforever.packet.game.PingMsg
import org.log4s.MDC import org.log4s.MDC
import MDCContextAware.Implicits._ import scodec.Attempt.{Failure, Successful}
import scodec.bits._
sealed trait CryptoSessionAPI sealed trait CryptoSessionAPI
final case class DropCryptoSession() extends 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 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.control._
import net.psforever.packet.game.LoginRespMessage.{LoginError, StationError, StationSubscriptionStatus}
import net.psforever.packet.game._ 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 org.log4s.MDC
import scodec.bits._ 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
import services.ServiceManager.Lookup import services.ServiceManager.Lookup
import services.account.{ReceiveIPAddress, RetrieveIPAddress, StoreAccountData} import services.account.{ReceiveIPAddress, RetrieveIPAddress, StoreAccountData}
import com.github.t3hnar.bcrypt._
import net.psforever.packet.game.LoginRespMessage.{LoginError, StationError, StationSubscriptionStatus} import scala.concurrent.Future
import net.psforever.persistence
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.{Failure, Success} import scala.util.{Failure, Success}
import scala.concurrent.Future
import Database._
import java.net.InetAddress
class LoginSessionActor extends Actor with MDCContextAware { class LoginSessionActor extends Actor with MDCContextAware {
private[this] val log = org.log4s.getLogger 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 akka.actor.{Actor, ActorRef, Cancellable, MDCContextAware}
import net.psforever.objects.Default
import net.psforever.packet._ import net.psforever.packet._
import net.psforever.packet.control.{HandleGamePacket, _}
import org.log4s.MDC
import scodec.Attempt.{Failure, Successful} import scodec.Attempt.{Failure, Successful}
import scodec.bits._ 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.annotation.tailrec
import scala.collection.mutable import scala.collection.mutable

View file

@ -1,13 +1,11 @@
package net.psforever.pslogin package net.psforever.login
import java.net.InetSocketAddress import java.net.InetSocketAddress
import akka.actor._ import akka.actor.MDCContextAware.Implicits._
import scodec.bits._ import akka.actor.{ActorContext, ActorRef, PoisonPill, _}
import akka.actor.{ActorContext, ActorRef, PoisonPill}
import com.github.nscala_time.time.Imports._ import com.github.nscala_time.time.Imports._
import MDCContextAware.Implicits._ import scodec.bits._
sealed trait SessionState sealed trait SessionState
final case class New() extends 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 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.SupervisorStrategy.Stop
import akka.actor._
import net.psforever.packet.PacketCoding import net.psforever.packet.PacketCoding
import net.psforever.packet.control.ConnectionClose import net.psforever.packet.control.ConnectionClose
import net.psforever.util.Config
import org.log4s.MDC
import scodec.bits._
import services.ServiceManager import services.ServiceManager
import services.ServiceManager.Lookup import services.ServiceManager.Lookup
import services.account.{IPAddress, StoreIPAddress} import services.account.{IPAddress, StoreIPAddress}
import scala.collection.mutable
import scala.concurrent.duration._ import scala.concurrent.duration._
sealed trait SessionRouterAPI sealed trait SessionRouterAPI

View file

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

View file

@ -1,4 +1,4 @@
package net.psforever.pslogin package net.psforever.login
import java.net.{InetAddress, InetSocketAddress} 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.actor.{Actor, ActorRef}
import akka.io._ import akka.io._
import scala.util.Random
import scala.collection.mutable import scala.collection.mutable
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.util.Random
/** Parameters for the Network simulator /** 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.actor.ActorRef
import akka.pattern.{AskTimeoutException, ask} import akka.pattern.{AskTimeoutException, ask}
import akka.util.Timeout import akka.util.Timeout
import net.psforever.objects.{AmmoBox, GlobalDefinitions, Player, Tool}
import net.psforever.objects.equipment.{Ammo, Equipment} import net.psforever.objects.equipment.{Ammo, Equipment}
import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver} import net.psforever.objects.guid.{GUIDTask, Task, TaskResolver}
import net.psforever.objects.inventory.{Container, InventoryItem} import net.psforever.objects.inventory.{Container, InventoryItem}
import net.psforever.objects.serverobject.PlanetSideServerObject import net.psforever.objects.serverobject.PlanetSideServerObject
import net.psforever.objects.serverobject.containable.Containable import net.psforever.objects.serverobject.containable.Containable
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.objects.{AmmoBox, GlobalDefinitions, Player, Tool}
import net.psforever.packet.game.ObjectHeldMessage import net.psforever.packet.game.ObjectHeldMessage
import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3} import net.psforever.types.{PlanetSideGUID, TransactionType, Vector3}
import services.Service import services.Service
@ -19,7 +19,7 @@ import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future import scala.concurrent.Future
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.language.implicitConversions import scala.language.implicitConversions
import scala.util.{Success, Failure} import scala.util.{Failure, Success}
object WorldSession { 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 scala.collection.mutable
import net.psforever.pslogin.Config
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
object CmdInternal { 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, ActorRef}
import akka.actor.Actor
import scala.collection.mutable.Map
import net.psforever.objects.zones.InterstellarCluster import net.psforever.objects.zones.InterstellarCluster
import scala.collection.mutable.Map
class CmdListPlayers(args: Array[String], services: Map[String, ActorRef]) extends Actor { class CmdListPlayers(args: Array[String], services: Map[String, ActorRef]) extends Actor {
private[this] val log = org.log4s.getLogger(self.path.name) 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 akka.actor.{Actor, ActorRef}
import scala.collection.mutable.Map import scala.collection.mutable.Map
class CmdShutdown(args: Array[String], services: Map[String, ActorRef]) extends Actor { 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 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._
import org.json4s.native.Serialization.write import org.json4s.native.Serialization.write
import scodec.bits._
import scodec.interop.akka._
import services.ServiceManager.Lookup import services.ServiceManager.Lookup
import services._ import services._
import scala.collection.mutable.Map
object PsAdminActor { object PsAdminActor {
val whiteSpaceRegex = """\s+""".r val whiteSpaceRegex = """\s+""".r
} }

View file

@ -1,4 +1,4 @@
package net.psforever.pslogin.psadmin package net.psforever.login.psadmin
import scala.collection.mutable 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_GMTELL, // /gmtell (actually causes normal /tell 0x20 when not a gm???)
CMT_NOTE, // /note CMT_NOTE, // /note
CMT_GMBROADCASTPOPUP, // /gmpopup CMT_GMBROADCASTPOPUP, // /gmpopup
U_CMT_GMTELLFROM, // ??? Recipient of /gmtell? U_CMT_GMTELLFROM, // Acknowledgement of /gmtell for sender
U_CMT_TELLFROM, // ??? Recipient of /t? U_CMT_TELLFROM, // Acknowledgement of /tell for sender
UNK_45, // ??? empty 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_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 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 java.nio.file.Paths
import com.typesafe.config.{Config => TypesafeConfig} 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 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 scala.reflect.ClassTag
import pureconfig.generic.auto._ // intellij: this is not unused
object Config { object Config {
// prog.home is defined when we are running from SBT pack // prog.home is defined when we are running from SBT pack
val directory: String = System.getProperty("prog.home") match { val directory: String = System.getProperty("prog.home") match {
case null => case null =>
Paths.get("config").toAbsolutePath().toString() Paths.get("config").toAbsolutePath.toString
case home => case home =>
Paths.get(home, "config").toAbsolutePath().toString() Paths.get(home, "config").toAbsolutePath.toString
} }
implicit def enumeratumIntConfigConvert[A <: IntEnumEntry](implicit implicit def enumeratumIntConfigConvert[A <: IntEnumEntry](implicit
@ -42,7 +41,7 @@ object Config {
ConfigSource.defaultApplication ConfigSource.defaultApplication
} }
val result = source.load[AppConfig] val result: Result[AppConfig] = source.load[AppConfig]
// Raw config object - prefer app when possible // Raw config object - prefer app when possible
lazy val config: TypesafeConfig = source.config().toOption.get lazy val config: TypesafeConfig = source.config().toOption.get
@ -88,7 +87,7 @@ case class DatabaseConfig(
database: String, database: String,
sslmode: String sslmode: String
) { ) {
def toJdbc = s"jdbc:postgresql://${host}:${port}/${database}" def toJdbc = s"jdbc:postgresql://$host:$port/$database"
} }
case class AntiCheatConfig( case class AntiCheatConfig(

View file

@ -1,7 +1,6 @@
package net.psforever.pslogin package net.psforever.util
import io.getquill.PostgresJAsyncContext import io.getquill.{PostgresJAsyncContext, SnakeCase}
import io.getquill.SnakeCase
import net.psforever.persistence import net.psforever.persistence
object Database { object Database {

View file

@ -1,13 +1,10 @@
package net.psforever.pslogin.csr package net.psforever.util
import net.psforever.types.Vector3 import net.psforever.types.Vector3
import scala.collection.mutable import scala.collection.mutable
import scala.util.Random 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"). * 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`. * 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 map the map name of the zone (this map is loaded)
* @param zonename the zone's internal name * @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. * 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() 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. * 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. * The keys in this map should be directly usable by the `/zone` command.
*/ */
private val zones = Map[String, CSRZoneImpl]( private val zones = Map[String, PointOfInterest](
"z1" -> CSRZoneImpl("Solsar", "map01", "z1"), "z1" -> PointOfInterest("Solsar", "map01", "z1"),
"z2" -> CSRZoneImpl("Hossin", "map02", "z2"), "z2" -> PointOfInterest("Hossin", "map02", "z2"),
"z3" -> CSRZoneImpl("Cyssor", "map03", "z3"), "z3" -> PointOfInterest("Cyssor", "map03", "z3"),
"z4" -> CSRZoneImpl("Ishundar", "map04", "z4"), "z4" -> PointOfInterest("Ishundar", "map04", "z4"),
"z5" -> CSRZoneImpl("Forseral", "map05", "z5"), "z5" -> PointOfInterest("Forseral", "map05", "z5"),
"z6" -> CSRZoneImpl("Ceryshen", "map06", "z6"), "z6" -> PointOfInterest("Ceryshen", "map06", "z6"),
"z7" -> CSRZoneImpl("Esamir", "map07", "z7"), "z7" -> PointOfInterest("Esamir", "map07", "z7"),
"z8" -> CSRZoneImpl("Oshur", "map08", "z8"), "z8" -> PointOfInterest("Oshur", "map08", "z8"),
"z9" -> CSRZoneImpl("Searhus", "map09", "z9"), "z9" -> PointOfInterest("Searhus", "map09", "z9"),
"z10" -> CSRZoneImpl("Amerish", "map10", "z10"), "z10" -> PointOfInterest("Amerish", "map10", "z10"),
"home1" -> CSRZoneImpl("NC Sanctuary", "map11", "home1"), "home1" -> PointOfInterest("NC Sanctuary", "map11", "home1"),
"home2" -> CSRZoneImpl("TR Sanctuary", "map12", "home2"), "home2" -> PointOfInterest("TR Sanctuary", "map12", "home2"),
"home3" -> CSRZoneImpl("VS Sanctuary", "map13", "home3"), "home3" -> PointOfInterest("VS Sanctuary", "map13", "home3"),
"tzshtr" -> CSRZoneImpl("VR Shooting Range TR", "map14", "tzshtr"), "tzshtr" -> PointOfInterest("VR Shooting Range TR", "map14", "tzshtr"),
"tzdrtr" -> CSRZoneImpl("VR Driving Range TR", "map15", "tzdrtr"), "tzdrtr" -> PointOfInterest("VR Driving Range TR", "map15", "tzdrtr"),
"tzcotr" -> CSRZoneImpl("VR Combat Zone TR", "map16", "tzcotr"), "tzcotr" -> PointOfInterest("VR Combat Zone TR", "map16", "tzcotr"),
"tzshvs" -> CSRZoneImpl("VR Shooting Range VS", "map14", "tzshvs"), "tzshvs" -> PointOfInterest("VR Shooting Range VS", "map14", "tzshvs"),
"tzdrvs" -> CSRZoneImpl("VR Driving Range VS", "map15", "tzdrvs"), "tzdrvs" -> PointOfInterest("VR Driving Range VS", "map15", "tzdrvs"),
"tzcovs" -> CSRZoneImpl("VR Combat Zone VS", "map16", "tzcovs"), "tzcovs" -> PointOfInterest("VR Combat Zone VS", "map16", "tzcovs"),
"tzshnc" -> CSRZoneImpl("VR Shooting Range NC", "map14", "tzshnc"), "tzshnc" -> PointOfInterest("VR Shooting Range NC", "map14", "tzshnc"),
"tzdrnc" -> CSRZoneImpl("VR Driving Range NC", "map15", "tzdrnc"), "tzdrnc" -> PointOfInterest("VR Driving Range NC", "map15", "tzdrnc"),
"tzconc" -> CSRZoneImpl("VR Combat Zone NC", "map16", "tzconc"), "tzconc" -> PointOfInterest("VR Combat Zone NC", "map16", "tzconc"),
"c1" -> CSRZoneImpl("Supai", "ugd01", "c1"), "c1" -> PointOfInterest("Supai", "ugd01", "c1"),
"c2" -> CSRZoneImpl("Hunhau", "ugd02", "c2"), "c2" -> PointOfInterest("Hunhau", "ugd02", "c2"),
"c3" -> CSRZoneImpl("Adlivun", "ugd03", "c3"), "c3" -> PointOfInterest("Adlivun", "ugd03", "c3"),
"c4" -> CSRZoneImpl("Byblos", "ugd04", "c4"), "c4" -> PointOfInterest("Byblos", "ugd04", "c4"),
"c5" -> CSRZoneImpl("Annwn", "ugd05", "c5"), "c5" -> PointOfInterest("Annwn", "ugd05", "c5"),
"c6" -> CSRZoneImpl("Drugaskan", "ugd06", "c6"), "c6" -> PointOfInterest("Drugaskan", "ugd06", "c6"),
"i4" -> CSRZoneImpl("Nexus", "map96", "i4"), "i4" -> PointOfInterest("Nexus", "map96", "i4"),
"i3" -> CSRZoneImpl("Desolation", "map97", "i3"), "i3" -> PointOfInterest("Desolation", "map97", "i3"),
"i2" -> CSRZoneImpl("Ascension", "map98", "i2"), "i2" -> PointOfInterest("Ascension", "map98", "i2"),
"i1" -> CSRZoneImpl("Extinction", "map99", "i1"), "i1" -> PointOfInterest("Extinction", "map99", "i1"),
"homebo" -> CSRZoneImpl("Black_ops_hq", "Black_ops_hq", "homebo"), "homebo" -> PointOfInterest("Black_ops_hq", "Black_ops_hq", "homebo"),
"station1" -> CSRZoneImpl("TR Station", "Station1", "station1"), "station1" -> PointOfInterest("TR Station", "Station1", "station1"),
"station2" -> CSRZoneImpl("NC Station", "Station2", "station2"), "station2" -> PointOfInterest("NC Station", "Station2", "station2"),
"station3" -> CSRZoneImpl("VS Station", "Station3", "station3") "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 map the map name of the zone (this map is loaded)
* @param zonename the zone's internal name * @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. * Get a valid `CSRZone`'s information.
* @param zoneId a name that describes the zone and should be searchable * @param zoneId a name that describes the zone and should be searchable
* @return the `CSRZone`, or `None` * @return the `CSRZone`, or `None`
*/ */
def get(zoneId: String): Option[CSRZoneImpl] = { def get(zoneId: String): Option[PointOfInterest] = {
var zId = zoneId.toLowerCase var zId = zoneId.toLowerCase
if (alias.get(zId).isDefined) if (alias.get(zId).isDefined)
zId = alias(zId) zId = alias(zId)
@ -151,16 +148,19 @@ object CSRZoneImpl {
/** /**
* Get a location within the `CSRZone`. * Get a location within the `CSRZone`.
* The location should be a facility or a warpgate or interesting. * The location should be a facility or a warpgate or interesting.
* @param zone the `CSRZone` * @param zoneId the `CSRZone`
* @param locId a name that describes a known location in the provided `CSRZone` and is searchable * @param locationId a name that describes a known location in the provided `CSRZone` and is searchable
* @return the coordinates of that location, or None * @return the coordinates of that location, or None
*/ */
def getWarpLocation(zone: CSRZoneImpl, locId: String): Option[Vector3] = { def getWarpLocation(zoneId: String, locationId: String): Option[Vector3] = {
val low_locId = locId.toLowerCase get(zoneId) match {
var location = zone.locations.get(low_locId) case Some(poi) =>
if (location.isEmpty) poi.locations.get(locationId) match {
location = zone.gates.get(low_locId) case Some(position) => Some(position)
location 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 * @param gateId a name that describes a known warpgate in the provided `CSRZone` and is searchable
* @return the coordinates of that warpgate, or None * @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) zone.gates.get(gateId.toLowerCase)
} }
@ -178,7 +178,7 @@ object CSRZoneImpl {
* @return all of the zonenames * @return all of the zonenames
*/ */
def list: String = { 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` * @param zone the `CSRZone`
* @return all of the location keys * @return all of the location keys
*/ */
def listLocations(zone: CSRZoneImpl): String = { def listLocations(zone: PointOfInterest): String = {
var out: String = "warps: " var out: String = "warps: "
if (zone.locations.nonEmpty) { if (zone.locations.nonEmpty) {
out += zone.locations.keys.toArray.sorted.mkString(", ") out += zone.locations.keys.toArray.sorted.mkString(", ")
@ -200,7 +200,7 @@ object CSRZoneImpl {
* @param zone the `CSRZone` * @param zone the `CSRZone`
* @return all of the warpgate keys * @return all of the warpgate keys
*/ */
def listWarpgates(zone: CSRZoneImpl): String = { def listWarpgates(zone: PointOfInterest): String = {
var out: String = "gatenames: " var out: String = "gatenames: "
if (zone.gates.isEmpty) if (zone.gates.isEmpty)
out += "none" out += "none"
@ -214,7 +214,7 @@ object CSRZoneImpl {
* @param zone the `CSRZone` * @param zone the `CSRZone`
* @return the coordinates of the spawn point * @return the coordinates of the spawn point
*/ */
def selectRandom(zone: CSRZoneImpl): Vector3 = { def selectRandom(zone: PointOfInterest): Vector3 = {
var outlets = zone.locations //random location? var outlets = zone.locations //random location?
if (outlets.nonEmpty) { if (outlets.nonEmpty) {
return outlets.values.toArray.apply(rand.nextInt(outlets.size)) 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.LocalProjectile
import net.psforever.objects.ballistics.Projectile import net.psforever.objects.ballistics.Projectile
import net.psforever.objects.zones.ZoneMap import net.psforever.objects.zones.ZoneMap
import zonemaps._ import net.psforever.zones.zonemaps._
import scala.concurrent.Future import scala.concurrent.Future
import scala.util.{Failure, Success} import scala.util.{Failure, Success}

View file

@ -1,4 +1,4 @@
package net.psforever.pslogin package net.psforever.zones
import akka.actor.ActorContext import akka.actor.ActorContext
import net.psforever.objects.GlobalDefinitions 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.serverobject.structures.WarpGate
import net.psforever.objects.zones.Zone import net.psforever.objects.zones.Zone
import net.psforever.types.PlanetSideEmpire import net.psforever.types.PlanetSideEmpire
import scala.concurrent.duration._ import scala.concurrent.duration._
import scala.concurrent.Await
import scala.collection.immutable.HashMap import scala.collection.immutable.HashMap
import scala.concurrent.Await
object Zones { object Zones {
val zones: HashMap[String, Zone] = HashMap( val zones: HashMap[String, Zone] = HashMap(
@ -547,8 +546,8 @@ object Zones {
* @return the duration * @return the duration
*/ */
def StandardTimeRules(defender: SourceEntry, attacker: SourceEntry): FiniteDuration = { def StandardTimeRules(defender: SourceEntry, attacker: SourceEntry): FiniteDuration = {
import net.psforever.objects.ballistics._
import net.psforever.objects.GlobalDefinitions import net.psforever.objects.GlobalDefinitions
import net.psforever.objects.ballistics._
if (attacker.Faction == defender.Faction) { if (attacker.Faction == defender.Faction) {
0 seconds 0 seconds
} else { } 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door 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.GlobalDefinitions._
import net.psforever.objects.serverobject.doors.Door import net.psforever.objects.serverobject.doors.Door

View file

@ -15,14 +15,14 @@ object Service {
} }
trait GenericEventBusMsg { trait GenericEventBusMsg {
def toChannel: String def channel: String
} }
class GenericEventBus[A <: GenericEventBusMsg] extends ActorEventBus with SubchannelClassification { class GenericEventBus[A <: GenericEventBusMsg] extends ActorEventBus with SubchannelClassification {
type Event = A type Event = A
type Classifier = String type Classifier = String
protected def classify(event: Event): Classifier = event.toChannel protected def classify(event: Event): Classifier = event.channel
protected def subclassification = protected def subclassification =
new Subclassification[Classifier] { new Subclassification[Classifier] {

View file

@ -12,7 +12,7 @@ import net.psforever.types.{ExoSuitType, PlanetSideEmpire, PlanetSideGUID, Trans
import services.GenericEventBusMsg import services.GenericEventBusMsg
final case class AvatarServiceResponse( final case class AvatarServiceResponse(
toChannel: String, channel: String,
avatar_guid: PlanetSideGUID, avatar_guid: PlanetSideGUID,
replyMessage: AvatarResponse.Response replyMessage: AvatarResponse.Response
) extends GenericEventBusMsg ) 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 // Copyright (c) 2017 PSForever
package services.chat package services.chat
import akka.actor.Actor import akka.actor.typed.receptionist.{Receptionist, ServiceKey}
import net.psforever.objects.LivePlayerList 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.packet.game.ChatMsg
import net.psforever.types.ChatMessageType import net.psforever.types.{ChatMessageType, PlanetSideEmpire, PlanetSideGUID}
import services.{GenericEventBus, Service}
class ChatService extends Actor { object ChatService {
private[this] val log = org.log4s.getLogger val ChatServiceKey: ServiceKey[Command] = ServiceKey[ChatService.Command]("chatService")
override def preStart = { def apply(): Behavior[Command] =
log.info("Starting....") 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 = { class ChatService(context: ActorContext[ChatService.Command]) extends AbstractBehavior[ChatService.Command](context) {
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())
case ChatServiceMessage(forChannel, action) => import ChatService._
action match { import ChatMessageType._
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)
)
)
case ChatAction.Note(player_guid, player_name, msg) => // note private[this] val log = org.log4s.getLogger
ChatEvents.publish( var subscriptions: List[JoinChannel] = List[JoinChannel]()
ChatServiceResponse(
s"/Chat/$forChannel", override def onMessage(msg: Command): Behavior[Command] = {
player_guid, msg match {
player_name, case subscription: JoinChannel =>
target = 1, subscriptions ++= List(subscription)
replyMessage = ChatMsg(ChatMessageType.U_CMT_GMTELLFROM, true, msg.recipient, msg.contents, None) this
)
) case LeaveChannel(actor, channel) =>
ChatEvents.publish( subscriptions = subscriptions.filter {
ChatServiceResponse( case JoinChannel(a, _, c) => actor != a && channel != c
s"/Chat/$forChannel", }
player_guid, this
player_name,
target = 1, case LeaveAllChannels(actor) =>
replyMessage = ChatMsg( subscriptions = subscriptions.filter {
ChatMessageType.CMT_GMTELL, case JoinChannel(a, _, _) => actor != a
true, }
"Server", this
"Why do you try to /note ? That's a GM command ! ... Or not, nobody can /note",
None case Message(session, message, channel) =>
) (channel, message.messageType) match {
) case (ChatChannel.Squad(_), CMT_SQUAD) =>
) case (ChatChannel.Default(), messageType) if messageType != CMT_SQUAD =>
case ChatAction.Squad(player_guid, player_name, cont, player_pos, player_faction, msg) => // squad case _ =>
ChatEvents.publish( log.error(s"invalid chat channel $channel for messageType ${message.messageType}")
ChatServiceResponse( return this
s"/Chat/$forChannel", }
player_guid, val subs = subscriptions.filter(_.channel == channel)
player_name, message.messageType match {
cont, case CMT_TELL | CMT_GMTELL =>
player_pos, subs.find(_.session.player.Name == session.player.Name).foreach {
player_faction, case JoinChannel(sender, _, _) =>
2, sender ! MessageResponse(
ChatMsg(ChatMessageType.CMT_SQUAD, msg.wideContents, player_name, msg.contents, None) session,
) message.copy(messageType = if (message.messageType == CMT_TELL) U_CMT_TELLFROM else U_CMT_GMTELLFROM),
) channel
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)
) )
) subs.find(_.session.player.Name == message.recipient) match {
// if(player_guid != PlanetSideGUID(0)) { case Some(JoinChannel(receiver, _, _)) =>
// receiver ! MessageResponse(session, message, channel)
// val args = msg.contents.split(" ") case None =>
// var silence_name : String = "" sender ! MessageResponse(
// var silence_time : Int = 5 session,
// if (args.length == 1) { ChatMsg(ChatMessageType.UNK_45, false, "", "@NoTell_Target", None),
// silence_name = args(0) channel
// } )
// 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 _ => ;
}
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 net.psforever.types.PlanetSideGUID
import services.GenericEventBusMsg import services.GenericEventBusMsg
final case class GalaxyServiceResponse(toChannel: String, replyMessage: GalaxyResponse.Response) final case class GalaxyServiceResponse(channel: String, replyMessage: GalaxyResponse.Response)
extends GenericEventBusMsg extends GenericEventBusMsg
object GalaxyResponse { object GalaxyResponse {

View file

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

View file

@ -2,15 +2,22 @@
package services.teamwork package services.teamwork
import akka.actor.{Actor, ActorRef, Terminated} import akka.actor.{Actor, ActorRef, Terminated}
import net.psforever.objects.{Avatar, LivePlayerList, Player}
import net.psforever.objects.definition.converter.StatConverter import net.psforever.objects.definition.converter.StatConverter
import net.psforever.objects.loadouts.SquadLoadout import net.psforever.objects.loadouts.SquadLoadout
import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures} import net.psforever.objects.teamwork.{Member, Squad, SquadFeatures}
import net.psforever.objects.zones.Zone 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 net.psforever.types._
import services.{GenericEventBus, Service} import services.{GenericEventBus, Service}
import services.teamwork.SquadAction
import scala.collection.concurrent.TrieMap import scala.collection.concurrent.TrieMap
import scala.collection.mutable import scala.collection.mutable

View file

@ -2,12 +2,11 @@
package services.teamwork package services.teamwork
import net.psforever.objects.teamwork.Squad 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 net.psforever.types.{PlanetSideGUID, SquadResponseType, SquadWaypoints}
import services.GenericEventBusMsg 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 extends GenericEventBusMsg
object SquadServiceResponse { object SquadServiceResponse {

View file

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

View file

@ -2,7 +2,9 @@ package net.psforever.pslogin
import java.net.InetAddress import java.net.InetAddress
import java.util.Locale import java.util.Locale
import akka.actor.{ActorSystem, Props}
import akka.{actor => classic}
import akka.actor.typed.ActorSystem
import akka.routing.RandomPool import akka.routing.RandomPool
import ch.qos.logback.classic.LoggerContext import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.joran.JoranConfigurator 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.Default
import net.psforever.objects.zones._ import net.psforever.objects.zones._
import net.psforever.objects.guid.TaskResolver import net.psforever.objects.guid.TaskResolver
import net.psforever.pslogin.psadmin.PsAdminActor
import org.slf4j import org.slf4j
import org.fusesource.jansi.Ansi._ import org.fusesource.jansi.Ansi._
import org.fusesource.jansi.Ansi.Color._ import org.fusesource.jansi.Ansi.Color._
@ -26,6 +27,22 @@ import org.flywaydb.core.Flyway
import java.nio.file.Paths import java.nio.file.Paths
import scopt.OParser 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 { object PsLogin {
private val logger = org.log4s.getLogger 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 */ /** 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) Default(system)
val typedSystem: ActorSystem[Nothing] = system.toTyped
/** Create pipelines for the login and world servers /** Create pipelines for the login and world servers
* *
* The first node in the pipe is an Actor that handles the crypto for protecting packets. * 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 * See SessionRouter.scala for a diagram
*/ */
val loginTemplate = List( val loginTemplate = List(
SessionPipeline("crypto-session-", Props[CryptoSessionActor]), SessionPipeline("crypto-session-", classic.Props[CryptoSessionActor]),
SessionPipeline("packet-session-", Props[PacketCodingActor]), SessionPipeline("packet-session-", classic.Props[PacketCodingActor]),
SessionPipeline("login-session-", Props[LoginSessionActor]) SessionPipeline("login-session-", classic.Props[LoginSessionActor])
) )
val worldTemplate = List( val worldTemplate = List(
SessionPipeline("crypto-session-", Props[CryptoSessionActor]), SessionPipeline("crypto-session-", classic.Props[CryptoSessionActor]),
SessionPipeline("packet-session-", Props[PacketCodingActor]), SessionPipeline("packet-session-", classic.Props[PacketCodingActor]),
SessionPipeline("world-session-", Props[WorldSessionActor]) SessionPipeline("world-session-", classic.Props[SessionActor])
) )
val netSim: Option[NetworkSimulatorParameters] = if (Config.app.developer.netSim.enable) { 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 continents = Zones.zones.values ++ Seq(Zone.Nowhere)
val serviceManager = ServiceManager.boot system.spawnAnonymous(ChatService())
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")
val loginRouter = Props(new SessionRouter("Login", loginTemplate)) val serviceManager = ServiceManager.boot
val worldRouter = Props(new SessionRouter("World", worldTemplate)) 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( 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" "login-udp-endpoint"
) )
val worldListener = system.actorOf( 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" "world-udp-endpoint"
) )
val adminListener = system.actorOf( val adminListener = system.actorOf(
Props( classic.Props(
new TcpListener( new TcpListener(
classOf[PsAdminActor], classOf[PsAdminActor],
"psadmin-client-", "net.psforever.login.psadmin-client-",
InetAddress.getByName(Config.app.admin.bind), InetAddress.getByName(Config.app.admin.bind),
Config.app.admin.port Config.app.admin.port
) )
), ),
"psadmin-tcp-endpoint" "net.psforever.login.psadmin-tcp-endpoint"
) )
logger.info( 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.actor.{ActorRef, MDCContextAware}
import akka.testkit.TestProbe import akka.testkit.TestProbe
import net.psforever.login.HelloFriend
import net.psforever.packet.{ControlPacket, GamePacket} import net.psforever.packet.{ControlPacket, GamePacket}
final case class MDCGamePacket(packet: GamePacket) final case class MDCGamePacket(packet: GamePacket)

View file

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

View file

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